igvmfilegen/
main.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Implements a command line utility to generate IGVM files.
5
6mod file_loader;
7mod identity_mapping;
8mod signed_measurement;
9mod vp_context_builder;
10
11use crate::file_loader::IgvmLoader;
12use crate::file_loader::LoaderIsolationType;
13use anyhow::Context;
14use anyhow::bail;
15use clap::Parser;
16use file_loader::IgvmLoaderRegister;
17use file_loader::IgvmVtlLoader;
18use igvm::IgvmFile;
19use igvm_defs::IGVM_FIXED_HEADER;
20use igvm_defs::SnpPolicy;
21use igvm_defs::TdxPolicy;
22use igvmfilegen_config::Config;
23use igvmfilegen_config::ConfigIsolationType;
24use igvmfilegen_config::Image;
25use igvmfilegen_config::LinuxImage;
26use igvmfilegen_config::ResourceType;
27use igvmfilegen_config::Resources;
28use igvmfilegen_config::SnpInjectionType;
29use igvmfilegen_config::UefiConfigType;
30use loader::importer::Aarch64Register;
31use loader::importer::GuestArch;
32use loader::importer::GuestArchKind;
33use loader::importer::ImageLoad;
34use loader::importer::X86Register;
35use loader::linux::InitrdConfig;
36use loader::paravisor::CommandLineType;
37use loader::paravisor::Vtl0Config;
38use loader::paravisor::Vtl0Linux;
39use std::io::Write;
40use std::path::PathBuf;
41use tracing_subscriber::EnvFilter;
42use tracing_subscriber::filter::LevelFilter;
43use underhill_confidentiality::OPENHCL_CONFIDENTIAL_DEBUG_ENV_VAR_NAME;
44use zerocopy::FromBytes;
45use zerocopy::IntoBytes;
46
47#[derive(Parser)]
48#[clap(name = "igvmfilegen", about = "Tool to generate IGVM files")]
49enum Options {
50    /// Dumps the contents of an IGVM file in a human-readable format
51    // TODO: Move into its own tool.
52    Dump {
53        /// Dump file path
54        #[clap(short, long = "filepath")]
55        file_path: PathBuf,
56    },
57    /// Build an IGVM file according to a manifest
58    Manifest {
59        /// Config manifest file path
60        #[clap(short, long = "manifest")]
61        manifest: PathBuf,
62        /// Resources file describing binary resources used to build the igvm
63        /// file.
64        #[clap(short, long = "resources")]
65        resources: PathBuf,
66        /// Output file path for the built igvm file
67        #[clap(short = 'o', long)]
68        output: PathBuf,
69        /// Additional debug validation when building IGVM files
70        #[clap(long)]
71        debug_validation: bool,
72    },
73}
74
75// TODO: Potential CLI flags:
76//       --report: Dump additional data like what memory ranges were accepted with what values
77
78fn main() -> anyhow::Result<()> {
79    let opts = Options::parse();
80    let filter = if std::env::var(EnvFilter::DEFAULT_ENV).is_ok() {
81        EnvFilter::from_default_env()
82    } else {
83        EnvFilter::default().add_directive(LevelFilter::INFO.into())
84    };
85    tracing_subscriber::fmt()
86        .log_internal_errors(true)
87        .with_writer(std::io::stderr)
88        .with_env_filter(filter)
89        .init();
90
91    match opts {
92        Options::Dump { file_path } => {
93            let image = fs_err::read(file_path).context("reading input file")?;
94            let fixed_header = IGVM_FIXED_HEADER::read_from_prefix(image.as_bytes())
95                .expect("Invalid fixed header")
96                .0; // TODO: zerocopy: use-rest-of-range (https://github.com/microsoft/openvmm/issues/759)
97
98            let igvm_data = IgvmFile::new_from_binary(&image, None).expect("should be valid");
99            println!("Total file size: {} bytes\n", fixed_header.total_file_size);
100            println!("{:#X?}", fixed_header);
101            println!("{}", igvm_data);
102            Ok(())
103        }
104        Options::Manifest {
105            manifest,
106            resources,
107            output,
108            debug_validation,
109        } => {
110            // Read the config from the JSON manifest path.
111            let config: Config = serde_json::from_str(
112                &fs_err::read_to_string(manifest).context("reading manifest")?,
113            )
114            .context("parsing manifest")?;
115
116            // Read resources and validate that it covers the required resources
117            // from the config.
118            let resources: Resources = serde_json::from_str(
119                &fs_err::read_to_string(resources).context("reading resources")?,
120            )
121            .context("parsing resources")?;
122
123            let required_resources = config.required_resources();
124            resources
125                .check_required(&required_resources)
126                .context("required resources not specified")?;
127
128            tracing::info!(
129                ?config,
130                ?resources,
131                "Building igvm file with given config and resources"
132            );
133
134            // Enable debug validation if specified or if running a debug build.
135            match config.guest_arch {
136                igvmfilegen_config::GuestArch::X64 => create_igvm_file::<X86Register>(
137                    config,
138                    resources,
139                    debug_validation || cfg!(debug_assertions),
140                    output,
141                ),
142                igvmfilegen_config::GuestArch::Aarch64 => create_igvm_file::<Aarch64Register>(
143                    config,
144                    resources,
145                    debug_validation || cfg!(debug_assertions),
146                    output,
147                ),
148            }
149        }
150    }
151}
152
153/// Create an IGVM file from the specified config
154fn create_igvm_file<R: IgvmfilegenRegister + GuestArch + 'static>(
155    igvm_config: Config,
156    resources: Resources,
157    debug_validation: bool,
158    output: PathBuf,
159) -> anyhow::Result<()> {
160    tracing::debug!(?igvm_config, "Creating IGVM file",);
161
162    let mut igvm_file: Option<IgvmFile> = None;
163    let mut map_files = Vec::new();
164    let base_path = output.file_stem().unwrap();
165    for config in igvm_config.guest_configs {
166        // Max VTL must be 2 or 0.
167        if config.max_vtl != 2 && config.max_vtl != 0 {
168            bail!("max_vtl must be 2 or 0");
169        }
170
171        let isolation_string = match config.isolation_type {
172            ConfigIsolationType::None => "none",
173            ConfigIsolationType::Vbs { .. } => "vbs",
174            ConfigIsolationType::Snp { .. } => "snp",
175            ConfigIsolationType::Tdx { .. } => "tdx",
176        };
177        let loader_isolation_type = match config.isolation_type {
178            ConfigIsolationType::None => LoaderIsolationType::None,
179            ConfigIsolationType::Vbs { enable_debug } => LoaderIsolationType::Vbs { enable_debug },
180            ConfigIsolationType::Snp {
181                shared_gpa_boundary_bits,
182                policy,
183                enable_debug,
184                injection_type,
185            } => LoaderIsolationType::Snp {
186                shared_gpa_boundary_bits,
187                policy: SnpPolicy::from(policy).with_debug(enable_debug as u8),
188                injection_type: match injection_type {
189                    SnpInjectionType::Normal => vp_context_builder::snp::InjectionType::Normal,
190                    SnpInjectionType::Restricted => {
191                        vp_context_builder::snp::InjectionType::Restricted
192                    }
193                },
194            },
195            ConfigIsolationType::Tdx {
196                enable_debug,
197                sept_ve_disable,
198            } => LoaderIsolationType::Tdx {
199                policy: TdxPolicy::new()
200                    .with_debug_allowed(enable_debug as u8)
201                    .with_sept_ve_disable(sept_ve_disable as u8),
202            },
203        };
204
205        // Max VTL of 2 implies paravisor.
206        let with_paravisor = config.max_vtl == 2;
207
208        let mut loader = IgvmLoader::<R>::new(with_paravisor, loader_isolation_type);
209
210        load_image(&mut loader.loader(), &config.image, &resources)?;
211
212        let igvm_output = loader
213            .finalize(config.guest_svn)
214            .context("finalizing loader")?;
215
216        // Merge the loaded guest into the overall IGVM file.
217        match &mut igvm_file {
218            Some(file) => file
219                .merge_simple(igvm_output.guest)
220                .context("merging guest into overall igvm file")?,
221            None => igvm_file = Some(igvm_output.guest),
222        }
223
224        map_files.push(igvm_output.map);
225
226        if let Some(doc) = igvm_output.doc {
227            // Write the measurement document to a file with the same name,
228            // but with -[isolation].json extension.
229            let doc_path = {
230                let mut name = base_path.to_os_string();
231                name.push("-");
232                name.push(isolation_string);
233                name.push(".json");
234                output.with_file_name(name)
235            };
236            tracing::info!(
237                path = %doc_path.display(),
238                "Writing document json file",
239            );
240            let mut doc_file = fs_err::OpenOptions::new()
241                .create(true)
242                .write(true)
243                .open(doc_path)
244                .context("creating doc file")?;
245
246            writeln!(
247                doc_file,
248                "{}",
249                serde_json::to_string(&doc).expect("json string")
250            )
251            .context("writing doc file")?;
252        }
253    }
254
255    let mut igvm_binary = Vec::new();
256    let igvm_file = igvm_file.expect("should have an igvm file");
257    igvm_file
258        .serialize(&mut igvm_binary)
259        .context("serializing igvm")?;
260
261    // If enabled, perform additional validation.
262    if debug_validation {
263        debug_validate_igvm_file(&igvm_file, &igvm_binary);
264    }
265
266    // Write the IGVM file to the specified file path in the config.
267    tracing::info!(
268        path = %output.display(),
269        "Writing output IGVM file",
270    );
271    fs_err::File::create(&output)
272        .context("creating igvm file")?
273        .write_all(&igvm_binary)
274        .context("writing igvm file")?;
275
276    // Write the map file display output to a file with the same name, but .map
277    // extension.
278    let map_path = {
279        let mut name = output.file_name().expect("has name").to_owned();
280        name.push(".map");
281        output.with_file_name(name)
282    };
283    tracing::info!(
284        path = %map_path.display(),
285        "Writing output map file",
286    );
287    let mut map_file = fs_err::OpenOptions::new()
288        .create(true)
289        .write(true)
290        .open(map_path)
291        .context("creating map file")?;
292
293    for map in map_files {
294        writeln!(map_file, "{}", map).context("writing map file")?;
295    }
296
297    Ok(())
298}
299
300/// Validate an in-memory IGVM file and the binary repr are equivalent.
301// TODO: should live in the igvm crate
302fn debug_validate_igvm_file(igvm_file: &IgvmFile, binary_file: &[u8]) {
303    use igvm::IgvmDirectiveHeader;
304    tracing::info!("Debug validation of serialized IGVM file.");
305
306    let igvm_reserialized = IgvmFile::new_from_binary(binary_file, None).expect("should be valid");
307
308    for (a, b) in igvm_file
309        .platforms()
310        .iter()
311        .zip(igvm_reserialized.platforms().iter())
312    {
313        assert_eq!(a, b);
314    }
315
316    for (a, b) in igvm_file
317        .initializations()
318        .iter()
319        .zip(igvm_reserialized.initializations().iter())
320    {
321        assert_eq!(a, b);
322    }
323
324    for (a, b) in igvm_file
325        .directives()
326        .iter()
327        .zip(igvm_reserialized.directives().iter())
328    {
329        match (a, b) {
330            (
331                IgvmDirectiveHeader::PageData {
332                    gpa: a_gpa,
333                    flags: a_flags,
334                    data_type: a_data_type,
335                    data: a_data,
336                    compatibility_mask: a_compmask,
337                },
338                IgvmDirectiveHeader::PageData {
339                    gpa: b_gpa,
340                    flags: b_flags,
341                    data_type: b_data_type,
342                    data: b_data,
343                    compatibility_mask: b_compmask,
344                },
345            ) => {
346                assert!(
347                    a_gpa == b_gpa
348                        && a_flags == b_flags
349                        && a_data_type == b_data_type
350                        && a_compmask == b_compmask
351                );
352
353                // data might not be the same length, as it gets padded out.
354                for i in 0..b_data.len() {
355                    if i < a_data.len() {
356                        assert_eq!(a_data[i], b_data[i]);
357                    } else {
358                        assert_eq!(0, b_data[i]);
359                    }
360                }
361            }
362            (
363                IgvmDirectiveHeader::ParameterArea {
364                    number_of_bytes: a_number_of_bytes,
365                    parameter_area_index: a_parameter_area_index,
366                    initial_data: a_initial_data,
367                },
368                IgvmDirectiveHeader::ParameterArea {
369                    number_of_bytes: b_number_of_bytes,
370                    parameter_area_index: b_parameter_area_index,
371                    initial_data: b_initial_data,
372                },
373            ) => {
374                assert!(
375                    a_number_of_bytes == b_number_of_bytes
376                        && a_parameter_area_index == b_parameter_area_index
377                );
378
379                // initial_data might be padded out just like page data.
380                for i in 0..b_initial_data.len() {
381                    if i < a_initial_data.len() {
382                        assert_eq!(a_initial_data[i], b_initial_data[i]);
383                    } else {
384                        assert_eq!(0, b_initial_data[i]);
385                    }
386                }
387            }
388            _ => assert_eq!(a, b),
389        }
390    }
391}
392
393/// A trait to specialize behavior of the file builder based on different
394/// register types for different architectures. Different methods may need to be
395/// called depending on the register type that represents the given architecture.
396trait IgvmfilegenRegister: IgvmLoaderRegister + 'static {
397    fn load_uefi(
398        importer: &mut dyn ImageLoad<Self>,
399        image: &[u8],
400        config: loader::uefi::ConfigType,
401    ) -> Result<loader::uefi::LoadInfo, loader::uefi::Error>;
402
403    fn load_linux_kernel_and_initrd<F>(
404        importer: &mut impl ImageLoad<Self>,
405        kernel_image: &mut F,
406        kernel_minimum_start_address: u64,
407        initrd: Option<InitrdConfig<'_>>,
408        device_tree_blob: Option<&[u8]>,
409    ) -> Result<loader::linux::LoadInfo, loader::linux::Error>
410    where
411        F: std::io::Read + std::io::Seek,
412        Self: GuestArch;
413
414    fn load_openhcl<F>(
415        importer: &mut dyn ImageLoad<Self>,
416        kernel_image: &mut F,
417        shim: &mut F,
418        sidecar: Option<&mut F>,
419        command_line: CommandLineType<'_>,
420        initrd: Option<&[u8]>,
421        memory_page_base: Option<u64>,
422        memory_page_count: u64,
423        vtl0_config: Vtl0Config<'_>,
424    ) -> Result<(), loader::paravisor::Error>
425    where
426        F: std::io::Read + std::io::Seek;
427}
428
429impl IgvmfilegenRegister for X86Register {
430    fn load_uefi(
431        importer: &mut dyn ImageLoad<Self>,
432        image: &[u8],
433        config: loader::uefi::ConfigType,
434    ) -> Result<loader::uefi::LoadInfo, loader::uefi::Error> {
435        loader::uefi::x86_64::load(importer, image, config)
436    }
437
438    fn load_linux_kernel_and_initrd<F>(
439        importer: &mut impl ImageLoad<Self>,
440        kernel_image: &mut F,
441        kernel_minimum_start_address: u64,
442        initrd: Option<InitrdConfig<'_>>,
443        _device_tree_blob: Option<&[u8]>,
444    ) -> Result<loader::linux::LoadInfo, loader::linux::Error>
445    where
446        F: std::io::Read + std::io::Seek,
447    {
448        loader::linux::load_kernel_and_initrd_x64(
449            importer,
450            kernel_image,
451            kernel_minimum_start_address,
452            initrd,
453        )
454    }
455
456    fn load_openhcl<F>(
457        importer: &mut dyn ImageLoad<Self>,
458        kernel_image: &mut F,
459        shim: &mut F,
460        sidecar: Option<&mut F>,
461        command_line: CommandLineType<'_>,
462        initrd: Option<&[u8]>,
463        memory_page_base: Option<u64>,
464        memory_page_count: u64,
465        vtl0_config: Vtl0Config<'_>,
466    ) -> Result<(), loader::paravisor::Error>
467    where
468        F: std::io::Read + std::io::Seek,
469    {
470        loader::paravisor::load_openhcl_x64(
471            importer,
472            kernel_image,
473            shim,
474            sidecar,
475            command_line,
476            initrd,
477            memory_page_base,
478            memory_page_count,
479            vtl0_config,
480        )
481    }
482}
483
484impl IgvmfilegenRegister for Aarch64Register {
485    fn load_uefi(
486        importer: &mut dyn ImageLoad<Self>,
487        image: &[u8],
488        config: loader::uefi::ConfigType,
489    ) -> Result<loader::uefi::LoadInfo, loader::uefi::Error> {
490        loader::uefi::aarch64::load(importer, image, config)
491    }
492
493    fn load_linux_kernel_and_initrd<F>(
494        importer: &mut impl ImageLoad<Self>,
495        kernel_image: &mut F,
496        kernel_minimum_start_address: u64,
497        initrd: Option<InitrdConfig<'_>>,
498        device_tree_blob: Option<&[u8]>,
499    ) -> Result<loader::linux::LoadInfo, loader::linux::Error>
500    where
501        F: std::io::Read + std::io::Seek,
502    {
503        loader::linux::load_kernel_and_initrd_arm64(
504            importer,
505            kernel_image,
506            kernel_minimum_start_address,
507            initrd,
508            device_tree_blob,
509        )
510    }
511
512    fn load_openhcl<F>(
513        importer: &mut dyn ImageLoad<Self>,
514        kernel_image: &mut F,
515        shim: &mut F,
516        _sidecar: Option<&mut F>,
517        command_line: CommandLineType<'_>,
518        initrd: Option<&[u8]>,
519        memory_page_base: Option<u64>,
520        memory_page_count: u64,
521        vtl0_config: Vtl0Config<'_>,
522    ) -> Result<(), loader::paravisor::Error>
523    where
524        F: std::io::Read + std::io::Seek,
525    {
526        loader::paravisor::load_openhcl_arm64(
527            importer,
528            kernel_image,
529            shim,
530            command_line,
531            initrd,
532            memory_page_base,
533            memory_page_count,
534            vtl0_config,
535        )
536    }
537}
538
539/// Load an image.
540fn load_image<'a, R: IgvmfilegenRegister + GuestArch + 'static>(
541    loader: &mut IgvmVtlLoader<'_, R>,
542    config: &'a Image,
543    resources: &'a Resources,
544) -> anyhow::Result<()> {
545    tracing::debug!(?config, "loading into VTL0");
546
547    match *config {
548        Image::None => {
549            // Nothing is loaded.
550        }
551        Image::Uefi { config_type } => {
552            load_uefi(loader, resources, config_type)?;
553        }
554        Image::Linux(ref linux) => {
555            load_linux(loader, linux, resources)?;
556        }
557        Image::Openhcl {
558            ref command_line,
559            static_command_line,
560            memory_page_base,
561            memory_page_count,
562            uefi,
563            ref linux,
564        } => {
565            if uefi && linux.is_some() {
566                anyhow::bail!("cannot include both UEFI and Linux images in OpenHCL image");
567            }
568
569            let kernel_path = resources
570                .get(ResourceType::UnderhillKernel)
571                .expect("validated present");
572            let mut kernel = fs_err::File::open(kernel_path).context(format!(
573                "reading underhill kernel image at {}",
574                kernel_path.display()
575            ))?;
576
577            let initrd = {
578                let initrd_path = resources
579                    .get(ResourceType::UnderhillInitrd)
580                    .expect("validated present");
581                Some(fs_err::read(initrd_path).context(format!(
582                    "reading underhill initrd at {}",
583                    initrd_path.display()
584                ))?)
585            };
586
587            let shim_path = resources
588                .get(ResourceType::OpenhclBoot)
589                .expect("validated present");
590            let mut shim = fs_err::File::open(shim_path)
591                .context(format!("reading underhill shim at {}", shim_path.display()))?;
592
593            let mut sidecar =
594                if let Some(sidecar_path) = resources.get(ResourceType::UnderhillSidecar) {
595                    Some(fs_err::File::open(sidecar_path).context("reading AP kernel")?)
596                } else {
597                    None
598                };
599
600            let initrd_slice = initrd.as_deref();
601
602            // TODO: While the paravisor supports multiple things that can be
603            // loaded in VTL0, we don't yet have updated file builder config for
604            // that.
605            //
606            // Since the host performs PCAT loading, each image that supports
607            // UEFI also supports PCAT boot. A future file builder config change
608            // will make this more explicit.
609            let vtl0_load_config = if uefi {
610                let mut inner_loader = loader.nested_loader();
611                let load_info = load_uefi(&mut inner_loader, resources, UefiConfigType::None)?;
612                let vp_context = inner_loader.take_vp_context();
613                Vtl0Config {
614                    supports_pcat: loader.loader().arch() == GuestArchKind::X86_64,
615                    supports_uefi: Some((load_info, vp_context)),
616                    supports_linux: None,
617                }
618            } else if let Some(linux) = linux {
619                let load_info = load_linux(&mut loader.nested_loader(), linux, resources)?;
620                Vtl0Config {
621                    supports_pcat: false,
622                    supports_uefi: None,
623                    supports_linux: Some(Vtl0Linux {
624                        command_line: &linux.command_line,
625                        load_info,
626                    }),
627                }
628            } else {
629                Vtl0Config {
630                    supports_pcat: false,
631                    supports_uefi: None,
632                    supports_linux: None,
633                }
634            };
635
636            let command_line = if loader.loader().confidential_debug() {
637                tracing::info!("enabling underhill confidential debug environment flag");
638                format!(
639                    "{command_line} {}=1",
640                    OPENHCL_CONFIDENTIAL_DEBUG_ENV_VAR_NAME
641                )
642            } else {
643                command_line.clone()
644            };
645
646            let command_line = if static_command_line {
647                CommandLineType::Static(&command_line)
648            } else {
649                CommandLineType::HostAppendable(&command_line)
650            };
651
652            R::load_openhcl(
653                loader,
654                &mut kernel,
655                &mut shim,
656                sidecar.as_mut(),
657                command_line,
658                initrd_slice,
659                memory_page_base,
660                memory_page_count,
661                vtl0_load_config,
662            )
663            .context("underhill kernel loader")?;
664        }
665    };
666
667    Ok(())
668}
669
670fn load_uefi<R: IgvmfilegenRegister + GuestArch + 'static>(
671    loader: &mut IgvmVtlLoader<'_, R>,
672    resources: &Resources,
673    config_type: UefiConfigType,
674) -> Result<loader::uefi::LoadInfo, anyhow::Error> {
675    let image_path = resources
676        .get(ResourceType::Uefi)
677        .expect("validated present");
678    let image = fs_err::read(image_path)
679        .context(format!("reading uefi image at {}", image_path.display()))?;
680    let config = match config_type {
681        UefiConfigType::None => loader::uefi::ConfigType::None,
682        UefiConfigType::Igvm => loader::uefi::ConfigType::Igvm,
683    };
684    let load_info = R::load_uefi(loader, &image, config).context("uefi loader")?;
685    Ok(load_info)
686}
687
688fn load_linux<R: IgvmfilegenRegister + GuestArch + 'static>(
689    loader: &mut IgvmVtlLoader<'_, R>,
690    config: &LinuxImage,
691    resources: &Resources,
692) -> Result<loader::linux::LoadInfo, anyhow::Error> {
693    let LinuxImage {
694        use_initrd,
695        command_line: _,
696    } = *config;
697    let kernel_path = resources
698        .get(ResourceType::LinuxKernel)
699        .expect("validated present");
700    let mut kernel = fs_err::File::open(kernel_path).context(format!(
701        "reading vtl0 kernel image at {}",
702        kernel_path.display()
703    ))?;
704    let initrd_vec = if use_initrd {
705        let initrd_path = resources
706            .get(ResourceType::LinuxInitrd)
707            .expect("validated present");
708        fs_err::read(initrd_path)
709            .context(format!("reading vtl0 initrd at {}", initrd_path.display()))?
710    } else {
711        Vec::new()
712    };
713    let initrd = if initrd_vec.is_empty() {
714        None
715    } else {
716        Some(InitrdConfig {
717            initrd_address: loader::linux::InitrdAddressType::AfterKernel,
718            initrd: &initrd_vec,
719        })
720    };
721    let load_info = R::load_linux_kernel_and_initrd(loader, &mut kernel, 0, initrd, None)
722        .context("loading linux kernel and initrd")?;
723    Ok(load_info)
724}