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
@@ -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);
|
||||
|
||||
|
||||
Reference in new issue
Block a user