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