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..cf518cb 100644 --- a/igvm/src/lib.rs +++ b/igvm/src/lib.rs @@ -377,6 +377,15 @@ 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.revision` tuple. + /// This describes the file as a whole, so at most one may be present. + IgvmVersion { + major: u16, + minor: u16, + patch: u16, + revision: u16, + }, #[cfg(feature = "corim")] #[cfg_attr(docsrs, doc(cfg(feature = "corim")))] CorimDocument { @@ -406,6 +415,7 @@ impl IgvmInitializationHeader { IgvmInitializationHeader::PageTableRelocationRegion { .. } => { size_of::() } + IgvmInitializationHeader::IgvmVersion { .. } => size_of::(), #[cfg(feature = "corim")] IgvmInitializationHeader::CorimDocument { .. } => size_of::(), #[cfg(feature = "corim")] @@ -432,6 +442,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 +557,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 +711,23 @@ 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, + revision, + } = read_header(&mut variable_headers)?; + + IgvmInitializationHeader::IgvmVersion { + major, + minor, + patch, + revision, + } + } #[cfg(feature = "corim")] IgvmVariableHeaderType::IGVM_VHT_CORIM_DOCUMENT if length == size_of::() => @@ -762,6 +793,7 @@ impl IgvmInitializationHeader { PageTableRelocationRegion { compatibility_mask, .. } => Some(*compatibility_mask), + IgvmVersion { .. } => None, #[cfg(feature = "corim")] CorimDocument { compatibility_mask, .. @@ -893,6 +925,25 @@ impl IgvmInitializationHeader { variable_headers, ); } + IgvmInitializationHeader::IgvmVersion { + major, + minor, + patch, + revision, + } => { + let info = IGVM_VHS_IGVM_VERSION { + major: *major, + minor: *minor, + patch: *patch, + revision: *revision, + }; + + append_header( + &info, + IgvmVariableHeaderType::IGVM_VHT_IGVM_VERSION, + variable_headers, + ); + } #[cfg(feature = "corim")] IgvmInitializationHeader::CorimDocument { compatibility_mask, @@ -1226,6 +1277,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 +2529,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 +2911,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 +3023,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 +3931,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 +4011,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 +4388,99 @@ mod tests { assert_igvm_equal(&a, &merged); } + #[test] + fn test_merge_version_equal() { + let version = || IgvmInitializationHeader::IgvmVersion { + major: 1, + minor: 2, + patch: 3, + revision: 4, + }; + 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, + revision: 4, + }], + 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, + revision: 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, + revision: 8, + }], + directive_headers: vec![], + }; + + a.merge(b).unwrap(); + + assert!(a + .initialization_headers + .contains(&IgvmInitializationHeader::IgvmVersion { + major: 5, + minor: 6, + patch: 7, + revision: 8, + })); + } + #[test] fn test_merge_simple() { let data1 = vec![1; PAGE_SIZE_4K as usize]; @@ -4699,7 +4890,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 +5223,121 @@ 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, + revision: 4, + }; + + let header = IgvmInitializationHeader::IgvmVersion { + major: 1, + minor: 2, + patch: 3, + revision: 4, + }; + + test_init_variable_header( + header, + 0, + IgvmVariableHeaderType::IGVM_VHT_IGVM_VERSION, + raw_header, + None, + ); + } + + #[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, + revision: 9, + }], + 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, + revision: 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, + revision: 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, + revision: 0, + }, + IgvmInitializationHeader::IgvmVersion { + major: 2, + minor: 0, + patch: 0, + revision: 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/src/serializer.rs b/igvm/src/serializer.rs index e826517..51cd679 100644 --- a/igvm/src/serializer.rs +++ b/igvm/src/serializer.rs @@ -82,6 +82,7 @@ pub struct IgvmSerializer<'a> { extra_init_headers: Vec, extra_directive_headers: Vec, suppressed_corim_masks: Vec, + suppress_igvm_version: bool, } impl<'a> IgvmSerializer<'a> { @@ -106,6 +107,7 @@ impl<'a> IgvmSerializer<'a> { extra_init_headers: Vec::new(), extra_directive_headers: Vec::new(), suppressed_corim_masks: Vec::new(), + suppress_igvm_version: false, }; // Eagerly compute the launch measurement for every supported @@ -453,6 +455,41 @@ impl<'a> IgvmSerializer<'a> { .map_err(|e| Error::CorimGeneration(e.to_string())) } + /// Set the IGVM version header for the file. + /// + /// The version describes the contents of the IGVM file as a whole as a + /// `major.minor.patch.revision` tuple, so at most one version header may + /// be present. Calling this replaces any version header previously staged + /// on the serializer, and any version header present in the base file is + /// suppressed when the output is serialized so that exactly one remains. + pub fn set_igvm_version( + &mut self, + major: u16, + minor: u16, + patch: u16, + revision: u16, + ) -> &IgvmInitializationHeader { + // Drop any version header previously staged on this serializer. + self.extra_init_headers + .retain(|h| !matches!(h, IgvmInitializationHeader::IgvmVersion { .. })); + + // Suppress any version header present in the base file so that only + // the one set here remains after serialization. + self.suppress_igvm_version = true; + + self.extra_init_headers + .push(IgvmInitializationHeader::IgvmVersion { + major, + minor, + patch, + revision, + }); + + self.extra_init_headers + .last() + .expect("just pushed a version header") + } + /// Attach an extra directive header that will be appended when /// serializing. /// @@ -476,24 +513,25 @@ impl<'a> IgvmSerializer<'a> { /// This produces the same binary format as [`IgvmFile::serialize`], /// but with additional initialization headers and directives appended. pub fn serialize(&self, output: &mut Vec) -> Result<(), Error> { - if self.extra_init_headers.is_empty() && self.extra_directive_headers.is_empty() { + if self.extra_init_headers.is_empty() + && self.extra_directive_headers.is_empty() + && !self.suppress_igvm_version + { // Fast path: nothing added, delegate directly. self.file.serialize(output) } else { // Clone the file and append the extra headers so that // the original IgvmFile::serialize handles all the work. let mut file = self.file.clone(); - file.initializations_mut().retain(|h| { - let mask = match h { - IgvmInitializationHeader::CorimDocument { - compatibility_mask, .. - } - | IgvmInitializationHeader::CorimSignature { - compatibility_mask, .. - } => *compatibility_mask, - _ => return true, - }; - !self.suppressed_corim_masks.contains(&mask) + file.initializations_mut().retain(|h| match h { + IgvmInitializationHeader::CorimDocument { + compatibility_mask, .. + } + | IgvmInitializationHeader::CorimSignature { + compatibility_mask, .. + } => !self.suppressed_corim_masks.contains(compatibility_mask), + IgvmInitializationHeader::IgvmVersion { .. } => !self.suppress_igvm_version, + _ => true, }); file.initializations_mut() .extend(self.extra_init_headers.iter().cloned()); @@ -922,6 +960,116 @@ mod tests { assert_eq!(file.directives().len(), directive_count_before); } + #[test] + fn set_igvm_version_appended_once() { + let file = make_vbs_file(); + let mut serializer = IgvmSerializer::new(&file).unwrap(); + + serializer.set_igvm_version(1, 2, 3, 4); + + let mut output = Vec::new(); + serializer.serialize(&mut output).unwrap(); + + let deserialized = IgvmFile::new_from_binary(&output, None).unwrap(); + let versions: Vec<_> = deserialized + .initializations() + .iter() + .filter_map(|h| match h { + IgvmInitializationHeader::IgvmVersion { + major, + minor, + patch, + revision, + } => Some((*major, *minor, *patch, *revision)), + _ => None, + }) + .collect(); + assert_eq!(versions, vec![(1, 2, 3, 4)]); + } + + #[test] + fn set_igvm_version_replaces_existing() { + // Build a file that already declares a version header. + 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, + revision: 0, + }], + vec![new_page_data(0, 1, &[0xAA; PAGE_SIZE_4K as usize])], + ) + .unwrap(); + + let mut serializer = IgvmSerializer::new(&file).unwrap(); + serializer.set_igvm_version(2, 5, 7, 9); + + let mut output = Vec::new(); + serializer.serialize(&mut output).unwrap(); + + let deserialized = IgvmFile::new_from_binary(&output, None).unwrap(); + let versions: Vec<_> = deserialized + .initializations() + .iter() + .filter_map(|h| match h { + IgvmInitializationHeader::IgvmVersion { + major, + minor, + patch, + revision, + } => Some((*major, *minor, *patch, *revision)), + _ => None, + }) + .collect(); + // The base file's version is suppressed; only the new one remains. + assert_eq!(versions, vec![(2, 5, 7, 9)]); + } + + #[test] + fn set_igvm_version_last_call_wins() { + let file = make_vbs_file(); + let mut serializer = IgvmSerializer::new(&file).unwrap(); + + serializer.set_igvm_version(1, 0, 0, 0); + serializer.set_igvm_version(9, 8, 7, 6); + + let mut output = Vec::new(); + serializer.serialize(&mut output).unwrap(); + + let deserialized = IgvmFile::new_from_binary(&output, None).unwrap(); + let versions: Vec<_> = deserialized + .initializations() + .iter() + .filter_map(|h| match h { + IgvmInitializationHeader::IgvmVersion { + major, + minor, + patch, + revision, + } => Some((*major, *minor, *patch, *revision)), + _ => None, + }) + .collect(); + assert_eq!(versions, vec![(9, 8, 7, 6)]); + } + + #[test] + fn file_not_mutated_after_set_igvm_version() { + let file = make_vbs_file(); + let init_count_before = file.initializations().len(); + + let mut serializer = IgvmSerializer::new(&file).unwrap(); + serializer.set_igvm_version(1, 2, 3, 4); + + // The original file should not have been mutated. + assert_eq!(file.initializations().len(), init_count_before); + } + #[test] fn set_corim_signature_preserves_document_and_updates_signature() { let original_document = b"existing-corim-document".to_vec(); 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..8e77019 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.revision` +/// tuple, 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, + /// The revision. + pub revision: 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);