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