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