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