Skip to main content

igvmfilegen_config/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Configuration for generating IGVM files. These are deserialized from a JSON
5//! manifest file used by the file builder.
6
7#![expect(missing_docs)]
8#![forbid(unsafe_code)]
9
10use igvm_defs::PAGE_SIZE_4K;
11use page_table::IdentityMapSize;
12use page_table::x64::X64_PTE_ADDRESS_BIT_RANGE;
13use product_policy::ProductPolicy;
14use serde::Deserialize;
15use serde::Serialize;
16use std::collections::HashMap;
17use std::ffi::CString;
18use std::path::PathBuf;
19
20/// The UEFI config type to pass to the UEFI loader.
21#[derive(Serialize, Deserialize, Debug, Copy, Clone)]
22#[serde(rename_all = "snake_case")]
23pub enum UefiConfigType {
24    /// No UEFI config set at load time.
25    None,
26    /// UEFI config is specified via IGVM parameters.
27    Igvm,
28}
29
30/// The interrupt injection type that should be used for VMPL0 on SNP.
31#[derive(Serialize, Deserialize, Debug)]
32#[serde(rename_all = "snake_case")]
33pub enum SnpInjectionType {
34    /// Normal injection.
35    Normal,
36    /// Restricted injection.
37    Restricted,
38}
39
40/// Secure AVIC type.
41#[derive(Serialize, Default, Deserialize, Debug)]
42#[serde(rename_all = "snake_case")]
43pub enum SecureAvicType {
44    /// Offload AVIC to the hardware.
45    Enabled,
46    /// The paravisor emulates APIC.
47    #[default]
48    Disabled,
49}
50
51/// The isolation type that should be used for the loader.
52#[derive(Serialize, Deserialize, Debug)]
53#[serde(rename_all = "snake_case")]
54pub enum ConfigIsolationType {
55    /// No isolation is present.
56    None,
57    /// Hypervisor based isolation (VBS) is present.
58    Vbs {
59        /// Boolean representing if the guest allows debugging
60        enable_debug: bool,
61    },
62    /// AMD SEV-SNP.
63    Snp {
64        /// The optional shared GPA boundary to configure for the guest. A
65        /// `None` value represents a guest that no shared GPA boundary is to be
66        /// configured.
67        shared_gpa_boundary_bits: Option<u8>,
68        /// The SEV-SNP policy for the guest.
69        policy: u64,
70        /// Boolean representing if the guest allows debugging
71        enable_debug: bool,
72        /// The interrupt injection type to use for the highest vmpl.
73        injection_type: SnpInjectionType,
74        /// Secure AVIC
75        #[serde(default)]
76        secure_avic: SecureAvicType,
77    },
78    /// Intel TDX.
79    Tdx {
80        /// Boolean representing if the guest allows debugging
81        enable_debug: bool,
82        /// Boolean representing if the guest is disallowed from handling
83        /// virtualization exceptions
84        sept_ve_disable: bool,
85    },
86}
87
88/// Configuration on what to load.
89#[derive(Serialize, Deserialize, Debug)]
90#[serde(rename_all = "snake_case")]
91pub enum Image {
92    /// Load nothing.
93    None,
94    /// Load UEFI.
95    Uefi { config_type: UefiConfigType },
96    /// Load the OpenHCL paravisor.
97    Openhcl {
98        /// The paravisor kernel command line.
99        #[serde(default)]
100        command_line: String,
101        /// If false, the host may provide additional kernel command line
102        /// parameters at runtime.
103        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
104        static_command_line: bool,
105        /// The base page number for paravisor memory. None means relocation is used.
106        #[serde(skip_serializing_if = "Option::is_none")]
107        memory_page_base: Option<u64>,
108        /// The number of pages for paravisor memory.
109        memory_page_count: u64,
110        /// Include the UEFI firmware for loading into the guest.
111        #[serde(default, skip_serializing_if = "std::ops::Not::not")]
112        uefi: bool,
113        /// Include the Linux kernel for loading into the guest.
114        #[serde(skip_serializing_if = "Option::is_none")]
115        linux: Option<LinuxImage>,
116        /// Optional measured product policy. When `Some`, the IGVM
117        /// build emits the policy into the measured VTL2 config region.
118        /// The manifest schema is the wire schema; see
119        /// [`product_policy`]. We don't gate this property behind a feature
120        /// to avoid having multiple igvmfilegen tools with and without the feature.
121        #[serde(default, skip_serializing_if = "Option::is_none")]
122        product_policy: Option<ProductPolicy>,
123    },
124    /// Load the Linux kernel.
125    /// TODO: Currently, this only works with underhill.
126    Linux(LinuxImage),
127    /// Load Linux directly into a self-contained SNP guest.
128    SnpLinuxDirect {
129        /// The Linux image to load.
130        linux: LinuxImage,
131        /// The number of virtual processors in the guest.
132        processor_count: u32,
133        /// The number of pages in the guest.
134        memory_page_count: u64,
135        /// The page-table address bit used as the SNP encryption bit.
136        c_bit_position: u8,
137    },
138}
139
140#[derive(Serialize, Deserialize, Debug)]
141#[serde(rename_all = "snake_case")]
142pub struct LinuxImage {
143    /// Load with an initrd.
144    pub use_initrd: bool,
145    /// The command line to boot the kernel with.
146    pub command_line: CString,
147}
148
149impl Image {
150    /// Get the required resources for this image config.
151    pub fn required_resources(&self) -> Vec<ResourceType> {
152        match *self {
153            Image::None => vec![],
154            Image::Uefi { .. } => vec![ResourceType::Uefi],
155            Image::Openhcl {
156                uefi, ref linux, ..
157            } => [
158                ResourceType::UnderhillKernel,
159                ResourceType::OpenhclBoot,
160                ResourceType::UnderhillInitrd,
161            ]
162            .into_iter()
163            .chain(if uefi { Some(ResourceType::Uefi) } else { None })
164            .chain(linux.iter().flat_map(|linux| linux.required_resources()))
165            .collect(),
166            Image::Linux(ref linux) => linux.required_resources(),
167            Image::SnpLinuxDirect { ref linux, .. } => linux
168                .required_resources()
169                .into_iter()
170                .chain([ResourceType::SnpBootshim])
171                .collect(),
172        }
173    }
174
175    /// Validate constraints intrinsic to this image configuration.
176    pub fn validate(&self) -> Result<(), ImageValidationError> {
177        if let Image::SnpLinuxDirect {
178            processor_count,
179            memory_page_count,
180            c_bit_position,
181            ..
182        } = *self
183        {
184            if processor_count == 0 {
185                return Err(ImageValidationError::ZeroProcessorCount);
186            }
187            if memory_page_count == 0 {
188                return Err(ImageValidationError::ZeroMemoryPageCount);
189            }
190
191            let memory_byte_count = memory_page_count
192                .checked_mul(PAGE_SIZE_4K)
193                .ok_or(ImageValidationError::MemoryByteCountTooLarge { memory_page_count })?;
194            if u32::try_from(memory_byte_count).is_err() {
195                return Err(ImageValidationError::MemoryByteCountTooLarge { memory_page_count });
196            }
197
198            // The Linux startup page tables identity-map the lower 4 GiB and
199            // set the C-bit by ORing it into each PTE. The bit must therefore
200            // be outside that mapped address range as well as inside the
201            // architectural PTE address field.
202            let valid_c_bit = X64_PTE_ADDRESS_BIT_RANGE.contains(&c_bit_position)
203                && (1u64 << c_bit_position) >= IdentityMapSize::Size4Gb.address_space_size();
204            if !valid_c_bit {
205                return Err(ImageValidationError::InvalidCBitPosition { c_bit_position });
206            }
207        }
208
209        Ok(())
210    }
211}
212
213/// Error returned when an image configuration contains invalid intrinsic fields.
214#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)]
215pub enum ImageValidationError {
216    /// The image requests no virtual processors.
217    #[error("processor_count must be nonzero")]
218    ZeroProcessorCount,
219    /// The image requests no guest memory.
220    #[error("memory_page_count must be nonzero")]
221    ZeroMemoryPageCount,
222    /// The requested memory byte count cannot be represented by an IGVM
223    /// required-memory directive.
224    #[error("memory_page_count {memory_page_count} has a byte count that does not fit in u32")]
225    MemoryByteCountTooLarge {
226        /// The invalid number of 4-KiB pages.
227        memory_page_count: u64,
228    },
229    /// The SNP C-bit overlaps the identity map or lies outside the x64 PTE
230    /// address field.
231    #[error(
232        "c_bit_position {c_bit_position} overlaps the 4-GiB identity map or lies outside the x64 PTE address field"
233    )]
234    InvalidCBitPosition {
235        /// The invalid C-bit position.
236        c_bit_position: u8,
237    },
238}
239
240impl LinuxImage {
241    fn required_resources(&self) -> Vec<ResourceType> {
242        [ResourceType::LinuxKernel]
243            .into_iter()
244            .chain(if self.use_initrd {
245                Some(ResourceType::LinuxInitrd)
246            } else {
247                None
248            })
249            .collect()
250    }
251}
252
253/// The config used to describe an initial guest context to be generated by the
254/// tool.
255#[derive(Serialize, Deserialize, Debug)]
256pub struct GuestConfig {
257    /// The SVN of this guest.
258    pub guest_svn: u32,
259    /// The maximum VTL to be enabled for the guest.
260    pub max_vtl: u8,
261    /// The isolation type to be used for the guest.
262    pub isolation_type: ConfigIsolationType,
263    /// The image to load into the guest.
264    pub image: Image,
265}
266
267/// The architecture of the igvm file.
268#[derive(Serialize, Deserialize, Debug)]
269#[serde(rename_all = "snake_case")]
270pub enum GuestArch {
271    /// x64
272    X64,
273    /// AArch64 aka ARM64
274    Aarch64,
275}
276
277/// The config used to describe a multi-architecture IGVM file containing
278/// multiple guests.
279#[derive(Serialize, Deserialize, Debug)]
280#[serde(rename_all = "snake_case")]
281pub struct Config {
282    /// The architecture of the igvm file.
283    pub guest_arch: GuestArch,
284    /// The array of guest configs to be used to generate a single IGVM file.
285    pub guest_configs: Vec<GuestConfig>,
286}
287
288impl Config {
289    /// Get a vec representing the required resources for this config.
290    pub fn required_resources(&self) -> Vec<ResourceType> {
291        let mut resources = vec![];
292        for guest_config in &self.guest_configs {
293            resources.extend(guest_config.image.required_resources());
294        }
295        resources
296    }
297}
298
299#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, Hash, PartialOrd, Ord)]
300#[serde(rename_all = "snake_case")]
301pub enum ResourceType {
302    Uefi,
303    UnderhillKernel,
304    OpenhclBoot,
305    UnderhillInitrd,
306    UnderhillSidecar,
307    LinuxKernel,
308    LinuxInitrd,
309    SnpBootshim,
310}
311
312/// Resources used by igvmfilegen to generate IGVM files. These are generated by
313/// build tooling and not checked into the repo.
314#[derive(Serialize, Deserialize, Debug)]
315#[serde(rename_all = "snake_case")]
316pub struct Resources {
317    /// The set of resources to use to generate IGVM files. These paths must be
318    /// absolute.
319    #[serde(deserialize_with = "parse::resources")]
320    resources: HashMap<ResourceType, PathBuf>,
321}
322
323mod parse {
324    use super::*;
325    use serde::Deserialize;
326    use serde::Deserializer;
327    use std::collections::HashMap;
328
329    pub fn resources<'de, D: Deserializer<'de>>(
330        d: D,
331    ) -> Result<HashMap<ResourceType, PathBuf>, D::Error> {
332        let resources: HashMap<ResourceType, PathBuf> = Deserialize::deserialize(d)?;
333
334        for (resource, path) in &resources {
335            if !path.is_absolute() {
336                return Err(serde::de::Error::custom(AbsolutePathError(
337                    *resource,
338                    path.clone(),
339                )));
340            }
341        }
342
343        Ok(resources)
344    }
345}
346
347/// Error returned when required resources are missing.
348#[derive(Debug)]
349pub struct MissingResourcesError(pub Vec<ResourceType>);
350
351impl std::fmt::Display for MissingResourcesError {
352    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
353        write!(f, "missing resources: {:?}", self.0)
354    }
355}
356
357impl std::error::Error for MissingResourcesError {}
358
359/// Error returned when a resource is not an absolute path.
360#[derive(Debug)]
361pub struct AbsolutePathError(ResourceType, PathBuf);
362
363impl std::fmt::Display for AbsolutePathError {
364    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
365        write!(
366            f,
367            "resource {:?} path is not absolute: {:?}",
368            self.0, self.1
369        )
370    }
371}
372
373impl std::error::Error for AbsolutePathError {}
374
375impl Resources {
376    /// Create a new set of resources. Returns an error if any of the paths are
377    /// not absolute.
378    pub fn new(resources: HashMap<ResourceType, PathBuf>) -> Result<Self, AbsolutePathError> {
379        for (resource, path) in &resources {
380            if !path.is_absolute() {
381                return Err(AbsolutePathError(*resource, path.clone()));
382            }
383        }
384
385        Ok(Resources { resources })
386    }
387
388    /// Get the resources for this set.
389    pub fn resources(&self) -> &HashMap<ResourceType, PathBuf> {
390        &self.resources
391    }
392
393    /// Get the resource path for a given resource type.
394    pub fn get(&self, resource: ResourceType) -> Option<&PathBuf> {
395        self.resources.get(&resource)
396    }
397
398    /// Check that the required resources are present. On error, returns which
399    /// resources are missing.
400    pub fn check_required(&self, required: &[ResourceType]) -> Result<(), MissingResourcesError> {
401        let mut missing = vec![];
402        for resource in required {
403            if !self.resources.contains_key(resource) {
404                missing.push(*resource);
405            }
406        }
407
408        if missing.is_empty() {
409            Ok(())
410        } else {
411            Err(MissingResourcesError(missing))
412        }
413    }
414}
415
416#[cfg(test)]
417mod test {
418    use super::*;
419
420    fn snp_linux_direct_image(
421        use_initrd: bool,
422        processor_count: u32,
423        memory_page_count: u64,
424        c_bit_position: u8,
425    ) -> Image {
426        Image::SnpLinuxDirect {
427            linux: LinuxImage {
428                use_initrd,
429                command_line: CString::new("console=ttyS0").unwrap(),
430            },
431            processor_count,
432            memory_page_count,
433            c_bit_position,
434        }
435    }
436
437    #[test]
438    fn parse_snp_linux_direct_manifest() {
439        let config: Config =
440            serde_json::from_str(include_str!("../../manifests/snp-linux-direct.json")).unwrap();
441
442        assert!(matches!(config.guest_arch, GuestArch::X64));
443        let [guest] = config.guest_configs.as_slice() else {
444            panic!("expected one guest config");
445        };
446        assert_eq!(guest.guest_svn, 1);
447        assert_eq!(guest.max_vtl, 0);
448        match &guest.isolation_type {
449            ConfigIsolationType::Snp {
450                shared_gpa_boundary_bits,
451                policy,
452                enable_debug,
453                injection_type,
454                secure_avic,
455            } => {
456                assert_eq!(*shared_gpa_boundary_bits, None);
457                assert_eq!(*policy, 196608);
458                assert!(*enable_debug);
459                assert!(matches!(injection_type, SnpInjectionType::Normal));
460                assert!(matches!(secure_avic, SecureAvicType::Disabled));
461            }
462
463            isolation_type => panic!("unexpected isolation type: {isolation_type:?}"),
464        }
465        match &guest.image {
466            Image::SnpLinuxDirect {
467                linux,
468                processor_count,
469                memory_page_count,
470                c_bit_position,
471            } => {
472                assert!(linux.use_initrd);
473                assert_eq!(
474                    linux.command_line.as_bytes(),
475                    b"console=ttyS0 earlyprintk=serial earlycon panic=-1"
476                );
477                assert_eq!(*processor_count, 1);
478                assert_eq!(*memory_page_count, 40960);
479                assert_eq!(*c_bit_position, 51);
480                guest.image.validate().unwrap();
481            }
482            image => panic!("unexpected image: {image:?}"),
483        }
484    }
485
486    #[test]
487    fn parse_multi_vp_snp_linux_direct_manifest() {
488        let config: Config = serde_json::from_str(include_str!(
489            "../../manifests/snp-linux-direct-multi-vp.json"
490        ))
491        .unwrap();
492        let [guest] = config.guest_configs.as_slice() else {
493            panic!("expected one guest config");
494        };
495        let Image::SnpLinuxDirect {
496            processor_count, ..
497        } = &guest.image
498        else {
499            panic!("expected SNP Linux-direct image");
500        };
501        assert_eq!(*processor_count, 2);
502        guest.image.validate().unwrap();
503    }
504
505    #[test]
506    fn parse_restricted_snp_linux_direct_manifest() {
507        let config: Config = serde_json::from_str(include_str!(
508            "../../manifests/snp-linux-direct-restricted.json"
509        ))
510        .unwrap();
511        let [guest] = config.guest_configs.as_slice() else {
512            panic!("expected one guest config");
513        };
514        assert!(matches!(
515            guest.isolation_type,
516            ConfigIsolationType::Snp {
517                injection_type: SnpInjectionType::Restricted,
518                ..
519            }
520        ));
521        guest.image.validate().unwrap();
522    }
523
524    #[test]
525    fn snp_linux_direct_required_resources_with_initrd() {
526        let image = snp_linux_direct_image(true, 1, 40960, 51);
527
528        assert_eq!(
529            image.required_resources(),
530            vec![
531                ResourceType::LinuxKernel,
532                ResourceType::LinuxInitrd,
533                ResourceType::SnpBootshim,
534            ]
535        );
536    }
537
538    #[test]
539    fn snp_linux_direct_required_resources_without_initrd() {
540        let image = snp_linux_direct_image(false, 1, 40960, 51);
541
542        assert_eq!(
543            image.required_resources(),
544            vec![ResourceType::LinuxKernel, ResourceType::SnpBootshim]
545        );
546    }
547
548    #[test]
549    fn snp_linux_direct_requires_bootshim_resource() {
550        let image = snp_linux_direct_image(false, 1, 40960, 51);
551        let resources = Resources {
552            resources: [(ResourceType::LinuxKernel, PathBuf::from("/kernel"))]
553                .into_iter()
554                .collect(),
555        };
556
557        assert_eq!(
558            resources
559                .check_required(&image.required_resources())
560                .unwrap_err()
561                .0,
562            [ResourceType::SnpBootshim]
563        );
564    }
565
566    #[test]
567    fn snp_linux_direct_rejects_zero_memory() {
568        let image = snp_linux_direct_image(false, 1, 0, 51);
569
570        assert_eq!(
571            image.validate(),
572            Err(ImageValidationError::ZeroMemoryPageCount)
573        );
574    }
575
576    #[test]
577    fn snp_linux_direct_rejects_oversized_memory() {
578        let memory_page_count = u64::from(u32::MAX) / PAGE_SIZE_4K + 1;
579        let image = snp_linux_direct_image(false, 1, memory_page_count, 51);
580
581        assert_eq!(
582            image.validate(),
583            Err(ImageValidationError::MemoryByteCountTooLarge { memory_page_count })
584        );
585    }
586
587    #[test]
588    fn snp_linux_direct_rejects_invalid_c_bit_positions() {
589        for c_bit_position in [11, 31, 52] {
590            let image = snp_linux_direct_image(false, 1, 40960, c_bit_position);
591
592            assert_eq!(
593                image.validate(),
594                Err(ImageValidationError::InvalidCBitPosition { c_bit_position })
595            );
596        }
597
598        snp_linux_direct_image(false, 1, 40960, 32)
599            .validate()
600            .unwrap();
601    }
602
603    #[test]
604    fn snp_linux_direct_rejects_zero_processors() {
605        let image = snp_linux_direct_image(false, 0, 40960, 51);
606
607        assert_eq!(
608            image.validate(),
609            Err(ImageValidationError::ZeroProcessorCount)
610        );
611    }
612
613    #[test]
614    fn non_absolute_path_new() {
615        let mut resources = HashMap::new();
616        resources.insert(ResourceType::Uefi, PathBuf::from("./uefi"));
617        let result = Resources::new(resources);
618        assert!(result.is_err());
619    }
620
621    #[test]
622    fn parse_non_absolute_path() {
623        let resources = r#"{"uefi":"./uefi"}"#;
624        let result: Result<Resources, _> = serde_json::from_str(resources);
625        assert!(result.is_err());
626    }
627
628    #[test]
629    fn missing_resources() {
630        let resources = Resources {
631            resources: HashMap::new(),
632        };
633        let required = vec![ResourceType::Uefi];
634        let result = resources.check_required(&required);
635        assert!(result.is_err());
636    }
637
638    #[test]
639    fn openhcl_image_without_product_policy_round_trips() {
640        // Older manifests omit the field; serialization must too.
641        let json = r#"{"openhcl":{"command_line":"","memory_page_count":10,"uefi":true}}"#;
642        let parsed: Image = serde_json::from_str(json).unwrap();
643        match &parsed {
644            Image::Openhcl { product_policy, .. } => assert!(product_policy.is_none()),
645            other => panic!("unexpected parse: {other:?}"),
646        }
647        let reserialized = serde_json::to_string(&parsed).unwrap();
648        assert!(
649            !reserialized.contains("product_policy"),
650            "policy field should be omitted when None: {reserialized}"
651        );
652    }
653
654    #[test]
655    fn openhcl_image_with_sivm_product_policy_deserializes() {
656        let json = r#"{
657            "openhcl": {
658                "command_line": "",
659                "memory_page_count": 10,
660                "uefi": true,
661                "product_policy": {
662                    "sivm": {
663                        "require_ephemeral_vmgs": true,
664                        "require_secure_boot": true,
665                        "require_secure_boot_vars": true,
666                        "require_bcd_integrity": true,
667                        "custom_uefi_json": "ZGVhZGJlZWY="
668                    }
669                }
670            }
671        }"#;
672        let parsed: Image = serde_json::from_str(json).unwrap();
673        match parsed {
674            Image::Openhcl {
675                product_policy: Some(policy),
676                ..
677            } => match policy {
678                ProductPolicy::Sivm(p) => {
679                    assert!(p.require_ephemeral_vmgs);
680                    assert!(p.require_secure_boot);
681                    assert!(p.require_secure_boot_vars);
682                    assert!(p.require_bcd_integrity);
683                    assert_eq!(p.custom_uefi_json, b"deadbeef");
684                }
685                ProductPolicy::Cwcow(p) => panic!("unexpected Cwcow policy: {p:?}"),
686            },
687            other => panic!("unexpected parse: {other:?}"),
688        }
689    }
690
691    #[test]
692    fn openhcl_image_with_null_product_policy_is_absent() {
693        let json = r#"{
694            "openhcl": {
695                "command_line": "",
696                "memory_page_count": 10,
697                "uefi": true,
698                "product_policy": null
699            }
700        }"#;
701        let parsed: Image = serde_json::from_str(json).unwrap();
702        match parsed {
703            Image::Openhcl { product_policy, .. } => assert!(product_policy.is_none()),
704            other => panic!("unexpected parse: {other:?}"),
705        }
706    }
707}