6 Commits

8 changed files with 231 additions and 89 deletions

2
Cargo.lock generated
View File

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

View File

@@ -2,7 +2,7 @@
name = "edera-sprout"
description = "Modern UEFI bootloader"
license = "Apache-2.0"
version = "0.0.7"
version = "0.0.8"
homepage = "https://sprout.edera.dev"
repository = "https://github.com/edera-dev/sprout"
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.
## 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
- [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] Chainload support
- [x] Linux boot support via EFI stub
- [x] Windows boot support via chainload
- [x] Load Linux initrd from disk
- [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
- [ ] Secure Boot support: work in progress
- [ ] UKI support: partial
- [ ] Windows boot support (untested via chainload)
- [ ] multiboot2 support
- [ ] Linux boot protocol (boot without EFI stub)
@@ -139,6 +156,7 @@ chainload.options = ["$options"]
chainload.linux-initrd = "$boot\\$initrd"
```
[Edera]: https://edera.dev
[Fedora Setup Guide]: ./docs/fedora-setup.md
[Generic Linux Setup Guide]: ./docs/generic-linux-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
QEMU_ACCEL="kvm"
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
if [ -n "${QEMU_ACCEL}" ]; then
set -- "${@}" "-accel" "kvm"
set -- "${@}" "-accel" "${QEMU_ACCEL}"
fi
if [ "${QEMU_GDB}" = "1" ]; then

View File

@@ -2,6 +2,8 @@
#![feature(uefi_std)]
use crate::context::{RootContext, SproutContext};
use crate::options::SproutOptions;
use crate::options::parser::OptionsRepresentable;
use crate::phases::phase;
use anyhow::{Context, Result};
use log::info;
@@ -51,7 +53,7 @@ fn main() -> Result<()> {
setup::init()?;
// 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.
// 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 std::collections::BTreeMap;
/// The Sprout options parser.
pub mod parser;
/// Default configuration file path.
const DEFAULT_CONFIG_PATH: &str = "\\sprout.toml";
@@ -23,95 +27,58 @@ impl Default for SproutOptions {
}
}
/// 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>>> {
// 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<_>>();
/// The options parser mechanism for Sprout.
impl OptionsRepresentable for SproutOptions {
/// Produce the [SproutOptions] structure.
type Output = Self;
// 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);
}
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);
/// All the Sprout options that are defined.
fn options() -> &'static [(&'static str, OptionDescription<'static>)] {
&[
(
"config",
OptionDescription {
description: "Path to Sprout configuration file",
form: OptionForm::Value,
},
),
(
"boot",
OptionDescription {
description: "Entry to boot, bypassing the menu",
form: OptionForm::Value,
},
),
(
"help",
OptionDescription {
description: "Display Sprout Help",
form: OptionForm::Help,
},
),
]
}
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.
let mut result = SproutOptions::default();
let options = parse_raw().context("unable to parse options")?;
/// Produces [SproutOptions] from the parsed raw `options` map.
fn produce(options: BTreeMap<String, Option<String>>) -> Result<Self> {
// Use the default value of sprout options and have the raw options be parsed into it.
let mut result = Self::default();
for (key, value) in options {
match key.as_str() {
"config" => {
// The configuration file to load.
result.config = value.context("--config option requires a value")?;
for (key, value) in options {
match key.as_str() {
"config" => {
// The configuration file to load.
result.config = value.context("--config option requires a value")?;
}
"boot" => {
// The entry to boot.
result.boot = Some(value.context("--boot option requires a value")?);
}
_ => bail!("unknown option: --{key}"),
}
"boot" => {
// The entry to boot.
result.boot = Some(value.context("--boot option requires a value")?);
}
_ => bail!("unknown option: --{key}"),
}
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)
}
}