Skip to content

Commit

Permalink
Implement TDVF and INIT_VCPU
Browse files Browse the repository at this point in the history
Adds a module that will provide an API for TDVF (TDX Virtual Firmware).

Adds the API to initialize a VCPU for TDX.

Signed-off-by: Jake Correnti <[email protected]>
  • Loading branch information
jakecorrenti committed May 23, 2024
1 parent e84bc27 commit 9e1a7a1
Show file tree
Hide file tree
Showing 6 changed files with 338 additions and 6 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
5 changes: 3 additions & 2 deletions src/launch/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
53 changes: 51 additions & 2 deletions src/launch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,12 @@ impl TdxVm {
/// Create a new TDX VM with KVM
pub fn new(kvm_fd: &Kvm, max_vcpus: u64) -> Result<Self, TdxError> {
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();

Expand Down Expand Up @@ -261,3 +263,50 @@ pub struct TdxCapabilities {

pub cpuid_configs: Vec<CpuidConfig>,
}

/// 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<TdxVcpu, TdxError> {
// 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(())
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// SPDX-License-Identifier: Apache-2.0

pub mod launch;
pub mod tdvf;
270 changes: 270 additions & 0 deletions src/tdvf/mod.rs
Original file line number Diff line number Diff line change
@@ -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<Uuid, Error> {
// 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<u16, Error> {
// 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<u8>,
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<Option<u32>, 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<u32, Error> {
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<u8> = 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<Vec<TdvfSection>, 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::<TdvfDescriptor>(),
)
})
.map_err(Error::TableRead)?;

if &descriptor.signature != b"TDVF" {
return Err(Error::InvalidDescriptorSignature);
}

let metadata_size = std::mem::size_of::<TdvfDescriptor>()
+ std::mem::size_of::<TdvfSection>() * 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::<TdvfSection>(),
)
})
.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<TdvfSection>) -> Option<&TdvfSection> {
for section in sections {
match section.section_type {
TdvfSectionType::TdHob => {
return Some(section);
}
_ => continue,
}
}
None
}
14 changes: 12 additions & 2 deletions tests/launch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(&sections).unwrap();
tdx_vcpu.init(hob_section.memory_address).unwrap();
}

0 comments on commit 9e1a7a1

Please sign in to comment.