Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
887616d
Stage 2 without the installer: the loader writes boot variables, the …
Japabu Sep 27, 2026
c5d9944
Clippy's and the scratch gate's findings, and the bench configs in AL…
Japabu Sep 27, 2026
0f9b5b7
The boot-next test names a second boot of the recovery stick
Japabu Sep 27, 2026
75ebe9a
loader: an ESP an entry already names is booted by that entry
Japabu Sep 27, 2026
fce5492
The bench test reads the bench's clock, and the harness flag has its …
Japabu Sep 27, 2026
0407f3e
Merge origin/main (a637f5cb: #535) into the update-without-Ubuntu branch
Japabu Sep 27, 2026
9f9da32
Review #539 round 1: a trial writes nothing it keeps, a readback is t…
Japabu Sep 27, 2026
1f3fa2b
userland: the lockfile names toyos-gpt under update
Japabu Sep 27, 2026
4de6c67
update tests: the trial is offered a newer image, and slot A is read …
Japabu Sep 27, 2026
22d5e97
Prose this round added or rewrote goes, but for one clause at each edit
Japabu Sep 27, 2026
f4b9041
Merge origin/main (c79d409b: #546) into the ABI-bump branch
Japabu Sep 27, 2026
fa94514
toyos-abi: version follows its own ABI addition, 0.17.0
Japabu Sep 27, 2026
4ca38b8
Merge remote-tracking branch 'origin/main' into wt/toyos-install
Japabu Sep 27, 2026
1f57634
Review #539 round 2: the refusals that stop a false green come back, …
Japabu Sep 27, 2026
f46a5a6
Merge remote-tracking branch 'origin/main' into wt/toyos-install
Japabu Sep 27, 2026
70b81c8
Review #539 round 3: one HARDDRIVE rule, over the device path's bytes
Japabu Sep 27, 2026
5cd1385
Review #539 round 3: the NOTEs, and the deletions the review owed
Japabu Sep 27, 2026
9b36e42
Merge remote-tracking branch 'origin/main' into wt/toyos-install
Japabu Sep 27, 2026
89d1d72
Merge fallout: main's SDK versions in every lockfile, and Guid::parse…
Japabu Sep 27, 2026
be959b5
One HARDDRIVE rule: boot_disk takes its disk from entry::partition
Japabu Sep 27, 2026
4def9c8
File: after a netd swap, sshd's port resets every connect for the res…
Japabu Sep 27, 2026
bdfc2c5
netd: a listener's socket that left Listen is handed over or listens …
Japabu Sep 27, 2026
d94ae08
File: the bench sometimes comes back two minutes late
Japabu Sep 27, 2026
abdb678
Merge remote-tracking branch 'origin/main' into wt/toyos-install
Japabu Sep 27, 2026
ee9ea0a
Merge wt/toyos-install into wt/toyos-install7
Japabu Sep 28, 2026
3ebb8b4
Merge origin/main into wt/toyos-install7
Japabu Sep 28, 2026
2d6d228
Review r6: one impl Guid, and the track keeps only what it owes
Japabu Sep 28, 2026
37f8a42
A boot that ends before its console closes is read on the UART too
Japabu Sep 28, 2026
ea1e30a
The loader takes its own ESP once, before anything can panic
Japabu Sep 28, 2026
7761abb
update --once refuses by name to drop another ESP's boot
Japabu Sep 28, 2026
19a37c6
A failed pass with nothing behind its own entry powers the machine off
Japabu Sep 28, 2026
c650fbc
The track owes the owner's last resort, and designs no tool for it
Japabu Sep 28, 2026
29ec77f
Merge origin/main into wt/toyos-install7
Japabu Sep 28, 2026
2383402
update_no_entry_powers_off clears the followers each pass reads, unti…
Japabu Sep 28, 2026
42208b5
Merge origin/main into wt/toyos-install7
Japabu Sep 28, 2026
133d5dc
Merge origin/main into wt/toyos-install7
Japabu Sep 28, 2026
b7299ab
Delete update_no_entry_powers_off: the pinned OVMF always writes an a…
Japabu Sep 28, 2026
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
3 changes: 2 additions & 1 deletion bootloader/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ sha2 = { version = "0.10", default-features = false, features = ["force-soft"] }
# `alloc`: `variable_keys` and `get_variable_boxed`, which are how the boot
# entry pointing at this image is found among the firmware's own variables.
uefi = { version = "0.26.0", default-features = false, features = ["alloc"] }
uefi-services = { version = "0.23.0", features = ["panic_handler", "logger"] }
# No `panic_handler`: the loader has its own.
uefi-services = { version = "0.23.0", default-features = false, features = ["logger"] }

# The one profile every guest binary is built with. Optimised, because an
# unoptimised guest mismeasures everything under TCG;
Expand Down
5 changes: 5 additions & 0 deletions bootloader/src/arch/aarch64.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@ use toyos_bootmap::Typing;
/// The machine the kernel image must be built for: the loader's own.
pub const ELF_MACHINE: toyos_elf::Machine = toyos_elf::Machine::Aarch64;

/// The removable-media path firmware boots an EFI system partition by when no
/// entry names a file on it (UEFI 2.10 §3.5.1.1): what every ToyOS image puts
/// its loader at, and what an entry this loader writes for an ESP names.
pub const REMOVABLE_PATH: &str = r"\EFI\BOOT\BOOTAA64.EFI";

/// How the boot map's descriptors are encoded.
pub use toyos_bootmap::aarch64 as encoding;

Expand Down
5 changes: 5 additions & 0 deletions bootloader/src/arch/x86_64.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ use toyos_abi::boot::KernelArgs;
/// The machine the kernel image must be built for: the loader's own.
pub const ELF_MACHINE: toyos_elf::Machine = toyos_elf::Machine::X86_64;

/// The removable-media path firmware boots an EFI system partition by when no
/// entry names a file on it (UEFI 2.10 §3.5.1.1): what every ToyOS image puts
/// its loader at, and what an entry this loader writes for an ESP names.
pub const REMOVABLE_PATH: &str = r"\EFI\BOOT\BOOTX64.EFI";

/// How the boot map's entries are encoded.
pub use toyos_bootmap::x86_64 as encoding;

Expand Down
197 changes: 46 additions & 151 deletions bootloader/src/bootnext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,9 @@
//!
//! **The chain only closes if the machine comes back here.** A panicked kernel
//! resets through the FADT register, the firmware consumes whatever `BootNext`
//! it was given, and on the owner's laptop the next entry in the boot order is
//! Ubuntu — which reuses the black-box page long before anything reads it. So
//! every boot that hands the machine to a kernel first names *this* loader as
//! the next boot, and the pass after the reset is the one that reads the page
//! and decides whether to go on.
//! it was given. So every boot that hands the machine to a kernel first names
//! *this* loader as the next boot, and the pass after the reset is the one that
//! reads the page and decides whether to go on.
//!
//! The entry is found by the GPT partition GUID of the volume this image was
//! loaded from, which is the same identity `efibootmgr --disk … --part 1` writes
Expand All @@ -15,178 +13,75 @@
//! that booted us from a removable-media fallback path has no entry of ours at
//! all and must be told so rather than have one guessed at.

use core::cell::OnceCell;

use uefi::prelude::*;
use uefi::proto::device_path::media::PartitionSignature;
use uefi::proto::device_path::{DevicePath, DeviceSubType, DeviceType};
use uefi::proto::device_path::DevicePath;
use uefi::proto::loaded_image::LoadedImage;
use uefi::table::runtime::{VariableAttributes, VariableVendor};
use uefi::CStr16;

use crate::bootvars;

/// The head of every line this module writes.
const HEAD: &str = "Boot chain:";

/// What a `Boot####` variable's name is after the four hex digits are taken off.
const ENTRY_PREFIX: &str = "Boot";
const ENTRY_DIGITS: usize = 4;

/// `EFI_LOAD_OPTION`'s fixed head: a `UINT32` of attributes and a `UINT16`
/// device-path length, then a null-terminated `CHAR16` description, then the
/// device path itself (UEFI 2.10 §3.1.3).
const LOAD_OPTION_HEAD: usize = 6;

/// Set `BootNext` to this image's own entry, or say by name why it could not be.
///
/// A refusal is not a failure of the boot: the kernel still runs and still seals
/// its page. What is lost is the *next* boot, so the line says exactly that
/// rather than reporting a variable write.
pub fn point_at_us(handle: Handle, system_table: &SystemTable<Boot>) {
let Some(ours) = our_partition(handle, system_table) else {
pub fn point_at_us(ours: Option<&[u8; 16]>, system_table: &SystemTable<Boot>) {
let Some(ours) = ours else {
return println!(
"{HEAD} firmware did not load this image off a GPT partition, so there is no entry \
of ours to come back to and the boot after a reset is the firmware's own"
);
};
let Some(entry) = entry_for(system_table, &ours) else {
return println!(
"{HEAD} no Boot#### entry on this machine names the partition this image came off, \
so the boot after a reset is the firmware's own"
);
let rt = system_table.runtime_services();
let entry = match bootvars::naming(rt, ours) {
Ok(Some(entry)) => entry,
Ok(None) => {
return println!(
"{HEAD} no active Boot#### entry on this machine names the partition this image came \
off, so the boot after a reset is the firmware's own"
)
}
Err(why) => return println!("{HEAD} {why}, so the boot after a reset is the firmware's own"),
};
let write = system_table.runtime_services().set_variable(
cstr16!("BootNext"),
&VariableVendor::GLOBAL_VARIABLE,
// Non-volatile, because it has to survive the reset that is the whole point.
VariableAttributes::NON_VOLATILE
| VariableAttributes::BOOTSERVICE_ACCESS
| VariableAttributes::RUNTIME_ACCESS,
&entry.to_le_bytes(),
);
match write {
match bootvars::boot_next(rt, entry) {
Ok(()) => println!("{HEAD} BootNext={entry:04X}, so this loader gets the machine back"),
Err(e) => println!(
"{HEAD} firmware refused BootNext={entry:04X} ({e}), so the boot after a reset is \
its own"
),
Err(why) => println!("{HEAD} {why}, so the boot after a reset is its own"),
}
}

/// The GPT partition GUID of the volume firmware loaded this image from.
fn our_partition(handle: Handle, system_table: &SystemTable<Boot>) -> Option<[u8; 16]> {
let bs = system_table.boot_services();
let image = bs.open_protocol_exclusive::<LoadedImage>(handle).ok()?;
let device = image.device()?;
let path = bs.open_protocol_exclusive::<DevicePath>(device).ok()?;
hard_drive_guid(path.node_iter())
}
/// This loader's own ESP, as [`take_ours`] found it.
///
/// Taken before anything can panic, because the panic handler cannot open
/// `LoadedImage` while the pass it interrupted holds that protocol exclusively.
/// One processor, no preemption, and no firmware callback reads the cell.
struct Ours(OnceCell<Option<[u8; 16]>>);

/// The GPT signature of the first HARDDRIVE node in a device path, or `None`
/// where the path has none — a network boot, or a disk with no GPT.
fn hard_drive_guid<'a>(nodes: impl Iterator<Item = &'a uefi::proto::device_path::DevicePathNode>) -> Option<[u8; 16]> {
for node in nodes {
if node.full_type() != (DeviceType::MEDIA, DeviceSubType::MEDIA_HARD_DRIVE) {
continue;
}
let hd = <&uefi::proto::device_path::media::HardDrive>::try_from(node).ok()?;
if let PartitionSignature::Guid(guid) = hd.partition_signature() {
return Some(guid.to_bytes());
}
}
None
}
// SAFETY: [`Ours`]'s own contract; nothing else in this crate names the type.
unsafe impl Sync for Ours {}

/// The number of the `Boot####` entry whose device path names `ours`.
///
/// Every entry is read rather than only those in `BootOrder`: an entry the owner
/// has moved out of the order is still ours and still the one to come back to.
fn entry_for(system_table: &SystemTable<Boot>, ours: &[u8; 16]) -> Option<u16> {
let rt = system_table.runtime_services();
let keys = rt.variable_keys().ok()?;
let mut found: Option<u16> = None;
for key in keys {
if key.vendor != VariableVendor::GLOBAL_VARIABLE {
continue;
}
let Ok(name) = key.name() else { continue };
let Some(number) = entry_number(name) else { continue };
let Ok((bytes, _)) = rt.get_variable_boxed(name, &key.vendor) else { continue };
if !load_option_names(&bytes, ours) {
continue;
}
// The lowest, so a machine carrying two entries for one partition is
// answered the same way twice rather than by whichever enumerated first.
found = Some(found.map_or(number, |seen: u16| seen.min(number)));
}
found
}
static OURS: Ours = Ours(OnceCell::new());

/// `Boot0003` is entry 3; anything else here is some other global variable.
fn entry_number(name: &CStr16) -> Option<u16> {
let mut chars = name.iter().map(|c| char::from(*c));
for want in ENTRY_PREFIX.chars() {
if chars.next()? != want {
return None;
}
}
let mut value: u16 = 0;
let mut digits = 0;
for ch in chars {
value = value.checked_mul(16)?.checked_add(ch.to_digit(16)? as u16)?;
digits += 1;
}
(digits == ENTRY_DIGITS).then_some(value)
/// Find this loader's own ESP, once per pass.
pub fn take_ours(handle: Handle, system_table: &SystemTable<Boot>) -> Option<[u8; 16]> {
let ours = our_partition(handle, system_table);
assert!(OURS.0.set(ours).is_ok(), "the loader's own ESP is taken once per pass");
ours
}

/// Whether an `EFI_LOAD_OPTION`'s device path carries `ours`.
///
/// **Walked as bytes, bounded by the slice, and never handed to a pointer
/// iterator.** These bytes are whatever a vendor's NVRAM holds: a node claiming
/// a length of zero is an endless walk and one claiming a length past the
/// variable is a read off the end of it, so both are refused here rather than
/// trusted to a walker that follows the lengths it is given.
fn load_option_names(option: &[u8], ours: &[u8; 16]) -> bool {
let Some(head) = option.get(..LOAD_OPTION_HEAD) else { return false };
let path_len = u16::from_le_bytes([head[4], head[5]]) as usize;
// The description is `CHAR16` and null-terminated, so the path starts after
// the first pair of zero bytes on an even offset from the head.
let mut at = LOAD_OPTION_HEAD;
loop {
let Some(pair) = option.get(at..at + 2) else { return false };
at += 2;
if pair == [0, 0] {
break;
}
}
let Some(mut path) = option.get(at..at.saturating_add(path_len)) else { return false };
while let Some(node) = path.get(..NODE_HEADER) {
let len = u16::from_le_bytes([node[2], node[3]]) as usize;
// A node shorter than its own header, or longer than what is left, ends
// the walk: neither can be stepped over.
let Some(this) = path.get(..len).filter(|_| len >= NODE_HEADER) else { return false };
if this[0] == MEDIA_HARD_DRIVE.0 && this[1] == MEDIA_HARD_DRIVE.1 {
if let Some(guid) = gpt_signature(this) {
return guid == *ours;
}
}
path = path.get(len..).unwrap_or(&[]);
}
false
/// What [`take_ours`] found, for the panic handler.
pub fn ours() -> Result<Option<[u8; 16]>, &'static str> {
OURS.0.get().copied().ok_or("this pass failed before the loader took its own ESP")
}

/// A device path node's type, subtype and length (UEFI 2.10 §10.2).
const NODE_HEADER: usize = 4;

/// The MEDIA/HARD_DRIVE node this looks for, as the two bytes it is on the wire.
const MEDIA_HARD_DRIVE: (u8, u8) = (4, 1);

/// A HARD_DRIVE node's GPT signature, or `None` where it names an MBR one or
/// the node is short (UEFI 2.10 §10.3.6: the signature is sixteen bytes at
/// offset 24, and `SignatureType` 2 is the GPT one).
fn gpt_signature(node: &[u8]) -> Option<[u8; 16]> {
const SIGNATURE: usize = 24;
const SIGNATURE_TYPE: usize = 41;
const GPT: u8 = 2;
if node.get(SIGNATURE_TYPE) != Some(&GPT) {
return None;
}
node.get(SIGNATURE..SIGNATURE + 16)?.try_into().ok()
/// The GPT partition GUID of the volume firmware loaded this image from.
fn our_partition(handle: Handle, system_table: &SystemTable<Boot>) -> Option<[u8; 16]> {
let bs = system_table.boot_services();
let image = bs.open_protocol_exclusive::<LoadedImage>(handle).ok()?;
let device = image.device()?;
let path = bs.open_protocol_exclusive::<DevicePath>(device).ok()?;
toyos_update::entry::partition(path.as_bytes()).ok().map(|(_, part)| part.guid)
}
Loading
Loading