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