Skip to content
Merged
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
2 changes: 1 addition & 1 deletion images/console.toml
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ restart = true
serves = ["block"]
devices = ["pci:01:08:02/1b36:0010"]

# The file servers: DATA, the log and the running slot's volume, one process
# The file servers: DATA, the log and the boot volume, one process
# each, serving every program the directories of its role. Started again when
# one ends, on the same ports.
[programs.fileserver]
Expand Down
2 changes: 1 addition & 1 deletion images/system.toml
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ restart = true
serves = ["block"]
devices = ["pci:01:08:02/1b36:0010"]

# The file servers: DATA, the log and the running slot's volume, one process
# The file servers: DATA, the log and the boot volume, one process
# each, serving every program the directories of its role. Started again when
# one ends, on the same ports.
[programs.fileserver]
Expand Down
24 changes: 0 additions & 24 deletions issues/diskserver-serves-the-slot-the-machine-runs-from.md

This file was deleted.

17 changes: 17 additions & 0 deletions issues/the-block-service-does-not-refuse-the-running-root.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
status: none
kind: rejected
opened: 2026-09-26
---

# The block service does not refuse the running ROOT

diskserver serves the partition the loader read ROOT from like any other:
the grant its connector was minted with decides whether a session opens it
and whether that session writes. It is not told which partition the machine
runs from and refuses nothing for being that partition.

The owner declined a blanket refusal on 2026-10-20. An authorized write there
can invalidate the stored image and leave the installation unbootable; that
is the grant holder's authority, and no automatic rollback is assumed to
recover it — the update policy is fix-forward.
27 changes: 13 additions & 14 deletions issues/the-running-slots-volume-is-claimable.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,23 @@ opened: 2026-09-26
The kernel holds the partition its ROOT image came from for the machine's life
(`kernel/src/rootfs.rs`, `hold_source`), so no claim can write the ROOT a boot
runs. It holds nothing of the same slot's FAT volume — the kernel, its boot
parameter and the signed header the loader read — so any process whose
`SysCap` carries `device` can claim that partition by its unique GUID and write
it. The test estate's `test-runner` holds `device`, and so would any row
a config gives it to.
parameter and the signed header the loader read — so on a disk the kernel
drives, any process whose `SysCap` carries `device` can claim that partition
by its unique GUID and write it. The test estate's `test-runner` holds
`device`, and so would any row a config gives it to.

`/system/bin/update` reaches it too. `slots::grant` (`lib/update/src/slots.rs`)
knows which volume is the running slot's only from the slot table, and
`update` writes that table: one naming a decoy volume beside the running ROOT
as slot A, and the running volume beside the idle ROOT as slot B, is granted
the running volume on the next boot. Nothing the kernel holds names the booted
volume, so init has nothing to check the table's word against.
`/system/bin/update` does not: the loader names the booted volume in the
inventory (`Role::Slot`), and `slots::grant` refuses an idle slot naming it,
whatever the table the updater wrote says.

A write there is not a silent compromise: the loader holds every byte to the
signature at the next boot and refuses the slot by name. It is a denial —
the running slot is refused next boot and the machine falls back to an older
image, or to nothing on a one-slot image.

