6 Commits

8 changed files with 231 additions and 89 deletions

2
Cargo.lock generated
View File

@@ -61,7 +61,7 @@ dependencies = [
[[package]] [[package]]
name = "edera-sprout" name = "edera-sprout"
version = "0.0.7" version = "0.0.8"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"image", "image",

View File

@@ -2,7 +2,7 @@
name = "edera-sprout" name = "edera-sprout"
description = "Modern UEFI bootloader" description = "Modern UEFI bootloader"
license = "Apache-2.0" license = "Apache-2.0"
version = "0.0.7" version = "0.0.8"
homepage = "https://sprout.edera.dev" homepage = "https://sprout.edera.dev"
repository = "https://github.com/edera-dev/sprout" repository = "https://github.com/edera-dev/sprout"
edition = "2024" edition = "2024"

View File

@@ -18,6 +18,23 @@ existing UEFI bootloader or booted by the hardware directly.
Sprout is licensed under Apache 2.0 and is open to modifications and contributions. Sprout is licensed under Apache 2.0 and is open to modifications and contributions.
## Background
At [Edera] we make compute isolation technology for a wide variety of environments, often ones we do not fully control.
Our technology utilizes a hypervisor to boot the host system to provide a new isolation mechanism that works
with or without hardware virtualization support. To do this we need to inject our hypervisor at boot time.
Unfortunately, GRUB, the most common bootloader on Linux systems today, utilizes a shell-script like
configuration system. Both the code that runs to generate a GRUB config and the GRUB config
itself is fully turing complete. This makes modifying boot configuration difficult and error-prone.
Sprout was designed to take in a machine-readable, writable, and modifiable configuration that treats boot information
like data plus configuration, and can be chained from both UEFI firmware and GRUB alike.
Sprout aims to be flexible, secure, and modern. Written in Rust, it handles data safely and uses unsafe code as little
as possible. It also critically must be easy to install into all common distributions, relying on simple principles to
simplify installation and usage.
## Documentation ## Documentation
- [Fedora Setup Guide] - [Fedora Setup Guide]
@@ -40,6 +57,7 @@ have secure boot support. In fact, as of writing, it doesn't even have a boot me
- [x] [Bootloader specification (BLS)](https://uapi-group.org/specifications/specs/boot_loader_specification/) support - [x] [Bootloader specification (BLS)](https://uapi-group.org/specifications/specs/boot_loader_specification/) support
- [x] Chainload support - [x] Chainload support
- [x] Linux boot support via EFI stub - [x] Linux boot support via EFI stub
- [x] Windows boot support via chainload
- [x] Load Linux initrd from disk - [x] Load Linux initrd from disk
- [x] Boot first configured entry - [x] Boot first configured entry
@@ -48,7 +66,6 @@ have secure boot support. In fact, as of writing, it doesn't even have a boot me
- [ ] Boot menu - [ ] Boot menu
- [ ] Secure Boot support: work in progress - [ ] Secure Boot support: work in progress
- [ ] UKI support: partial - [ ] UKI support: partial
- [ ] Windows boot support (untested via chainload)
- [ ] multiboot2 support - [ ] multiboot2 support
- [ ] Linux boot protocol (boot without EFI stub) - [ ] Linux boot protocol (boot without EFI stub)
@@ -139,6 +156,7 @@ chainload.options = ["$options"]
chainload.linux-initrd = "$boot\\$initrd" chainload.linux-initrd = "$boot\\$initrd"
``` ```
[Edera]: https://edera.dev
[Fedora Setup Guide]: ./docs/fedora-setup.md [Fedora Setup Guide]: ./docs/fedora-setup.md
[Generic Linux Setup Guide]: ./docs/generic-linux-setup.md [Generic Linux Setup Guide]: ./docs/generic-linux-setup.md
[Windows Setup Guide]: ./docs/windows-setup.md [Windows Setup Guide]: ./docs/windows-setup.md

View File

@@ -41,3 +41,8 @@ if [ -z "${QEMU_ACCEL}" ] && [ "${TARGET_ARCH}" = "${HOST_ARCH}" ] &&
grep -E '^flags.*:.+ vmx .*' /proc/cpuinfo >/dev/null; then grep -E '^flags.*:.+ vmx .*' /proc/cpuinfo >/dev/null; then
QEMU_ACCEL="kvm" QEMU_ACCEL="kvm"
fi fi
if [ "$(uname)" = "Darwin" ] && [ "${TARGET_ARCH}" = "${HOST_ARCH}" ] &&
[ "$(sysctl -n kern.hv_support 2>&1 || true)" = "1" ]; then
QEMU_ACCEL="hvf"
fi

View File

@@ -19,7 +19,7 @@ elif [ "${TARGET_ARCH}" = "aarch64" ]; then
fi fi
if [ -n "${QEMU_ACCEL}" ]; then if [ -n "${QEMU_ACCEL}" ]; then
set -- "${@}" "-accel" "kvm" set -- "${@}" "-accel" "${QEMU_ACCEL}"
fi fi
if [ "${QEMU_GDB}" = "1" ]; then if [ "${QEMU_GDB}" = "1" ]; then

View File

@@ -2,6 +2,8 @@
#![feature(uefi_std)] #![feature(uefi_std)]
use crate::context::{RootContext, SproutContext}; use crate::context::{RootContext, SproutContext};
use crate::options::SproutOptions;
use crate::options::parser::OptionsRepresentable;
use crate::phases::phase; use crate::phases::phase;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use log::info; use log::info;
@@ -51,7 +53,7 @@ fn main() -> Result<()> {
setup::init()?; setup::init()?;
// Parse the options to the sprout executable. // Parse the options to the sprout executable.
let options = options::parse().context("unable to parse options")?; let options = SproutOptions::parse().context("unable to parse options")?;
// Load the configuration of sprout. // Load the configuration of sprout.
// At this point, the configuration has been validated and the specified // At this point, the configuration has been validated and the specified

View File

@@ -1,6 +1,10 @@
use crate::options::parser::{OptionDescription, OptionForm, OptionsRepresentable};
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use std::collections::BTreeMap; use std::collections::BTreeMap;
/// The Sprout options parser.
pub mod parser;
/// Default configuration file path. /// Default configuration file path.
const DEFAULT_CONFIG_PATH: &str = "\\sprout.toml"; const DEFAULT_CONFIG_PATH: &str = "\\sprout.toml";
@@ -23,80 +27,42 @@ impl Default for SproutOptions {
} }
} }
/// For minimalism, we don't want a full argument parser. Instead, we use /// The options parser mechanism for Sprout.
/// a simple --xyz = xyz: None and --abc 123 = abc: Some("123") format. impl OptionsRepresentable for SproutOptions {
/// We also support --abc=123 = abc: Some("123") format. /// Produce the [SproutOptions] structure.
fn parse_raw() -> Result<BTreeMap<String, Option<String>>> { type Output = Self;
// Collect all the arguments to Sprout.
// Skip the first argument which is the path to our executable.
let args = std::env::args().skip(1).collect::<Vec<_>>();
// Represent options as key-value pairs. /// All the Sprout options that are defined.
let mut options = BTreeMap::new(); fn options() -> &'static [(&'static str, OptionDescription<'static>)] {
&[
// Iterators makes this way easier. (
let mut iterator = args.into_iter().peekable(); "config",
OptionDescription {
loop { description: "Path to Sprout configuration file",
// Consume the next option, if any. form: OptionForm::Value,
let Some(option) = iterator.next() else { },
break; ),
}; (
"boot",
// If the doesn't start with --, that is invalid. OptionDescription {
if !option.starts_with("--") { description: "Entry to boot, bypassing the menu",
bail!("invalid option: {option}"); form: OptionForm::Value,
},
),
(
"help",
OptionDescription {
description: "Display Sprout Help",
form: OptionForm::Help,
},
),
]
} }
// Strip the -- prefix off. /// Produces [SproutOptions] from the parsed raw `options` map.
let mut option = option["--".len()..].trim().to_string(); fn produce(options: BTreeMap<String, Option<String>>) -> Result<Self> {
// An optional value.
let mut value = None;
// Check if the option is of the form --abc=123
if option.contains("=") {
let Some((part_key, part_value)) = option.split_once("=") else {
bail!("invalid option: {option}");
};
let part_key = part_key.to_string();
let part_value = part_value.to_string();
option = part_key;
value = Some(part_value);
}
if value.is_none() {
// Check for the next value.
let maybe_next = iterator.peek();
// If the next value isn't another option, set the value to the next value.
// Otherwise, it is an empty string.
value = if let Some(next) = maybe_next
&& !next.starts_with("--")
{
iterator.next()
} else {
None
};
}
// Error on empty option names.
if option.is_empty() {
bail!("invalid empty option: {option}");
}
// Insert the option and the value into the map.
options.insert(option, value);
}
Ok(options)
}
/// Parse the arguments to Sprout as a [SproutOptions] structure.
pub fn parse() -> Result<SproutOptions> {
// Use the default value of sprout options and have the raw options be parsed into it. // Use the default value of sprout options and have the raw options be parsed into it.
let mut result = SproutOptions::default(); let mut result = Self::default();
let options = parse_raw().context("unable to parse options")?;
for (key, value) in options { for (key, value) in options {
match key.as_str() { match key.as_str() {
@@ -114,4 +80,5 @@ pub fn parse() -> Result<SproutOptions> {
} }
} }
Ok(result) Ok(result)
}
} }

