Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 31 additions & 18 deletions crates/boot/src/actions/chainload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,10 @@ 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};
use edera_sprout_parsing::combine_options;
use eficore::bootloader_interface::BootloaderInterface;
use eficore::loader::source::ImageSource;
use eficore::loader::{ImageLoadRequest, ImageLoader};
Expand Down Expand Up @@ -62,25 +63,37 @@ pub fn chainload(context: Rc<SproutContext>, configuration: &ChainloadConfigurat
.set_load_options(options.as_ptr() as *const u8, options.num_bytes() as u32);
}

// Stamp the initrd path, if provided.
let initrd = configuration
.linux_initrd
.as_ref()
.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 content = eficore::path::read_file_contents(
Some(context.root().loaded_image_path()?),
&linux_initrd,
)
.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")?;

// Only proceed if the configuration actually contains initrd paths.
if !configuration.initrd.is_empty() {
// Initialize a master buffer to hold the combined bytes of all initrd files.
let mut content = Vec::new();

// Iterate through each path provided in the configuration.
for item in &configuration.initrd {
// "Stamp" the path to replace variables (like disk labels) with real values.
let stamped_path = context.stamp(item);

// Read the file contents from the EFI partition.
let mut segment = eficore::path::read_file_contents(
Some(context.root().loaded_image_path()?),
&stamped_path,
).context("unable to read linux initrd segment")?;

// Append this segment's bytes to the end of our master buffer.
// The kernel will see these as a continuous stream of CPIO archives.
content.append(&mut segment);
}

// Register the full, concatenated buffer with the EFI stack using the Linux-specific GUID.
// This allows the Linux EFI stub to discover the initrd in memory.
let handle = MediaLoaderHandle::register(
LINUX_EFI_INITRD_MEDIA_GUID,
content.into_boxed_slice()
).context("unable to register initrd")?;

initrd_handle = Some(handle);
}

Expand Down
20 changes: 12 additions & 8 deletions crates/boot/src/actions/edera.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use alloc::{format, vec};
use anyhow::{Context, Result};
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};
use edera_sprout_parsing::{build_xen_config, combine_options};
use eficore::media_loader::{
MediaLoaderHandle,
constants::xen::{
Expand Down Expand Up @@ -74,12 +74,16 @@ pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) ->
// Create a vector of media loaders to drop them only after this function completes.
let mut media_loaders = vec![config, kernel];

// Register the initrd if it is provided.
if let Some(initrd) = empty_is_none(configuration.initrd.as_ref()) {
let initrd =
register_media_loader_file(&context, XEN_EFI_RAMDISK_MEDIA_GUID, "initrd", initrd)
.context("unable to register initrd media loader")?;
media_loaders.push(initrd);
// Register each initrd segment, filtering out any empty strings or placeholders.
for initrd_path in configuration.initrd.iter().filter(|s| !s.is_empty()) {
let handle = register_media_loader_file(
&context,
XEN_EFI_RAMDISK_MEDIA_GUID,
"initrd",
initrd_path
).context("unable to register initrd segment media loader")?;

media_loaders.push(handle);
}

// Chainload to the Xen EFI stub.
Expand All @@ -88,7 +92,7 @@ pub fn edera(context: Rc<SproutContext>, configuration: &EderaConfiguration) ->
&ChainloadConfiguration {
path: configuration.xen.clone(),
options: vec![],
linux_initrd: None,
initrd: vec![],
},
)
.context("unable to chainload to xen");
Expand Down
2 changes: 1 addition & 1 deletion crates/boot/src/autoconfigure/bls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ pub fn scan(
let chainload = ChainloadConfiguration {
path: format!("{}\\$chainload", root),
options: vec!["$options".to_string()],
linux_initrd: Some(format!("{}\\$initrd", root)),
initrd: alloc::vec![format!("{}\\$initrd", root)],
};

// Insert the chainload action into the configuration.
Expand Down
2 changes: 1 addition & 1 deletion crates/boot/src/autoconfigure/linux.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ pub fn scan(
let chainload = ChainloadConfiguration {
path: "$kernel".to_string(),
options: vec!["$linux-options".to_string()],
linux_initrd: Some("$initrd".to_string()),
initrd: alloc::vec!["$initrd".to_string()],
};

// Insert the chainload action into the configuration.
Expand Down
40 changes: 33 additions & 7 deletions crates/config/src/actions/chainload.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,47 @@
use alloc::string::String;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
use serde::{Deserialize, Deserializer, Serialize};

// Naming the return type vastly improves readability
type DeResult<'de, D, T> = core::result::Result<T, <D as Deserializer<'de>>::Error>;

// Type of vector for a list of strings
type StringList = Vec<String>;

// Helper function to allow "linux-initrd" to be either a single string
// or a list of strings in the TOML file.
fn string_or_vec<'de, D>(deserializer: D) -> DeResult<'de, D, StringList>
where
D: Deserializer<'de>, {

#[derive(Deserialize)]
#[serde(untagged)]
enum StringOrVec {
String(String),
Vec(Vec<String>),
}

match StringOrVec::deserialize(deserializer)? {
StringOrVec::String(s) => Ok(alloc::vec![s]),
// Wrap the vector in Ok() to match the return type
StringOrVec::Vec(v) => Ok(v),
}
}

/// The configuration of the chainload action.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct ChainloadConfiguration {
/// The path to the image to chainload.
/// This can be a Linux EFI stub (vmlinuz usually) or a standard EFI executable.
pub path: String,

/// The options to pass to the image.
/// The options are concatenated by a space and then passed to the EFI application.
#[serde(default)]
pub options: Vec<String>,
/// An optional path to a Linux initrd.
/// This uses the `LINUX_EFI_INITRD_MEDIA_GUID` mechanism to load the initrd into the EFI stack.
/// For Linux, you can also use initrd=\path\to\initrd as an option, but this option is
/// generally better and safer as it can support additional load options in the future.
#[serde(default, rename = "linux-initrd")]
pub linux_initrd: Option<String>,

/// The path(s) to the initrd to use for the image.
/// Supports both a single string or an array of strings.
#[serde(default, rename = "linux-initrd", deserialize_with = "string_or_vec")]
pub initrd: Vec<String>,
}
10 changes: 6 additions & 4 deletions crates/config/src/actions/edera.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ use alloc::string::String;
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};

