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::identity_mapping::Measurement;
7use crate::identity_mapping::SnpMeasurement;
8use crate::identity_mapping::TdxMeasurement;
9use crate::identity_mapping::VbsMeasurement;
10use crate::signed_measurement::generate_snp_measurement;
11use crate::signed_measurement::generate_tdx_measurement;
12use crate::signed_measurement::generate_vbs_measurement;
13use crate::vp_context_builder::VpContextBuilder;
14use crate::vp_context_builder::VpContextPageState;
15use crate::vp_context_builder::VpContextState;
16use crate::vp_context_builder::snp::InjectionType;
17use crate::vp_context_builder::snp::SecureAvic;
18use crate::vp_context_builder::snp::SnpHardwareContext;
19use crate::vp_context_builder::tdx::TdxHardwareContext;
20use crate::vp_context_builder::vbs::VbsRegister;
21use crate::vp_context_builder::vbs::VbsVpContext;
22use anyhow::Context;
23use hvdef::Vtl;
24use igvm::IgvmDirectiveHeader;
25use igvm::IgvmFile;
26use igvm::IgvmInitializationHeader;
27use igvm::IgvmPlatformHeader;
28use igvm::IgvmRelocatableRegion;
29use igvm::IgvmRevision;
30use igvm::snp_defs::SevVmsa;
31use igvm_defs::IGVM_VHS_PARAMETER;
32use igvm_defs::IGVM_VHS_PARAMETER_INSERT;
33use igvm_defs::IGVM_VHS_SUPPORTED_PLATFORM;
34use igvm_defs::IgvmPageDataFlags;
35use igvm_defs::IgvmPageDataType;
36use igvm_defs::IgvmPlatformType;
37use igvm_defs::PAGE_SIZE_4K;
38use igvm_defs::SnpPolicy;
39use igvm_defs::TdxPolicy;
40use loader::importer::Aarch64Register;
41use loader::importer::BootPageAcceptance;
42use loader::importer::GuestArch;
43use loader::importer::GuestArchKind;
44use loader::importer::IgvmParameterType;
45use loader::importer::ImageLoad;
46use loader::importer::IsolationConfig;
47use loader::importer::IsolationType;
48use loader::importer::ParameterAreaIndex;
49use loader::importer::X86Register;
50use memory_range::MemoryRange;
51use range_map_vec::Entry;
52use range_map_vec::RangeMap;
53use sha2::Digest;
54use sha2::Sha384;
55use std::collections::BTreeMap;
56use std::fmt::Debug;
57use std::fmt::Display;
58use zerocopy::FromBytes;
59use zerocopy::IntoBytes;
60
61pub const DEFAULT_COMPATIBILITY_MASK: u32 = 0x1;
62
63const TDX_SHARED_GPA_BOUNDARY_BITS: u8 = 47;
64
65fn to_igvm_vtl(vtl: Vtl) -> igvm::hv_defs::Vtl {
66    match vtl {
67        Vtl::Vtl0 => igvm::hv_defs::Vtl::Vtl0,
68        Vtl::Vtl1 => igvm::hv_defs::Vtl::Vtl1,
69        Vtl::Vtl2 => igvm::hv_defs::Vtl::Vtl2,
70    }
71}
72
73/// Page table relocation information kept for debugging purposes.
74// Allow dead code because clippy doesn't count #[derive(Debug)] as non-dead code usage.
75#[expect(dead_code)]
76#[derive(Debug, Clone)]
77struct PageTableRegion {
78    gpa: u64,
79    size_pages: u64,
80    used_size_pages: u64,
81}
82
83#[derive(Debug, Clone)]
84enum RelocationType {
85    PageTable(PageTableRegion),
86    Normal(IgvmRelocatableRegion),
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
90struct RangeInfo {
91    tag: String,
92    acceptance: BootPageAcceptance,
93}
94
95pub struct IgvmLoader<R: VbsRegister + GuestArch> {
96    accepted_ranges: RangeMap<u64, RangeInfo>,
97    relocatable_regions: RangeMap<u64, RelocationType>,
98    required_memory: Vec<RequiredMemory>,
99    page_table_region: Option<PageTableRegion>,
100    platform_header: IgvmPlatformHeader,
101    initialization_headers: Vec<IgvmInitializationHeader>,
102    directives: Vec<IgvmDirectiveHeader>,
103    page_data_directives: Vec<IgvmDirectiveHeader>,
104    vp_context: Option<Box<dyn VpContextBuilder<Register = R>>>,
105    max_vtl: Vtl,
106    parameter_areas: BTreeMap<(u64, u32), u32>,
107    isolation_type: LoaderIsolationType,
108    paravisor_present: bool,
109    imported_regions_config_page: Option<u64>,
110}
111
112pub struct IgvmVtlLoader<'a, R: VbsRegister + GuestArch> {
113    loader: &'a mut IgvmLoader<R>,
114    vtl: Vtl,
115    vp_context: Option<VbsVpContext<R>>,
116}
117
118impl<R: VbsRegister + GuestArch> IgvmVtlLoader<'_, R> {
119    pub fn loader(&self) -> &IgvmLoader<R> {
120        self.loader
121    }
122
123    /// Returns a loader for importing an inner image as part of the actual
124    /// (paravisor) image to load.
125    ///
126    /// Use `take_vp_context` on the returned loader to get the VP context that
127    /// the paravisor should load.
128    pub fn nested_loader(&mut self) -> IgvmVtlLoader<'_, R> {
129        IgvmVtlLoader {
130            loader: &mut *self.loader,
131            vtl: Vtl::Vtl0,
132            vp_context: Some(VbsVpContext::new(self.vtl)),
133        }
134    }
135
136    pub fn take_vp_context(&mut self) -> Vec<u8> {
137        self.vp_context
138            .take()
139            .map_or_else(Vec::new, |vp| vp.as_page())
140    }
141}
142
143#[derive(Copy, Clone, Debug, PartialEq, Eq)]
144pub enum LoaderIsolationType {
145    None,
146    Vbs {
147        enable_debug: bool,
148    },
149    Snp {
150        shared_gpa_boundary_bits: Option<u8>,
151        policy: SnpPolicy,
152        injection_type: InjectionType,
153        secure_avic: SecureAvic,
154        // TODO SNP: SNP Keys? Other data?
155    },
156    Tdx {
157        policy: TdxPolicy,
158    },
159}
160
161/// A trait to specialize behavior based on different register types for
162/// different architectures.
163pub trait IgvmLoaderRegister: VbsRegister {
164    /// Perform arch specific initialization.
165    fn init(
166        with_paravisor: bool,
167        max_vtl: Vtl,
168        isolation: LoaderIsolationType,
169    ) -> (
170        IgvmPlatformHeader,
171        Vec<IgvmInitializationHeader>,
172        Box<dyn VpContextBuilder<Register = Self>>,
173    );
174
175    /// Generate a measurement based on isolation type.
176    fn generate_measurement(
177        isolation: LoaderIsolationType,
178        initialization_headers: &[IgvmInitializationHeader],
179        directive_headers: &mut Vec<IgvmDirectiveHeader>,
180        svn: u32,
181        debug_enabled: bool,
182    ) -> anyhow::Result<Option<Measurement>>;
183
184    /// The IGVM file revision to use for the built igvm file.
185    fn igvm_revision() -> IgvmRevision;
186}
187
188impl IgvmLoaderRegister for X86Register {
189    fn init(
190        with_paravisor: bool,
191        max_vtl: Vtl,
192        isolation: LoaderIsolationType,
193    ) -> (
194        IgvmPlatformHeader,
195        Vec<IgvmInitializationHeader>,
196        Box<dyn VpContextBuilder<Register = Self>>,
197    ) {
198        match isolation {
199            LoaderIsolationType::None | LoaderIsolationType::Vbs { .. } => {
200                unreachable!("should be handled by common code")
201            }
202            LoaderIsolationType::Snp {
203                shared_gpa_boundary_bits,
204                policy,
205                injection_type,
206                secure_avic,
207            } => {
208                // TODO SNP: assumed that shared_gpa_boundary is always available.
209                let shared_gpa_boundary =
210                    1 << shared_gpa_boundary_bits.expect("shared gpa boundary must be set");
211
212                // Add SNP Platform header
213                let info = IGVM_VHS_SUPPORTED_PLATFORM {
214                    compatibility_mask: DEFAULT_COMPATIBILITY_MASK,
215                    highest_vtl: max_vtl as u8,
216                    platform_type: IgvmPlatformType::SEV_SNP,
217                    platform_version: igvm_defs::IGVM_SEV_SNP_PLATFORM_VERSION,
218                    shared_gpa_boundary,
219                };
220
221                let platform_header = IgvmPlatformHeader::SupportedPlatform(info);
222
223                let init_header = IgvmInitializationHeader::GuestPolicy {
224                    policy: policy.into(),
225                    compatibility_mask: DEFAULT_COMPATIBILITY_MASK,
226                };
227
228                let vp_context_builder = Box::new(SnpHardwareContext::new(
229                    max_vtl,
230                    !with_paravisor,
231                    shared_gpa_boundary,
232                    injection_type,
233                    secure_avic,
234                ));
235
236                (platform_header, vec![init_header], vp_context_builder)
237            }
238            LoaderIsolationType::Tdx { policy } => {
239                // NOTE: TDX always has a shared_gpa_boundary and has it at 47 bits.
240                let info = IGVM_VHS_SUPPORTED_PLATFORM {
241                    compatibility_mask: DEFAULT_COMPATIBILITY_MASK,
242                    highest_vtl: max_vtl as u8,
243                    platform_type: IgvmPlatformType::TDX,
244                    platform_version: igvm_defs::IGVM_TDX_PLATFORM_VERSION,
245                    shared_gpa_boundary: 1 << TDX_SHARED_GPA_BOUNDARY_BITS,
246                };
247
248                let platform_header = IgvmPlatformHeader::SupportedPlatform(info);
249
250                let mut init_headers = Vec::new();
251                if u64::from(policy) != 0 {
252                    init_headers.push(IgvmInitializationHeader::GuestPolicy {
253                        policy: policy.into(),
254                        compatibility_mask: DEFAULT_COMPATIBILITY_MASK,
255                    });
256                }
257
258                let vp_context_builder = Box::new(TdxHardwareContext::new(!with_paravisor));
259
260                (platform_header, init_headers, vp_context_builder)
261            }
262        }
263    }
264
265    fn generate_measurement(
266        isolation: LoaderIsolationType,
267        initialization_headers: &[IgvmInitializationHeader],
268        directive_headers: &mut Vec<IgvmDirectiveHeader>,
269        svn: u32,
270        debug_enabled: bool,
271    ) -> anyhow::Result<Option<Measurement>> {
272        let measurement = match isolation {
273            LoaderIsolationType::Snp { .. } => {
274                let ld = generate_snp_measurement(initialization_headers, directive_headers, svn)
275                    .context("generating snp measurement failed")?;
276                Some(Measurement::Snp(SnpMeasurement::new(
277                    ld,
278                    svn,
279                    debug_enabled,
280                )))
281            }
282            LoaderIsolationType::Tdx { .. } => {
283                let mrtd = generate_tdx_measurement(directive_headers)
284                    .context("generating tdx measurement failed")?;
285                Some(Measurement::Tdx(TdxMeasurement::new(
286                    mrtd,
287                    svn,
288                    debug_enabled,
289                )))
290            }
291            LoaderIsolationType::Vbs { enable_debug } => {
292                let boot_digest = generate_vbs_measurement(directive_headers, enable_debug, svn)
293                    .context("generating vbs measurement failed")?;
294                Some(Measurement::Vbs(VbsMeasurement::new(
295                    boot_digest,
296                    svn,
297                    debug_enabled,
298                )))
299            }
300            _ => None,
301        };
302        Ok(measurement)
303    }
304
305    fn igvm_revision() -> IgvmRevision {
306        // For now, x86 built files always uses V1 of the IGVM format. This is
307        // to maintain compatibility with older OS repo loaders that do not
308        // understand the V2 format.
309        IgvmRevision::V1
310    }
311}
312
313impl IgvmLoaderRegister for Aarch64Register {
314    fn init(
315        _with_paravisor: bool,
316        _max_vtl: Vtl,
317        _isolation: LoaderIsolationType,
318    ) -> (
319        IgvmPlatformHeader,
320        Vec<IgvmInitializationHeader>,
321        Box<dyn VpContextBuilder<Register = Self>>,
322    ) {
323        unreachable!("should never be called")
324    }
325
326    fn generate_measurement(
327        _isolation: LoaderIsolationType,
328        _initialization_headers: &[IgvmInitializationHeader],
329        _directive_headers: &mut Vec<IgvmDirectiveHeader>,
330        _svn: u32,
331        _debug_enabled: bool,
332    ) -> anyhow::Result<Option<Measurement>> {
333        Ok(None)
334    }
335
336    fn igvm_revision() -> IgvmRevision {
337        // AArch64 IGVM files are always V2.
338        IgvmRevision::V2 {
339            arch: igvm::Arch::AArch64,
340            page_size: 4096,
341        }
342    }
343}
344
345#[derive(Debug, Clone)]
346struct RequiredMemory {
347    range: MemoryRange,
348    vtl2_protectable: bool,
349}
350
351/// A map file representing information about a given generated IGVM file from a
352/// loader.
353///
354/// This can be used to save additional information about the layout of the
355/// address space that importing an IGVM file will create.
356#[derive(Debug)]
357pub struct MapFile {
358    isolation: LoaderIsolationType,
359    required_memory: Vec<RequiredMemory>,
360    accepted_ranges: Vec<(MemoryRange, RangeInfo)>,
361    relocatable_regions: Vec<(MemoryRange, RelocationType)>,
362}
363
364impl MapFile {
365    /// Emit this map file information to tracing::info.
366    pub fn emit_tracing(&self) {
367        tracing::info!(isolation = ?self.isolation, "IGVM file isolation");
368        tracing::info!("IGVM file layout:");
369        for (range, info) in self.accepted_ranges.iter() {
370            tracing::info!(
371                tag = info.tag,
372                size_bytes = range.len(),
373                "{:#x} - {:#x}",
374                range.start(),
375                range.end(),
376            );
377        }
378
379        if !self.required_memory.is_empty() {
380            tracing::info!("IGVM file required memory:");
381            for region in &self.required_memory {
382                tracing::info!(
383                    size_bytes = region.range.len(),
384                    vtl2_protectable = region.vtl2_protectable,
385                    "{:#x} - {:#x}",
386                    region.range.start(),
387                    region.range.end(),
388                );
389            }
390        }
391
392        if !self.relocatable_regions.is_empty() {
393            tracing::info!("IGVM file relocatable regions:");
394            for (range, info) in self.relocatable_regions.iter().rev() {
395                match info {
396                    RelocationType::PageTable(region) => {
397                        tracing::info!(
398                            size_bytes = region.size_pages * PAGE_SIZE_4K,
399                            "{:#x} - {:#x} pagetable relocation region",
400                            region.gpa,
401                            range.end(),
402                        );
403                    }
404                    RelocationType::Normal(region) => {
405                        tracing::info!(
406                            base_gpa = format_args!("{:#x}", region.base_gpa),
407                            size_bytes = region.size,
408                            minimum_relocation_gpa =
409                                format_args!("{:#x}", region.minimum_relocation_gpa),
410                            maximum_relocation_gpa =
411                                format_args!("{:#x}", region.maximum_relocation_gpa),
412                            relocation_alignment = region.relocation_alignment,
413                            "{:#x} - {:#x} relocation region",
414                            region.base_gpa,
415                            range.end(),
416                        );
417                    }
418                }
419            }
420        }
421    }
422}
423
424impl Display for MapFile {
425    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
426        writeln!(f, "IGVM file isolation: {:?}", self.isolation)?;
427
428        writeln!(f, "IGVM file layout:")?;
429        for (range, info) in &self.accepted_ranges {
430            writeln!(
431                f,
432                "  {:016x} - {:016x} ({:#x} bytes) {}",
433                range.start(),
434                range.end(),
435                range.len(),
436                info.tag
437            )?;
438        }
439
440        if !self.required_memory.is_empty() {
441            writeln!(f, "IGVM file required memory:")?;
442            for region in &self.required_memory {
443                writeln!(
444                    f,
445                    "  {:016x} - {:016x} ({:#x} bytes) {}",
446                    region.range.start(),
447                    region.range.end(),
448                    region.range.len(),
449                    if region.vtl2_protectable {
450                        "VTL2 protectable"
451                    } else {
452                        ""
453                    }
454                )?;
455            }
456        }
457
458        if !self.relocatable_regions.is_empty() {
459            writeln!(f, "IGVM file relocatable regions:")?;
460            for (range, info) in &self.relocatable_regions {
461                match info {
462                    RelocationType::PageTable(region) => {
463                        writeln!(
464                            f,
465                            "  {:016x} - {:016x} ({:#x} bytes) pagetable relocation region",
466                            region.gpa,
467                            range.end(),
468                            region.size_pages * PAGE_SIZE_4K,
469                        )?;
470                    }
471                    RelocationType::Normal(region) => {
472                        writeln!(
473                            f,
474                            "  {:016x} - {:016x} ({:#x} bytes) relocation region",
475                            region.base_gpa,
476                            range.end(),
477                            region.size
478                        )?;
479                    }
480                }
481            }
482        }
483
484        Ok(())
485    }
486}
487
488/// Returns output from finalize
489#[derive(Debug)]
490pub struct IgvmOutput {
491    pub guest: IgvmFile,
492    pub map: MapFile,
493    pub doc: Option<Measurement>,
494}
495
496impl<R: IgvmLoaderRegister + GuestArch + 'static> IgvmLoader<R> {
497    pub fn new(with_paravisor: bool, isolation_type: LoaderIsolationType) -> Self {
498        let vp_context_builder: Option<Box<dyn VpContextBuilder<Register = R>>>;
499        let platform_header;
500        let max_vtl = if with_paravisor { Vtl::Vtl2 } else { Vtl::Vtl0 };
501        let initialization_headers;
502
503        match isolation_type {
504            LoaderIsolationType::None | LoaderIsolationType::Vbs { .. } => {
505                vp_context_builder = Some(Box::new(VbsVpContext::<R>::new(max_vtl)));
506
507                // Add VBS platform header
508                let info = IGVM_VHS_SUPPORTED_PLATFORM {
509                    compatibility_mask: DEFAULT_COMPATIBILITY_MASK,
510                    highest_vtl: max_vtl as u8,
511                    platform_type: IgvmPlatformType::VSM_ISOLATION,
512                    platform_version: igvm_defs::IGVM_VSM_ISOLATION_PLATFORM_VERSION,
513                    shared_gpa_boundary: 0,
514                };
515
516                platform_header = IgvmPlatformHeader::SupportedPlatform(info);
517                initialization_headers = Vec::new();
518            }
519            _ => {
520                let (header, init_headers, vp_builder) =
521                    R::init(with_paravisor, max_vtl, isolation_type);
522                platform_header = header;
523                initialization_headers = init_headers;
524                vp_context_builder = Some(vp_builder);
525            }
526        }
527
528        IgvmLoader {
529            accepted_ranges: RangeMap::new(),
530            relocatable_regions: RangeMap::new(),
531            required_memory: Vec::new(),
532            page_table_region: None,
533            platform_header,
534            initialization_headers,
535            directives: Vec::new(),
536            page_data_directives: Vec::new(),
537            vp_context: vp_context_builder,
538            max_vtl,
539            parameter_areas: BTreeMap::new(),
540            isolation_type,
541            paravisor_present: with_paravisor,
542            imported_regions_config_page: None,
543        }
544    }
545
546    fn generate_cryptographic_hash_of_shared_pages(&mut self) -> Vec<u8> {
547        // Sort the page data directives by GPA to ensure the hash is consistent.
548        self.page_data_directives
549            .sort_unstable_by_key(|directive| match directive {
550                IgvmDirectiveHeader::PageData { gpa, .. } => *gpa,
551                _ => unreachable!("all directives should be IgvmDirectiveHeader::PageData"),
552            });
553
554        // Generate the hash of the unaccepted pages.
555        let mut hasher = Sha384::new();
556        self.page_data_directives.iter().for_each(|directive| {
557            if let IgvmDirectiveHeader::PageData {
558                gpa: _,
559                compatibility_mask: _,
560                flags,
561                data_type,
562                data,
563            } = directive
564            {
565                if *data_type == IgvmPageDataType::NORMAL && flags.shared() {
566                    // Measure the pages. If the data length is smaller than a page then zero extend
567                    // the data to a full page.
568                    let mut zero_data;
569                    let data_to_hash = if data.len() < PAGE_SIZE_4K as usize {
570                        zero_data = vec![0; PAGE_SIZE_4K as usize];
571                        zero_data[..data.len()].copy_from_slice(data);
572                        &zero_data
573                    } else {
574                        data
575                    };
576
577                    hasher.update(data_to_hash);
578                }
579            }
580        });
581        hasher.finalize().to_vec()
582    }
583
584    /// Finalize the loader state, returning an IGVM file.
585    pub fn finalize(mut self, guest_svn: u32) -> anyhow::Result<IgvmOutput> {
586        // Finalize any VP state.
587        let mut state = Vec::new();
588        self.vp_context.take().unwrap().finalize(&mut state);
589
590        for context in state {
591            match context {
592                VpContextState::Page(VpContextPageState {
593                    page_base,
594                    page_count,
595                    acceptance,
596                    data,
597                }) => {
598                    self.import_pages(page_base, page_count, "vp-context-page", acceptance, &data)
599                        .context("failed to import vp context page")?;
600                }
601                VpContextState::Directive(directive) => {
602                    self.directives.push(directive);
603                }
604            }
605        }
606
607        // Merge adjacent accepted ranges with the same tag and acceptance
608        // to undo fragmentation from chunked imports.
609        self.accepted_ranges
610            .merge_adjacent(range_map_vec::u64_is_adjacent);
611
612        // Put list of accepted pages into the config region, if there
613        if let Some(page_base) = self.imported_regions_config_page {
614            let mut imported_regions_data: Vec<_> = self.imported_regions();
615
616            // Add this config page as well
617            imported_regions_data.push(loader_defs::paravisor::ImportedRegionDescriptor::new(
618                page_base, 1, true,
619            ));
620
621            // The accepted regions have been guaranteed to not overlap,
622            // so just sort by the base page number
623            imported_regions_data.sort_by_key(|region| region.base_page_number);
624
625            // All shared pages have been imported. Generate the secure cryptographic hash of the unaccepted
626            // imported pages.
627            let hash = self.generate_cryptographic_hash_of_shared_pages();
628            let page_header = loader_defs::paravisor::ImportedRegionsPageHeader {
629                sha384_hash: hash
630                    .as_bytes()
631                    .try_into()
632                    .expect("hash should be correct size"),
633            };
634
635            let mut imported_regions_page = page_header.as_bytes().to_vec();
636
637            // Append the (sorted) imported region data.
638            imported_regions_page.extend_from_slice(imported_regions_data.as_bytes());
639
640            // This list should be measured
641            self.import_pages(
642                page_base,
643                1,
644                "loader-imported-regions",
645                BootPageAcceptance::Exclusive,
646                imported_regions_page.as_bytes(),
647            )
648            .context("failed to import config regions")?;
649        }
650
651        // Finalize parameter pages with insert directives.
652        for ((page_base, _page_count), index) in self.parameter_areas.iter() {
653            self.directives.push(IgvmDirectiveHeader::ParameterInsert(
654                IGVM_VHS_PARAMETER_INSERT {
655                    gpa: page_base * PAGE_SIZE_4K,
656                    compatibility_mask: DEFAULT_COMPATIBILITY_MASK,
657                    parameter_area_index: *index,
658                },
659            ));
660        }
661
662        // Merge the page_data_directives into the others directives. This must be done before
663        // generating the launch measurement.
664        self.directives.append(&mut self.page_data_directives);
665
666        // Generate the launch measurement for the isolation type being used.
667        // The measurement is output for external signing.
668        let debug_enabled = self.confidential_debug();
669        let doc = R::generate_measurement(
670            self.isolation_type,
671            &self.initialization_headers,
672            &mut self.directives,
673            guest_svn,
674            debug_enabled,
675        )?;
676
677        // Display a report about the build igvm file's layout.
678        let map_file = MapFile {
679            isolation: self.isolation_type,
680            required_memory: self.required_memory,
681            accepted_ranges: self
682                .accepted_ranges
683                .iter()
684                .rev()
685                .map(|(range, info)| {
686                    (
687                        MemoryRange::from_4k_gpn_range(*range.start()..(range.end() + 1)),
688                        info.clone(),
689                    )
690                })
691                .collect(),
692            relocatable_regions: self
693                .relocatable_regions
694                .iter()
695                .rev()
696                .map(|(range, info)| {
697                    (
698                        MemoryRange::new(*range.start()..(range.end() + 1)),
699                        info.clone(),
700                    )
701                })
702                .collect(),
703        };
704
705        map_file.emit_tracing();
706
707        // Create an IGVM file with the loader's internal state.
708        let igvm_file = IgvmFile::new(
709            R::igvm_revision(),
710            vec![self.platform_header],
711            self.initialization_headers,
712            self.directives,
713        )
714        .context("unable to create igvm file")?;
715
716        let output = IgvmOutput {
717            guest: igvm_file,
718            map: map_file,
719            doc,
720        };
721        Ok(output)
722    }
723
724    /// Accept a new page range with a given acceptance into the map of accepted
725    /// ranges.
726    fn accept_new_range(
727        &mut self,
728        page_base: u64,
729        page_count: u64,
730        tag: &str,
731        acceptance: BootPageAcceptance,
732    ) -> anyhow::Result<()> {
733        let page_end = page_base + page_count - 1;
734        match self.accepted_ranges.entry(page_base..=page_end) {
735            Entry::Overlapping(entry) => {
736                let (overlap_start, overlap_end, ref overlap_info) = *entry.get();
737                Err(anyhow::anyhow!(
738                    "{} at {} ({:?}) overlaps {} at {}",
739                    tag,
740                    MemoryRange::from_4k_gpn_range(page_base..page_end + 1),
741                    acceptance,
742                    overlap_info.tag,
743                    MemoryRange::from_4k_gpn_range(overlap_start..overlap_end + 1),
744                ))
745            }
746            Entry::Vacant(entry) => {
747                entry.insert(RangeInfo {
748                    tag: tag.to_string(),
749                    acceptance,
750                });
751                Ok(())
752            }
753        }
754    }
755
756    fn imported_regions(&self) -> Vec<loader_defs::paravisor::ImportedRegionDescriptor> {
757        // N.B. If the imported regions page grows too large, contiguous
758        // regions with the same acceptance type (but different tags) could
759        // be coalesced here to reduce the descriptor count.
760        self.accepted_ranges
761            .iter()
762            .map(|(r, info)| {
763                loader_defs::paravisor::ImportedRegionDescriptor::new(
764                    *r.start(),
765                    r.end() - r.start() + 1,
766                    info.acceptance != BootPageAcceptance::Shared,
767                )
768            })
769            .collect()
770    }
771
772    /// The guest architecture used by this loader.
773    pub fn arch(&self) -> GuestArchKind {
774        R::arch()
775    }
776
777    /// Returns true if this is an isolated guest with debug enabled, false
778    /// otherwise.
779    pub fn confidential_debug(&self) -> bool {
780        match self.isolation_type {
781            LoaderIsolationType::Vbs { enable_debug } => enable_debug,
782            LoaderIsolationType::Snp { policy, .. } => policy.debug() == 1,
783            LoaderIsolationType::Tdx { policy } => policy.debug_allowed() == 1,
784            _ => false,
785        }
786    }
787
788    pub fn loader(&mut self) -> IgvmVtlLoader<'_, R> {
789        IgvmVtlLoader {
790            vtl: self.max_vtl,
791            loader: self,
792            vp_context: None,
793        }
794    }
795
796    fn import_pages(
797        &mut self,
798        page_base: u64,
799        page_count: u64,
800        debug_tag: &str,
801        acceptance: BootPageAcceptance,
802        mut data: &[u8],
803    ) -> Result<(), anyhow::Error> {
804        tracing::debug!(
805            page_base,
806            ?acceptance,
807            page_count,
808            data_size = data.len(),
809            "Importing page",
810        );
811
812        // Pages must not overlap already accepted ranges
813        self.accept_new_range(page_base, page_count, debug_tag, acceptance)?;
814
815        // Page count must be larger or equal to data.
816        if page_count * PAGE_SIZE_4K < data.len() as u64 {
817            anyhow::bail!(
818                "data len {:x} is larger than page_count {page_count:x}",
819                data.len()
820            );
821        }
822
823        // VpContext imports are handled differently, as they have a different IGVM header
824        // type than normal data pages.
825        if acceptance == BootPageAcceptance::VpContext {
826            // This is only supported on SNP currently.
827            match self.isolation_type {
828                LoaderIsolationType::Snp { .. } => {}
829                _ => {
830                    anyhow::bail!("vpcontext acceptance only supported on SNP");
831                }
832            }
833
834            // Data size must match SNP VMSA size.
835            if data.len() != size_of::<SevVmsa>() {
836                anyhow::bail!("data len {:x} does not match VMSA size", data.len());
837            }
838
839            // Page count must be 1.
840            if page_count != 1 {
841                anyhow::bail!("page count {page_count:x} for snp vmsa is not 1");
842            }
843
844            self.directives.push(IgvmDirectiveHeader::SnpVpContext {
845                gpa: page_base * PAGE_SIZE_4K,
846                compatibility_mask: DEFAULT_COMPATIBILITY_MASK,
847                vp_index: 0,
848                vmsa: Box::new(SevVmsa::read_from_bytes(data).expect("should be correct size")), // TODO: zerocopy: map_err (https://github.com/microsoft/openvmm/issues/759)
849            });
850        } else {
851            for page in page_base..page_base + page_count {
852                let (data_type, flags) = match acceptance {
853                    BootPageAcceptance::Exclusive => {
854                        (IgvmPageDataType::NORMAL, IgvmPageDataFlags::new())
855                    }
856                    BootPageAcceptance::ExclusiveUnmeasured => (
857                        IgvmPageDataType::NORMAL,
858                        IgvmPageDataFlags::new().with_unmeasured(true),
859                    ),
860                    BootPageAcceptance::ErrorPage => todo!(),
861                    BootPageAcceptance::SecretsPage => {
862                        (IgvmPageDataType::SECRETS, IgvmPageDataFlags::new())
863                    }
864                    BootPageAcceptance::CpuidPage => {
865                        (IgvmPageDataType::CPUID_DATA, IgvmPageDataFlags::new())
866                    }
867                    BootPageAcceptance::CpuidExtendedStatePage => {
868                        (IgvmPageDataType::CPUID_XF, IgvmPageDataFlags::new())
869                    }
870                    BootPageAcceptance::VpContext => unreachable!(),
871                    BootPageAcceptance::Shared => (
872                        IgvmPageDataType::NORMAL,
873                        IgvmPageDataFlags::new().with_shared(true),
874                    ),
875                };
876
877                // Split data slice into data to be imported for this page and remaining.
878                let import_data_len = std::cmp::min(PAGE_SIZE_4K as usize, data.len());
879                let (import_data, data_remaining) = data.split_at(import_data_len);
880                data = data_remaining;
881
882                self.page_data_directives
883                    .push(IgvmDirectiveHeader::PageData {
884                        gpa: page * PAGE_SIZE_4K,
885                        compatibility_mask: DEFAULT_COMPATIBILITY_MASK,
886                        flags,
887                        data_type,
888                        data: import_data.to_vec(),
889                    });
890            }
891        }
892
893        Ok(())
894    }
895}
896
897impl<R: IgvmLoaderRegister + GuestArch + 'static> ImageLoad<R> for IgvmVtlLoader<'_, R> {
898    fn isolation_config(&self) -> IsolationConfig {
899        match self.loader.isolation_type {
900            LoaderIsolationType::None => IsolationConfig {
901                paravisor_present: self.loader.paravisor_present,
902                isolation_type: IsolationType::None,
903                shared_gpa_boundary_bits: None,
904            },
905            LoaderIsolationType::Vbs { .. } => IsolationConfig {
906                paravisor_present: self.loader.paravisor_present,
907                isolation_type: IsolationType::Vbs,
908                shared_gpa_boundary_bits: None,
909            },
910            LoaderIsolationType::Snp {
911                shared_gpa_boundary_bits,
912                policy: _,
913                injection_type: _,
914                secure_avic: _,
915            } => IsolationConfig {
916                paravisor_present: self.loader.paravisor_present,
917                isolation_type: IsolationType::Snp,
918                shared_gpa_boundary_bits,
919            },
920            LoaderIsolationType::Tdx { .. } => IsolationConfig {
921                paravisor_present: self.loader.paravisor_present,
922                isolation_type: IsolationType::Tdx,
923                shared_gpa_boundary_bits: Some(TDX_SHARED_GPA_BOUNDARY_BITS),
924            },
925        }
926    }
927
928    fn create_parameter_area(
929        &mut self,
930        page_base: u64,
931        page_count: u32,
932        debug_tag: &str,
933    ) -> anyhow::Result<ParameterAreaIndex> {
934        self.create_parameter_area_with_data(page_base, page_count, debug_tag, &[])
935    }
936
937    fn create_parameter_area_with_data(
938        &mut self,
939        page_base: u64,
940        page_count: u32,
941        debug_tag: &str,
942        initial_data: &[u8],
943    ) -> anyhow::Result<ParameterAreaIndex> {
944        let area_id = (page_base, page_count);
945
946        // Allocate a new parameter area, that must not overlap other accepted ranges.
947        self.loader.accept_new_range(
948            page_base,
949            page_count as u64,
950            debug_tag,
951            BootPageAcceptance::ExclusiveUnmeasured,
952        )?;
953
954        let index: u32 = self
955            .loader
956            .parameter_areas
957            .len()
958            .try_into()
959            .expect("parameter area greater than u32");
960        self.loader.parameter_areas.insert(area_id, index);
961
962        // Add the newly allocated parameter area index to headers.
963        self.loader
964            .directives
965            .push(IgvmDirectiveHeader::ParameterArea {
966                number_of_bytes: page_count as u64 * PAGE_SIZE_4K,
967                parameter_area_index: index,
968                initial_data: initial_data.to_vec(),
969            });
970
971        tracing::debug!(
972            index,
973            page_base,
974            page_count,
975            initial_data_len = initial_data.len(),
976            "Creating new parameter area",
977        );
978
979        Ok(ParameterAreaIndex(index))
980    }
981
982    fn import_parameter(
983        &mut self,
984        parameter_area: ParameterAreaIndex,
985        byte_offset: u32,
986        parameter_type: IgvmParameterType,
987    ) -> anyhow::Result<()> {
988        let index = parameter_area.0;
989
990        if index >= self.loader.parameter_areas.len() as u32 {
991            anyhow::bail!("invalid parameter area index: {:x}", index);
992        }
993
994        tracing::debug!(
995            ?parameter_type,
996            parameter_area_index = parameter_area.0,
997            byte_offset,
998            "Importing parameter",
999        );
1000
1001        let info = IGVM_VHS_PARAMETER {
1002            parameter_area_index: index,
1003            byte_offset,
1004        };
1005
1006        let header = match parameter_type {
1007            IgvmParameterType::VpCount => IgvmDirectiveHeader::VpCount(info),
1008            IgvmParameterType::Srat => IgvmDirectiveHeader::Srat(info),
1009            IgvmParameterType::Madt => IgvmDirectiveHeader::Madt(info),
1010            IgvmParameterType::Slit => IgvmDirectiveHeader::Slit(info),
1011            IgvmParameterType::Pptt => IgvmDirectiveHeader::Pptt(info),
1012            IgvmParameterType::MmioRanges => IgvmDirectiveHeader::MmioRanges(info),
1013            IgvmParameterType::MemoryMap => IgvmDirectiveHeader::MemoryMap(info),
1014            IgvmParameterType::CommandLine => IgvmDirectiveHeader::CommandLine(info),
1015            IgvmParameterType::DeviceTree => IgvmDirectiveHeader::DeviceTree(info),
1016        };
1017
1018        self.loader.directives.push(header);
1019
1020        Ok(())
1021    }
1022
1023    fn import_pages(
1024        &mut self,
1025        page_base: u64,
1026        page_count: u64,
1027        debug_tag: &str,
1028        acceptance: BootPageAcceptance,
1029        data: &[u8],
1030    ) -> anyhow::Result<()> {
1031        self.loader
1032            .import_pages(page_base, page_count, debug_tag, acceptance, data)
1033    }
1034
1035    fn import_vp_register(&mut self, register: R) -> anyhow::Result<()> {
1036        if let Some(vp_context) = &mut self.vp_context {
1037            vp_context.import_vp_register(register)
1038        } else {
1039            self.loader
1040                .vp_context
1041                .as_mut()
1042                .unwrap()
1043                .import_vp_register(register);
1044        }
1045
1046        Ok(())
1047    }
1048
1049    fn verify_startup_memory_available(
1050        &mut self,
1051        page_base: u64,
1052        page_count: u64,
1053        memory_type: loader::importer::StartupMemoryType,
1054    ) -> anyhow::Result<()> {
1055        let gpa = page_base * PAGE_SIZE_4K;
1056        let compatibility_mask = DEFAULT_COMPATIBILITY_MASK;
1057        let number_of_bytes = (page_count * PAGE_SIZE_4K)
1058            .try_into()
1059            .expect("startup memory request overflowed u32");
1060
1061        tracing::trace!(
1062            page_base,
1063            page_count,
1064            ?memory_type,
1065            number_of_bytes,
1066            "verify memory"
1067        );
1068
1069        // Set VTL2 protectable flag on isolation types which make sense
1070        // TODO SNP: Temporarily allow this on all isolation types to force the host to generate
1071        // the correct device tree structures.
1072        let vtl2_protectable =
1073            memory_type == loader::importer::StartupMemoryType::Vtl2ProtectableRam;
1074
1075        self.loader
1076            .directives
1077            .push(IgvmDirectiveHeader::RequiredMemory {
1078                gpa,
1079                compatibility_mask,
1080                number_of_bytes,
1081                vtl2_protectable,
1082            });
1083
1084        self.loader.required_memory.push(RequiredMemory {
1085            range: MemoryRange::new(gpa..gpa + number_of_bytes as u64),
1086            vtl2_protectable,
1087        });
1088
1089        Ok(())
1090    }
1091
1092    fn set_vp_context_page(&mut self, page_base: u64) -> anyhow::Result<()> {
1093        self.loader
1094            .vp_context
1095            .as_mut()
1096            .unwrap()
1097            .set_vp_context_memory(page_base);
1098
1099        Ok(())
1100    }
1101
1102    fn relocation_region(
1103        &mut self,
1104        gpa: u64,
1105        size_bytes: u64,
1106        relocation_alignment: u64,
1107        minimum_relocation_gpa: u64,
1108        maximum_relocation_gpa: u64,
1109        apply_rip_offset: bool,
1110        apply_gdtr_offset: bool,
1111        vp_index: u16,
1112    ) -> anyhow::Result<()> {
1113        if let Some(overlap) = self
1114            .loader
1115            .relocatable_regions
1116            .get_range(gpa..=(gpa + size_bytes - 1))
1117        {
1118            anyhow::bail!(
1119                "new relocation region overlaps existing region {:?}",
1120                overlap
1121            );
1122        }
1123
1124        if !size_bytes.is_multiple_of(PAGE_SIZE_4K) {
1125            anyhow::bail!("relocation size {size_bytes:#x} must be a multiple of 4K");
1126        }
1127
1128        if !relocation_alignment.is_multiple_of(PAGE_SIZE_4K) {
1129            anyhow::bail!(
1130                "relocation alignment {relocation_alignment:#x} must be a multiple of 4K"
1131            );
1132        }
1133
1134        if !gpa.is_multiple_of(relocation_alignment) {
1135            anyhow::bail!(
1136                "relocation base {gpa:#x} must be aligned to relocation alignment {relocation_alignment:#x}"
1137            );
1138        }
1139
1140        if !minimum_relocation_gpa.is_multiple_of(relocation_alignment) {
1141            anyhow::bail!(
1142                "relocation minimum GPA {minimum_relocation_gpa:#x} must be aligned to relocation alignment {relocation_alignment:#x}"
1143            );
1144        }
1145
1146        if !maximum_relocation_gpa.is_multiple_of(relocation_alignment) {
1147            anyhow::bail!(
1148                "relocation maximum GPA {maximum_relocation_gpa:#x} must be aligned to relocation alignment {relocation_alignment:#x}"
1149            );
1150        }
1151
1152        self.loader
1153            .initialization_headers
1154            .push(IgvmInitializationHeader::RelocatableRegion {
1155                compatibility_mask: DEFAULT_COMPATIBILITY_MASK,
1156                relocation_alignment,
1157                relocation_region_gpa: gpa,
1158                relocation_region_size: size_bytes,
1159                minimum_relocation_gpa,
1160                maximum_relocation_gpa,
1161                is_vtl2: self.vtl == Vtl::Vtl2,
1162                apply_rip_offset,
1163                apply_gdtr_offset,
1164                vp_index,
1165                vtl: to_igvm_vtl(self.vtl),
1166            });
1167
1168        self.loader.relocatable_regions.insert(
1169            gpa..=gpa + size_bytes - 1,
1170            RelocationType::Normal(IgvmRelocatableRegion {
1171                base_gpa: gpa,
1172                size: size_bytes,
1173                minimum_relocation_gpa,
1174                maximum_relocation_gpa,
1175                relocation_alignment,
1176                is_vtl2: self.vtl == Vtl::Vtl2,
1177                apply_rip_offset,
1178                apply_gdtr_offset,
1179                vp_index,
1180                vtl: to_igvm_vtl(self.vtl),
1181            }),
1182        );
1183
1184        Ok(())
1185    }
1186
1187    fn page_table_relocation(
1188        &mut self,
1189        page_table_gpa: u64,
1190        size_pages: u64,
1191        used_size_pages: u64,
1192        vp_index: u16,
1193    ) -> anyhow::Result<()> {
1194        // can only be one set
1195        if let Some(region) = &self.loader.page_table_region {
1196            anyhow::bail!("page table relocation already set {:?}", region)
1197        }
1198
1199        if used_size_pages > size_pages {
1200            anyhow::bail!(
1201                "used size pages {used_size_pages:#x} cannot be greater than size pages {size_pages:#x}"
1202            );
1203        }
1204
1205        let end_gpa = page_table_gpa + size_pages * PAGE_SIZE_4K - 1;
1206
1207        // cannot override other relocatable regions
1208        if let Some(overlap) = self
1209            .loader
1210            .relocatable_regions
1211            .get_range(page_table_gpa..=end_gpa)
1212        {
1213            anyhow::bail!(
1214                "new page table relocation region overlaps existing region {:?}",
1215                overlap
1216            );
1217        }
1218
1219        self.loader.initialization_headers.push(
1220            IgvmInitializationHeader::PageTableRelocationRegion {
1221                compatibility_mask: DEFAULT_COMPATIBILITY_MASK,
1222                gpa: page_table_gpa,
1223                size: size_pages * PAGE_SIZE_4K,
1224                used_size: used_size_pages * PAGE_SIZE_4K,
1225                vp_index,
1226                vtl: to_igvm_vtl(self.vtl),
1227            },
1228        );
1229
1230        let region = PageTableRegion {
1231            gpa: page_table_gpa,
1232            size_pages,
1233            used_size_pages,
1234        };
1235
1236        self.loader.relocatable_regions.insert(
1237            page_table_gpa..=end_gpa,
1238            RelocationType::PageTable(region.clone()),
1239        );
1240
1241        self.loader.page_table_region = Some(region);
1242
1243        Ok(())
1244    }
1245
1246    fn set_imported_regions_config_page(&mut self, page_base: u64) {
1247        self.loader.imported_regions_config_page = Some(page_base);
1248    }
1249}
1250
1251#[cfg(test)]
1252mod tests {
1253    use super::IgvmLoader;
1254    use super::*;
1255    use crate::identity_mapping::Measurement;
1256    use loader::importer::BootPageAcceptance;
1257    use loader::importer::ImageLoad;
1258    use loader_defs::paravisor::ImportedRegionDescriptor;
1259
1260    #[test]
1261    fn test_snp_measurement() {
1262        use igvm_defs::SnpPolicy;
1263        let ref_ld: [u8; 48] = [
1264            136, 154, 25, 56, 108, 130, 226, 33, 155, 222, 211, 233, 42, 118, 78, 140, 0, 194, 155,
1265            150, 109, 4, 166, 98, 188, 166, 207, 223, 236, 100, 123, 144, 81, 153, 86, 83, 57, 7,
1266            131, 132, 101, 87, 145, 50, 99, 215, 28, 79,
1267        ];
1268
1269        let mut loader = IgvmLoader::<X86Register>::new(
1270            true,
1271            LoaderIsolationType::Snp {
1272                shared_gpa_boundary_bits: Some(39),
1273                policy: SnpPolicy::from((0x1 << 17) | (0x1 << 16) | (0x1f)),
1274                injection_type: InjectionType::Restricted,
1275                secure_avic: SecureAvic::Enabled,
1276            },
1277        );
1278        let data = vec![0, 5];
1279        loader
1280            .import_pages(0, 5, "data", BootPageAcceptance::Exclusive, &data)
1281            .unwrap();
1282        loader
1283            .import_pages(5, 5, "data", BootPageAcceptance::ExclusiveUnmeasured, &data)
1284            .unwrap();
1285        loader
1286            .import_pages(10, 1, "data", BootPageAcceptance::Exclusive, &data)
1287            .unwrap();
1288        loader
1289            .import_pages(20, 1, "data", BootPageAcceptance::Shared, &data)
1290            .unwrap();
1291
1292        let igvm_output = loader.finalize(1).unwrap();
1293        let doc = igvm_output.doc.expect("doc");
1294        let Measurement::Snp(snp_measurement) = doc else {
1295            panic!("known to be snp")
1296        };
1297        assert_eq!(ref_ld, snp_measurement.series[0].reference.snp_ld);
1298    }
1299
1300    #[test]
1301    fn test_tdx_measurement() {
1302        let ref_mrtd: [u8; 48] = [
1303            214, 88, 54, 138, 48, 41, 223, 124, 152, 113, 236, 159, 157, 24, 134, 87, 36, 12, 163,
1304            162, 115, 128, 222, 247, 130, 13, 114, 103, 87, 67, 73, 89, 166, 251, 86, 245, 63, 209,
1305            246, 246, 164, 240, 96, 164, 22, 183, 142, 219,
1306        ];
1307
1308        let mut loader = IgvmLoader::<X86Register>::new(
1309            true,
1310            LoaderIsolationType::Tdx {
1311                policy: TdxPolicy::new()
1312                    .with_debug_allowed(0u8)
1313                    .with_sept_ve_disable(0u8),
1314            },
1315        );
1316        let data = vec![0, 5];
1317        loader
1318            .import_pages(0, 5, "data", BootPageAcceptance::Exclusive, &data)
1319            .unwrap();
1320        loader
1321            .import_pages(5, 5, "data", BootPageAcceptance::ExclusiveUnmeasured, &data)
1322            .unwrap();
1323        loader
1324            .import_pages(10, 1, "data", BootPageAcceptance::Exclusive, &data)
1325            .unwrap();
1326        loader
1327            .import_pages(20, 1, "data", BootPageAcceptance::Shared, &data)
1328            .unwrap();
1329
1330        let igvm_output = loader.finalize(1).unwrap();
1331        let doc = igvm_output.doc.expect("doc");
1332        let Measurement::Tdx(tdx_measurement) = doc else {
1333            panic!("known to be tdx")
1334        };
1335        assert_eq!(ref_mrtd, tdx_measurement.series[0].reference.tdx_mrtd);
1336    }
1337
1338    #[test]
1339    fn test_vbs_digest() {
1340        let ref_digest: [u8; 32] = [
1341            0x30, 0x13, 0x4C, 0x9B, 0xB8, 0x9C, 0xD7, 0x2D, 0x8A, 0x41, 0x8D, 0x1E, 0x7A, 0xFB,
1342            0x75, 0x92, 0x7F, 0x45, 0xE8, 0x57, 0x1D, 0xDA, 0x7A, 0xC7, 0xBE, 0x87, 0xD4, 0xB6,
1343            0xC7, 0x2C, 0xA6, 0x4C,
1344        ];
1345        let mut loader = IgvmLoader::<X86Register>::new(
1346            true,
1347            LoaderIsolationType::Vbs {
1348                enable_debug: false,
1349            },
1350        );
1351        {
1352            let mut loader = loader.loader();
1353
1354            let data = vec![0, 5];
1355            loader
1356                .import_pages(0, 5, "data", BootPageAcceptance::Exclusive, &data)
1357                .unwrap();
1358            loader
1359                .import_pages(5, 5, "data", BootPageAcceptance::ExclusiveUnmeasured, &data)
1360                .unwrap();
1361            loader
1362                .import_pages(10, 1, "data", BootPageAcceptance::Exclusive, &data)
1363                .unwrap();
1364            loader
1365                .import_pages(20, 1, "data", BootPageAcceptance::Shared, &data)
1366                .unwrap();
1367        }
1368
1369        let igvm_output = loader.finalize(1).unwrap();
1370        let doc = igvm_output.doc.expect("doc");
1371        let Measurement::Vbs(vbs_measurement) = doc else {
1372            panic!("known to be vbs")
1373        };
1374        assert_eq!(
1375            ref_digest,
1376            vbs_measurement.series[0].reference.vbs_boot_digest
1377        );
1378    }
1379
1380    #[test]
1381    fn test_accepted_regions() {
1382        let mut loader = IgvmLoader::<X86Register>::new(true, LoaderIsolationType::None);
1383
1384        let data = vec![0, 5];
1385        loader
1386            .import_pages(0, 5, "test1", BootPageAcceptance::Exclusive, &data)
1387            .unwrap();
1388
1389        loader
1390            .import_pages(15, 5, "test2", BootPageAcceptance::Exclusive, &data)
1391            .unwrap();
1392
1393        loader
1394            .import_pages(10, 5, "test3", BootPageAcceptance::Exclusive, &data)
1395            .unwrap();
1396
1397        assert_eq!(
1398            loader.imported_regions(),
1399            vec![
1400                ImportedRegionDescriptor::new(15, 5, true),
1401                ImportedRegionDescriptor::new(10, 5, true),
1402                ImportedRegionDescriptor::new(0, 5, true),
1403            ]
1404        );
1405
1406        loader
1407            .import_pages(20, 10, "test1", BootPageAcceptance::Exclusive, &data)
1408            .unwrap();
1409
1410        loader
1411            .import_pages(30, 1, "test2", BootPageAcceptance::Exclusive, &data)
1412            .unwrap();
1413
1414        assert_eq!(
1415            loader.imported_regions(),
1416            vec![
1417                ImportedRegionDescriptor::new(30, 1, true),
1418                ImportedRegionDescriptor::new(20, 10, true),
1419                ImportedRegionDescriptor::new(15, 5, true),
1420                ImportedRegionDescriptor::new(10, 5, true),
1421                ImportedRegionDescriptor::new(0, 5, true),
1422            ]
1423        );
1424    }
1425}