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
3 changes: 2 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

27 changes: 0 additions & 27 deletions issues/a-file-server-can-open-every-partition-diskserver-serves.md

This file was deleted.

This file was deleted.

26 changes: 0 additions & 26 deletions issues/the-block-port-opens-every-partition-of-the-disk.md

This file was deleted.

25 changes: 25 additions & 0 deletions issues/the-boot-volumes-server-holds-a-claim-that-writes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
status: open
kind: defect
opened: 2026-10-10
---

# The boot volume's server holds a claim that writes

On a disk the kernel drives — the boot stick, until usbd serves it — the
supervisor endows the boot volume's file server a partition claim on the
running slot's volume (`storage_endowment`, `userland/supervisor/src/main.rs`).
A claim carries `Rights::WRITE` and no `DUP` (`initial_rights`,
`kernel/src/object/ops.rs`), so nothing can hand that server a duplicate
narrowed to reading: the volume the loader reads the kernel from stays
unwritten only by that server's promise to mount it read-only. On a disk the
block service serves, the same server's grant does not write, and diskserver
answers a write through it `ReadOnly` (`block_grants_reach_their_partitions`).

**Exit**: the boot volume's server on the boot stick holds a partition it
cannot write — a read-only session once usbd serves the stick, or a claim the
kernel mints without `WRITE` — and a test's write through it is refused.

## Owner

The usbd cutover of `issues/the-kernel-is-small-interrupts-post-and-threads-wait.md`; unheld.
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
status: open
kind: tooling
opened: 2026-10-10
---

# The launcher boots off a stick though an image on NVMe can update itself

`cargo run`'s machine (`src/qemu.rs`) boots its image off a USB stick beside
an NVMe disk, `target/nvme.img`. It did so because only a disk the kernel
drives could have its slots written; `update` now writes the idle slot through
diskserver's sessions, and `update_writes_the_idle_slot_through_the_block_service`
installs into a second slot on a guest booted off its NVMe disk. So the
development machine still boots through the kernel's USB storage path, which
usbd is to replace, and not through diskserver, which the T14 runs on once
ToyOS is installed on its internal disk.

**Exit**: `cargo run`'s machine boots its image off its NVMe disk, and
`update` run on it installs into its idle slot.

## Owner