/// The configuration of the edera action which boots the Edera hypervisor.
/// Edera is based on Xen but modified significantly with a Rust stack.
/// Sprout is a component of the Edera stack and provides the boot functionality of Xen.
type StringList = Vec<String>;

// The configuration of the edera action which boots the Edera hypervisor.
// Edera is based on Xen but modified significantly with a Rust stack.
// Sprout is a component of the Edera stack and provides the boot functionality of Xen.
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
pub struct EderaConfiguration {
/// The path to the Xen hypervisor EFI image.
Expand All @@ -13,7 +15,7 @@ pub struct EderaConfiguration {
pub kernel: String,
/// The path to the initrd to load for dom0.
#[serde(default)]
pub initrd: Option<String>,
pub initrd: StringList,
/// The options to pass to the kernel.
#[serde(default, rename = "kernel-options")]
pub kernel_options: Vec<String>,
Expand Down
22 changes: 1 addition & 21 deletions crates/parsing/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ pub fn build_matrix(input: &BTreeMap<String, Vec<String>>) -> Vec<BTreeMap<Strin
/// Combine a sequence of strings into a single string, separated by spaces, ignoring empty strings.
pub fn combine_options<T: AsRef<str>>(options: impl Iterator<Item = T>) -> String {
options
.flat_map(|item| empty_is_none(Some(item)))
.map(|item| item.as_ref().to_string())
.filter(|item| !item.is_empty())
.collect::<Vec<_>>()
.join(" ")
}
Expand All @@ -96,11 +96,6 @@ pub fn unique_hash(input: &str) -> String {
hex::encode(Sha256::digest(input.as_bytes()))
}

/// Filter a string-like Option `input` such that an empty string is [None].
pub fn empty_is_none<T: AsRef<str>>(input: Option<T>) -> Option<T> {
input.filter(|input| !input.as_ref().is_empty())
}

/// Build a Xen EFI stub configuration file from pre-stamped `xen_options` and `kernel_options`.
/// The returned string is in the Xen ini-like config file format.
pub fn build_xen_config(xen_options: &str, kernel_options: &str) -> String {
Expand Down Expand Up @@ -287,21 +282,6 @@ mod tests {
assert_eq!(result, "");
}

#[test]
fn empty_is_none_returns_none_for_empty_string() {
assert!(empty_is_none(Some("")).is_none());
}

#[test]
fn empty_is_none_returns_some_for_nonempty_string() {
assert_eq!(empty_is_none(Some("x")), Some("x"));
}

#[test]
fn empty_is_none_passthrough_on_none() {
assert!(empty_is_none(None::<&str>).is_none());
}

#[test]
fn unique_hash_is_deterministic() {
assert_eq!(unique_hash("hello"), unique_hash("hello"));
Expand Down