From 913255bd9efabdd8adf62cade9f6c101da737e0b Mon Sep 17 00:00:00 2001 From: SethHollandsworth Date: Thu, 16 Jul 2026 11:55:50 -0400 Subject: [PATCH] adding header for the semantic version of the igvm file --- igvm/src/c_api.rs | 2 + igvm/src/lib.rs | 315 ++++++++++++++++++++++++++++++++- igvm_c/cbindgen_igvm_defs.toml | 1 + igvm_defs/src/lib.rs | 26 +++ 4 files changed, 343 insertions(+), 1 deletion(-) diff --git a/igvm/src/c_api.rs b/igvm/src/c_api.rs index 29d0d81..58ba4fa 100644 --- a/igvm/src/c_api.rs +++ b/igvm/src/c_api.rs @@ -65,6 +65,7 @@ pub enum IgvmResult { IGVMAPI_CORIM_GENERATION = -29, #[cfg(feature = "corim")] IGVMAPI_MEASUREMENT_FAILED = -30, + IGVMAPI_MERGE_IGVM_VERSION_MISMATCH = -31, } type IgvmHandle = i32; @@ -169,6 +170,7 @@ fn translate_error(error: Error) -> IgvmResult { Error::UnsupportedPageSize(_) => IgvmResult::IGVMAPI_UNSUPPORTED_PAGE_SIZE, Error::InvalidFixedHeaderArch(_) => IgvmResult::IGVMAPI_INVALID_FIXED_HEADER_ARCH, Error::MergeRevision => IgvmResult::IGVMAPI_MERGE_REVISION, + Error::MergeIgvmVersionMismatch => IgvmResult::IGVMAPI_MERGE_IGVM_VERSION_MISMATCH, Error::InvalidCcaPolicyCompatibilityMask(_) => { IgvmResult::IGVMAPI_INVALID_CCA_POLICY_COMPATIBILITY_MASK } diff --git a/igvm/src/lib.rs b/igvm/src/lib.rs index 1600737..67c6ca5 100644 --- a/igvm/src/lib.rs +++ b/igvm/src/lib.rs @@ -377,6 +377,10 @@ pub enum IgvmInitializationHeader { vp_index: u16, vtl: Vtl, }, + /// Represents an [`IGVM_VHS_IGVM_VERSION`], describing the version of the + /// contents of this IGVM file as a `major.minor.patch` triple. This + /// describes the file as a whole, so at most one may be present. + IgvmVersion { major: u16, minor: u16, patch: u16 }, #[cfg(feature = "corim")] #[cfg_attr(docsrs, doc(cfg(feature = "corim")))] CorimDocument { @@ -406,6 +410,7 @@ impl IgvmInitializationHeader { IgvmInitializationHeader::PageTableRelocationRegion { .. } => { size_of::() } + IgvmInitializationHeader::IgvmVersion { .. } => size_of::(), #[cfg(feature = "corim")] IgvmInitializationHeader::CorimDocument { .. } => size_of::(), #[cfg(feature = "corim")] @@ -432,6 +437,9 @@ impl IgvmInitializationHeader { IgvmInitializationHeader::PageTableRelocationRegion { .. } => { IgvmVariableHeaderType::IGVM_VHT_PAGE_TABLE_RELOCATION_REGION } + IgvmInitializationHeader::IgvmVersion { .. } => { + IgvmVariableHeaderType::IGVM_VHT_IGVM_VERSION + } #[cfg(feature = "corim")] IgvmInitializationHeader::CorimDocument { .. } => { IgvmVariableHeaderType::IGVM_VHT_CORIM_DOCUMENT @@ -544,6 +552,7 @@ impl IgvmInitializationHeader { Ok(()) } + IgvmInitializationHeader::IgvmVersion { .. } => Ok(()), // TODO: validate CoRIM document has the minimum fields required // described by the corresponding specification for that platform. #[cfg(feature = "corim")] @@ -697,6 +706,26 @@ impl IgvmInitializationHeader { vtl: vtl.try_into().map_err(|_| BinaryHeaderError::InvalidVtl)?, } } + IgvmVariableHeaderType::IGVM_VHT_IGVM_VERSION + if length == size_of::() => + { + let IGVM_VHS_IGVM_VERSION { + major, + minor, + patch, + reserved, + } = read_header(&mut variable_headers)?; + + if reserved != 0 { + return Err(BinaryHeaderError::ReservedNotZero); + } + + IgvmInitializationHeader::IgvmVersion { + major, + minor, + patch, + } + } #[cfg(feature = "corim")] IgvmVariableHeaderType::IGVM_VHT_CORIM_DOCUMENT if length == size_of::() => @@ -762,6 +791,7 @@ impl IgvmInitializationHeader { PageTableRelocationRegion { compatibility_mask, .. } => Some(*compatibility_mask), + IgvmVersion { .. } => None, #[cfg(feature = "corim")] CorimDocument { compatibility_mask, .. @@ -893,6 +923,24 @@ impl IgvmInitializationHeader { variable_headers, ); } + IgvmInitializationHeader::IgvmVersion { + major, + minor, + patch, + } => { + let info = IGVM_VHS_IGVM_VERSION { + major: *major, + minor: *minor, + patch: *patch, + reserved: 0, + }; + + append_header( + &info, + IgvmVariableHeaderType::IGVM_VHT_IGVM_VERSION, + variable_headers, + ); + } #[cfg(feature = "corim")] IgvmInitializationHeader::CorimDocument { compatibility_mask, @@ -1226,6 +1274,8 @@ pub enum BinaryHeaderError { UnsupportedX64Register(#[from] registers::UnsupportedRegister), #[error("unsupported AArch64 register")] UnsupportedAArch64Register(#[from] registers::UnsupportedRegister), + #[error("multiple igvm version headers in a single file")] + MultipleIgvmVersions, #[cfg(feature = "corim")] #[error("multiple corim documents for a given compatibility mask {0:x}")] MultipleCorimDocuments(u32), @@ -2476,6 +2526,8 @@ pub enum Error { InvalidFixedHeaderArch(u32), #[error("merged igvm files are not the same revision")] MergeRevision, + #[error("merged igvm files declare mismatched versions")] + MergeIgvmVersionMismatch, #[cfg(feature = "corim")] #[error("CoRIM generation failed: {0}")] CorimGeneration(String), @@ -2856,6 +2908,10 @@ impl IgvmFile { Ok(()) }; + // A version header describes the file as a whole, so at most one is + // allowed. + let mut igvm_version_seen = false; + // Track which compatibility masks have had a corim document header, // only one allowed per compatibility mask. #[cfg(feature = "corim")] @@ -2964,6 +3020,16 @@ impl IgvmFile { } // TODO: validate SNP policy compatibility mask specifies SNP IgvmInitializationHeader::GuestPolicy { .. } => {} + IgvmInitializationHeader::IgvmVersion { .. } => { + // A version header describes the file as a whole, so at + // most one is allowed. + if igvm_version_seen { + return Err(Error::InvalidBinaryInitializationHeader( + BinaryHeaderError::MultipleIgvmVersions, + )); + } + igvm_version_seen = true; + } #[cfg(feature = "corim")] IgvmInitializationHeader::CorimDocument { compatibility_mask, .. @@ -3862,6 +3928,9 @@ impl IgvmFile { IgvmInitializationHeader::PageTableRelocationRegion { compatibility_mask, .. } => fixup_mask(compatibility_mask), + // A version header describes the file as a whole and has no + // compatibility mask to fix up. + IgvmInitializationHeader::IgvmVersion { .. } => {} #[cfg(feature = "corim")] IgvmInitializationHeader::CorimDocument { compatibility_mask, .. @@ -3939,6 +4008,32 @@ impl IgvmFile { } } + // A version header describes the file as a whole, so the merged result + // may contain at most one. If both files declare a version, they must + // match exactly; only a single copy is retained. + let self_version = self + .initialization_headers + .iter() + .find(|h| matches!(h, IgvmInitializationHeader::IgvmVersion { .. })) + .cloned(); + if let Some(self_version) = self_version { + let mut mismatch = false; + other.initialization_headers.retain(|h| { + if matches!(h, IgvmInitializationHeader::IgvmVersion { .. }) { + if *h != self_version { + mismatch = true; + } + // Drop `other`'s version header; `self` keeps its own. + false + } else { + true + } + }); + if mismatch { + return Err(Error::MergeIgvmVersionMismatch); + } + } + // Non-directive headers are just appeneded to the current file. self.platform_headers.append(&mut other.platform_headers); self.initialization_headers @@ -4290,6 +4385,94 @@ mod tests { assert_igvm_equal(&a, &merged); } + #[test] + fn test_merge_version_equal() { + let version = || IgvmInitializationHeader::IgvmVersion { + major: 1, + minor: 2, + patch: 3, + }; + let mut a = IgvmFile { + revision: IgvmRevision::V1, + platform_headers: vec![new_platform(0x1, IgvmPlatformType::VSM_ISOLATION)], + initialization_headers: vec![version()], + directive_headers: vec![], + }; + let b = IgvmFile { + revision: IgvmRevision::V1, + platform_headers: vec![new_platform(0x1, IgvmPlatformType::SEV_SNP)], + initialization_headers: vec![version()], + directive_headers: vec![], + }; + + a.merge(b).unwrap(); + + // Only a single version header remains after merging. + assert_eq!( + a.initialization_headers + .iter() + .filter(|h| matches!(h, IgvmInitializationHeader::IgvmVersion { .. })) + .count(), + 1 + ); + } + + #[test] + fn test_merge_version_mismatch() { + let mut a = IgvmFile { + revision: IgvmRevision::V1, + platform_headers: vec![new_platform(0x1, IgvmPlatformType::VSM_ISOLATION)], + initialization_headers: vec![IgvmInitializationHeader::IgvmVersion { + major: 1, + minor: 2, + patch: 3, + }], + directive_headers: vec![], + }; + let b = IgvmFile { + revision: IgvmRevision::V1, + platform_headers: vec![new_platform(0x1, IgvmPlatformType::SEV_SNP)], + initialization_headers: vec![IgvmInitializationHeader::IgvmVersion { + major: 2, + minor: 0, + patch: 0, + }], + directive_headers: vec![], + }; + + assert!(matches!(a.merge(b), Err(Error::MergeIgvmVersionMismatch))); + } + + #[test] + fn test_merge_version_one_side() { + let mut a = IgvmFile { + revision: IgvmRevision::V1, + platform_headers: vec![new_platform(0x1, IgvmPlatformType::VSM_ISOLATION)], + initialization_headers: vec![], + directive_headers: vec![], + }; + let b = IgvmFile { + revision: IgvmRevision::V1, + platform_headers: vec![new_platform(0x1, IgvmPlatformType::SEV_SNP)], + initialization_headers: vec![IgvmInitializationHeader::IgvmVersion { + major: 5, + minor: 6, + patch: 7, + }], + directive_headers: vec![], + }; + + a.merge(b).unwrap(); + + assert!(a + .initialization_headers + .contains(&IgvmInitializationHeader::IgvmVersion { + major: 5, + minor: 6, + patch: 7, + })); + } + #[test] fn test_merge_simple() { let data1 = vec![1; PAGE_SIZE_4K as usize]; @@ -4699,7 +4882,6 @@ mod tests { /// Test an initialization variable header matches the supplied args. Also /// tests round-trip serialization/deserialization. - #[cfg(feature = "corim")] fn test_init_variable_header( header: IgvmInitializationHeader, file_data_offset: u32, @@ -5033,6 +5215,137 @@ mod tests { assert_reserved_not_zero(raw_header); } + #[test] + fn test_igvm_version() { + let raw_header = IGVM_VHS_IGVM_VERSION { + major: 1, + minor: 2, + patch: 3, + reserved: 0, + }; + + let header = IgvmInitializationHeader::IgvmVersion { + major: 1, + minor: 2, + patch: 3, + }; + + test_init_variable_header( + header, + 0, + IgvmVariableHeaderType::IGVM_VHT_IGVM_VERSION, + raw_header, + None, + ); + } + + #[test] + fn test_igvm_version_reserved_must_be_zero() { + let raw_header = IGVM_VHS_IGVM_VERSION { + major: 1, + minor: 2, + patch: 3, + reserved: 1, + }; + + let mut binary_header = Vec::new(); + append_header( + &raw_header, + IgvmVariableHeaderType::IGVM_VHT_IGVM_VERSION, + &mut binary_header, + ); + + assert!(matches!( + IgvmInitializationHeader::new_from_binary_split(&binary_header, &[], 0), + Err(BinaryHeaderError::ReservedNotZero) + )); + } + + #[test] + fn test_igvm_version_roundtrip() { + let data = vec![1; PAGE_SIZE_4K as usize]; + + let file = IgvmFile { + revision: IgvmRevision::V2 { + arch: Arch::X64, + page_size: PAGE_SIZE_4K as u32, + }, + platform_headers: vec![new_platform(0x1, IgvmPlatformType::VSM_ISOLATION)], + initialization_headers: vec![IgvmInitializationHeader::IgvmVersion { + major: 4, + minor: 0, + patch: 7, + }], + directive_headers: vec![new_page_data(0, 1, &data)], + }; + + let mut binary_file = Vec::new(); + file.serialize(&mut binary_file).unwrap(); + + let deserialized = IgvmFile::new_from_binary(&binary_file, None).unwrap(); + assert_igvm_equal(&file, &deserialized); + } + + #[test] + fn test_igvm_version_no_compatibility_mask() { + // A version header describes the whole file and has no associated + // compatibility mask. + let header = IgvmInitializationHeader::IgvmVersion { + major: 1, + minor: 0, + patch: 0, + }; + assert_eq!(header.compatibility_mask(), None); + } + + #[test] + fn test_igvm_version_single_valid() { + let file = IgvmFile::new( + IgvmRevision::V2 { + arch: Arch::X64, + page_size: PAGE_SIZE_4K as u32, + }, + vec![new_platform(0x1, IgvmPlatformType::VSM_ISOLATION)], + vec![IgvmInitializationHeader::IgvmVersion { + major: 1, + minor: 0, + patch: 0, + }], + vec![], + ); + assert!(file.is_ok()); + } + + #[test] + fn test_igvm_version_multiple_error() { + let file = IgvmFile::new( + IgvmRevision::V2 { + arch: Arch::X64, + page_size: PAGE_SIZE_4K as u32, + }, + vec![new_platform(0x1, IgvmPlatformType::VSM_ISOLATION)], + vec![ + IgvmInitializationHeader::IgvmVersion { + major: 1, + minor: 0, + patch: 0, + }, + IgvmInitializationHeader::IgvmVersion { + major: 2, + minor: 0, + patch: 0, + }, + ], + vec![], + ); + assert!(matches!( + file, + Err(Error::InvalidBinaryInitializationHeader( + BinaryHeaderError::MultipleIgvmVersions + )) + )); + } + #[test] fn test_cca_policy_requires_cca_platform() { let file = IgvmFile::new( diff --git a/igvm_c/cbindgen_igvm_defs.toml b/igvm_c/cbindgen_igvm_defs.toml index 1a08efa..1d60fbc 100644 --- a/igvm_c/cbindgen_igvm_defs.toml +++ b/igvm_c/cbindgen_igvm_defs.toml @@ -57,6 +57,7 @@ include = ["IGVM_FIXED_HEADER", "IGVM_VHS_SUPPORTED_PLATFORM", "IGVM_VHS_GUEST_POLICY", "IGVM_VHS_CCA_POLICY", + "IGVM_VHS_IGVM_VERSION", "CCA_POLICY_ATTR_DEBUG", "CCA_POLICY_ATTR_MEC", "GuestPolicy", diff --git a/igvm_defs/src/lib.rs b/igvm_defs/src/lib.rs index b122c40..8d76448 100644 --- a/igvm_defs/src/lib.rs +++ b/igvm_defs/src/lib.rs @@ -289,6 +289,9 @@ pub enum IgvmVariableHeaderType { IGVM_VHT_CORIM_SIGNATURE = 0x105, /// A CCA policy structure described by [`IGVM_VHS_CCA_POLICY`]. IGVM_VHT_CCA_POLICY = 0x106, + /// The version of the contents of the IGVM file, described by + /// [`IGVM_VHS_IGVM_VERSION`]. + IGVM_VHT_IGVM_VERSION = 0x107, // These are IGVM_VHT_RANGE_DIRECTIVE structures. /// A parameter area structure described by [`IGVM_VHS_PARAMETER_AREA`]. @@ -1404,3 +1407,26 @@ pub struct IGVM_VHS_CORIM_DATA { /// Reserved. pub reserved: u32, } + +/// The version of the contents described by an IGVM file, described by +/// [`IgvmVariableHeaderType::IGVM_VHT_IGVM_VERSION`]. +/// +/// This describes the file as a whole, so at most one may be present in an +/// IGVM file. The version is expressed as a `major.minor.patch` triple, +/// following semantic versioning conventions. +#[repr(C)] +#[derive(Copy, Clone, Debug, IntoBytes, Immutable, KnownLayout, FromBytes, PartialEq, Eq)] +pub struct IGVM_VHS_IGVM_VERSION { + /// The major version. + pub major: u16, + /// The minor version. + pub minor: u16, + /// The patch version. + pub patch: u16, + /// Reserved, must be zero. + pub reserved: u16, +} + +// The variable header section must remain 8-byte aligned, so this structure's +// size must be a multiple of 8. +const_assert_eq!(size_of::(), 8);