Skip to main content

igvmfilegen/
file_loader.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Implements a loader that serializes the loaded state into the IGVM binary format.
5
6use crate::vp_context_builder::VpContextBuilder;
7use crate::vp_context_builder::VpContextPageState;
8use crate::vp_context_builder::VpContextState;
9use crate::vp_context_builder::snp::InjectionType;
10use crate::vp_context_builder::snp::SecureAvic;
11use crate::vp_context_builder::snp::SnpHardwareContext;
12use crate::vp_context_builder::tdx::TdxHardwareContext;
13use crate::vp_context_builder::vbs::VbsRegister;
14use crate::vp_context_builder::vbs::VbsVpContext;
15use anyhow::Context;
16use crypto::sha_384::Sha384;
17use hvdef::Vtl;
18use igvm::IgvmDirectiveHeader;
19use igvm::IgvmFile;
20use igvm::IgvmInitializationHeader;
21use igvm::IgvmPlatformHeader;
22use igvm::IgvmRelocatableRegion;
23use igvm::IgvmRevision;
24use igvm::snp_defs::SevVmsa;
25use igvm_defs::IGVM_VHS_PARAMETER;
26use igvm_defs::IGVM_VHS_PARAMETER_INSERT;
27use igvm_defs::IGVM_VHS_SUPPORTED_PLATFORM;
28use igvm_defs::IgvmPageDataFlags;
29use igvm_defs::IgvmPageDataType;
30use igvm_defs::IgvmPlatformType;
31use igvm_defs::PAGE_SIZE_4K;
32use igvm_defs::SnpPolicy;
33use igvm_defs::TdxPolicy;
34use loader::importer::Aarch64Register;
35use loader::importer::BootPageAcceptance;
36use loader::importer::GuestArch;
37use loader::importer::GuestArchKind;
38use loader::importer::IgvmParameterType;
39use loader::importer::ImageLoad;
40use loader::importer::IsolationConfig;
41use loader::importer::IsolationType;
42use loader::importer::ParameterAreaIndex;
43use loader::importer::X86Register;
44use memory_range::MemoryRange;
45use range_map_vec::Entry;
46use range_map_vec::RangeMap;
47use std::collections::BTreeMap;
48use std::collections::BTreeSet;
49use std::fmt::Debug;
50use std::fmt::Display;
51use zerocopy::FromBytes;
52use zerocopy::IntoBytes;
53
54pub const DEFAULT_COMPATIBILITY_MASK: u32 = 0x1;
55
56const TDX_SHARED_GPA_BOUNDARY_BITS: u8 = 47;
57
58fn to_igvm_vtl(vtl: Vtl) -> igvm::hv_defs::Vtl {
59    match vtl {
60        Vtl::Vtl0 => igvm::hv_defs::Vtl::Vtl0,
61        Vtl::Vtl1 => igvm::hv_defs::Vtl::Vtl1,
62        Vtl::Vtl2 => igvm::hv_defs::Vtl::Vtl2,
63    }
64}
65
66/// Page table relocation information kept for debugging purposes.
67// Allow dead code because clippy doesn't count #[derive(Debug)] as non-dead code usage.
68#[expect(dead_code)]
69#[derive(Debug, Clone)]
70struct PageTableRegion {
71    gpa: u64,
72    size_pages: u64,
73    used_size_pages: u64,
74}
75
76#[derive(Debug, Clone)]
77enum RelocationType {
78    PageTable(PageTableRegion),
79    Normal(IgvmRelocatableRegion),
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
83struct RangeInfo {
84    tag: String,
85    acceptance: BootPageAcceptance,
86}
87
88/// Additional finalization needed by a self-contained SNP Linux-direct image.
89#[derive(Debug, Copy, Clone)]
90pub struct SnpLinuxDirectConfig {
91    pub policy: SnpPolicy,
92    pub c_bit_mask: u64,
93    pub ram_page_count: u64,
94    pub vmsa_page: Option<u64>,
95    pub injection_type: InjectionType,
96}
97
98pub struct IgvmLoader<R: VbsRegister + GuestArch> {
99    accepted_ranges: RangeMap<u64, RangeInfo>,
100    relocatable_regions: RangeMap<u64, RelocationType>,
101    required_memory: Vec<RequiredMemory>,
102    page_table_region: Option<PageTableRegion>,
103    platform_header: IgvmPlatformHeader,
104    initialization_headers: Vec<IgvmInitializationHeader>,
105    directives: Vec<IgvmDirectiveHeader>,
106    page_data_directives: Vec<IgvmDirectiveHeader>,
107    vp_context: Option<Box<dyn VpContextBuilder<Register = R>>>,
108    max_vtl: Vtl,
109    parameter_areas: BTreeMap<(u64, u32), u32>,
110    isolation_type: LoaderIsolationType,
111    paravisor_present: bool,
112    imported_regions_config_page: Option<u64>,
113    expected_page_hashes_config_page: Option<u64>,
114    snp_linux_direct: Option<SnpLinuxDirectConfig>,
115}
116
117pub struct IgvmVtlLoader<'a, R: VbsRegister + GuestArch> {
118    loader: &'a mut IgvmLoader<R>,
119    vtl: Vtl,
120    vp_context: Option<VbsVpContext<R>>,
121}
122
123impl<R: VbsRegister + GuestArch> IgvmVtlLoader<'_, R> {
124    pub fn loader(&self) -> &IgvmLoader<R> {
125        self.loader
126    }
127
128    /// Returns a loader for importing an inner image as part of the actual
129    /// (paravisor) image to load.
130    ///
131    /// Use `take_vp_context` on the returned loader to get the VP context that
132    /// the paravisor should load.
133    pub fn nested_loader(&mut self) -> IgvmVtlLoader<'_, R> {
134        IgvmVtlLoader {
135            loader: &mut *self.loader,
136            vtl: Vtl::Vtl0,
137            vp_context: Some(VbsVpContext::new(self.vtl)),
138        }
139    }
140
141    pub fn take_vp_context(&mut self) -> Vec<u8> {
142        self.vp_context
143            .take()
144            .map_or_else(Vec::new, |vp| vp.as_page())
145    }
146}
147
148#[derive(Copy, Clone, Debug, PartialEq, Eq)]
149pub enum LoaderIsolationType {
150    None,
151    Vbs {
152        enable_debug: bool,
153    },
154    Snp {
155        shared_gpa_boundary_bits: Option<u8>,
156        policy: SnpPolicy,
157        injection_type: InjectionType,
158        secure_avic: SecureAvic,
159        // TODO SNP: SNP Keys? Other data?
160    },
161    Tdx {
162        policy: TdxPolicy,
163    },
164}
165
166/// A trait to specialize behavior based on different register types for
167/// different architectures.
168pub trait IgvmLoaderRegister: VbsRegister {
169    /// Perform arch specific initialization.
170    fn init(
171        with_paravisor: bool,
172        max_vtl: Vtl,
173        isolation: LoaderIsolationType,
174    ) -> (
175        IgvmPlatformHeader,
176        Vec<IgvmInitializationHeader>,
177        Box<dyn VpContextBuilder<Register = Self>>,
178    );
179
180    /// The IGVM file revision to use for the built igvm file.
181    fn igvm_revision() -> IgvmRevision;
182}
183
184impl IgvmLoaderRegister for X86Register {
185    fn init(
186        with_paravisor: bool,
187        max_vtl: Vtl,
188        isolation: LoaderIsolationType,
189    ) -> (
190        IgvmPlatformHeader,
191        Vec<IgvmInitializationHeader>,
192        Box<dyn VpContextBuilder<Register = Self>>,
193    ) {
194        match isolation {
195            LoaderIsolationType::None | LoaderIsolationType::Vbs { .. } => {
196                unreachable!("should be handled by common code")
197            }
198            LoaderIsolationType::Snp {
199                shared_gpa_boundary_bits,
200                policy,
201                injection_type,
202                secure_avic,
203            } => {
204                // TODO SNP: assumed that shared_gpa_boundary is always available.
205                let shared_gpa_boundary =
206                    1 << shared_gpa_boundary_bits.expect("shared gpa boundary must be set");
207
208                // Add SNP Platform header
209                let info = IGVM_VHS_SUPPORTED_PLATFORM {
210                    compatibility_mask: DEFAULT_COMPATIBILITY_MASK,
211                    highest_vtl: max_vtl as u8,
212                    platform_type: IgvmPlatformType::SEV_SNP,
213                    platform_version: igvm_defs::IGVM_SEV_SNP_PLATFORM_VERSION,
214                    shared_gpa_boundary,
215                };
216
217                let platform_header = IgvmPlatformHeader::SupportedPlatform(info);
218
219                let init_header = IgvmInitializationHeader::GuestPolicy {
220                    policy: policy.into(),
221                    compatibility_mask: DEFAULT_COMPATIBILITY_MASK,
222                };
223
224                let vp_context_builder = Box::new(SnpHardwareContext::new(
225                    max_vtl,
226                    !with_paravisor,
227                    shared_gpa_boundary,
228                    injection_type,
229                    secure_avic,
230                ));
231
232                (platform_header, vec![init_header], vp_context_builder)
233            }
234            LoaderIsolationType::Tdx { policy } => {
235                // NOTE: TDX always has a shared_gpa_boundary and has it at 47 bits.
236                let info = IGVM_VHS_SUPPORTED_PLATFORM {
237                    compatibility_mask: DEFAULT_COMPATIBILITY_MASK,
238                    highest_vtl: max_vtl as u8,
239                    platform_type: IgvmPlatformType::TDX,
240                    platform_version: igvm_defs::IGVM_TDX_PLATFORM_VERSION,
241                    shared_gpa_boundary: 1 << TDX_SHARED_GPA_BOUNDARY_BITS,
242                };
243
244                let platform_header = IgvmPlatformHeader::SupportedPlatform(info);
245
246                let mut init_headers = Vec::new();
247                if u64::from(policy) != 0 {
248                    init_headers.push(IgvmInitializationHeader::GuestPolicy {
249                        policy: policy.into(),
250                        compatibility_mask: DEFAULT_COMPATIBILITY_MASK,
251                    });
252                }
253
254                let vp_context_builder = Box::new(TdxHardwareContext::new(!with_paravisor));
255
256                (platform_header, init_headers, vp_context_builder)
257            }
258        }
259    }
260
261    fn igvm_revision() -> IgvmRevision {
262        // For now, x86 built files always uses V1 of the IGVM format. This is
263        // to maintain compatibility with older OS repo loaders that do not
264        // understand the V2 format.
265        IgvmRevision::V1
266    }
267}
268
269impl IgvmLoaderRegister for Aarch64Register {
270    fn init(
271        _with_paravisor: bool,
272        _max_vtl: Vtl,
273        _isolation: LoaderIsolationType,
274    ) -> (
275        IgvmPlatformHeader,
276        Vec<IgvmInitializationHeader>,
277        Box<dyn VpContextBuilder<Register = Self>>,
278    ) {
279        unreachable!("should never be called")
280    }
281
282    fn igvm_revision() -> IgvmRevision {
283        // AArch64 IGVM files are always V2.
284        IgvmRevision::V2 {
285            arch: igvm::Arch::AArch64,
286            page_size: 4096,
287        }
288    }
289}
290
291#[derive(Debug, Clone)]
292struct RequiredMemory {
293    range: MemoryRange,
294    vtl2_protectable: bool,
295}
296
297/// A map file representing information about a given generated IGVM file from a
298/// loader.
299///
300/// This can be used to save additional information about the layout of the
301/// address space that importing an IGVM file will create.
302#[derive(Debug)]
303pub struct MapFile {
304    isolation: LoaderIsolationType,
305    required_memory: Vec<RequiredMemory>,
306    accepted_ranges: Vec<(MemoryRange, RangeInfo)>,
307    relocatable_regions: Vec<(MemoryRange, RelocationType)>,
308    reported_ranges: Vec<(MemoryRange, String)>,
309}
310
311impl MapFile {
312    /// Adds a map-only annotation for a range that is not represented by an
313    /// IGVM directive.
314    ///
315    /// Reported ranges appear under `IGVM file reported ranges` in tracing and
316    /// the written map file. They do not change the generated IGVM file.
317    pub fn report_range(&mut self, range: MemoryRange, tag: impl Into<String>) {
318        self.reported_ranges.push((range, tag.into()));
319    }
320
321    /// Emit this map file information to tracing::info.
322    pub fn emit_tracing(&self) {
323        tracing::info!(isolation = ?self.isolation, "IGVM file isolation");
324        tracing::info!("IGVM file layout:");
325        for (range, info) in self.accepted_ranges.iter() {
326            tracing::info!(
327                tag = info.tag,
328                size_bytes = range.len(),
329                "{:#x} - {:#x}",
330                range.start(),
331                range.end(),
332            );
333        }
334
335        if !self.required_memory.is_empty() {
336            tracing::info!("IGVM file required memory:");
337            for region in &self.required_memory {
338                tracing::info!(
339                    size_bytes = region.range.len(),
340                    vtl2_protectable = region.vtl2_protectable,
341                    "{:#x} - {:#x}",
342                    region.range.start(),
343                    region.range.end(),
344                );
345            }
346        }
347
348        if !self.relocatable_regions.is_empty() {
349            tracing::info!("IGVM file relocatable regions:");
350            for (range, info) in self.relocatable_regions.iter().rev() {
351                match info {
352                    RelocationType::PageTable(region) => {
353                        tracing::info!(
354                            size_bytes = region.size_pages * PAGE_SIZE_4K,
355                            "{:#x} - {:#x} pagetable relocation region",
356                            region.gpa,
357                            range.end(),
358                        );
359                    }
360                    RelocationType::Normal(region) => {
361                        tracing::info!(
362                            base_gpa = format_args!("{:#x}", region.base_gpa),
363                            size_bytes = region.size,
364                            minimum_relocation_gpa =
365                                format_args!("{:#x}", region.minimum_relocation_gpa),
366                            maximum_relocation_gpa =
367                                format_args!("{:#x}", region.maximum_relocation_gpa),
368                            relocation_alignment = region.relocation_alignment,
369                            "{:#x} - {:#x} relocation region",
370                            region.base_gpa,
371                            range.end(),
372                        );
373                    }
374                }
375            }
376        }
377
378        if !self.reported_ranges.is_empty() {
379            tracing::info!("IGVM file reported ranges:");
380            for (range, tag) in &self.reported_ranges {
381                tracing::info!(
382                    size_bytes = range.len(),
383                    "{:#x} - {:#x} {}",
384                    range.start(),
385                    range.end(),
386                    tag,
387                );
388            }
389        }
390    }
391}
392
393impl Display for MapFile {
394    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
395        writeln!(f, "IGVM file isolation: {:?}", self.isolation)?;
396
397        writeln!(f, "IGVM file layout:")?;
398        for (range, info) in &self.accepted_ranges {
399            writeln!(
400                f,
401                "  {:016x} - {:016x} ({:#x} bytes) {}",
402                range.start(),
403                range.end(),
404                range.len(),
405                info.tag
406            )?;
407        }
408
409        if !self.required_memory.is_empty() {
410            writeln!(f, "IGVM file required memory:")?;
411            for region in &self.required_memory {
412                writeln!(
413                    f,
414                    "  {:016x} - {:016x} ({:#x} bytes) {}",
415                    region.range.start(),
416                    region.range.end(),
417                    region.range.len(),
418                    if region.vtl2_protectable {
419                        "VTL2 protectable"
420                    } else {
421                        ""
422                    }
423                )?;
424            }
425        }
426
427        if !self.relocatable_regions.is_empty() {
428            writeln!(f, "IGVM file relocatable regions:")?;
429            for (range, info) in &self.relocatable_regions {
430                match info {
431                    RelocationType::PageTable(region) => {
432                        writeln!(
433                            f,
434                            "  {:016x} - {:016x} ({:#x} bytes) pagetable relocation region",
435                            region.gpa,
436                            range.end(),
437                            region.size_pages * PAGE_SIZE_4K,
438                        )?;
439                    }
440                    RelocationType::Normal(region) => {
441                        writeln!(
442                            f,
443                            "  {:016x} - {:016x} ({:#x} bytes) relocation region",
444                            region.base_gpa,
445                            range.end(),
446                            region.size
447                        )?;
448                    }
449                }
450            }
451        }
452
453        if !self.reported_ranges.is_empty() {
454            writeln!(f, "IGVM file reported ranges:")?;
455            for (range, tag) in &self.reported_ranges {
456                writeln!(
457                    f,
458                    "  {:016x} - {:016x} ({:#x} bytes) {}",
459                    range.start(),
460                    range.end(),
461                    range.len(),
462                    tag
463                )?;
464            }
465        }
466
467        Ok(())
468    }
469}
470
471/// Returns output from finalize
472#[derive(Debug)]
473pub struct IgvmOutput {
474    pub guest: IgvmFile,
475    pub map: MapFile,
476}
477
478impl IgvmLoader<X86Register> {
479    /// Create a loader for a self-contained SNP Linux-direct image.
480    pub fn new_snp_linux_direct(config: SnpLinuxDirectConfig) -> Self {
481        let isolation_type = LoaderIsolationType::Snp {
482            shared_gpa_boundary_bits: None,
483            policy: config.policy,
484            injection_type: config.injection_type,
485            secure_avic: SecureAvic::Disabled,
486        };
487        let platform_header = IgvmPlatformHeader::SupportedPlatform(IGVM_VHS_SUPPORTED_PLATFORM {
488            compatibility_mask: DEFAULT_COMPATIBILITY_MASK,
489            highest_vtl: Vtl::Vtl0 as u8,
490            platform_type: IgvmPlatformType::SEV_SNP,
491            platform_version: igvm_defs::IGVM_SEV_SNP_PLATFORM_VERSION,
492            shared_gpa_boundary: 0,
493        });
494        let initialization_headers = vec![IgvmInitializationHeader::GuestPolicy {
495            policy: config.policy.into(),
496            compatibility_mask: DEFAULT_COMPATIBILITY_MASK,
497        }];
498        let mut vp_context =
499            SnpHardwareContext::new_linux_direct(config.c_bit_mask, config.injection_type);
500        if let Some(page) = config.vmsa_page {
501            vp_context.set_vp_context_memory(page);
502        }
503
504        Self {
505            accepted_ranges: RangeMap::new(),
506            relocatable_regions: RangeMap::new(),
507            required_memory: Vec::new(),
508            page_table_region: None,
509            platform_header,
510            initialization_headers,
511            directives: Vec::new(),
512            page_data_directives: Vec::new(),
513            vp_context: Some(Box::new(vp_context)),
514            max_vtl: Vtl::Vtl0,
515            parameter_areas: BTreeMap::new(),
516            isolation_type,
517            paravisor_present: false,
518            imported_regions_config_page: None,
519            expected_page_hashes_config_page: None,
520            snp_linux_direct: Some(config),
521        }
522    }
523}
524
525impl<R: IgvmLoaderRegister + GuestArch + 'static> IgvmLoader<R> {
526    pub fn new(with_paravisor: bool, isolation_type: LoaderIsolationType) -> Self {
527        let vp_context_builder: Option<Box<dyn VpContextBuilder<Register = R>>>;
528        let platform_header;
529        let max_vtl = if with_paravisor { Vtl::Vtl2 } else { Vtl::Vtl0 };
530        let initialization_headers;
531
532        match isolation_type {
533            LoaderIsolationType::None | LoaderIsolationType::Vbs { .. } => {
534                vp_context_builder = Some(Box::new(VbsVpContext::<R>::new(max_vtl)));
535
536                // Add VBS platform header
537                let info = IGVM_VHS_SUPPORTED_PLATFORM {
538                    compatibility_mask: DEFAULT_COMPATIBILITY_MASK,
539                    highest_vtl: max_vtl as u8,
540                    platform_type: IgvmPlatformType::VSM_ISOLATION,
541                    platform_version: igvm_defs::IGVM_VSM_ISOLATION_PLATFORM_VERSION,
542                    shared_gpa_boundary: 0,
543                };
544
545                platform_header = IgvmPlatformHeader::SupportedPlatform(info);
546                initialization_headers = Vec::new();
547            }
548            _ => {
549                let (header, init_headers, vp_builder) =
550                    R::init(with_paravisor, max_vtl, isolation_type);
551                platform_header = header;
552                initialization_headers = init_headers;
553                vp_context_builder = Some(vp_builder);
554            }
555        }
556
557        IgvmLoader {
558            accepted_ranges: RangeMap::new(),
559            relocatable_regions: RangeMap::new(),
560            required_memory: Vec::new(),
561            page_table_region: None,
562            platform_header,
563            initialization_headers,
564            directives: Vec::new(),
565            page_data_directives: Vec::new(),
566            vp_context: vp_context_builder,
567            max_vtl,
568            parameter_areas: BTreeMap::new(),
569            isolation_type,
570            paravisor_present: with_paravisor,
571            imported_regions_config_page: None,
572            expected_page_hashes_config_page: None,
573            snp_linux_direct: None,
574        }
575    }
576
577    /// Adds the fixed-memory contract required by a self-contained
578    /// Linux-direct image.
579    ///
580    /// The generic Linux loader imports only the pages that contain boot data
581    /// and does not call `verify_startup_memory_available`. UEFI and paravisor
582    /// loaders use that callback to emit their required-memory directives, but
583    /// a direct Linux IGVM must describe its complete startup RAM here. When
584    /// requested, this also imports every otherwise-unused RAM page as measured
585    /// zero data. The caller then places the BSP VMSA after those page updates.
586    fn finalize_snp_linux_direct(&mut self, config: SnpLinuxDirectConfig) -> anyhow::Result<()> {
587        anyhow::ensure!(
588            self.required_memory.is_empty(),
589            "SNP Linux-direct image already contains a required-memory directive"
590        );
591        let ram_size = config
592            .ram_page_count
593            .checked_mul(PAGE_SIZE_4K)
594            .context("RAM size overflow")?;
595        let number_of_bytes = ram_size
596            .try_into()
597            .context("RAM size does not fit in an IGVM required-memory directive")?;
598        self.directives.insert(
599            0,
600            IgvmDirectiveHeader::RequiredMemory {
601                gpa: 0,
602                compatibility_mask: DEFAULT_COMPATIBILITY_MASK,
603                number_of_bytes,
604                vtl2_protectable: false,
605            },
606        );
607        self.required_memory.push(RequiredMemory {
608            range: MemoryRange::new(0..ram_size),
609            vtl2_protectable: false,
610        });
611
612        let mut page_data_pages = BTreeSet::new();
613        for directive in &self.page_data_directives {
614            let IgvmDirectiveHeader::PageData { gpa, .. } = directive else {
615                unreachable!("page_data_directives contains only PageData")
616            };
617            anyhow::ensure!(
618                gpa.is_multiple_of(PAGE_SIZE_4K),
619                "unaligned page-data GPA {gpa:#x}"
620            );
621            let page = gpa / PAGE_SIZE_4K;
622            anyhow::ensure!(
623                page < config.ram_page_count,
624                "page-data GPA {gpa:#x} lies outside configured RAM"
625            );
626            anyhow::ensure!(
627                page_data_pages.insert(page),
628                "duplicate page-data GPA {gpa:#x}"
629            );
630        }
631
632        self.page_data_directives
633            .sort_unstable_by_key(|directive| match directive {
634                IgvmDirectiveHeader::PageData { gpa, .. } => *gpa,
635                _ => unreachable!("page_data_directives contains only PageData"),
636            });
637
638        let vmsa_count = self
639            .directives
640            .iter()
641            .filter(|directive| matches!(directive, IgvmDirectiveHeader::SnpVpContext { .. }))
642            .count();
643        anyhow::ensure!(
644            vmsa_count == 1,
645            "expected one SNP BSP VMSA context, found {vmsa_count}"
646        );
647        Ok(())
648    }
649
650    /// Compute both the combined SHA-384 over all shared (unmeasured) pages
651    /// (matching the value stored in `ImportedRegionsPageHeader::sha384_hash`)
652    /// and a per-page array of SHA-384s (one entry per 4 KB shared page, in
653    /// ascending-GPA order) suitable for the expected-page-hashes region.
654    ///
655    /// The per-page array is what the boot shim uses to identify which
656    /// individual pages diverged from the measured baseline on a hash
657    /// mismatch.
658    fn generate_cryptographic_hashes_of_shared_pages(
659        &mut self,
660    ) -> (Vec<u8>, Vec<loader_defs::paravisor::ExpectedPageHash>) {
661        // Sort the page data directives by GPA to ensure the hashes are
662        // consistent and that the per-page array is in ascending-GPA order.
663        self.page_data_directives
664            .sort_unstable_by_key(|directive| match directive {
665                IgvmDirectiveHeader::PageData { gpa, .. } => *gpa,
666                _ => unreachable!("all directives should be IgvmDirectiveHeader::PageData"),
667            });
668
669        let mut combined = Sha384::new();
670        let mut per_page = Vec::new();
671        self.page_data_directives.iter().for_each(|directive| {
672            if let IgvmDirectiveHeader::PageData {
673                gpa: _,
674                compatibility_mask: _,
675                flags,
676                data_type,
677                data,
678            } = directive
679            {
680                if *data_type == IgvmPageDataType::NORMAL && flags.shared() {
681                    // Measure the pages. If the data length is smaller than a page then zero extend
682                    // the data to a full page.
683                    let mut zero_data;
684                    let data_to_hash = if data.len() < PAGE_SIZE_4K as usize {
685                        zero_data = vec![0; PAGE_SIZE_4K as usize];
686                        zero_data[..data.len()].copy_from_slice(data);
687                        &zero_data
688                    } else {
689                        data
690                    };
691
692                    combined.update(data_to_hash);
693
694                    // Per-page hash: a fresh Sha384 fed the same zero-
695                    // extended page bytes.
696                    let mut per = Sha384::new();
697                    per.update(data_to_hash);
698                    let hash: [u8; 48] = per
699                        .finish()
700                        .as_bytes()
701                        .try_into()
702                        .expect("sha384 output should be 48 bytes");
703                    per_page.push(loader_defs::paravisor::ExpectedPageHash { sha384_hash: hash });
704                }
705            }
706        });
707        (combined.finish().to_vec(), per_page)
708    }
709
710    /// Finalize the loader state, returning an IGVM file.
711    pub fn finalize(mut self) -> anyhow::Result<IgvmOutput> {
712        // Finalize any VP state.
713        let mut state = Vec::new();
714        self.vp_context.take().unwrap().finalize(&mut state);
715
716        for context in state {
717            match context {
718                VpContextState::Page(VpContextPageState {
719                    page_base,
720                    page_count,
721                    acceptance,
722                    data,
723                }) => {
724                    self.import_pages(page_base, page_count, "vp-context-page", acceptance, &data)
725                        .context("failed to import vp context page")?;
726                }
727                VpContextState::Directive(directive) => {
728                    self.directives.push(directive);
729                }
730            }
731        }
732
733        // Merge adjacent accepted ranges with the same tag and acceptance
734        // to undo fragmentation from chunked imports.
735        self.accepted_ranges
736            .merge_adjacent(range_map_vec::u64_is_adjacent);
737
738        // Put list of accepted pages into the config region, if there
739        if let Some(page_base) = self.imported_regions_config_page {
740            // All shared pages have been imported. Generate both the combined
741            // cryptographic hash of the unaccepted (shared) imported pages
742            // (stored in the header) and the per-page hash array (imported
743            // separately below into the expected-page-hashes region).
744            let (combined_hash, per_page_hashes) =
745                self.generate_cryptographic_hashes_of_shared_pages();
746
747            // Emit the per-page expected-hashes region *first* if we have
748            // one, so that when we snapshot `imported_regions_data` below
749            // the descriptor list already covers it. Otherwise the shim
750            // would see this Exclusive region in the RMP (loader-pvalidated)
751            // but not in its imported-regions list, and would try to
752            // PVALIDATE it again -- resulting in `MemorySecurityViolation
753            // { carry_flag: 1 }` at the first page of the region.
754            //
755            // This is a separate measured region rather than an extension
756            // of the imported-regions page header so that older consumers
757            // of `ImportedRegionsPageHeader` see the exact same layout as
758            // before.
759            if let Some(hashes_page_base) = self.expected_page_hashes_config_page {
760                use loader_defs::paravisor::{
761                    EXPECTED_PAGE_HASH_MAX_COUNT, EXPECTED_PAGE_HASHES_MAGIC,
762                    EXPECTED_PAGE_HASHES_VERSION, ExpectedPageHashesHeader,
763                    PARAVISOR_MEASURED_VTL2_CONFIG_PAGE_HASHES_SIZE_PAGES,
764                };
765
766                let count = per_page_hashes.len();
767                if count > EXPECTED_PAGE_HASH_MAX_COUNT {
768                    anyhow::bail!(
769                        "expected-page-hashes region overflow: {} pages > {} max \
770                         (increase PARAVISOR_MEASURED_VTL2_CONFIG_PAGE_HASHES_SIZE_PAGES)",
771                        count,
772                        EXPECTED_PAGE_HASH_MAX_COUNT,
773                    );
774                }
775
776                let hashes_header = ExpectedPageHashesHeader {
777                    magic: EXPECTED_PAGE_HASHES_MAGIC,
778                    version: EXPECTED_PAGE_HASHES_VERSION,
779                    page_hash_count: count as u32,
780                    reserved: 0,
781                };
782
783                let region_bytes_capacity = PARAVISOR_MEASURED_VTL2_CONFIG_PAGE_HASHES_SIZE_PAGES
784                    as usize
785                    * PAGE_SIZE_4K as usize;
786                let mut region = Vec::with_capacity(region_bytes_capacity);
787                region.extend_from_slice(hashes_header.as_bytes());
788                region.extend_from_slice(per_page_hashes.as_bytes());
789                // Zero-pad to fill the whole reserved region so measurement
790                // sees a deterministic image regardless of how many pages
791                // this build ended up with.
792                region.resize(region_bytes_capacity, 0);
793
794                self.import_pages(
795                    hashes_page_base,
796                    PARAVISOR_MEASURED_VTL2_CONFIG_PAGE_HASHES_SIZE_PAGES,
797                    "loader-expected-page-hashes",
798                    BootPageAcceptance::Exclusive,
799                    &region,
800                )
801                .context("failed to import expected-page-hashes region")?;
802            }
803
804            // Snapshot accepted_ranges *after* the hashes region (if any)
805            // has been imported so it appears in the descriptor list.
806            let mut imported_regions_data: Vec<_> = self.imported_regions();
807
808            // Add this config page as well (still not in accepted_ranges
809            // until the import_pages below runs).
810            imported_regions_data.push(loader_defs::paravisor::ImportedRegionDescriptor::new(
811                page_base, 1, true,
812            ));
813
814            // The accepted regions have been guaranteed to not overlap,
815            // so just sort by the base page number
816            imported_regions_data.sort_by_key(|region| region.base_page_number);
817
818            let page_header = loader_defs::paravisor::ImportedRegionsPageHeader {
819                sha384_hash: combined_hash
820                    .as_bytes()
821                    .try_into()
822                    .expect("hash should be correct size"),
823            };
824
825            let mut imported_regions_page = page_header.as_bytes().to_vec();
826
827            // Append the (sorted) imported region data.
828            imported_regions_page.extend_from_slice(imported_regions_data.as_bytes());
829
830            // This list should be measured
831            self.import_pages(
832                page_base,
833                1,
834                "loader-imported-regions",
835                BootPageAcceptance::Exclusive,
836                imported_regions_page.as_bytes(),
837            )
838            .context("failed to import config regions")?;
839        }
840
841        // Finalize parameter pages with insert directives.
842        for ((page_base, _page_count), index) in self.parameter_areas.iter() {
843            self.directives.push(IgvmDirectiveHeader::ParameterInsert(
844                IGVM_VHS_PARAMETER_INSERT {
845                    gpa: page_base * PAGE_SIZE_4K,
846                    compatibility_mask: DEFAULT_COMPATIBILITY_MASK,
847                    parameter_area_index: *index,
848                },
849            ));
850        }
851
852        if let Some(config) = self.snp_linux_direct {
853            self.finalize_snp_linux_direct(config)?;
854        }
855
856        // Merge the page_data_directives into the others directives. This
857        // must be done before constructing the IGVM file so that subsequent
858        // measurement computation (in `IgvmSerializer`) sees the full set
859        // of directives.
860        self.directives.append(&mut self.page_data_directives);
861
862        // Display a report about the build igvm file's layout.
863        let map_file = MapFile {
864            isolation: self.isolation_type,
865            required_memory: self.required_memory,
866            accepted_ranges: self
867                .accepted_ranges
868                .iter()
869                .rev()
870                .map(|(range, info)| {
871                    (
872                        MemoryRange::from_4k_gpn_range(*range.start()..(range.end() + 1)),
873                        info.clone(),
874                    )
875                })
876                .collect(),
877            relocatable_regions: self
878                .relocatable_regions
879                .iter()
880                .rev()
881                .map(|(range, info)| {
882                    (
883                        MemoryRange::new(*range.start()..(range.end() + 1)),
884                        info.clone(),
885                    )
886                })
887                .collect(),
888            reported_ranges: Vec::new(),
889        };
890
891        // Create an IGVM file with the loader's internal state.
892        let igvm_file = IgvmFile::new(
893            R::igvm_revision(),
894            vec![self.platform_header],
895            self.initialization_headers,
896            self.directives,
897        )
898        .context("unable to create igvm file")?;
899
900        let output = IgvmOutput {
901            guest: igvm_file,
902            map: map_file,
903        };
904        Ok(output)
905    }
906
907    /// Accept a new page range with a given acceptance into the map of accepted
908    /// ranges.
909    fn accept_new_range(
910        &mut self,
911        page_base: u64,
912        page_count: u64,
913        tag: &str,
914        acceptance: BootPageAcceptance,
915    ) -> anyhow::Result<()> {
916        let page_end = page_base + page_count - 1;
917        match self.accepted_ranges.entry(page_base..=page_end) {
918            Entry::Overlapping(entry) => {
919                let (overlap_start, overlap_end, ref overlap_info) = *entry.get();
920                Err(anyhow::anyhow!(
921                    "{} at {} ({:?}) overlaps {} at {}",
922                    tag,
923                    MemoryRange::from_4k_gpn_range(page_base..page_end + 1),
924                    acceptance,
925                    overlap_info.tag,
926                    MemoryRange::from_4k_gpn_range(overlap_start..overlap_end + 1),
927                ))
928            }
929            Entry::Vacant(entry) => {
930                entry.insert(RangeInfo {
931                    tag: tag.to_string(),
932                    acceptance,
933                });
934                Ok(())
935            }
936        }
937    }
938
939    fn imported_regions(&self) -> Vec<loader_defs::paravisor::ImportedRegionDescriptor> {
940        // N.B. If the imported regions page grows too large, contiguous
941        // regions with the same acceptance type (but different tags) could
942        // be coalesced here to reduce the descriptor count.
943        self.accepted_ranges
944            .iter()
945            .map(|(r, info)| {
946                loader_defs::paravisor::ImportedRegionDescriptor::new(
947                    *r.start(),
948                    r.end() - r.start() + 1,
949                    info.acceptance != BootPageAcceptance::Shared,
950                )
951            })
952            .collect()
953    }
954
955    /// The guest architecture used by this loader.
956    pub fn arch(&self) -> GuestArchKind {
957        R::arch()
958    }
959
960    /// Returns the first GPA after all imported page-data directives.
961    pub fn next_available_gpa(&self) -> anyhow::Result<u64> {
962        self.page_data_directives
963            .iter()
964            .filter_map(|directive| match directive {
965                IgvmDirectiveHeader::PageData { gpa, .. } => Some(*gpa),
966                _ => None,
967            })
968            .max()
969            .map_or(Ok(0), |gpa| {
970                gpa.checked_add(PAGE_SIZE_4K)
971                    .context("next imported address overflow")
972            })
973    }
974
975    /// Returns unimported RAM page ranges for an SNP Linux-direct image.
976    pub fn unimported_ram_ranges(
977        &self,
978        additional_imported_pages: impl IntoIterator<Item = u64>,
979    ) -> anyhow::Result<Vec<std::ops::Range<u64>>> {
980        let config = self
981            .snp_linux_direct
982            .context("unimported RAM ranges require an SNP Linux-direct loader")?;
983        let mut imported_pages = BTreeSet::new();
984        for (range, _) in self.accepted_ranges.iter() {
985            for page in range.clone() {
986                if page < config.ram_page_count {
987                    imported_pages.insert(page);
988                }
989            }
990        }
991        imported_pages.extend(additional_imported_pages);
992        anyhow::ensure!(
993            imported_pages
994                .last()
995                .is_none_or(|page| *page < config.ram_page_count),
996            "an imported page lies outside RAM"
997        );
998
999        let mut ranges = Vec::new();
1000        let mut cursor = 0;
1001        for page in imported_pages {
1002            if cursor < page {
1003                ranges.push(cursor..page);
1004            }
1005            cursor = page + 1;
1006        }
1007        if cursor < config.ram_page_count {
1008            ranges.push(cursor..config.ram_page_count);
1009        }
1010        Ok(ranges)
1011    }
1012
1013    pub fn loader(&mut self) -> IgvmVtlLoader<'_, R> {
1014        IgvmVtlLoader {
1015            vtl: self.max_vtl,
1016            loader: self,
1017            vp_context: None,
1018        }
1019    }
1020
1021    fn import_pages(
1022        &mut self,
1023        page_base: u64,
1024        page_count: u64,
1025        debug_tag: &'static str,
1026        acceptance: BootPageAcceptance,
1027        mut data: &[u8],
1028    ) -> Result<(), anyhow::Error> {
1029        tracing::debug!(
1030            page_base,
1031            ?acceptance,
1032            page_count,
1033            data_size = data.len(),
1034            "Importing page",
1035        );
1036
1037        anyhow::ensure!(page_count != 0, "cannot import an empty page range");
1038        page_base
1039            .checked_add(page_count)
1040            .context("imported page range overflow")?;
1041
1042        // Pages must not overlap already accepted ranges
1043        self.accept_new_range(page_base, page_count, debug_tag, acceptance)?;
1044
1045        // Page count must be larger or equal to data.
1046        if page_count * PAGE_SIZE_4K < data.len() as u64 {
1047            anyhow::bail!(
1048                "data len {:x} is larger than page_count {page_count:x}",
1049                data.len()
1050            );
1051        }
1052
1053        // VpContext imports are handled differently, as they have a different IGVM header
1054        // type than normal data pages.
1055        if acceptance == BootPageAcceptance::VpContext {
1056            // This is only supported on SNP currently.
1057            match self.isolation_type {
1058                LoaderIsolationType::Snp { .. } => {}
1059                _ => {
1060                    anyhow::bail!("vpcontext acceptance only supported on SNP");
1061                }
1062            }
1063
1064            // The VP context builder produces the architectural VMSA
1065            // (`x86defs::snp::SevVmsa`, 1648 bytes); the igvm crate's `SevVmsa`
1066            // is padded out to a full 4K page, so accept input in the range
1067            // [architectural size, padded size] and zero-pad it to the padded
1068            // size before reading. Anything smaller than the architectural size
1069            // would be silently zero-extended into a malformed VMSA, so reject
1070            // it.
1071            if data.len() < size_of::<x86defs::snp::SevVmsa>() {
1072                anyhow::bail!(
1073                    "data len {:x} is smaller than the architectural VMSA size {:x}",
1074                    data.len(),
1075                    size_of::<x86defs::snp::SevVmsa>()
1076                );
1077            }
1078            if data.len() > size_of::<SevVmsa>() {
1079                anyhow::bail!(
1080                    "data len {:x} exceeds VMSA size {:x}",
1081                    data.len(),
1082                    size_of::<SevVmsa>()
1083                );
1084            }
1085
1086            // Page count must be 1.
1087            if page_count != 1 {
1088                anyhow::bail!("page count {page_count:x} for snp vmsa is not 1");
1089            }
1090
1091            let mut padded = vec![0u8; size_of::<SevVmsa>()];
1092            padded[..data.len()].copy_from_slice(data);
1093
1094            let vmsa = SevVmsa::read_from_bytes(padded.as_slice()).expect("should be correct size");
1095            if let Some(config) = self.snp_linux_direct {
1096                anyhow::ensure!(vmsa.rip != 0, "Linux loader did not provide an entry point");
1097                anyhow::ensure!(
1098                    vmsa.cr3 & config.c_bit_mask != 0,
1099                    "initial CR3 does not contain the configured SNP C-bit"
1100                );
1101                anyhow::ensure!(
1102                    !vmsa.sev_features.vtom() && vmsa.virtual_tom == 0,
1103                    "C-bit image must not enable vTOM"
1104                );
1105            }
1106            self.directives.push(IgvmDirectiveHeader::SnpVpContext {
1107                gpa: page_base * PAGE_SIZE_4K,
1108                compatibility_mask: DEFAULT_COMPATIBILITY_MASK,
1109                vp_index: 0,
1110                vmsa: Box::new(vmsa),
1111            });
1112        } else {
1113            for page in page_base..page_base + page_count {
1114                let (data_type, flags) = match acceptance {
1115                    BootPageAcceptance::Exclusive => {
1116                        (IgvmPageDataType::NORMAL, IgvmPageDataFlags::new())
1117                    }
1118                    BootPageAcceptance::ExclusiveUnmeasured => (
1119                        IgvmPageDataType::NORMAL,
1120                        IgvmPageDataFlags::new().with_unmeasured(true),
1121                    ),
1122                    BootPageAcceptance::SecretsPage => {
1123                        (IgvmPageDataType::SECRETS, IgvmPageDataFlags::new())
1124                    }
1125                    BootPageAcceptance::CpuidPage => {
1126                        (IgvmPageDataType::CPUID_DATA, IgvmPageDataFlags::new())
1127                    }
1128                    BootPageAcceptance::CpuidExtendedStatePage => {
1129                        (IgvmPageDataType::CPUID_XF, IgvmPageDataFlags::new())
1130                    }
1131                    BootPageAcceptance::VpContext => unreachable!(),
1132                    BootPageAcceptance::Shared => (
1133                        IgvmPageDataType::NORMAL,
1134                        IgvmPageDataFlags::new().with_shared(true),
1135                    ),
1136                };
1137
1138                // Split data slice into data to be imported for this page and remaining.
1139                let import_data_len = std::cmp::min(PAGE_SIZE_4K as usize, data.len());
1140                let (import_data, data_remaining) = data.split_at(import_data_len);
1141                data = data_remaining;
1142
1143                self.page_data_directives
1144                    .push(IgvmDirectiveHeader::PageData {
1145                        gpa: page * PAGE_SIZE_4K,
1146                        compatibility_mask: DEFAULT_COMPATIBILITY_MASK,
1147                        flags,
1148                        data_type,
1149                        data: import_data.to_vec(),
1150                    });
1151            }
1152        }
1153
1154        Ok(())
1155    }
1156}
1157
1158impl<R: IgvmLoaderRegister + GuestArch + 'static> ImageLoad<R> for IgvmVtlLoader<'_, R> {
1159    fn isolation_config(&self) -> IsolationConfig {
1160        match self.loader.isolation_type {
1161            LoaderIsolationType::None => IsolationConfig {
1162                paravisor_present: self.loader.paravisor_present,
1163                isolation_type: IsolationType::None,
1164                shared_gpa_boundary_bits: None,
1165            },
1166            LoaderIsolationType::Vbs { .. } => IsolationConfig {
1167                paravisor_present: self.loader.paravisor_present,
1168                isolation_type: IsolationType::Vbs,
1169                shared_gpa_boundary_bits: None,
1170            },
1171            LoaderIsolationType::Snp {
1172                shared_gpa_boundary_bits,
1173                policy: _,
1174                injection_type: _,
1175                secure_avic: _,
1176            } => IsolationConfig {
1177                paravisor_present: self.loader.paravisor_present,
1178                isolation_type: IsolationType::Snp,
1179                shared_gpa_boundary_bits,
1180            },
1181            LoaderIsolationType::Tdx { .. } => IsolationConfig {
1182                paravisor_present: self.loader.paravisor_present,
1183                isolation_type: IsolationType::Tdx,
1184                shared_gpa_boundary_bits: Some(TDX_SHARED_GPA_BOUNDARY_BITS),
1185            },
1186        }
1187    }
1188
1189    fn create_parameter_area(
1190        &mut self,
1191        page_base: u64,
1192        page_count: u32,
1193        debug_tag: &str,
1194    ) -> anyhow::Result<ParameterAreaIndex> {
1195        self.create_parameter_area_with_data(page_base, page_count, debug_tag, &[])
1196    }
1197
1198    fn create_parameter_area_with_data(
1199        &mut self,
1200        page_base: u64,
1201        page_count: u32,
1202        debug_tag: &str,
1203        initial_data: &[u8],
1204    ) -> anyhow::Result<ParameterAreaIndex> {
1205        let area_id = (page_base, page_count);
1206
1207        // Allocate a new parameter area, that must not overlap other accepted ranges.
1208        self.loader.accept_new_range(
1209            page_base,
1210            page_count as u64,
1211            debug_tag,
1212            BootPageAcceptance::ExclusiveUnmeasured,
1213        )?;
1214
1215        let index: u32 = self
1216            .loader
1217            .parameter_areas
1218            .len()
1219            .try_into()
1220            .expect("parameter area greater than u32");
1221        self.loader.parameter_areas.insert(area_id, index);
1222
1223        // Add the newly allocated parameter area index to headers.
1224        self.loader
1225            .directives
1226            .push(IgvmDirectiveHeader::ParameterArea {
1227                number_of_bytes: page_count as u64 * PAGE_SIZE_4K,
1228                parameter_area_index: index,
1229                initial_data: initial_data.to_vec(),
1230            });
1231
1232        tracing::debug!(
1233            index,
1234            page_base,
1235            page_count,
1236            initial_data_len = initial_data.len(),
1237            "Creating new parameter area",
1238        );
1239
1240        Ok(ParameterAreaIndex(index))
1241    }
1242
1243    fn import_parameter(
1244        &mut self,
1245        parameter_area: ParameterAreaIndex,
1246        byte_offset: u32,
1247        parameter_type: IgvmParameterType,
1248    ) -> anyhow::Result<()> {
1249        let index = parameter_area.0;
1250
1251        if index >= self.loader.parameter_areas.len() as u32 {
1252            anyhow::bail!("invalid parameter area index: {:x}", index);
1253        }
1254
1255        tracing::debug!(
1256            ?parameter_type,
1257            parameter_area_index = parameter_area.0,
1258            byte_offset,
1259            "Importing parameter",
1260        );
1261
1262        let info = IGVM_VHS_PARAMETER {
1263            parameter_area_index: index,
1264            byte_offset,
1265        };
1266
1267        let header = match parameter_type {
1268            IgvmParameterType::VpCount => IgvmDirectiveHeader::VpCount(info),
1269            IgvmParameterType::Srat => IgvmDirectiveHeader::Srat(info),
1270            IgvmParameterType::Madt => IgvmDirectiveHeader::Madt(info),
1271            IgvmParameterType::Slit => IgvmDirectiveHeader::Slit(info),
1272            IgvmParameterType::Pptt => IgvmDirectiveHeader::Pptt(info),
1273            IgvmParameterType::MmioRanges => IgvmDirectiveHeader::MmioRanges(info),
1274            IgvmParameterType::MemoryMap => IgvmDirectiveHeader::MemoryMap(info),
1275            IgvmParameterType::CommandLine => IgvmDirectiveHeader::CommandLine(info),
1276            IgvmParameterType::DeviceTree => IgvmDirectiveHeader::DeviceTree(info),
1277        };
1278
1279        self.loader.directives.push(header);
1280
1281        Ok(())
1282    }
1283
1284    fn import_pages(
1285        &mut self,
1286        page_base: u64,
1287        page_count: u64,
1288        debug_tag: &'static str,
1289        acceptance: BootPageAcceptance,
1290        data: &[u8],
1291    ) -> anyhow::Result<()> {
1292        self.loader
1293            .import_pages(page_base, page_count, debug_tag, acceptance, data)
1294    }
1295
1296    fn import_vp_register(&mut self, register: R) -> anyhow::Result<()> {
1297        if let Some(vp_context) = &mut self.vp_context {
1298            vp_context.import_vp_register(register)
1299        } else {
1300            self.loader
1301                .vp_context
1302                .as_mut()
1303                .unwrap()
1304                .import_vp_register(register);
1305        }
1306
1307        Ok(())
1308    }
1309
1310    fn verify_startup_memory_available(
1311        &mut self,
1312        page_base: u64,
1313        page_count: u64,
1314        memory_type: loader::importer::StartupMemoryType,
1315    ) -> anyhow::Result<()> {
1316        let gpa = page_base * PAGE_SIZE_4K;
1317        let compatibility_mask = DEFAULT_COMPATIBILITY_MASK;
1318        let number_of_bytes = (page_count * PAGE_SIZE_4K)
1319            .try_into()
1320            .expect("startup memory request overflowed u32");
1321
1322        tracing::trace!(
1323            page_base,
1324            page_count,
1325            ?memory_type,
1326            number_of_bytes,
1327            "verify memory"
1328        );
1329
1330        // Set VTL2 protectable flag on isolation types which make sense
1331        // TODO SNP: Temporarily allow this on all isolation types to force the host to generate
1332        // the correct device tree structures.
1333        let vtl2_protectable =
1334            memory_type == loader::importer::StartupMemoryType::Vtl2ProtectableRam;
1335
1336        self.loader
1337            .directives
1338            .push(IgvmDirectiveHeader::RequiredMemory {
1339                gpa,
1340                compatibility_mask,
1341                number_of_bytes,
1342                vtl2_protectable,
1343            });
1344
1345        self.loader.required_memory.push(RequiredMemory {
1346            range: MemoryRange::new(gpa..gpa + number_of_bytes as u64),
1347            vtl2_protectable,
1348        });
1349
1350        Ok(())
1351    }
1352
1353    fn set_vp_context_page(&mut self, page_base: u64) -> anyhow::Result<()> {
1354        if let Some(config) = &mut self.loader.snp_linux_direct {
1355            anyhow::ensure!(
1356                page_base < config.ram_page_count,
1357                "Linux-selected VP context page lies outside RAM"
1358            );
1359            if config.vmsa_page.is_some() {
1360                return Ok(());
1361            }
1362            config.vmsa_page = Some(page_base);
1363        }
1364        self.loader
1365            .vp_context
1366            .as_mut()
1367            .unwrap()
1368            .set_vp_context_memory(page_base);
1369
1370        Ok(())
1371    }
1372
1373    fn relocation_region(
1374        &mut self,
1375        gpa: u64,
1376        size_bytes: u64,
1377        relocation_alignment: u64,
1378        minimum_relocation_gpa: u64,
1379        maximum_relocation_gpa: u64,
1380        apply_rip_offset: bool,
1381        apply_gdtr_offset: bool,
1382        vp_index: u16,
1383    ) -> anyhow::Result<()> {
1384        if let Some(overlap) = self
1385            .loader
1386            .relocatable_regions
1387            .get_range(gpa..=(gpa + size_bytes - 1))
1388        {
1389            anyhow::bail!(
1390                "new relocation region overlaps existing region {:?}",
1391                overlap
1392            );
1393        }
1394
1395        if !size_bytes.is_multiple_of(PAGE_SIZE_4K) {
1396            anyhow::bail!("relocation size {size_bytes:#x} must be a multiple of 4K");
1397        }
1398
1399        if !relocation_alignment.is_multiple_of(PAGE_SIZE_4K) {
1400            anyhow::bail!(
1401                "relocation alignment {relocation_alignment:#x} must be a multiple of 4K"
1402            );
1403        }
1404
1405        if !gpa.is_multiple_of(relocation_alignment) {
1406            anyhow::bail!(
1407                "relocation base {gpa:#x} must be aligned to relocation alignment {relocation_alignment:#x}"
1408            );
1409        }
1410
1411        if !minimum_relocation_gpa.is_multiple_of(relocation_alignment) {
1412            anyhow::bail!(
1413                "relocation minimum GPA {minimum_relocation_gpa:#x} must be aligned to relocation alignment {relocation_alignment:#x}"
1414            );
1415        }
1416
1417        if !maximum_relocation_gpa.is_multiple_of(relocation_alignment) {
1418            anyhow::bail!(
1419                "relocation maximum GPA {maximum_relocation_gpa:#x} must be aligned to relocation alignment {relocation_alignment:#x}"
1420            );
1421        }
1422
1423        self.loader
1424            .initialization_headers
1425            .push(IgvmInitializationHeader::RelocatableRegion {
1426                compatibility_mask: DEFAULT_COMPATIBILITY_MASK,
1427                relocation_alignment,
1428                relocation_region_gpa: gpa,
1429                relocation_region_size: size_bytes,
1430                minimum_relocation_gpa,
1431                maximum_relocation_gpa,
1432                is_vtl2: self.vtl == Vtl::Vtl2,
1433                apply_rip_offset,
1434                apply_gdtr_offset,
1435                vp_index,
1436                vtl: to_igvm_vtl(self.vtl),
1437            });
1438
1439        self.loader.relocatable_regions.insert(
1440            gpa..=gpa + size_bytes - 1,
1441            RelocationType::Normal(IgvmRelocatableRegion {
1442                base_gpa: gpa,
1443                size: size_bytes,
1444                minimum_relocation_gpa,
1445                maximum_relocation_gpa,
1446                relocation_alignment,
1447                is_vtl2: self.vtl == Vtl::Vtl2,
1448                apply_rip_offset,
1449                apply_gdtr_offset,
1450                vp_index,
1451                vtl: to_igvm_vtl(self.vtl),
1452            }),
1453        );
1454
1455        Ok(())
1456    }
1457
1458    fn page_table_relocation(
1459        &mut self,
1460        page_table_gpa: u64,
1461        size_pages: u64,
1462        used_size_pages: u64,
1463        vp_index: u16,
1464    ) -> anyhow::Result<()> {
1465        // can only be one set
1466        if let Some(region) = &self.loader.page_table_region {
1467            anyhow::bail!("page table relocation already set {:?}", region)
1468        }
1469
1470        if used_size_pages > size_pages {
1471            anyhow::bail!(
1472                "used size pages {used_size_pages:#x} cannot be greater than size pages {size_pages:#x}"
1473            );
1474        }
1475
1476        let end_gpa = page_table_gpa + size_pages * PAGE_SIZE_4K - 1;
1477
1478        // cannot override other relocatable regions
1479        if let Some(overlap) = self
1480            .loader
1481            .relocatable_regions
1482            .get_range(page_table_gpa..=end_gpa)
1483        {
1484            anyhow::bail!(
1485                "new page table relocation region overlaps existing region {:?}",
1486                overlap
1487            );
1488        }
1489
1490        self.loader.initialization_headers.push(
1491            IgvmInitializationHeader::PageTableRelocationRegion {
1492                compatibility_mask: DEFAULT_COMPATIBILITY_MASK,
1493                gpa: page_table_gpa,
1494                size: size_pages * PAGE_SIZE_4K,
1495                used_size: used_size_pages * PAGE_SIZE_4K,
1496                vp_index,
1497                vtl: to_igvm_vtl(self.vtl),
1498            },
1499        );
1500
1501        let region = PageTableRegion {
1502            gpa: page_table_gpa,
1503            size_pages,
1504            used_size_pages,
1505        };
1506
1507        self.loader.relocatable_regions.insert(
1508            page_table_gpa..=end_gpa,
1509            RelocationType::PageTable(region.clone()),
1510        );
1511
1512        self.loader.page_table_region = Some(region);
1513
1514        Ok(())
1515    }
1516
1517    fn set_imported_regions_config_page(&mut self, page_base: u64) {
1518        self.loader.imported_regions_config_page = Some(page_base);
1519    }
1520
1521    fn set_expected_page_hashes_config_page(&mut self, page_base: u64) {
1522        self.loader.expected_page_hashes_config_page = Some(page_base);
1523    }
1524}
1525
1526#[cfg(test)]
1527mod tests {
1528    use super::IgvmLoader;
1529    use super::*;
1530    use igvm::IgvmSerializer;
1531    use loader::importer::BootPageAcceptance;
1532    use loader::importer::ImageLoad;
1533    use loader_defs::paravisor::ImportedRegionDescriptor;
1534
1535    #[test]
1536    fn reported_ranges_appear_in_map_output() {
1537        let mut map = MapFile {
1538            isolation: LoaderIsolationType::None,
1539            required_memory: Vec::new(),
1540            accepted_ranges: Vec::new(),
1541            relocatable_regions: Vec::new(),
1542            reported_ranges: Vec::new(),
1543        };
1544        map.report_range(
1545            MemoryRange::new(0x1000..0x3000),
1546            "snp-bootshim-accepted-ram [PVALIDATE]",
1547        );
1548
1549        assert!(map.to_string().contains(concat!(
1550            "IGVM file reported ranges:\n",
1551            "  0000000000001000 - 0000000000003000 (0x2000 bytes) ",
1552            "snp-bootshim-accepted-ram [PVALIDATE]\n",
1553        )));
1554    }
1555
1556    #[test]
1557    fn test_snp_measurement() {
1558        use igvm_defs::SnpPolicy;
1559        let ref_ld: [u8; 48] = [
1560            136, 154, 25, 56, 108, 130, 226, 33, 155, 222, 211, 233, 42, 118, 78, 140, 0, 194, 155,
1561            150, 109, 4, 166, 98, 188, 166, 207, 223, 236, 100, 123, 144, 81, 153, 86, 83, 57, 7,
1562            131, 132, 101, 87, 145, 50, 99, 215, 28, 79,
1563        ];
1564
1565        let mut loader = IgvmLoader::<X86Register>::new(
1566            true,
1567            LoaderIsolationType::Snp {
1568                shared_gpa_boundary_bits: Some(39),
1569                policy: SnpPolicy::from((0x1 << 17) | (0x1 << 16) | (0x1f)),
1570                injection_type: InjectionType::Restricted,
1571                secure_avic: SecureAvic::Enabled,
1572            },
1573        );
1574        let data = vec![0, 5];
1575        loader
1576            .import_pages(0, 5, "data", BootPageAcceptance::Exclusive, &data)
1577            .unwrap();
1578        loader
1579            .import_pages(5, 5, "data", BootPageAcceptance::ExclusiveUnmeasured, &data)
1580            .unwrap();
1581        loader
1582            .import_pages(10, 1, "data", BootPageAcceptance::Exclusive, &data)
1583            .unwrap();
1584        loader
1585            .import_pages(20, 1, "data", BootPageAcceptance::Shared, &data)
1586            .unwrap();
1587
1588        let igvm_output = loader.finalize().unwrap();
1589        let serializer = IgvmSerializer::new(&igvm_output.guest).unwrap();
1590        let measurement = serializer
1591            .measurement_for(IgvmPlatformType::SEV_SNP)
1592            .expect("snp measurement");
1593        assert_eq!(ref_ld.as_slice(), measurement.digest.as_slice());
1594    }
1595
1596    #[test]
1597    fn test_tdx_measurement() {
1598        let ref_mrtd: [u8; 48] = [
1599            200, 137, 46, 40, 88, 218, 231, 7, 90, 231, 125, 247, 18, 243, 41, 158, 32, 81, 49, 30,
1600            168, 163, 220, 29, 216, 52, 151, 164, 255, 25, 88, 0, 246, 62, 147, 140, 34, 201, 70,
1601            89, 34, 32, 239, 182, 77, 169, 96, 235,
1602        ];
1603
1604        let mut loader = IgvmLoader::<X86Register>::new(
1605            true,
1606            LoaderIsolationType::Tdx {
1607                policy: TdxPolicy::new()
1608                    .with_debug_allowed(0u8)
1609                    .with_sept_ve_disable(0u8),
1610            },
1611        );
1612        let data = vec![0, 5];
1613        loader
1614            .import_pages(0, 5, "data", BootPageAcceptance::Exclusive, &data)
1615            .unwrap();
1616        loader
1617            .import_pages(5, 5, "data", BootPageAcceptance::ExclusiveUnmeasured, &data)
1618            .unwrap();
1619        loader
1620            .import_pages(10, 1, "data", BootPageAcceptance::Exclusive, &data)
1621            .unwrap();
1622        loader
1623            .import_pages(20, 1, "data", BootPageAcceptance::Shared, &data)
1624            .unwrap();
1625
1626        let igvm_output = loader.finalize().unwrap();
1627        let serializer = IgvmSerializer::new(&igvm_output.guest).unwrap();
1628        let measurement = serializer
1629            .measurement_for(IgvmPlatformType::TDX)
1630            .expect("tdx measurement");
1631        assert_eq!(ref_mrtd.as_slice(), measurement.digest.as_slice());
1632    }
1633
1634    #[test]
1635    fn test_vbs_digest() {
1636        let ref_digest: [u8; 32] = [
1637            0x30, 0x13, 0x4C, 0x9B, 0xB8, 0x9C, 0xD7, 0x2D, 0x8A, 0x41, 0x8D, 0x1E, 0x7A, 0xFB,
1638            0x75, 0x92, 0x7F, 0x45, 0xE8, 0x57, 0x1D, 0xDA, 0x7A, 0xC7, 0xBE, 0x87, 0xD4, 0xB6,
1639            0xC7, 0x2C, 0xA6, 0x4C,
1640        ];
1641        let mut loader = IgvmLoader::<X86Register>::new(
1642            true,
1643            LoaderIsolationType::Vbs {
1644                enable_debug: false,
1645            },
1646        );
1647        {
1648            let mut loader = loader.loader();
1649
1650            let data = vec![0, 5];
1651            loader
1652                .import_pages(0, 5, "data", BootPageAcceptance::Exclusive, &data)
1653                .unwrap();
1654            loader
1655                .import_pages(5, 5, "data", BootPageAcceptance::ExclusiveUnmeasured, &data)
1656                .unwrap();
1657            loader
1658                .import_pages(10, 1, "data", BootPageAcceptance::Exclusive, &data)
1659                .unwrap();
1660            loader
1661                .import_pages(20, 1, "data", BootPageAcceptance::Shared, &data)
1662                .unwrap();
1663        }
1664
1665        let igvm_output = loader.finalize().unwrap();
1666        let serializer = IgvmSerializer::new(&igvm_output.guest).unwrap();
1667        let measurement = serializer
1668            .measurement_for(IgvmPlatformType::VSM_ISOLATION)
1669            .expect("vbs measurement");
1670        assert_eq!(ref_digest.as_slice(), measurement.digest.as_slice());
1671    }
1672
1673    #[test]
1674    fn test_accepted_regions() {
1675        let mut loader = IgvmLoader::<X86Register>::new(true, LoaderIsolationType::None);
1676
1677        let data = vec![0, 5];
1678        loader
1679            .import_pages(0, 5, "test1", BootPageAcceptance::Exclusive, &data)
1680            .unwrap();
1681
1682        loader
1683            .import_pages(15, 5, "test2", BootPageAcceptance::Exclusive, &data)
1684            .unwrap();
1685
1686        loader
1687            .import_pages(10, 5, "test3", BootPageAcceptance::Exclusive, &data)
1688            .unwrap();
1689
1690        assert_eq!(
1691            loader.imported_regions(),
1692            vec![
1693                ImportedRegionDescriptor::new(15, 5, true),
1694                ImportedRegionDescriptor::new(10, 5, true),
1695                ImportedRegionDescriptor::new(0, 5, true),
1696            ]
1697        );
1698
1699        loader
1700            .import_pages(20, 10, "test1", BootPageAcceptance::Exclusive, &data)
1701            .unwrap();
1702
1703        loader
1704            .import_pages(30, 1, "test2", BootPageAcceptance::Exclusive, &data)
1705            .unwrap();
1706
1707        assert_eq!(
1708            loader.imported_regions(),
1709            vec![
1710                ImportedRegionDescriptor::new(30, 1, true),
1711                ImportedRegionDescriptor::new(20, 10, true),
1712                ImportedRegionDescriptor::new(15, 5, true),
1713                ImportedRegionDescriptor::new(10, 5, true),
1714                ImportedRegionDescriptor::new(0, 5, true),
1715            ]
1716        );
1717    }
1718}