**Exit**: the loader hands the kernel the booted slot's volume by its unique
GUID beside ROOT's, and the kernel holds it as it holds ROOT's, so a claim on
it is refused as `PermissionDenied` like every partition the kernel holds; a
guest test asserts the refusal.
**Exit**: the kernel's USB storage path is deleted with its xHCI driver
(`issues/the-kernel-is-small-interrupts-post-and-threads-wait.md`, step 10).
The kernel then serves no partition claim, `hold_source` goes with it, and
the running slot's volume and ROOT are reached only through the block
service's grants (`issues/the-block-service-does-not-refuse-the-running-root.md`).
No kernel hold of the volume is added in the meantime.
14 changes: 9 additions & 5 deletions kernel/src/gpt.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
//! The partitions of the disks this kernel drives — the USB disks, until usbd
//! drives the controller — and the ones the bootloader named.
//!
//! **The loader names three partitions** ([`KernelArgs`]): the ROOT it read,
//! the running slot's volume and the log's. This kernel mounts none of them
//! but ROOT, which is in memory (`rootfs`); it answers their GUIDs in the
//! inventory ([`loaded`]) so the file servers for those roles find them, on
//! whatever disk and whoever drives it.
//! **The loader names four partitions** ([`KernelArgs`]): the ROOT it read,
//! the booted slot's volume, the log's and the ESP firmware loaded it from
//! (`Role::Boot`). This kernel mounts none of them but ROOT, which is in
//! memory (`rootfs`); it answers their GUIDs in the inventory ([`loaded`]),
//! so the log and boot file servers find theirs and the supervisor knows the
//! running slot, on whatever disk and whoever drives it.
//!
//! The boot partition's location must match firmware's account on a disk
//! [`probe`] read, and [`probe_usb_disks`] waits for the disk that carries it
Expand Down Expand Up @@ -165,6 +166,9 @@ pub fn init(args: &KernelArgs) {
if args.root_partition_guid != [0; 16] {
loaded.push((Role::Root, Guid(args.root_partition_guid)));
}
if args.slot_volume_guid != [0; 16] {
loaded.push((Role::Slot, Guid(args.slot_volume_guid)));
}
if args.log_partition_guid != [0; 16] {
let log_guid = Guid(args.log_partition_guid);
log!("gpt: the boot volume names {log_guid} as the log partition");
Expand Down
1 change: 1 addition & 0 deletions lib/inspect/src/dev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ pub fn render(machine: &Machine, records: &[Record]) -> Result<BTreeMap<String,
Role::Root => "root",
Role::Boot => "boot",
Role::Log => "log",
Role::Slot => "slot",
};
out.put(format!("loaded.{role}"), guid(l.unique_guid).into())?;
}
Expand Down
5 changes: 5 additions & 0 deletions lib/update/src/policy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ pub enum Refusal {
Hash(&'static str),
/// A section could not be read.
Unreadable(&'static str),
/// The partition the slot table names for a section is not of its type.
Type(&'static str),
}

impl Refusal {
Expand All @@ -60,6 +62,7 @@ impl Refusal {
Self::BelowFloor { .. } | Self::Older { .. } => "version",
Self::Hash(_) => "hash",
Self::Unreadable(_) => "unreadable",
Self::Type(_) => "type",
}
}
}
Expand All @@ -78,6 +81,7 @@ impl core::fmt::Display for Refusal {
Self::Older { version, than } => write!(f, "its version {version} is older than {than}"),
Self::Hash(section) => write!(f, "its {section} is not the bytes its signed header names"),
Self::Unreadable(what) => write!(f, "its {what} could not be read"),
Self::Type(what) => write!(f, "its {what} partition is not typed as one"),
}
}
}
Expand Down Expand Up @@ -163,6 +167,7 @@ mod tests {
Refusal::BelowFloor { version: 1, floor: 2 },
Refusal::Hash("kernel"),
Refusal::Unreadable("root"),
Refusal::Type("root"),
] {
assert!(!r.word().is_empty() && !r.word().contains([',', ' ', ':', '=']), "{r:?}");
}
Expand Down
49 changes: 40 additions & 9 deletions lib/update/src/slots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -290,19 +290,26 @@ pub struct Kinds {
}

