Skip to main content

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