Skip to main content

igvmfilegen/
snp_linux_direct.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Generation strategy for a self-contained SNP Linux-direct IGVM.
5
6use crate::file_loader::IgvmLoader;
7use crate::file_loader::IgvmOutput;
8use crate::file_loader::SnpLinuxDirectConfig;
9use crate::vp_context_builder::snp::InjectionType;
10use anyhow::Context;
11use anyhow::ensure;
12use chipset_resources::pm::DEFAULT_ACPI_IRQ;
13use chipset_resources::pm::DEFAULT_PM_PIO_BASE;
14use igvm_defs::SnpPolicy;
15use igvmfilegen_config::LinuxImage;
16use igvmfilegen_config::ResourceType;
17use igvmfilegen_config::Resources;
18use igvmfilegen_config::SnpInjectionType;
19use loader::importer::BootPageAcceptance;
20use loader::importer::ImageLoad;
21use loader::importer::X86Register;
22use loader::linux::InitrdAddressType;
23use loader::linux::InitrdConfig;
24use loader_defs::linux::SNP_BOOT_SHIM_MAX_RANGES;
25use loader_defs::linux::SNP_BOOT_SHIM_PARAMS_MAGIC;
26use loader_defs::linux::SNP_BOOT_SHIM_PARAMS_VERSION;
27use loader_defs::linux::SnpBootShimParams;
28use loader_defs::linux::SnpBootShimRange;
29use memory_range::MemoryRange;
30use serial_16550_resources::ComPort;
31use std::io::Seek;
32use vm_topology::memory::MemoryLayout;
33use vm_topology::pcie::PcieHostBridge;
34use vm_topology::processor::ProcessorTopology;
35use vm_topology::processor::TopologyBuilder;
36use vm_topology::processor::x86::X86Topology;
37use vmm_core::acpi_builder::AcpiArchConfig;
38use vmm_core::acpi_builder::AcpiTablesBuilder;
39use zerocopy::FromZeros;
40use zerocopy::IntoBytes;
41
42const PAGE_SIZE: u64 = igvm_defs::PAGE_SIZE_4K;
43/// KVM hardcodes the initial VMSA at this GPA and measures it during launch
44/// finish, after all userspace-provided launch-update pages.
45///
46/// Keep the BSP context at this address until KVM supports userspace-supplied
47/// VMSA pages. The MSHV path validates this address against the partition's
48/// physical address width and maps separate userspace backing for the VMSA.
49const KVM_VMSA_GPA: u64 = 0xffff_ffff_f000;
50
51/// Inputs for the deterministic SNP Linux-direct guest layout.
52pub struct BuildParams<'a> {
53    /// The Linux payload configuration.
54    pub linux: &'a LinuxImage,
55    /// The processor count described by the embedded topology and ACPI tables.
56    pub processor_count: u32,
57    /// The number of measured 4-KiB RAM pages.
58    pub memory_page_count: u64,
59    /// The page-table address bit used as the SNP encryption bit.
60    pub c_bit_position: u8,
61    /// The SNP guest policy.
62    pub policy: SnpPolicy,
63    /// The SNP interrupt-injection mode.
64    pub injection_type: &'a SnpInjectionType,
65    /// The kernel, optional initrd, and bootshim resources.
66    pub resources: &'a Resources,
67}
68
69/// The fixed platform layout embedded in the bring-up IGVM.
70struct FixedGuestLayout {
71    memory: MemoryLayout,
72    processors: ProcessorTopology<X86Topology>,
73    pcie_host_bridges: Vec<PcieHostBridge>,
74}
75
76impl FixedGuestLayout {
77    fn new(memory_page_count: u64, processor_count: u32) -> anyhow::Result<Self> {
78        let memory_size = memory_page_count
79            .checked_mul(PAGE_SIZE)
80            .context("RAM size overflow")?;
81        let memory = MemoryLayout::new(memory_size, &[], &[], &[], None)
82            .context("building memory layout")?;
83        let processors = TopologyBuilder::new_x86()
84            .build(processor_count)
85            .context("building processor topology")?;
86
87        Ok(Self {
88            memory,
89            processors,
90            pcie_host_bridges: Vec::new(),
91        })
92    }
93
94    fn acpi_builder(&self) -> AcpiTablesBuilder<'_, X86Topology> {
95        // This profile embeds OpenVMM's standard PC-compatible chipset
96        // contract. The ACPI values must match the devices supplied by the
97        // backend.
98        //
99        // TODO: Accept an external platform description when this bring-up
100        // profile needs layouts other than the fixed OpenVMM defaults.
101        AcpiTablesBuilder {
102            processor_topology: &self.processors,
103            mem_layout: &self.memory,
104            cache_topology: None,
105            pcie_host_bridges: &self.pcie_host_bridges,
106            slit_info: None,
107            generic_initiators: &[],
108            arch: AcpiArchConfig::X86 {
109                with_ioapic: true,
110                with_pic: true,
111                with_pit: true,
112                with_psp: false,
113                pm_base: DEFAULT_PM_PIO_BASE,
114                acpi_irq: DEFAULT_ACPI_IRQ,
115                iommu: None,
116            },
117        }
118    }
119}
120
121fn open_linux_resources(
122    linux: &LinuxImage,
123    resources: &Resources,
124) -> anyhow::Result<(fs_err::File, Option<fs_err::File>)> {
125    let kernel_path = resources
126        .get(ResourceType::LinuxKernel)
127        .context("Linux kernel resource is missing")?;
128    let kernel = fs_err::File::open(kernel_path)
129        .with_context(|| format!("opening kernel {}", kernel_path.display()))?;
130
131    let initrd = if linux.use_initrd {
132        let initrd_path = resources
133            .get(ResourceType::LinuxInitrd)
134            .context("Linux initrd resource is missing")?;
135        Some(
136            fs_err::File::open(initrd_path)
137                .with_context(|| format!("opening initrd {}", initrd_path.display()))?,
138        )
139    } else {
140        None
141    };
142
143    Ok((kernel, initrd))
144}
145
146fn initrd_config(initrd: &mut Option<fs_err::File>) -> anyhow::Result<Option<InitrdConfig<'_>>> {
147    let Some(initrd) = initrd else {
148        return Ok(None);
149    };
150    let size = initrd
151        .seek(std::io::SeekFrom::End(0))
152        .context("measuring initrd")?;
153    initrd.rewind().context("rewinding initrd")?;
154    Ok(Some(InitrdConfig {
155        initrd_address: InitrdAddressType::AfterKernel,
156        initrd,
157        size,
158    }))
159}
160
161fn new_loader(
162    policy: SnpPolicy,
163    c_bit_position: u8,
164    memory_page_count: u64,
165    injection_type: &SnpInjectionType,
166) -> IgvmLoader<X86Register> {
167    IgvmLoader::new_snp_linux_direct(SnpLinuxDirectConfig {
168        policy,
169        c_bit_mask: 1u64 << c_bit_position,
170        ram_page_count: memory_page_count,
171        vmsa_page: Some(KVM_VMSA_GPA / PAGE_SIZE),
172        injection_type: match injection_type {
173            SnpInjectionType::Normal => InjectionType::Normal,
174            SnpInjectionType::Restricted => InjectionType::Restricted,
175        },
176    })
177}
178
179/// Builds the deterministic topology, ACPI tables, Linux payload, and BSP
180/// launch context embedded in the standalone SNP IGVM.
181pub fn build(params: BuildParams<'_>) -> anyhow::Result<IgvmOutput> {
182    let BuildParams {
183        linux,
184        processor_count,
185        memory_page_count,
186        c_bit_position,
187        policy,
188        injection_type,
189        resources,
190    } = params;
191
192    let layout = FixedGuestLayout::new(memory_page_count, processor_count)?;
193    let acpi_builder = layout.acpi_builder();
194    let (mut kernel, mut initrd) = open_linux_resources(linux, resources)?;
195    let initrd_config = initrd_config(&mut initrd)?;
196    let mut loader = new_loader(policy, c_bit_position, memory_page_count, injection_type);
197    let com1 = ComPort::Com1;
198
199    let load_info = loader::linux::load_x86(
200        &mut loader.loader(),
201        &mut kernel,
202        initrd_config,
203        &linux.command_line,
204        &layout.memory,
205        |gpa| {
206            let tables = acpi_builder.build_acpi_tables(gpa, |dsdt| {
207                dsdt.add_apic();
208                dsdt.add_uart(b"\\_SB.UAR1", b"COM1", 1, com1.io_port(), com1.irq().into());
209                dsdt.add_rtc();
210            });
211            loader::linux::AcpiTables {
212                rsdp: tables.rsdp,
213                tables: tables.tables,
214            }
215        },
216        None,
217        Some(loader::linux::SnpBootConfig {
218            c_bit: c_bit_position,
219        }),
220    )
221    .context("loading direct-Linux image")?;
222
223    let kernel_runtime_end = kernel_runtime_end(
224        load_info.kernel.gpa,
225        load_info.kernel.size,
226        load_info
227            .bzimage_setup_header
228            .as_ref()
229            .map(|header| (u64::from(header.pref_address), u64::from(header.init_size))),
230    )?;
231    let bootshim_ranges = load_bootshim_and_handoff(
232        &mut loader,
233        resources,
234        load_info.kernel.entrypoint,
235        loader::linux::ZERO_PAGE_BASE,
236        memory_page_count,
237        kernel_runtime_end,
238    )?;
239
240    let mut output = loader.finalize().context("finalizing SNP IGVM")?;
241    for range in bootshim_ranges {
242        output.map.report_range(
243            MemoryRange::from_4k_gpn_range(range.start_gpn..range.start_gpn + range.page_count),
244            "snp-bootshim-accepted-ram [PVALIDATE]",
245        );
246    }
247    Ok(output)
248}
249
250/// Places the bootshim and its measured handoff page.
251///
252/// The Linux loader first imports the kernel, initrd, boot metadata, and SNP
253/// special pages. The bootshim is placed at the first page after both those
254/// imports and the kernel's runtime image. Its parameter page follows the
255/// bootshim. The BSP starts at the bootshim entry point with RSI pointing to
256/// that parameter page.
257///
258/// The parameter page lists every gap in configured RAM that has no measured
259/// page-data directive. The bootshim makes those pages private, validates
260/// them, and then enters Linux with RSI restored to the Linux zero page.
261fn load_bootshim_and_handoff(
262    loader: &mut IgvmLoader<X86Register>,
263    resources: &Resources,
264    linux_entry: u64,
265    linux_zero_page: u64,
266    memory_page_count: u64,
267    kernel_runtime_end: u64,
268) -> anyhow::Result<Vec<SnpBootShimRange>> {
269    let shim_base = align_up_to_page(loader.next_available_gpa()?.max(kernel_runtime_end));
270
271    let bootshim_path = resources
272        .get(ResourceType::SnpBootshim)
273        .context("SNP bootshim resource is missing")?;
274    let mut bootshim = fs_err::File::open(bootshim_path)
275        .with_context(|| format!("opening SNP bootshim {}", bootshim_path.display()))?;
276    let shim_load_info = loader::elf::load_static_elf(
277        &mut loader.loader(),
278        &mut bootshim,
279        0,
280        shim_base,
281        false,
282        BootPageAcceptance::Exclusive,
283        "snp-bootshim",
284    )
285    .context("loading SNP bootshim")?;
286
287    let params_gpa = align_up_to_page(shim_load_info.next_available_address);
288    let params_page = params_gpa / PAGE_SIZE;
289    ensure!(
290        params_page < memory_page_count,
291        "SNP bootshim parameter page lies outside configured RAM"
292    );
293
294    let bootshim_ranges = loader
295        .unimported_ram_ranges([params_page])?
296        .into_iter()
297        .map(|range| SnpBootShimRange {
298            start_gpn: range.start,
299            page_count: range.end - range.start,
300        })
301        .collect::<Vec<_>>();
302
303    let bootshim_params = build_bootshim_params(
304        linux_entry,
305        linux_zero_page,
306        memory_page_count * PAGE_SIZE,
307        &bootshim_ranges,
308    )?;
309    {
310        let mut importer = loader.loader();
311        importer
312            .import_pages(
313                params_page,
314                1,
315                "snp-bootshim-params",
316                BootPageAcceptance::Exclusive,
317                bootshim_params.as_bytes(),
318            )
319            .context("importing SNP bootshim parameters")?;
320        importer.import_vp_register(X86Register::Rip(shim_load_info.entrypoint))?;
321        importer.import_vp_register(X86Register::Rsi(params_gpa))?;
322    }
323
324    Ok(bootshim_ranges)
325}
326
327fn align_up_to_page(value: u64) -> u64 {
328    value
329        .checked_add(PAGE_SIZE - 1)
330        .expect("page alignment overflow")
331        & !(PAGE_SIZE - 1)
332}
333
334fn kernel_runtime_end(
335    load_gpa: u64,
336    image_size: u64,
337    bzimage_runtime: Option<(u64, u64)>,
338) -> anyhow::Result<u64> {
339    let (runtime_gpa, runtime_size) = bzimage_runtime
340        .map(|(preferred_gpa, init_size)| (load_gpa.max(preferred_gpa), init_size))
341        .unwrap_or((load_gpa, image_size));
342    runtime_gpa
343        .checked_add(runtime_size)
344        .context("Linux runtime image end overflow")
345}
346
347fn build_bootshim_params(
348    linux_entry: u64,
349    linux_zero_page: u64,
350    ram_end: u64,
351    ranges: &[SnpBootShimRange],
352) -> anyhow::Result<SnpBootShimParams> {
353    ensure!(
354        ranges.len() <= SNP_BOOT_SHIM_MAX_RANGES,
355        "sparse SNP layout requires {} bootshim ranges, but the parameter page supports at most {}",
356        ranges.len(),
357        SNP_BOOT_SHIM_MAX_RANGES
358    );
359    let mut params = SnpBootShimParams::new_zeroed();
360    params.magic = SNP_BOOT_SHIM_PARAMS_MAGIC;
361    params.version = SNP_BOOT_SHIM_PARAMS_VERSION;
362    params.range_count = ranges
363        .len()
364        .try_into()
365        .context("SNP bootshim range count does not fit in u32")?;
366    params.linux_entry = linux_entry;
367    params.linux_zero_page = linux_zero_page;
368    params.ram_end = ram_end;
369    params.ranges[..ranges.len()].copy_from_slice(ranges);
370    Ok(params)
371}
372
373#[cfg(test)]
374mod tests {
375    use super::*;
376    use crate::vp_context_builder::VpContextBuilder;
377    use crate::vp_context_builder::snp::SnpHardwareContext;
378    use igvm::IgvmDirectiveHeader;
379    use igvm::IgvmFile;
380    use igvm::IgvmInitializationHeader;
381    use igvm::IgvmPlatformHeader;
382    use igvm::IgvmSerializer;
383    use igvm_defs::IgvmPageDataType;
384    use igvm_defs::IgvmPlatformType;
385    use loader::importer::BootPageAcceptance;
386    use loader::importer::ImageLoad;
387    use std::collections::BTreeSet;
388    use test_with_tracing::test;
389    use zerocopy::FromBytes;
390
391    const COMPATIBILITY_MASK: u32 = 1;
392    const TEST_POLICY: u64 = 0x30000;
393    const TEST_C_BIT_MASK: u64 = 1 << 51;
394    const TEST_SHIM_ENTRY: u64 = 0x100000;
395
396    fn test_loader_with_injection(
397        ram_page_count: u64,
398        injection_type: InjectionType,
399    ) -> IgvmLoader<X86Register> {
400        IgvmLoader::new_snp_linux_direct(SnpLinuxDirectConfig {
401            policy: SnpPolicy::from(TEST_POLICY),
402            c_bit_mask: TEST_C_BIT_MASK,
403            ram_page_count,
404            vmsa_page: Some(KVM_VMSA_GPA / PAGE_SIZE),
405            injection_type,
406        })
407    }
408
409    fn test_loader(ram_page_count: u64) -> IgvmLoader<X86Register> {
410        test_loader_with_injection(ram_page_count, InjectionType::Normal)
411    }
412
413    fn import_test_registers(importer: &mut dyn ImageLoad<X86Register>, params_gpa: u64) {
414        for register in [
415            X86Register::Rip(TEST_SHIM_ENTRY),
416            X86Register::Rsi(params_gpa),
417            X86Register::Cr3(0x4000 | TEST_C_BIT_MASK),
418            X86Register::Cr0(x86defs::X64_CR0_PE | x86defs::X64_CR0_PG),
419            X86Register::Cr4(x86defs::X64_CR4_PAE),
420            X86Register::Efer(x86defs::X64_EFER_LME | x86defs::X64_EFER_LMA),
421        ] {
422            importer.import_vp_register(register).unwrap();
423        }
424    }
425
426    fn assert_serialized_igvm_shape(
427        igvm: &IgvmFile,
428        ram_page_count: u64,
429        expected_pages: &[u64],
430        expected_handoff: (u64, u64),
431    ) {
432        let serializer = IgvmSerializer::new(igvm).unwrap();
433        let expected_launch_digest = serializer
434            .measurement_for(IgvmPlatformType::SEV_SNP)
435            .unwrap()
436            .digest
437            .clone();
438        let mut binary = Vec::new();
439        serializer.serialize(&mut binary).unwrap();
440        let reparsed = IgvmFile::new_from_binary(&binary, Some(igvm::IsolationType::Snp)).unwrap();
441
442        assert_eq!(reparsed.platforms().len(), 1);
443        let IgvmPlatformHeader::SupportedPlatform(platform) = &reparsed.platforms()[0];
444        assert_eq!(platform.platform_type, IgvmPlatformType::SEV_SNP);
445        assert_eq!(platform.highest_vtl, 0);
446        assert_eq!(platform.shared_gpa_boundary, 0);
447        assert!(reparsed.initializations().iter().any(|header| matches!(
448            header,
449            IgvmInitializationHeader::GuestPolicy {
450                policy: TEST_POLICY,
451                compatibility_mask: COMPATIBILITY_MASK,
452            }
453        )));
454
455        let mut covered_pages = BTreeSet::new();
456        let mut required_memory_count = 0;
457        let mut next_vmsa_index = 0;
458        let mut secrets_count = 0;
459        let mut cpuid_count = 0;
460
461        for directive in reparsed.directives() {
462            match directive {
463                IgvmDirectiveHeader::PageData {
464                    gpa,
465                    flags,
466                    data_type,
467                    data,
468                    ..
469                } => {
470                    assert!(gpa.is_multiple_of(PAGE_SIZE));
471                    let page = gpa / PAGE_SIZE;
472                    assert!(page < ram_page_count);
473                    assert!(covered_pages.insert(page));
474                    assert!(!flags.shared() && !flags.unmeasured());
475                    assert!(data.len() <= PAGE_SIZE as usize);
476                    match *data_type {
477                        IgvmPageDataType::SECRETS => secrets_count += 1,
478                        IgvmPageDataType::CPUID_DATA => cpuid_count += 1,
479                        IgvmPageDataType::NORMAL => {}
480                        unexpected => panic!("unexpected PageData type {unexpected:?}"),
481                    }
482                }
483                IgvmDirectiveHeader::SnpVpContext {
484                    gpa,
485                    vp_index,
486                    vmsa,
487                    ..
488                } => {
489                    assert_eq!(*gpa, KVM_VMSA_GPA);
490                    assert_eq!(u32::from(*vp_index), next_vmsa_index);
491                    assert_eq!(vmsa.rip, expected_handoff.0);
492                    assert_eq!(vmsa.rsi, expected_handoff.1);
493                    assert!(vmsa.sev_features.snp());
494                    assert!(!vmsa.sev_features.vtom());
495                    assert_eq!(vmsa.virtual_tom, 0);
496                    assert_ne!(vmsa.cr3 & TEST_C_BIT_MASK, 0);
497                    assert_ne!(vmsa.cr0 & x86defs::X64_CR0_ET, 0);
498                    assert_ne!(vmsa.cr4 & x86defs::X64_CR4_MCE, 0);
499                    assert_eq!(vmsa.rflags, u64::from(x86defs::RFlags::at_reset()));
500                    assert_eq!(vmsa.dr6, 0xffff_0ff0);
501                    assert_eq!(vmsa.dr7, 0x400);
502                    assert_eq!(vmsa.x87_fcw, x86defs::xsave::INIT_FCW);
503                    assert_eq!(vmsa.mxcsr, x86defs::xsave::DEFAULT_MXCSR);
504                    next_vmsa_index += 1;
505                }
506                IgvmDirectiveHeader::RequiredMemory {
507                    gpa,
508                    number_of_bytes,
509                    vtl2_protectable,
510                    ..
511                } => {
512                    assert_eq!(*gpa, 0);
513                    assert_eq!(u64::from(*number_of_bytes), ram_page_count * PAGE_SIZE);
514                    assert!(!vtl2_protectable);
515                    required_memory_count += 1;
516                }
517                unexpected => panic!("unexpected directive in fixed SNP IGVM: {unexpected:?}"),
518            }
519        }
520
521        assert_eq!(
522            covered_pages,
523            expected_pages.iter().copied().collect::<BTreeSet<_>>()
524        );
525        assert_eq!(required_memory_count, 1);
526        assert_eq!(next_vmsa_index, 1);
527        assert_eq!(secrets_count, 1);
528        assert_eq!(cpuid_count, 1);
529
530        let measured_digest = IgvmSerializer::new(&reparsed)
531            .unwrap()
532            .measurement_for(IgvmPlatformType::SEV_SNP)
533            .unwrap()
534            .digest
535            .clone();
536        assert_eq!(measured_digest, expected_launch_digest);
537    }
538
539    #[test]
540    fn fixed_layout_supports_one_and_many_processors() {
541        for processor_count in [1, 4] {
542            let layout = FixedGuestLayout::new(64, processor_count).unwrap();
543            assert_eq!(layout.processors.vp_count(), processor_count);
544            assert_eq!(layout.memory.ram().len(), 1);
545        }
546    }
547
548    #[test]
549    fn bzimage_runtime_uses_preferred_address_above_load_address() {
550        assert_eq!(
551            kernel_runtime_end(0x100000, 0x200000, Some((0x400000, 0x300000))).unwrap(),
552            0x700000
553        );
554    }
555
556    #[test]
557    fn bzimage_runtime_uses_load_address_above_preferred_address() {
558        assert_eq!(
559            kernel_runtime_end(0x400000, 0x200000, Some((0x100000, 0x300000))).unwrap(),
560            0x700000
561        );
562    }
563
564    #[test]
565    fn raw_kernel_runtime_uses_image_size() {
566        assert_eq!(
567            kernel_runtime_end(0x100000, 0x200000, None).unwrap(),
568            0x300000
569        );
570    }
571
572    #[test]
573    fn rejects_kernel_runtime_end_overflow() {
574        assert!(kernel_runtime_end(u64::MAX, 1, None).is_err());
575        assert!(kernel_runtime_end(0, 0, Some((u64::MAX, 1))).is_err());
576    }
577
578    #[test]
579    fn vmsa_uses_c_bit_model() {
580        let params_gpa = 0x6000;
581        let mut context =
582            SnpHardwareContext::new_linux_direct(TEST_C_BIT_MASK, InjectionType::Normal);
583        for register in [
584            X86Register::Rip(TEST_SHIM_ENTRY),
585            X86Register::Rsi(params_gpa),
586            X86Register::Cr3(0x4000 | TEST_C_BIT_MASK),
587            X86Register::Cr0(x86defs::X64_CR0_PE | x86defs::X64_CR0_PG),
588            X86Register::Cr4(x86defs::X64_CR4_PAE),
589            X86Register::Efer(x86defs::X64_EFER_LME | x86defs::X64_EFER_LMA),
590        ] {
591            context.import_vp_register(register);
592        }
593
594        let vmsa = context.vmsa();
595        assert_eq!(vmsa.rip, TEST_SHIM_ENTRY);
596        assert_eq!(vmsa.rsi, params_gpa);
597        assert_ne!(vmsa.cr3 & TEST_C_BIT_MASK, 0);
598        assert_ne!(vmsa.cr0 & x86defs::X64_CR0_ET, 0);
599        assert_ne!(vmsa.cr4 & x86defs::X64_CR4_MCE, 0);
600        assert!(vmsa.sev_features.snp());
601        assert!(!vmsa.sev_features.vtom());
602        assert!(!vmsa.sev_features.debug_swap());
603        assert_eq!(vmsa.virtual_tom, 0);
604        assert_eq!(vmsa.rflags, u64::from(x86defs::RFlags::at_reset()));
605        assert_eq!(vmsa.dr6, 0xffff_0ff0);
606        assert_eq!(vmsa.dr7, 0x400);
607        assert_eq!(vmsa.tr.limit, 0xffff);
608        assert_eq!(vmsa.tr.attrib, 0x8b);
609        assert_eq!(vmsa.ldtr.limit, 0xffff);
610        assert_eq!(vmsa.ldtr.attrib, 0x82);
611        assert_eq!(vmsa.idtr.limit, 0xffff);
612        assert_eq!(vmsa.x87_fcw, x86defs::xsave::INIT_FCW);
613        assert_eq!(vmsa.mxcsr, x86defs::xsave::DEFAULT_MXCSR);
614    }
615
616    #[test]
617    fn normal_and_restricted_vmsas_use_the_selected_injection_mode() {
618        for (injection_type, restricted) in [
619            (InjectionType::Normal, false),
620            (InjectionType::Restricted, true),
621        ] {
622            let mut loader = test_loader_with_injection(1, injection_type);
623            import_test_registers(&mut loader.loader(), 0x6000);
624            let output = loader.finalize().unwrap();
625            let vmsa = output
626                .guest
627                .directives()
628                .iter()
629                .find_map(|directive| match directive {
630                    IgvmDirectiveHeader::SnpVpContext { vmsa, .. } => Some(vmsa),
631                    _ => None,
632                })
633                .unwrap();
634            assert!(vmsa.sev_features.snp());
635            assert_eq!(vmsa.sev_features.restrict_injection(), restricted);
636            assert!(!vmsa.sev_features.alternate_injection());
637        }
638    }
639
640    #[test]
641    fn sparse_igvm_serializes_and_preserves_measurement() {
642        const RAM_PAGE_COUNT: u64 = 8;
643        const PARAMS_PAGE: u64 = 6;
644        let params_gpa = PARAMS_PAGE * PAGE_SIZE;
645        let mut loader = test_loader(RAM_PAGE_COUNT);
646        {
647            let mut importer = loader.loader();
648            importer
649                .import_pages(1, 1, "secrets", BootPageAcceptance::SecretsPage, &[])
650                .unwrap();
651            importer
652                .import_pages(2, 1, "cpuid", BootPageAcceptance::CpuidPage, &[])
653                .unwrap();
654        }
655
656        let ranges = loader.unimported_ram_ranges([PARAMS_PAGE]).unwrap();
657        assert_eq!(ranges, [0..1, 3..6, 7..8]);
658        let ranges = ranges
659            .iter()
660            .map(|range| SnpBootShimRange {
661                start_gpn: range.start,
662                page_count: range.end - range.start,
663            })
664            .collect::<Vec<_>>();
665        let params = build_bootshim_params(
666            0x200000,
667            loader::linux::ZERO_PAGE_BASE,
668            RAM_PAGE_COUNT * PAGE_SIZE,
669            &ranges,
670        )
671        .unwrap();
672        {
673            let mut importer = loader.loader();
674            importer
675                .import_pages(
676                    PARAMS_PAGE,
677                    1,
678                    "snp-bootshim-params",
679                    BootPageAcceptance::Exclusive,
680                    params.as_bytes(),
681                )
682                .unwrap();
683            import_test_registers(&mut importer, params_gpa);
684        }
685
686        let output = loader.finalize().unwrap();
687        let mut imported_pages = Vec::new();
688        let mut required_memory = None;
689        let mut handoff = None;
690        let mut serialized_params = None;
691        for directive in output.guest.directives() {
692            match directive {
693                IgvmDirectiveHeader::PageData {
694                    gpa,
695                    data_type,
696                    data,
697                    ..
698                } => {
699                    imported_pages.push(*gpa / PAGE_SIZE);
700                    if *gpa == params_gpa {
701                        assert_eq!(*data_type, IgvmPageDataType::NORMAL);
702                        let mut page = [0; PAGE_SIZE as usize];
703                        page[..data.len()].copy_from_slice(data);
704                        serialized_params =
705                            Some(SnpBootShimParams::read_from_bytes(&page).unwrap());
706                    }
707                }
708                IgvmDirectiveHeader::RequiredMemory {
709                    gpa,
710                    number_of_bytes,
711                    ..
712                } => required_memory = Some((*gpa, *number_of_bytes)),
713                IgvmDirectiveHeader::SnpVpContext { vmsa, .. } => {
714                    handoff = Some((vmsa.rip, vmsa.rsi));
715                }
716                _ => {}
717            }
718        }
719
720        imported_pages.sort_unstable();
721        assert_eq!(imported_pages, [1, 2, PARAMS_PAGE]);
722        assert_eq!(
723            required_memory,
724            Some((0, (RAM_PAGE_COUNT * PAGE_SIZE) as u32))
725        );
726        assert_eq!(handoff, Some((TEST_SHIM_ENTRY, params_gpa)));
727        assert_eq!(serialized_params, Some(params));
728        assert_serialized_igvm_shape(
729            &output.guest,
730            RAM_PAGE_COUNT,
731            &[1, 2, PARAMS_PAGE],
732            (TEST_SHIM_ENTRY, params_gpa),
733        );
734    }
735
736    #[test]
737    fn sparse_image_emits_only_the_bsp_vmsa() {
738        let mut loader = test_loader(1);
739        import_test_registers(&mut loader.loader(), 0x2000);
740
741        let output = loader.finalize().unwrap();
742        let vp_indexes = output
743            .guest
744            .directives()
745            .iter()
746            .filter_map(|directive| match directive {
747                IgvmDirectiveHeader::SnpVpContext { vp_index, .. } => Some(*vp_index),
748                _ => None,
749            })
750            .collect::<Vec<_>>();
751        assert_eq!(vp_indexes, [0]);
752    }
753
754    #[test]
755    fn overlapping_import_does_not_mutate_existing_pages() {
756        let mut loader = test_loader(4);
757        {
758            let mut importer = loader.loader();
759            importer
760                .import_pages(1, 1, "first", BootPageAcceptance::Exclusive, &[0xaa])
761                .unwrap();
762            let error = importer
763                .import_pages(
764                    0,
765                    2,
766                    "overlap",
767                    BootPageAcceptance::Exclusive,
768                    &[0xbb; PAGE_SIZE as usize * 2],
769                )
770                .unwrap_err();
771            assert!(error.to_string().contains("overlaps"));
772            import_test_registers(&mut importer, 0x2000);
773        }
774
775        let output = loader.finalize().unwrap();
776        let page = output
777            .guest
778            .directives()
779            .iter()
780            .find_map(|directive| match directive {
781                IgvmDirectiveHeader::PageData { gpa, data, .. } if *gpa == PAGE_SIZE => Some(data),
782                _ => None,
783            })
784            .unwrap();
785        assert_eq!(page, &[0xaa]);
786    }
787
788    #[test]
789    fn rejects_too_many_bootshim_ranges() {
790        let ranges = vec![
791            SnpBootShimRange {
792                start_gpn: 0,
793                page_count: 1,
794            };
795            SNP_BOOT_SHIM_MAX_RANGES + 1
796        ];
797        assert!(
798            build_bootshim_params(0x100000, 0x2000, 0x200000, &ranges)
799                .unwrap_err()
800                .to_string()
801                .contains("supports at most")
802        );
803    }
804
805    #[test]
806    #[should_panic(expected = "page alignment overflow")]
807    fn bootshim_placement_overflow_panics() {
808        align_up_to_page(u64::MAX);
809    }
810}