The launcher, `src/qemu.rs`; unheld.
12 changes: 11 additions & 1 deletion src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ struct ProgramConfig {
/// `toyos_manifest::syscap_rights` takes. A handful of rows in the whole
/// tree declare one.
syscap: Vec<String>,
/// The idle slot, granted as partition claims (`toyos_manifest::Program::slots`).
/// The idle slot (`toyos_manifest::Program::slots`).
slots: bool,
/// A system service: the supervisor starts it with `HOME` at its own `/state/<name>`
/// and makes that directory, where every other row gets the session's.
Expand Down Expand Up @@ -593,6 +593,11 @@ fn held_by_their_holders_alone(config: &SystemConfig) -> Result<(), String> {
if name != toyos_update::slots::HOLDER && program.slots {
return Err(format!("`{name}` asks for `slots`, which only `{}` may hold", toyos_update::slots::HOLDER));
}
// The supervisor grants the idle slot to a launch alone, so a row it
// starts at boot, or again, would run holding nothing.
if program.slots && config.boot.start.contains(name) {
return Err(format!("`{name}` asks for `slots` and is in `[boot] start`, and the slots are granted to a launch alone"));
}
}
if config.apps.receives.iter().any(|r| r == toyos_swap::PORT) {
return Err(format!("`[apps] receives` names `{}`, which only `{}` may hold", toyos_swap::PORT, toyos_swap::HOLDER));
Expand Down Expand Up @@ -2916,6 +2921,7 @@ mod tests {
"diag/system.toml",
"console/system.toml",
"tests/acpicase/system.toml",
"tests/blockgrantcase/system.toml",
"tests/jobcase/system.toml",
"tests/latencycase/system.toml",
"tests/logstallcase/system.toml",
Expand All @@ -2924,6 +2930,7 @@ mod tests {
"tests/netcase/system.toml",
"tests/panelcase/system.toml",
"tests/proctreecase/system.toml",
"tests/slotscase/system.toml",
"tests/testcases/system.toml",
"tests/virtjobcase/system.toml",
"tests/virtpaniccase/system.toml",
Expand Down Expand Up @@ -2988,6 +2995,9 @@ mod tests {
assert!(held_by_their_holders_alone(&apps).is_err());
let slots: SystemConfig = toml::from_str("[programs.shell]\nslots = true\n").unwrap();
assert!(held_by_their_holders_alone(&slots).is_err());
let booted: SystemConfig =
toml::from_str("[boot]\nstart = [\"update\"]\n[programs.update]\nslots = true\n").unwrap();
assert!(held_by_their_holders_alone(&booted).is_err());
}

/// Every committed config passes, and each refusal has a config that
Expand Down
20 changes: 20 additions & 0 deletions tests/blockgrantcase/system.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# A block service's grants, judged by `block_grants_reach_their_partitions`:
# diskserver is built into the image and started by nothing but the one job,
# `partition_grant`, which holds the claim it hands it and the acceptor every
# grant is minted on. No file server runs, so no partition of the disk the
# machine booted from is held but the one the job opens.
[boot]
start = ["logkeeper", "test-runner"]

[programs.logkeeper]
service = true
syscap = ["logread"]

# `device` because the job mints the controller's claim it hands diskserver,
# `dup` because test-runner hands a job its capability as a duplicate and
# hands none without it, and `inventory` because the job reads which
# partitions the loader named.
[programs.test-runner]
syscap = ["device", "dup", "inventory"]

[programs.diskserver]
16 changes: 15 additions & 1 deletion tests/common/qemu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1025,6 +1025,10 @@ pub struct BootOptions {
/// calling CPU's affinity: the firmware side's own account of what the
/// kernel asked of it.
pub psci_trace: Option<PathBuf>,
/// A second slot beside the one the image boots, with room for an
/// update's ROOT of this many bytes: a machine that updates itself.
/// `None` for every guest whose subject is not the update.
pub second_slot: Option<u64>,
}

impl BootOptions {
Expand Down Expand Up @@ -1062,6 +1066,7 @@ impl Default for BootOptions {
ready_marker: DEFAULT_READY,
extra_root_files: Vec::new(),
psci_trace: None,
second_slot: None,
}
}
}
Expand Down Expand Up @@ -1143,6 +1148,7 @@ fn build_boot_image_with(
kernel_features: &[&str],
kernel_params: &[&str],
debug_wait: bool,
second_slot: Option<u64>,
) -> Vec<u8> {
// **The two fields have the same type, so swapping them compiles.** It
// happened once, in this file's own conversion: the shared boot handed
Expand Down Expand Up @@ -1213,7 +1219,8 @@ fn build_boot_image_with(
);

let quiet = !VERBOSE.load(Ordering::Relaxed);
let plan = toyos_build::build::Plan::new(arch, &config_path, kernel_features, kernel_params);
let mut plan = toyos_build::build::Plan::new(arch, &config_path, kernel_features, kernel_params);
plan.second = second_slot.map(|root_bytes| toyos_build::image::SecondSlot { root_bytes });
toyos_build::build::build_test_image(&compile::repo_root(), &plan, quiet, &extra_files)
}

Expand Down Expand Up @@ -1295,6 +1302,7 @@ impl QemuInstance {
&features,
&params,
options.debug_wait,
options.second_slot,
);
let storage = options.profile.shape().storage;
match storage {
Expand Down Expand Up @@ -1602,6 +1610,12 @@ impl QemuInstance {
self.sockets.qmp.as_deref().expect("qmp_socket needs BootOptions { qmp: true }")
}

/// The disk this guest booted from, which it writes: read back while the
/// guest runs, since the instance's end deletes it.
pub fn boot_image(&self) -> &Path {
&self.boot_image
}

pub fn run_test(&mut self, name: &str, timeout: Duration) -> TestResult {
self.run_test_paced(name, timeout, |_, _| {})
}
Expand Down
28 changes: 28 additions & 0 deletions tests/slotscase/system.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# The update, judged by `update_writes_the_idle_slot_through_the_block_service`:
# a machine booted off its NVMe disk, whose slots are diskserver's partitions,
# runs `update` as a login runs it. test-runner is a `login` row listing it,
# so the job it runs launches `update` in a login session of its own.
[boot]
start = ["logkeeper", "diskserver", "fileserver", "test-runner"]

[programs.logkeeper]
service = true
syscap = ["logread"]

[programs.test-runner]
login = true
starts = ["update"]

[programs.update]
slots = true

[programs.diskserver]
service = true
restart = true
serves = ["block"]
devices = ["pci:1b36:0010"]

[programs.fileserver]
restart = true
roles = ["data", "log", "boot"]
receives = ["block"]
41 changes: 41 additions & 0 deletions tests/toyos-rust-tests/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading