diff --git a/Cargo.toml b/Cargo.toml index c0ce766..f22a057 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,4 +16,5 @@ rust-version = "1.71" bitflags = "2.4.2" kvm-bindings = "0.7.0" kvm-ioctls = "0.16.0" +uuid = "1.8.0" vmm-sys-util = "0.12.1" diff --git a/src/launch/linux.rs b/src/launch/linux.rs index 11f6b89..c8ee663 100644 --- a/src/launch/linux.rs +++ b/src/launch/linux.rs @@ -5,8 +5,9 @@ pub const NR_CPUID_CONFIGS: usize = 12; /// Trust Domain eXtensions sub-ioctl() commands #[repr(u32)] pub enum CmdId { - GetCapabilities = 0, - InitVm = 1, + GetCapabilities, + InitVm, + InitVcpu, } /// Contains information for the sub-ioctl() command to be run. This is diff --git a/src/launch/mod.rs b/src/launch/mod.rs index 6bb39be..07bdb79 100644 --- a/src/launch/mod.rs +++ b/src/launch/mod.rs @@ -21,10 +21,12 @@ impl TdxVm { /// Create a new TDX VM with KVM pub fn new(kvm_fd: &Kvm, max_vcpus: u64) -> Result { let vm_fd = kvm_fd.create_vm_with_type(KVM_X86_TDX_VM)?; - let mut cap: kvm_enable_cap = Default::default(); // TDX requires that MAX_VCPUS and SPLIT_IRQCHIP be set - cap.cap = KVM_CAP_MAX_VCPUS; + let mut cap: kvm_enable_cap = kvm_enable_cap { + cap: KVM_CAP_MAX_VCPUS, + ..Default::default() + }; cap.args[0] = max_vcpus; vm_fd.enable_cap(&cap).unwrap(); @@ -261,3 +263,50 @@ pub struct TdxCapabilities { pub cpuid_configs: Vec, } + +/// Manually create the wrapper for KVM_MEMORY_ENCRYPT_OP since `kvm_ioctls` doesn't +/// support `.encrypt_op` for vcpu fds +use vmm_sys_util::*; +ioctl_iowr_nr!( + KVM_MEMORY_ENCRYPT_OP, + kvm_bindings::KVMIO, + 0xba, + std::os::raw::c_ulong +); + +pub struct TdxVcpu { + pub fd: kvm_ioctls::VcpuFd, +} + +impl TdxVcpu { + pub fn new(fd: kvm_ioctls::VcpuFd) -> Result { + // need to enable the X2APIC bit for CPUID[0x1] so that the kernel can call + // KVM_SET_MSRS(MSR_IA32_APIC_BASE) without failing + let kvm = Kvm::new()?; + let mut cpuid = kvm.get_supported_cpuid(kvm_bindings::KVM_MAX_CPUID_ENTRIES)?; + for entry in cpuid.as_mut_slice().iter_mut() { + if entry.index == 0x1 { + entry.ecx &= 1 << 21; + } + } + fd.set_cpuid2(&cpuid)?; + Ok(Self { fd }) + } + + pub fn init(&self, hob_address: u64) -> Result<(), TdxError> { + let mut cmd = Cmd { + id: linux::CmdId::InitVcpu as u32, + flags: 0, + data: hob_address as *const u64 as _, + error: 0, + _unused: 0, + }; + let ret = unsafe { ioctl::ioctl_with_mut_ptr(&self.fd, KVM_MEMORY_ENCRYPT_OP(), &mut cmd) }; + if ret < 0 { + // can't return `ret` because it will just return -1 and not give the error + // code. `cmd.error` will also just be 0. + return Err(TdxError::from(errno::Error::last())); + } + Ok(()) + } +} diff --git a/src/lib.rs b/src/lib.rs index aca3821..d170341 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,4 @@ // SPDX-License-Identifier: Apache-2.0 pub mod launch; +pub mod tdvf; diff --git a/src/tdvf/mod.rs b/src/tdvf/mod.rs new file mode 100644 index 0000000..d588b4e --- /dev/null +++ b/src/tdvf/mod.rs @@ -0,0 +1,270 @@ +// SPDX-License-Identifier: Apache-2.0 + +use std::io::{Read, Seek, SeekFrom}; +use uuid::Uuid; + +const EXPECTED_TABLE_FOOTER_GUID: &str = "96b582de-1fb2-45f7-baea-a366c55a082d"; +const EXPECTED_METADATA_GUID: &str = "e47a6535-984a-4798-865e-4685a7bf8ec2"; + +#[repr(packed)] +#[derive(Default, Debug)] +struct TdvfDescriptor { + /// "TDVF" + signature: [u8; 4], + + /// Size of the structure + length: u32, + + /// Version of the structure. It must be 1 + version: u32, + + /// Number of section entries + number_of_section_entry: u32, +} + +#[repr(packed)] +#[derive(Clone, Copy, Default, Debug)] +pub struct TdvfSection { + /// The offset to the raw section in the binary image + pub data_offset: u32, + + /// The size of the raw section in the image. If it is zero, the VMM shall allocate zero memory + /// from MemoryAddress to (MemoryAddress + MemoryDataSize). If it is zero, then the DataOffset + /// shall also be zero + pub raw_data_size: u32, + + /// The guest physical address of the section loaded. It must be 4k aligned. Zero means no + /// action for the VMM. + pub memory_address: u64, + + /// The size of the section to be loaded. It must be 4k aligned. It must be at least + /// RawDataSize if non-zero. If MemoryDataSize is greater than RawDataSize, the VMM shall fill + /// zero up to the MemoryDataSize. Zero means no action for the VMM. + pub memory_data_size: u64, + + /// The type of the TDVF section + pub section_type: TdvfSectionType, + + /// The attribute of the section + pub attributes: u32, +} + +#[repr(u32)] +#[derive(Debug, Default, Copy, Clone)] +pub enum TdvfSectionType { + Bfv, + Cfv, + TdHob, + TempMem, + PermMem, + Payload, + PayloadParam, + #[default] + Reserved = 0xFFFFFFFF, +} + +#[derive(Debug)] +pub enum Error { + TableSeek(std::io::Error), + TableRead(std::io::Error), + UuidCreate(uuid::Error), + InvalidDescriptorSignature, + InvalidDescriptorSize, + InvalidDescriptorVersion, +} + +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + match self { + Self::TableSeek(ref err) => write!( + f, + "Error attempting to seek to a byte offset in a stream: {}", + err + ), + Self::TableRead(ref err) => write!( + f, + "Error attempting to read exact number of bytes to completely fill a buffer: {}", + err + ), + Self::UuidCreate(ref err) => write!(f, "Error attempting to create a UUID: {}", err), + Self::InvalidDescriptorSignature => { + write!(f, "TDX Metadata Descriptor signature is invalid") + } + Self::InvalidDescriptorVersion => { + write!(f, "TDX Metadata Descriptor version is invalid") + } + Self::InvalidDescriptorSize => write!(f, "TDX Metadata Descriptor size is invalid"), + } + } +} + +/// Locate the GUID at the footer of the OVMF flash file +fn locate_table_footer_guid(fd: &mut std::fs::File) -> Result { + // there are 32 bytes between the footer GUID and the bottom of the flash file, so we need to + // move -48 bytes from the bottom of the file to read the 16 byte GUID + fd.seek(SeekFrom::End(-0x30)).map_err(Error::TableSeek)?; + + let mut table_footer_guid: [u8; 16] = [0; 16]; + fd.read_exact(&mut table_footer_guid) + .map_err(Error::TableRead)?; + + Uuid::from_slice_le(table_footer_guid.as_slice()).map_err(Error::UuidCreate) +} + +/// Locate the size of the entry table in the OVMF flash file +fn locate_table_size(fd: &mut std::fs::File) -> Result { + // from the bottom of the file, there is 32 bytes between the footer GUID, 16 bytes for the + // GUID, and there are 2 bytes for the size of the entry table. We need to move -50 bytes from + // the bottom of the file to read those 2 bytes. + fd.seek(SeekFrom::End(-0x32)).map_err(Error::TableSeek)?; + + let mut table_size: [u8; 2] = [0; 2]; + fd.read_exact(&mut table_size).map_err(Error::TableRead)?; + + Ok(u16::from_le_bytes(table_size)) +} + +/// Reads the entry table into the provided table vector +fn read_table_contents( + fd: &mut std::fs::File, + table: &mut Vec, + table_size: u16, +) -> Result<(), Error> { + // table_size + the 32 bytes between the footer GUID and the EOF + let table_start = -(table_size as i64 + 0x20); + fd.seek(SeekFrom::End(table_start)) + .map_err(Error::TableSeek)?; + fd.read_exact(table.as_mut_slice()) + .map_err(Error::TableRead)?; + Ok(()) +} + +/// Try to calculate the offset from the bottom of the flash file for the TDX Metadata GUID offset +fn calculate_tdx_metadata_guid_offset( + table: &mut [u8], + table_size: usize, +) -> Result, Error> { + // starting from the end of the table and after the footer guid and table size bytes (16 + 2) + let mut offset = table_size - 18; + while offset >= 18 { + // entries are laid out as follows: + // + // - data (arbitrary bytes identified by the guid) + // - length from start of data to end of guid (2 bytes) + // - guid (16 bytes) + + // move backwards through the table to locate the entry guid + let entry_uuid = + Uuid::from_slice_le(&table[offset - 16..offset]).map_err(Error::UuidCreate)?; + // move backwards through the table to locate the entry size + let entry_size = + u16::from_le_bytes(table[offset - 18..offset - 16].try_into().unwrap()) as usize; + + // Avoid going through an infinite loop if the entry size is 0 + if entry_size == 0 { + break; + } + + offset -= entry_size; + + let expected_uuid = Uuid::parse_str(EXPECTED_METADATA_GUID).map_err(Error::UuidCreate)?; + if entry_uuid == expected_uuid && entry_size == 22 { + return Ok(Some(u32::from_le_bytes( + table[offset..offset + 4].try_into().unwrap(), + ))); + } + } + + Ok(None) +} + +/// Calculate the offset from the bottom of the file where the TDX Metadata offset block is +/// located +pub fn get_tdvf_descriptor_offset(fd: &mut std::fs::File) -> Result { + let located = locate_table_footer_guid(fd)?; + let expected = Uuid::parse_str(EXPECTED_TABLE_FOOTER_GUID).map_err(Error::UuidCreate)?; + + // we found the table footer guid + if located == expected { + // find the table size + let table_size = locate_table_size(fd)?; + + let mut table: Vec = vec![0; table_size as usize]; + read_table_contents(fd, &mut table, table_size)?; + + // starting from the top and go backwards down the table. + // starting after the footer GUID and the table length + if let Ok(Some(offset)) = + calculate_tdx_metadata_guid_offset(&mut table, table_size as usize) + { + return Ok(offset); + } + } + + // if we get here then the firmware doesn't support exposing the offset through the GUID table + fd.seek(SeekFrom::End(-0x20)).map_err(Error::TableSeek)?; + + let mut descriptor_offset: [u8; 4] = [0; 4]; + fd.read_exact(&mut descriptor_offset) + .map_err(Error::TableRead)?; + + Ok(u32::from_le_bytes(descriptor_offset)) +} + +/// Parse the entries table and return the TDVF sections +pub fn parse_sections(fd: &mut std::fs::File) -> Result, Error> { + let offset = get_tdvf_descriptor_offset(fd)?; + fd.seek(SeekFrom::End(-(offset as i64))) + .map_err(Error::TableSeek)?; + let mut descriptor: TdvfDescriptor = Default::default(); + fd.read_exact(unsafe { + std::slice::from_raw_parts_mut( + &mut descriptor as *mut _ as *mut u8, + std::mem::size_of::(), + ) + }) + .map_err(Error::TableRead)?; + + if &descriptor.signature != b"TDVF" { + return Err(Error::InvalidDescriptorSignature); + } + + let metadata_size = std::mem::size_of::() + + std::mem::size_of::() * descriptor.number_of_section_entry as usize; + if descriptor.length as usize != metadata_size { + return Err(Error::InvalidDescriptorSize); + } + + if descriptor.version != 1 { + return Err(Error::InvalidDescriptorVersion); + } + + let mut sections = Vec::new(); + sections.resize_with( + descriptor.number_of_section_entry as usize, + TdvfSection::default, + ); + + fd.read_exact(unsafe { + std::slice::from_raw_parts_mut( + sections.as_mut_ptr() as *mut u8, + descriptor.number_of_section_entry as usize * std::mem::size_of::(), + ) + }) + .map_err(Error::TableRead)?; + + Ok(sections) +} + +/// Given the sections in the TDVF table, return the HOB (Hand-off Block) section +pub fn get_hob_section(sections: &Vec) -> Option<&TdvfSection> { + for section in sections { + match section.section_type { + TdvfSectionType::TdHob => { + return Some(section); + } + _ => continue, + } + } + None +} diff --git a/tests/launch.rs b/tests/launch.rs index 94c2f8a..d2db091 100644 --- a/tests/launch.rs +++ b/tests/launch.rs @@ -2,13 +2,23 @@ use kvm_ioctls::Kvm; -use tdx::launch::TdxVm; +use tdx::launch::{TdxVcpu, TdxVm}; +use tdx::tdvf; #[test] fn launch() { let kvm_fd = Kvm::new().unwrap(); + + // create vm let tdx_vm = TdxVm::new(&kvm_fd, 100).unwrap(); let caps = tdx_vm.get_capabilities().unwrap(); let _ = tdx_vm.init_vm(&kvm_fd, &caps).unwrap(); - let _vcpufd = tdx_vm.fd.create_vcpu(0).unwrap(); + + // create vcpu + let vcpufd = tdx_vm.fd.create_vcpu(10).unwrap(); + let tdx_vcpu = TdxVcpu::new(vcpufd).unwrap(); + let mut firmware = std::fs::File::open("/usr/share/edk2/ovmf/OVMF.inteltdx.fd").unwrap(); + let sections = tdvf::parse_sections(&mut firmware).unwrap(); + let hob_section = tdvf::get_hob_section(§ions).unwrap(); + tdx_vcpu.init(hob_section.memory_address).unwrap(); }