mirror of
https://github.com/edera-dev/sprout.git
synced 2026-11-04 19:52:22 +00:00
feat(initrd): support chaining multiple initrds (#101)
Add linux-initrd-chain to the chainload action and initrd-chain to the edera action. Each file in the list is read in order and concatenated into a single initrd. The chain can't be combined with the single initrd option on the same action. BLS entries with multiple initrds, either as repeated initrd lines or as multiple paths on one line, previously only loaded the last one. Parse all of them and set them in the initrd-0 through initrd-7 slots, which the autoconfigured BLS action now chains. Unused slots stamp to the filesystem root and are skipped. Entries with more than 8 initrds are skipped with a warning.
This commit is contained in:
8 files changed
+157
-57
No files matched your search
+32
-15
@@ -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<String>,
|
||||
/// The path to the linux kernel.
|
||||
pub linux: Option<String>,
|
||||
/// The path to the initrd.
|
||||
pub initrd: Option<String>,
|
||||
/// The paths to the initrds, in the order they appear.
|
||||
pub initrd: Vec<String>,
|
||||
/// The path to an EFI image.
|
||||
pub efi: Option<String>,
|
||||
/// The sort key for the entry.
|
||||
@@ -38,7 +39,7 @@ impl FromStr for BlsEntry {
|
||||
let mut title: Option<String> = None;
|
||||
let mut options: Option<String> = None;
|
||||
let mut linux: Option<String> = None;
|
||||
let mut initrd: Option<String> = None;
|
||||
let mut initrd: Vec<String> = Vec::new();
|
||||
let mut efi: Option<String> = None;
|
||||
let mut sort_key: Option<String> = None;
|
||||
let mut version: Option<String> = 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<String> {
|
||||
pub fn initrd_paths(&self) -> Vec<String> {
|
||||
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]
|
||||
|
||||
@@ -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<SproutContext>, path: &str) -> Result<Option<Vec<u8>>> {
|
||||
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<SproutContext>, 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<SproutContext>, 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<Vec<u8>> = 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")?;
|
||||
|
||||
@@ -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<SproutContext>, 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<SproutContext>, 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<SproutContext>, 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<SproutContext>, configuration: &EderaConfiguration) ->
|
||||
path: configuration.xen.clone(),
|
||||
options: vec![],
|
||||
linux_initrd: None,
|
||||
linux_initrd_chain: vec![],
|
||||
},
|
||||
)
|
||||
.context("unable to chainload to xen");
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<String>) -> Vec<String> {
|
||||
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<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
|
||||
continue;
|
||||
}
|
||||
|
||||
// Put the initrds through a quirk modifier to support Fedora.
|
||||
let initrds = quirk_initrd_remove_tuned(entry.initrd_paths());
|
||||
|
||||
// Skip entries with more initrds than there are slots, as they can't be fully loaded.
|
||||
if initrds.len() > 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<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
|
||||
let version = entry.version().unwrap_or_default();
|
||||
let machine_id = entry.machine_id().unwrap_or_default();
|
||||
|
||||
// Put the initrd through a quirk modifier to support Fedora.
|
||||
let initrd = quirk_initrd_remove_tuned(entry.initrd_path().unwrap_or_default());
|
||||
|
||||
// Combine the title with the version if a version is present, except if it already contains it.
|
||||
// Sometimes BLS will have a version in the title already, and this makes it unique.
|
||||
let title_full = if !version.is_empty() && !title_base.contains(&version) {
|
||||
@@ -163,7 +179,19 @@ pub fn generate(context: Rc<SproutContext>, bls: &BlsConfiguration) -> Result<Ve
|
||||
context.set("title", title_full);
|
||||
context.set("chainload", chainload);
|
||||
context.set("options", options);
|
||||
context.set("initrd", initrd);
|
||||
// The initrd value keeps the last initrd, which is what it held before
|
||||
// multiple initrds were supported.
|
||||
context.set("initrd", initrds.last().cloned().unwrap_or_default());
|
||||
|
||||
// Set every initrd slot, even the unused ones. An unset slot would let "$initrd-1"
|
||||
// match "$initrd" instead, producing the initrd value followed by "-1".
|
||||
for slot in 0..BLS_INITRD_SLOTS {
|
||||
context.set(
|
||||
format!("initrd-{}", slot),
|
||||
initrds.get(slot).cloned().unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
|
||||
context.set("version", version);
|
||||
context.set("machine-id", machine_id);
|
||||
|
||||
|
||||
@@ -18,4 +18,9 @@ pub struct ChainloadConfiguration {
|
||||
/// generally better and safer as it can support additional load options in the future.
|
||||
#[serde(default, rename = "linux-initrd")]
|
||||
pub linux_initrd: Option<String>,
|
||||
/// 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<String>,
|
||||
}
|
||||
@@ -14,6 +14,11 @@ pub struct EderaConfiguration {
|
||||
/// The path to the initrd to load for dom0.
|
||||
#[serde(default)]
|
||||
pub initrd: Option<String>,
|
||||
/// 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<String>,
|
||||
/// The options to pass to the kernel.
|
||||
#[serde(default, rename = "kernel-options")]
|
||||
pub kernel_options: Vec<String>,
|
||||
|
||||
Reference in new issue
Block a user