/// The idle slot a grant may claim, given the ROOT this boot runs
/// (`running`) and every partition the machine lists.
/// (`running`), the volume it was booted from (`volume`), both as the loader
/// named them, and every partition the machine lists.
///
/// **The table is the grantee's to write**, so nothing it names is taken on
/// its word: each of the idle slot's two partitions must be on the running
/// ROOT's disk, of its kind's type, and neither of the running slot's — else
/// the holder of one grant could name its next one anywhere.
pub fn grant(table: &Table, running: &Listed, listed: &[Listed], kinds: Kinds) -> Result<(Which, Slot), NoIdle> {
/// ROOT's disk, of its kind's type, and neither of the running slot's as the
/// loader named it — else the holder of one grant could name its next one
/// anywhere.
pub fn grant(
table: &Table,
running: &Listed,
volume: [u8; 16],
listed: &[Listed],
kinds: Kinds,
) -> Result<(Which, Slot), NoIdle> {
let (idle, slot) = idle(table, &running.unique_guid)?;
let runs = table.slot(idle.other()).expect("`idle` found the running slot in the table");
for (part, guid, kind) in [("volume", slot.boot, kinds.boot), ("ROOT", slot.root, kinds.root)] {
let mut named = listed.iter().filter(|p| p.unique_guid == guid);
let why = match (named.next(), named.next()) {
_ if guid == runs.boot || guid == runs.root => Some(Stray::Running),
_ if guid == volume || guid == running.unique_guid => Some(Stray::Running),
(None, _) => Some(Stray::Unlisted),
(Some(p), None) if p.device != running.device => Some(Stray::OtherDisk),
(Some(p), None) if p.type_guid != kind => Some(Stray::Type),
Expand Down Expand Up @@ -423,12 +430,13 @@ mod tests {
at(0, [0xEF; 16], ESP),
at(1, KINDS.boot, [5; 16]),
];
let volume = [1; 16];
let t = table(Which::A, 1);
assert_eq!(grant(&t, &running, &listed, KINDS).map(|(w, _)| w), Ok(Which::B));
assert_eq!(grant(&t, &running, volume, &listed, KINDS).map(|(w, _)| w), Ok(Which::B));
let bent = |boot: [u8; 16], root: [u8; 16]| {
let mut t = t;
t.slots[1] = Some(Slot { boot, root, version: 0 });
grant(&t, &running, &listed, KINDS)
grant(&t, &running, volume, &listed, KINDS)
};
let stray = |part, why| Err(NoIdle::Stray { part, why });
assert_eq!(bent([1; 16], [4; 16]), stray("volume", Stray::Running), "the running slot's volume");
Expand All @@ -439,6 +447,29 @@ mod tests {
assert_eq!(bent([9; 16], [4; 16]), stray("volume", Stray::Unlisted));
let mut twice = listed.to_vec();
twice.push(at(1, KINDS.boot, [3; 16]));
assert_eq!(grant(&t, &running, &twice, KINDS), stray("volume", Stray::Duplicate), "a GUID two disks carry");
assert_eq!(grant(&t, &running, volume, &twice, KINDS), stray("volume", Stray::Duplicate), "a GUID two disks carry");
}

/// **The running slot is the loader's word, not the table's**: a table
/// pairing a decoy volume with the running ROOT, and the booted volume
/// with the idle ROOT, is refused the booted volume.
#[test]
fn a_table_cannot_name_the_booted_volume_idle() {
const KINDS: Kinds = Kinds { boot: [0xB0; 16], root: [0xA0; 16] };
let at = |type_guid, unique_guid| Listed { device: 0, type_guid, unique_guid };
let (booted, decoy, running, idle_root) = ([1; 16], [3; 16], [2; 16], [4; 16]);
let listed = [at(KINDS.boot, booted), at(KINDS.boot, decoy), at(KINDS.root, running), at(KINDS.root, idle_root)];
let decoyed = Table {
sequence: 2,
marked: Which::A,
slots: [
Some(Slot { boot: decoy, root: running, version: 1 }),
Some(Slot { boot: booted, root: idle_root, version: 0 }),
],
};
assert_eq!(
grant(&decoyed, &at(KINDS.root, running), booted, &listed, KINDS),
Err(NoIdle::Stray { part: "volume", why: Stray::Running })
);
}
}
5 changes: 3 additions & 2 deletions loader/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ fn report_reach(what: &str, at: u64, len: u64) {
}

#[allow(clippy::too_many_arguments)]
fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec<u8>, cmdline: vec::Vec<u8>, rsdp_addr: u64, gop: Option<GopInfo>, boot_part: Option<BootPartition>, log_partition_guid: [u8; 16], root_image: rootimage::RootImage, microcode: Option<&'static [u8]>, entry_counter: u64, wall_clock: Option<(toyos_wallclock::Civil, u64)>, system_table: SystemTable) -> ! {
fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec<u8>, cmdline: vec::Vec<u8>, rsdp_addr: u64, gop: Option<GopInfo>, boot_part: Option<BootPartition>, log_partition_guid: [u8; 16], root_image: rootimage::RootImage, slot_volume_guid: [u8; 16], microcode: Option<&'static [u8]>, entry_counter: u64, wall_clock: Option<(toyos_wallclock::Civil, u64)>, system_table: SystemTable) -> ! {
// Said before it is refused, for `report_reach`'s reason.
match arch::cpu_as_entered() {
Ok(None) => {}
Expand Down Expand Up @@ -664,6 +664,7 @@ fn start_kernel(kernel: LoadedKernel, kernel_elf_bytes: vec::Vec<u8>, cmdline: v
gop_edid_len: 0,
microcode_addr,
microcode_len,
slot_volume_guid,
};
if let Some(edid) = gop.as_ref().and_then(|g| g.edid) {
kernel_args.gop_edid = edid;
Expand Down Expand Up @@ -994,5 +995,5 @@ fn main(handle: Handle, system_table: SystemTable) -> Status {
watchdog::arm(&system_table, rsdp_addr, params);

println!("Starting kernel...");
start_kernel(loaded_kernel, kernel_bytes, cmdline, rsdp_addr, gop, boot_part, log_guid, chosen.root, microcode, entry_counter, wall_clock, system_table);
start_kernel(loaded_kernel, kernel_bytes, cmdline, rsdp_addr, gop, boot_part, log_guid, chosen.root, chosen.volume, microcode, entry_counter, wall_clock, system_table);
}
13 changes: 10 additions & 3 deletions loader/src/slot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ pub struct Chosen {
pub kernel: Vec<u8>,
pub cmdline: Vec<u8>,
pub root: RootImage,
/// The unique GUID of its volume, which the kernel and its parameter were read from.
pub volume: [u8; 16],
/// The slot tried before this one, and why it was refused.
pub refused: Option<(Which, Refusal)>,
}
Expand Down Expand Up @@ -121,12 +123,17 @@ fn verify(
) -> Result<Chosen, Refusal> {
let letter = which.letter();
// First, because it costs one read of the table and refuses by name a
// ROOT its own table does not vouch for — one overlapping a neighbour —
// before a byte of the slot is read.
// ROOT its own table does not vouch for — one overlapping a neighbour, or
// of another type — before a byte of the slot is read.
let part = disk.locate(slot.root).map_err(|why| {
println!("{HEAD} {letter}: {why}");
Refusal::Unreadable("root")
})?;
let stated = part.partition().type_guid();
if stated != toyos_gpt::Guid::TOYOS_ROOT {
println!("{HEAD} {letter}: ROOT's partition is typed {stated}, not TOYOS-ROOT");
return Err(Refusal::Type("root"));
}
let (header, digest) = signed_header(bs, which, &slot)?;
if record.is_some_and(|record| record::died(record, which, &digest)) {
return Err(Refusal::Died);
Expand All @@ -150,7 +157,7 @@ fn verify(
return Err(Refusal::Hash("root"));
}
println!("{HEAD} {letter}: kernel, cmdline and ROOT are the bytes the signed header names");
Ok(Chosen { which, version: header.version, digest, kernel, cmdline, root, refused: None })
Ok(Chosen { which, version: header.version, digest, kernel, cmdline, root, volume: slot.boot, refused: None })
}

/// `slot`'s signed header, held to [`KEY`], and its SHA-256.
Expand Down
8 changes: 7 additions & 1 deletion sdk/abi/src/boot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,10 @@ pub struct KernelArgs {
/// holds none for the CPU, or the CPU's architecture loads none.
pub microcode_addr: u64,
pub microcode_len: u64,
/// The unique GUID of the booted slot's volume, the partition the loader
/// read the kernel and its parameter from, raw as in the GPT entry like
/// [`Self::root_partition_guid`]; zero with no image.
pub slot_volume_guid: [u8; 16],
}

/// The bytes of [`KernelArgs::gop_edid`]: one EDID base block.
Expand Down Expand Up @@ -273,7 +277,8 @@ const _: () = {
assert!(offset_of!(KernelArgs, gop_edid_len) == 1464);
assert!(offset_of!(KernelArgs, microcode_addr) == 1472);
assert!(offset_of!(KernelArgs, microcode_len) == 1480);
assert!(size_of::<KernelArgs>() == 1488);
assert!(offset_of!(KernelArgs, slot_volume_guid) == 1488);
assert!(size_of::<KernelArgs>() == 1504);
assert!(LAYOUT as i32 > 1440 || (LAYOUT as i32) < -1440);
assert!(align_of::<KernelArgs>() == 8);
assert!(size_of::<RootBridgeWindow>() == 16);
Expand Down Expand Up @@ -377,6 +382,7 @@ mod tests {
gop_edid_len: 0,
microcode_addr: 0,
microcode_len: 0,
slot_volume_guid: [0; 16],
};

#[test]
Expand Down
6 changes: 5 additions & 1 deletion sdk/abi/src/inventory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,10 +216,12 @@ pub struct Claim {
pub enum Role {
/// The ROOT this boot runs, read whole by the loader.
Root,
/// The running slot's volume: the kernel and its parameter.
/// The partition firmware loaded the loader from.
Boot,
/// The log partition.
Log,
/// The booted slot's volume: the kernel and its parameter.
Slot,
}

/// A partition the loader named, by its unique GUID: what a file server for
Expand Down Expand Up @@ -417,6 +419,7 @@ impl Record {
Role::Root => 0,
Role::Boot => 1,
Role::Log => 2,
Role::Slot => 3,
},
);
o.bytes(8, &l.unique_guid);
Expand Down Expand Up @@ -490,6 +493,7 @@ impl Record {
0 => Some(Role::Root),
1 => Some(Role::Boot),
2 => Some(Role::Log),
3 => Some(Role::Slot),
_ => None,
})?,
unique_guid: r.array(8),
Expand Down
Loading