150
src/options/parser.rs Normal file
View File

@@ -0,0 +1,150 @@
use anyhow::{Context, Result, bail};
use std::collections::BTreeMap;
/// The type of option. This disambiguates different behavior
/// of how options are handled.
#[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub enum OptionForm {
/// A flag, like --verbose.
Flag,
/// A value, in the form --abc 123 or --abc=123.
Value,
/// Help flag, like --help.
Help,
}
/// The description of an option, used in the options parser
/// to make decisions about how to progress.
#[derive(Debug, Clone)]
pub struct OptionDescription<'a> {
/// The description of the option.
pub description: &'a str,
/// The type of option to parse as.
pub form: OptionForm,
}
/// Represents a type that can be parsed from command line arguments.
/// This is a super minimal options parser mechanism just for Sprout.
pub trait OptionsRepresentable {
/// The output type that parsing will produce.
type Output;
/// The configured options for this type. This should describe all the options
/// that are valid to produce the type. The left hand side is the name of the option,
/// and the right hand side is the description.
fn options() -> &'static [(&'static str, OptionDescription<'static>)];
/// Produces the type by taking the `options` and processing it into the output.
fn produce(options: BTreeMap<String, Option<String>>) -> Result<Self::Output>;
/// For minimalism, we don't want a full argument parser. Instead, we use
/// a simple --xyz = xyz: None and --abc 123 = abc: Some("123") format.
/// We also support --abc=123 = abc: Some("123") format.
fn parse_raw() -> Result<BTreeMap<String, Option<String>>> {
// Access the configured options for this type.
let configured: BTreeMap<_, _> = BTreeMap::from_iter(Self::options().to_vec());
// Collect all the arguments to Sprout.
// Skip the first argument which is the path to our executable.
let args = std::env::args().skip(1).collect::<Vec<_>>();
// Represent options as key-value pairs.
let mut options = BTreeMap::new();
// Iterators makes this way easier.
let mut iterator = args.into_iter().peekable();
loop {
// Consume the next option, if any.
let Some(option) = iterator.next() else {
break;
};
// If the doesn't start with --, that is invalid.
if !option.starts_with("--") {
bail!("invalid option: {option}");
}
// Strip the -- prefix off.
let mut option = option["--".len()..].trim().to_string();
// An optional value.
let mut value = None;
// Check if the option is of the form --abc=123
if option.contains("=") {
let Some((part_key, part_value)) = option.split_once("=") else {
bail!("invalid option: {option}");
};
let part_key = part_key.to_string();
let part_value = part_value.to_string();
option = part_key;
value = Some(part_value);
}
// Error on empty option names.
if option.is_empty() {
bail!("invalid empty option");
}
// Find the description of the configured option, if any.
let Some(description) = configured.get(option.as_str()) else {
bail!("invalid option: --{option}");
};
// Check if the option requires a value and error if none was provided.
if description.form == OptionForm::Value && value.is_none() {
// Check for the next value.
let maybe_next = iterator.peek();
// If the next value isn't another option, set the value to the next value.
// Otherwise, it is an empty string.
value = if let Some(next) = maybe_next
&& !next.starts_with("--")
{
iterator.next()
} else {
None
};
}
// If the option form does not support a value and there is a value, error.
if description.form != OptionForm::Value && value.is_some() {
bail!("option --{} does not take a value", option);
}
// Handle the --help flag case.
if description.form == OptionForm::Help {
// Generic configured options output.
println!("Configured Options:");
for (name, description) in &configured {
println!(
" --{}{}: {}",
name,
if description.form == OptionForm::Value {
" <value>"
} else {
""
},
description.description
);
}
// Exit because the help has been displayed.
std::process::exit(1);
}
// Insert the option and the value into the map.
options.insert(option, value);
}
Ok(options)
}
/// Parses the program arguments as a [Self::Output], calling [Self::parse_raw] and [Self::produce].
fn parse() -> Result<Self::Output> {
// Parse the program arguments into a raw map.
let options = Self::parse_raw().context("unable to parse options")?;
// Produce the options from the map.
Self::produce(options)
}
}