diff --git a/crates/bls/src/lib.rs b/crates/bls/src/lib.rs index 1e297b3..6d2a26d 100644 --- a/crates/bls/src/lib.rs +++ b/crates/bls/src/lib.rs @@ -2,6 +2,7 @@ extern crate alloc; use alloc::string::{String, ToString}; +use alloc::vec::Vec; use anyhow::{Error, Result}; use core::{cmp::Ordering, iter::Peekable, str::FromStr}; @@ -15,8 +16,8 @@ pub struct BlsEntry { pub options: Option, /// The path to the linux kernel. pub linux: Option, - /// The path to the initrd. - pub initrd: Option, + /// The paths to the initrds, in the order they appear. + pub initrd: Vec, /// The path to an EFI image. pub efi: Option, /// The sort key for the entry. @@ -38,7 +39,7 @@ impl FromStr for BlsEntry { let mut title: Option = None; let mut options: Option = None; let mut linux: Option = None; - let mut initrd: Option = None; + let mut initrd: Vec = Vec::new(); let mut efi: Option = None; let mut sort_key: Option = None; let mut version: Option = None; @@ -75,9 +76,10 @@ impl FromStr for BlsEntry { linux = Some(value.trim().to_string()); } - // The path to the initrd. + // The path to an initrd. An entry can have multiple initrds, either as + // repeated initrd lines or as multiple paths separated by whitespace. "initrd" => { - initrd = Some(value.trim().to_string()); + initrd.extend(value.split_whitespace().map(|path| path.to_string())); } // The path to an EFI image. @@ -134,12 +136,13 @@ impl BlsEntry { .map(|path| path.replace('/', "\\").trim_start_matches('\\').to_string()) } - /// Fetches the path to an initrd to pass to the kernel, if any. + /// Fetches the paths to the initrds to pass to the kernel, in order. /// It also converts / to \\ to match EFI path style. - pub fn initrd_path(&self) -> Option { + pub fn initrd_paths(&self) -> Vec { self.initrd - .clone() + .iter() .map(|path| path.replace('/', "\\").trim_start_matches('\\').to_string()) + .collect() } /// Fetches the options to pass to the kernel, if any. @@ -431,7 +434,7 @@ mod tests { assert!(entry.title.is_none()); assert!(entry.linux.is_none()); assert!(entry.efi.is_none()); - assert!(entry.initrd.is_none()); + assert!(entry.initrd.is_empty()); assert!(entry.options.is_none()); assert!(entry.sort_key.is_none()); assert!(entry.version.is_none()); @@ -455,7 +458,7 @@ efi /EFI/fedora/shimx64.efi assert_eq!(entry.version.as_deref(), Some("6.5.6-300.fc39.x86_64")); assert_eq!(entry.machine_id.as_deref(), Some("abc123def456")); assert_eq!(entry.linux.as_deref(), Some("/boot/vmlinuz-6.5.6")); - assert_eq!(entry.initrd.as_deref(), Some("/boot/initrd-6.5.6.img")); + assert_eq!(entry.initrd, ["/boot/initrd-6.5.6.img"]); assert_eq!(entry.options.as_deref(), Some("root=/dev/sda1 ro quiet")); assert_eq!(entry.sort_key.as_deref(), Some("fedora")); assert_eq!(entry.efi.as_deref(), Some("/EFI/fedora/shimx64.efi")); @@ -555,15 +558,29 @@ efi /EFI/fedora/shimx64.efi } #[test] - fn initrd_path_normalises_slashes() { - let entry: BlsEntry = "linux /vmlinuz\ninitrd /boot/initrd.img\n".parse().unwrap(); - assert_eq!(entry.initrd_path().as_deref(), Some("boot\\initrd.img")); + fn parse_multiple_initrd_lines_in_order() { + let input = "linux /vmlinuz\ninitrd /intel-ucode.img\ninitrd /initramfs.img\n"; + let entry: BlsEntry = input.parse().unwrap(); + assert_eq!(entry.initrd, ["/intel-ucode.img", "/initramfs.img"]); } #[test] - fn initrd_path_none_when_not_set() { + fn parse_multiple_initrds_on_one_line() { + let input = "linux /vmlinuz\ninitrd /intel-ucode.img /initramfs.img\n"; + let entry: BlsEntry = input.parse().unwrap(); + assert_eq!(entry.initrd, ["/intel-ucode.img", "/initramfs.img"]); + } + + #[test] + fn initrd_paths_normalises_slashes() { + let entry: BlsEntry = "linux /vmlinuz\ninitrd /boot/initrd.img\n".parse().unwrap(); + assert_eq!(entry.initrd_paths(), ["boot\\initrd.img"]); + } + + #[test] + fn initrd_paths_empty_when_not_set() { let entry: BlsEntry = "linux /vmlinuz\n".parse().unwrap(); - assert!(entry.initrd_path().is_none()); + assert!(entry.initrd_paths().is_empty()); } #[test] diff --git a/crates/boot/src/actions/chainload.rs b/crates/boot/src/actions/chainload.rs index 2a6ced7..c4533d9 100644 --- a/crates/boot/src/actions/chainload.rs +++ b/crates/boot/src/actions/chainload.rs @@ -2,6 +2,7 @@ use crate::context::SproutContext; use crate::phases::before_handoff; use alloc::boxed::Box; use alloc::rc::Rc; +use alloc::vec::Vec; use anyhow::{Context, Result, bail}; use edera_sprout_config::actions::chainload::ChainloadConfiguration; use edera_sprout_parsing::{combine_options, empty_is_none}; @@ -13,8 +14,34 @@ use eficore::media_loader::constants::linux::LINUX_EFI_INITRD_MEDIA_GUID; use uefi::CString16; use uefi::proto::loaded_image::LoadedImage; +/// Read the Linux initrd at `path` relative to the sprout image. +/// Provides [None] if the path refers to the root of a filesystem rather than a file. +fn read_linux_initrd(context: &Rc, path: &str) -> Result>> { + let resolved = eficore::path::resolve_path(Some(context.root().loaded_image_path()?), path) + .context("unable to resolve linux initrd path")?; + + // A path without a file component refers to the root of the filesystem, not an initrd. + // This happens when a path template like "$root\\$initrd-0" is stamped with an empty + // initrd value, such as a BLS entry without an initrd or an unused BLS initrd slot. + let subpath = eficore::path::device_path_subpath(&resolved.full_path) + .context("unable to get linux initrd subpath")?; + if subpath.trim_matches('\\').is_empty() { + return Ok(None); + } + + let content = resolved + .read_file() + .context("unable to read linux initrd")?; + Ok(Some(content)) +} + /// Executes the chainload action using the specified `configuration` inside the provided `context`. pub fn chainload(context: Rc, configuration: &ChainloadConfiguration) -> Result<()> { + // The initrd can be provided as either a single initrd or a chain, but not both. + if configuration.linux_initrd.is_some() && !configuration.linux_initrd_chain.is_empty() { + bail!("linux-initrd and linux-initrd-chain cannot be used together"); + } + // Retrieve the current image handle of sprout. let sprout_image = uefi::boot::image_handle(); @@ -66,39 +93,38 @@ pub fn chainload(context: Rc, configuration: &ChainloadConfigurat } } - // Stamp the initrd path, if provided. - let initrd = configuration + // Stamp the initrd paths. A single linux-initrd is read the same way as a chain of one. + let initrd_paths = configuration .linux_initrd - .as_ref() + .iter() + .chain(configuration.linux_initrd_chain.iter()) .map(|item| context.stamp(item)); - // The initrd can be None or empty, so we need to collapse that into a single Option. - let initrd = empty_is_none(initrd); - // If an initrd is provided, register it with the EFI stack. - let mut initrd_handle = None; - if let Some(linux_initrd) = initrd { - let resolved_initrd = - eficore::path::resolve_path(Some(context.root().loaded_image_path()?), &linux_initrd) - .context("unable to resolve linux initrd path")?; - - // A path without a file component refers to the root of the filesystem, not an initrd. - // This happens when a path template like "$root\\$initrd" is stamped with an empty - // initrd value, such as a BLS entry without an initrd. - let initrd_subpath = eficore::path::device_path_subpath(&resolved_initrd.full_path) - .context("unable to get linux initrd subpath")?; - if !initrd_subpath.trim_matches('\\').is_empty() { - let content = resolved_initrd - .read_file() - .context("unable to read linux initrd")?; - let handle = MediaLoaderHandle::register( - LINUX_EFI_INITRD_MEDIA_GUID, - content.into_boxed_slice(), - ) - .context("unable to register linux initrd")?; - initrd_handle = Some(handle); + // Read each initrd and concatenate the contents in order. + // Paths that are empty after stamping are skipped. + let mut initrd: Option> = None; + for path in initrd_paths { + let Some(path) = empty_is_none(Some(path)) else { + continue; + }; + let Some(content) = read_linux_initrd(&context, &path)? else { + continue; + }; + match initrd.as_mut() { + Some(initrd) => initrd.extend_from_slice(&content), + None => initrd = Some(content), } } + // If an initrd was read, register it with the EFI stack. + let mut initrd_handle = None; + if let Some(content) = initrd { + let handle = + MediaLoaderHandle::register(LINUX_EFI_INITRD_MEDIA_GUID, content.into_boxed_slice()) + .context("unable to register linux initrd")?; + initrd_handle = Some(handle); + } + // Mark execution of an entry in the bootloader interface. BootloaderInterface::mark_exec(context.root().timer()) .context("unable to mark execution of boot entry in bootloader interface")?; diff --git a/crates/boot/src/actions/edera.rs b/crates/boot/src/actions/edera.rs index 578744e..2fe8abf 100644 --- a/crates/boot/src/actions/edera.rs +++ b/crates/boot/src/actions/edera.rs @@ -2,7 +2,7 @@ use crate::{actions, context::SproutContext}; use alloc::rc::Rc; use alloc::vec::Vec; use alloc::{format, vec}; -use anyhow::{Context, Result}; +use anyhow::{Context, Result, bail}; use edera_sprout_config::actions::chainload::ChainloadConfiguration; use edera_sprout_config::actions::edera::EderaConfiguration; use edera_sprout_parsing::{build_xen_config, combine_options, empty_is_none}; @@ -40,6 +40,11 @@ fn read_loader_payload(context: &Rc, what: &str, path: &str) -> R /// Executes the edera action which will boot the Edera hypervisor with the specified /// `configuration` and `context`. This action uses Edera-specific Xen EFI stub functionality. pub fn edera(context: Rc, configuration: &EderaConfiguration) -> Result<()> { + // The initrd can be provided as either a single initrd or a chain, but not both. + if configuration.initrd.is_some() && !configuration.initrd_chain.is_empty() { + bail!("initrd and initrd-chain cannot be used together"); + } + // Only register the initrd media loader if the user actually configured one. let xen_opts = combine_options(context.stamp_iter(configuration.xen_options.iter())); let dom0_args = combine_options(context.stamp_iter(configuration.kernel_options.iter())); @@ -80,12 +85,18 @@ pub fn edera(context: Rc, configuration: &EderaConfiguration) -> register_media_loader_bytes(XEN_EFI_KERNEL_MEDIA_GUID, "kernel", kernel_bytes) .context("unable to register kernel media loader")?; - // Extend PCR 9 with the initrd bytes (empty when no initrd is - // configured). - let initrd_bytes = match empty_is_none(configuration.initrd.as_ref()) { - Some(p) => read_loader_payload(&context, "initrd", p)?, - None => Vec::new(), - }; + // Read the initrd, or each initrd in the chain concatenated in order, then + // extend PCR 9 with the combined bytes (empty when no initrd is configured). + let mut initrd_bytes = Vec::new(); + for p in configuration + .initrd + .iter() + .chain(configuration.initrd_chain.iter()) + { + if let Some(p) = empty_is_none(Some(p)) { + initrd_bytes.extend(read_loader_payload(&context, "initrd", p)?); + } + } PlatformTpm::log_event( PlatformTpm::PCR_INITRD, &initrd_bytes, @@ -111,6 +122,7 @@ pub fn edera(context: Rc, configuration: &EderaConfiguration) -> path: configuration.xen.clone(), options: vec![], linux_initrd: None, + linux_initrd_chain: vec![], }, ) .context("unable to chainload to xen"); diff --git a/crates/boot/src/autoconfigure/bls.rs b/crates/boot/src/autoconfigure/bls.rs index 87b1095..3a45871 100644 --- a/crates/boot/src/autoconfigure/bls.rs +++ b/crates/boot/src/autoconfigure/bls.rs @@ -1,3 +1,4 @@ +use crate::generators::bls::BLS_INITRD_SLOTS; use alloc::string::ToString; use alloc::{format, vec}; use anyhow::{Context, Result}; @@ -83,10 +84,15 @@ pub fn scan( // Generate a chainload configuration for BLS. // BLS will provide these values to us. + // Every initrd slot is chained in order. Unused slots stamp to the root of the + // filesystem, which the chainload action skips. let chainload = ChainloadConfiguration { path: format!("{}\\$chainload", root), options: vec!["$options".to_string()], - linux_initrd: Some(format!("{}\\$initrd", root)), + linux_initrd: None, + linux_initrd_chain: (0..BLS_INITRD_SLOTS) + .map(|slot| format!("{}\\$initrd-{}", root, slot)) + .collect(), }; // Insert the chainload action into the configuration. diff --git a/crates/boot/src/autoconfigure/linux.rs b/crates/boot/src/autoconfigure/linux.rs index b9f336c..8d4e196 100644 --- a/crates/boot/src/autoconfigure/linux.rs +++ b/crates/boot/src/autoconfigure/linux.rs @@ -232,6 +232,7 @@ pub fn scan( path: "$kernel".to_string(), options: vec!["$linux-options".to_string()], linux_initrd: Some("$initrd".to_string()), + linux_initrd_chain: vec![], }; // Insert the chainload action into the configuration. diff --git a/crates/boot/src/generators/bls.rs b/crates/boot/src/generators/bls.rs index bf9bb8c..03f95b7 100644 --- a/crates/boot/src/generators/bls.rs +++ b/crates/boot/src/generators/bls.rs @@ -18,11 +18,18 @@ use uefi::{ proto::media::fs::SimpleFileSystem, }; +/// The number of initrd slots set on each BLS entry. +/// Entries set `initrd-0` through `initrd-7`, and slots without an initrd are empty. +pub const BLS_INITRD_SLOTS: usize = 8; + // TODO(azenla): remove this once variable substitution is implemented. -/// This function is used to remove the `tuned_initrd` variable from entry values. +/// This function is used to remove the `tuned_initrd` variable from the initrd paths. /// Fedora uses tuned which adds an initrd that shouldn't be used. -fn quirk_initrd_remove_tuned(input: String) -> String { - input.replace("$tuned_initrd", "").trim().to_string() +fn quirk_initrd_remove_tuned(paths: Vec) -> Vec { + paths + .into_iter() + .filter(|path| path != "$tuned_initrd") + .collect() } /// Sorts two entries according to the BLS sort system. @@ -139,6 +146,18 @@ pub fn generate(context: Rc, bls: &BlsConfiguration) -> Result BLS_INITRD_SLOTS { + warn!( + "bls entry {} has more than {} initrds, skipping", + name, BLS_INITRD_SLOTS + ); + continue; + } + // Produce a new sprout context for the entry with the extracted values. let mut context = context.fork(); @@ -148,9 +167,6 @@ pub fn generate(context: Rc, bls: &BlsConfiguration) -> Result, bls: &BlsConfiguration) -> Result, + /// An optional list of paths to Linux initrds to chain together. + /// The contents of each initrd are concatenated in order and loaded as a single initrd + /// using the same mechanism as `linux-initrd`. This cannot be used with `linux-initrd`. + #[serde(default, rename = "linux-initrd-chain")] + pub linux_initrd_chain: Vec, } diff --git a/crates/config/src/actions/edera.rs b/crates/config/src/actions/edera.rs index 6577610..7721753 100644 --- a/crates/config/src/actions/edera.rs +++ b/crates/config/src/actions/edera.rs @@ -14,6 +14,11 @@ pub struct EderaConfiguration { /// The path to the initrd to load for dom0. #[serde(default)] pub initrd: Option, + /// The paths to the initrds to chain together for dom0. + /// The contents of each initrd are concatenated in order and loaded as a single initrd. + /// This cannot be used with `initrd`. + #[serde(default, rename = "initrd-chain")] + pub initrd_chain: Vec, /// The options to pass to the kernel. #[serde(default, rename = "kernel-options")] pub kernel_options: Vec,