diff --git a/Cargo.lock b/Cargo.lock index c88d05279b3..35b85941106 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5251,6 +5251,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" name = "supervisor" version = "0.1.0" dependencies = [ + "diskserver", "toyos", "toyos-abi", "toyos-blockring", @@ -6261,8 +6262,8 @@ checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" name = "update" version = "0.1.0" dependencies = [ + "diskserver", "toyos", - "toyos-abi", "toyos-fat32", "toyos-sha2", "toyos-update", diff --git a/issues/a-file-server-can-open-every-partition-diskserver-serves.md b/issues/a-file-server-can-open-every-partition-diskserver-serves.md deleted file mode 100644 index 16be5d4c25e..00000000000 --- a/issues/a-file-server-can-open-every-partition-diskserver-serves.md +++ /dev/null @@ -1,27 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-27 ---- - -# A file server can open every partition blockd serves - -init gives each `fsd` role the `block` connector (`receives = ["block"]`), and a -session on it is opened by unique GUID (`toyos_blockring::wire::MSG_OPEN`) for -any partition blockd serves but ROOT. So the log's server can open DATA, and -the boot volume's server — whose volume is read-only — can open a partition to -write. Nothing in the protocol ties a session to the role that asked for it; -what keeps each server on its own partition is the argument init passes it, -which is not authority. - -The partition claim init mints for a role on a disk the kernel drives -(`storage_endowment` in `userland/init/src/main.rs`) is the same: the boot -volume's server is endowed a claim that writes, on the ESP the firmware loads -the loader from. Before the file servers the kernel refused a `/boot` write -itself; now only that server's promise to mount its volume read-only does. - -**Exit**: init hands each file server a capability to its own partition's -session and nothing else — a port blockd serves per partition, or a session -init opens and moves — and the boot volume's server a claim that cannot -write, with negative controls: the log's server asking for DATA is refused, -and a write through the boot server's claim is refused by the kernel. diff --git a/issues/an-image-on-a-disk-diskserver-drives-cannot-write-its-slots.md b/issues/an-image-on-a-disk-diskserver-drives-cannot-write-its-slots.md deleted file mode 100644 index 0224d7d2dc7..00000000000 --- a/issues/an-image-on-a-disk-diskserver-drives-cannot-write-its-slots.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-27 ---- - -# An image on a disk diskserver drives cannot write its slots - -The updater writes the idle slot through a partition claim -(`SYS_DEVICE_CLAIM` with `DeviceType::Partition`, granted by `system.toml`'s -`slots` row), and the kernel answers a claim only on the disks it drives: the -USB disks. The kernel drives no NVMe controller: `diskserver` does -(`userland/diskserver`). So a machine booted from an image on its NVMe disk — -every guest whose profile's storage is `Storage::Disk` -(`tests/common/qemu.rs`), and the T14 once ToyOS is installed on its internal -disk — finds its slots nowhere a claim reaches, and an update is refused at -the claim: `slot_grant` (`userland/supervisor/src/main.rs`) asks the inventory -for the ROOT partition the kernel holds, and the kernel lists no partition of -a disk it does not drive. Read off that function, and not run: the tree holds -no test that runs an update on any machine, which is why nothing fails. - -The launcher (`src/qemu.rs`) boots every machine it starts off a USB stick -for this reason: a machine that updates itself keeps its image where a claim -reaches its slots, and moves onto its NVMe disk only once this issue's exit -is met. - -**Exit**: the slots of an image on a disk `diskserver` drives are written -through a session the supervisor grants the updater on `diskserver`'s port, -judged by an update test that boots a profile whose storage is -`Storage::Disk`. - -## Owner - -`userland/supervisor` and `userland/diskserver`; unheld. diff --git a/issues/the-block-port-opens-every-partition-of-the-disk.md b/issues/the-block-port-opens-every-partition-of-the-disk.md deleted file mode 100644 index 29f8de3ccda..00000000000 --- a/issues/the-block-port-opens-every-partition-of-the-disk.md +++ /dev/null @@ -1,26 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-26 ---- - -# The block port opens every partition of the disk - -A holder of blockd's `block` connector (`toyos_blockring::PORT`) may open a -session on any partition blockd serves, named by nothing but its unique GUID -(`userland/blockd/src/main.rs`, `Service::open`). The kernel's partition claims -are minted one partition at a time from a manifest row, so a program the -manifest gives one partition reaches that one; a program given blockd's port -reaches the whole disk. - -Not fixed with blockd's first landing because nothing yet hands the port to a -program: no manifest row starts blockd, and its only client is the test that -supervises it. Scoping it means the authority to open a partition has to be -something init mints per row — a connector per partition, or a session -capability blockd issues and init hands on — which is the manifest wiring the -small-kernel track's steps 6 and 7 build -(`issues/the-kernel-is-small-interrupts-post-and-threads-wait.md`). - -**Exit condition.** What a program holds names the partitions its manifest row -gives it and no others, and a guest test holding one partition's authority is -refused another partition of the same disk by name. diff --git a/issues/the-boot-volumes-server-holds-a-claim-that-writes.md b/issues/the-boot-volumes-server-holds-a-claim-that-writes.md new file mode 100644 index 00000000000..a1809fb4560 --- /dev/null +++ b/issues/the-boot-volumes-server-holds-a-claim-that-writes.md @@ -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. diff --git a/issues/the-launcher-boots-off-a-stick-though-an-image-on-nvme-can-update-itself.md b/issues/the-launcher-boots-off-a-stick-though-an-image-on-nvme-can-update-itself.md new file mode 100644 index 00000000000..f8c7429eff5 --- /dev/null +++ b/issues/the-launcher-boots-off-a-stick-though-an-image-on-nvme-can-update-itself.md @@ -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. diff --git a/src/build.rs b/src/build.rs index 35243301d93..63a8eefa7c9 100644 --- a/src/build.rs +++ b/src/build.rs @@ -157,7 +157,7 @@ struct ProgramConfig { /// `toyos_manifest::syscap_rights` takes. A handful of rows in the whole /// tree declare one. syscap: Vec, - /// 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/` /// and makes that directory, where every other row gets the session's. @@ -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)); @@ -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", @@ -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", @@ -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 diff --git a/tests/blockgrantcase/system.toml b/tests/blockgrantcase/system.toml new file mode 100644 index 00000000000..3711b5819a2 --- /dev/null +++ b/tests/blockgrantcase/system.toml @@ -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] diff --git a/tests/common/qemu.rs b/tests/common/qemu.rs index 4724772cb93..178266532e4 100644 --- a/tests/common/qemu.rs +++ b/tests/common/qemu.rs @@ -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, + /// 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, } impl BootOptions { @@ -1062,6 +1066,7 @@ impl Default for BootOptions { ready_marker: DEFAULT_READY, extra_root_files: Vec::new(), psci_trace: None, + second_slot: None, } } } @@ -1143,6 +1148,7 @@ fn build_boot_image_with( kernel_features: &[&str], kernel_params: &[&str], debug_wait: bool, + second_slot: Option, ) -> Vec { // **The two fields have the same type, so swapping them compiles.** It // happened once, in this file's own conversion: the shared boot handed @@ -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) } @@ -1295,6 +1302,7 @@ impl QemuInstance { &features, ¶ms, options.debug_wait, + options.second_slot, ); let storage = options.profile.shape().storage; match storage { @@ -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, |_, _| {}) } diff --git a/tests/slotscase/system.toml b/tests/slotscase/system.toml new file mode 100644 index 00000000000..0e85e895f0c --- /dev/null +++ b/tests/slotscase/system.toml @@ -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"] diff --git a/tests/toyos-rust-tests/Cargo.lock b/tests/toyos-rust-tests/Cargo.lock index 84089081454..2878efca2ff 100644 --- a/tests/toyos-rust-tests/Cargo.lock +++ b/tests/toyos-rust-tests/Cargo.lock @@ -144,6 +144,17 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f" +[[package]] +name = "diskserver" +version = "0.0.0" +dependencies = [ + "toyos", + "toyos-abi", + "toyos-blockhold", + "toyos-blockring", + "toyos-gpt", +] + [[package]] name = "dispatch2" version = "0.3.1" @@ -814,10 +825,26 @@ dependencies = [ name = "toyos-abi" version = "0.16.0" +[[package]] +name = "toyos-blockhold" +version = "0.1.0" + +[[package]] +name = "toyos-blockring" +version = "0.1.0" +dependencies = [ + "toyos-blockhold", + "toyos-transport", +] + [[package]] name = "toyos-font" version = "0.2.0" +[[package]] +name = "toyos-gpt" +version = "0.1.0" + [[package]] name = "toyos-inspect" version = "0.1.0" @@ -842,6 +869,7 @@ version = "0.1.0" dependencies = [ "acpiserver-api", "cpal", + "diskserver", "inspect", "libloading", "logkeeper-api", @@ -850,6 +878,8 @@ dependencies = [ "rustls", "toyos", "toyos-abi", + "toyos-blockring", + "toyos-gpt", "toyos-inspect", "toyos-logstream", "toyos-tco", @@ -869,6 +899,17 @@ dependencies = [ "toyos-abi", ] +[[package]] +name = "toyos-transport" +version = "0.1.0" +dependencies = [ + "toyos-untrusted", +] + +[[package]] +name = "toyos-untrusted" +version = "0.1.0" + [[package]] name = "toyos-window" version = "0.20.0" diff --git a/tests/toyos-rust-tests/Cargo.toml b/tests/toyos-rust-tests/Cargo.toml index 32329f52a83..6bf957ba47a 100644 --- a/tests/toyos-rust-tests/Cargo.toml +++ b/tests/toyos-rust-tests/Cargo.toml @@ -17,6 +17,11 @@ toyos-logstream = { path = "../../toyos-logstream" } logkeeper-api = { path = "../../userland/logkeeper-api" } # The ACPI server's count line, for `acpi_hold`'s wait on it. acpiserver-api = { path = "../../userland/acpiserver-api" } +# A block service's session, its grants and the partition types, for +# `partition_grant` and `update_idle_slot`. +diskserver = { path = "../../userland/diskserver" } +toyos-blockring = { path = "../../toyos-blockring" } +toyos-gpt = { path = "../../toyos-gpt" } # The diary's decoder, for `trace_read` and `trace_flood`. toyos-trace = { path = "../../toyos-trace" } # The reader's asker, for `hda_client_stall`. diff --git a/tests/toyos-rust-tests/src/bin/partition_grant.rs b/tests/toyos-rust-tests/src/bin/partition_grant.rs new file mode 100644 index 00000000000..7dcee56b5a1 --- /dev/null +++ b/tests/toyos-rust-tests/src/bin/partition_grant.rs @@ -0,0 +1,119 @@ +//! A block service's grants, asked of diskserver serving the disk this +//! machine booted from: this job holds the controller's claim and the block +//! port's acceptor, starts diskserver with both as the supervisor starts it, +//! and mints every grant on the acceptor as the supervisor mints a file +//! server's. +//! +//! - One partition's grant lists that partition alone, opens it, and is +//! refused another partition of the same disk by name — one nothing holds, +//! so nothing but the grant stands in the way. +//! - A type's grant lists and opens its type's partition and is refused one +//! of another type. +//! - A session whose grant does not write is refused a write, and reads. +//! - The port's own connector, which carries no grant, lists and opens +//! nothing. +//! - The partition the machine runs from is held, whatever reaches it. + +use std::os::toyos::process::CommandExt; +use std::process::Command; + +use diskserver::{Error, Outcome, Session}; +use toyos::endow::{Endowments, SYSCAP_LABEL}; +use toyos::namespace::{self, Namespace}; +use toyos::port::{self, Connector}; +use toyos::syscap::SysCap; +use toyos::AsHandle; +use toyos_abi::inventory::{RawRecord, Record, Role}; +use toyos_abi::syscall::{DeviceRequest, DEV_PREFIX, SERVE_PREFIX}; +use toyos_blockring::wire::{Grant, Refusal, Scope}; +use toyos_blockring::PORT; + +/// The controller diskserver's row names, which this boot starts no row for. +const CONTROLLER: &str = "pci:1b36:0010"; + +fn names(connector: &Connector) -> Namespace { + namespace::build().add(PORT, connector).finish().expect("partition_grant: a namespace of one connector") +} + +fn listed(connector: &Connector) -> Result, Error> { + diskserver::list(&names(connector), PORT).map(|all| all.into_iter().map(|l| l.unique).collect()) +} + +fn open(connector: &Connector, guid: [u8; 16]) -> Result { + Session::open(names(connector), PORT, guid) +} + +fn main() { + let cap: SysCap = Endowments::get().take(SYSCAP_LABEL).expect("test-runner endows a device-minting capability"); + let records: Vec = cap.records(|n| vec![RawRecord::EMPTY; n]).expect("partition_grant: the inventory"); + let loaded = |role: Role| { + records + .iter() + .find_map(|r| match r { + Record::Loaded(l) if l.role == role => Some(l.unique_guid), + _ => None, + }) + .unwrap_or_else(|| panic!("partition_grant: the loader named no {role:?}")) + }; + let (log, boot, root) = (loaded(Role::Log), loaded(Role::Boot), loaded(Role::Root)); + + let Some(DeviceRequest::Pci(id)) = DeviceRequest::parse(CONTROLLER) else { unreachable!("a PCI request") }; + let claim: toyos::Device = cap.claim_pci(id).expect("partition_grant: the NVMe controller nothing else claims"); + let (acceptor, bare) = port::create().expect("partition_grant: the block port"); + let served = toyos_abi::syscall::dup(acceptor.as_handle()).expect("partition_grant: the acceptor for diskserver"); + let mut text = [0u8; toyos_abi::part::GUID_TEXT_LEN]; + let running = toyos_abi::part::PartGuid(root).write_text(&mut text).to_string(); + let mut server = Command::new("/system/bin/diskserver") + .args(["--running", &running]) + .endow(&format!("{DEV_PREFIX}{CONTROLLER}"), claim.into_raw().0) + .endow(&format!("{SERVE_PREFIX}{PORT}"), served.0) + .spawn() + .expect("partition_grant: diskserver"); + let mint = |scope: Scope, writes: bool| { + acceptor.mint(&Grant { scope, writes }.encode()).expect("partition_grant: a grant minted") + }; + + let logs = mint(Scope::Unique(log), true); + assert_eq!(listed(&logs), Ok(vec![log]), "the log's grant listed more than the log"); + let held = open(&logs, log).expect("partition_grant: the log's grant opens the log"); + assert_eq!( + open(&logs, boot).err(), + Some(Error::Refused(Refusal::NotGranted)), + "the log's grant opened the boot volume, which nothing holds" + ); + drop(held); + println!("partition_grant: the log's grant opened the log and was refused the boot volume NotGranted"); + + let data = mint(Scope::Kind(toyos_gpt::Guid::TOYOS_DATA.0), true); + let [data_part] = listed(&data).expect("partition_grant: DATA's grant lists")[..] else { + panic!("partition_grant: DATA's grant lists other than one partition") + }; + let held = open(&data, data_part).expect("partition_grant: DATA's grant opens DATA"); + assert_eq!(open(&data, log).err(), Some(Error::Refused(Refusal::NotGranted)), "DATA's grant opened the log"); + drop(held); + println!("partition_grant: DATA's grant opened DATA and was refused the log NotGranted"); + + let boots = mint(Scope::Unique(boot), false); + let mut volume = open(&boots, boot).expect("partition_grant: the boot volume's grant opens it"); + let (read, first) = volume.read(0, 1).expect("partition_grant: a read of the boot volume"); + let first = first.filter(|_| read == Outcome::Done).expect("partition_grant: the boot volume's first block"); + // The bytes it holds: a write let through changes nothing. + assert_eq!( + volume.write(0, &first), + Ok(Outcome::ReadOnly), + "a session whose grant does not write wrote the boot volume" + ); + drop(volume); + println!("partition_grant: the boot volume's read-only grant read it and was refused a write ReadOnly"); + + assert_eq!(listed(&bare), Err(Error::Refused(Refusal::NotGranted)), "the port's own connector listed"); + assert_eq!(open(&bare, log).err(), Some(Error::Refused(Refusal::NotGranted)), "the port's own connector opened"); + println!("partition_grant: the port's own connector was refused a listing and an open NotGranted"); + + let roots = mint(Scope::Unique(root), true); + assert_eq!(open(&roots, root).err(), Some(Error::Refused(Refusal::Held)), "a grant opened the running ROOT"); + println!("partition_grant: the running ROOT was refused Held to the grant naming it"); + + server.kill().expect("partition_grant: diskserver ends"); + server.wait().expect("partition_grant: diskserver reaped"); +} diff --git a/tests/toyos-rust-tests/src/bin/update_idle_slot.rs b/tests/toyos-rust-tests/src/bin/update_idle_slot.rs new file mode 100644 index 00000000000..1215995da8d --- /dev/null +++ b/tests/toyos-rust-tests/src/bin/update_idle_slot.rs @@ -0,0 +1,22 @@ +//! `update` run as `ssh update < image` runs it, on the signed +//! image the harness put on ROOT: launched through this job's launcher, so +//! the supervisor grants it the idle slot, with the image as its standard +//! input. What it says is this job's; whether the slot holds the image is +//! the harness's to read off the disk. + +use std::process::{Command, Stdio}; + +/// Where the harness puts the image (`tests/slotscase`). +const IMAGE: &str = "/system/share/update-test.img"; + +fn main() { + let image = std::fs::File::open(IMAGE).expect("update_idle_slot: the image the harness staged"); + let out = Command::new("/system/bin/update") + .stdin(Stdio::from(image)) + .output() + .expect("update_idle_slot: update launched"); + for line in String::from_utf8_lossy(&out.stdout).lines() { + println!("update_idle_slot: update said: {line}"); + } + assert!(out.status.success(), "update_idle_slot: update ended {:?}", out.status); +} diff --git a/tests/toyos.rs b/tests/toyos.rs index 89398e9d578..7fa5cd2aa1d 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -189,6 +189,14 @@ const RUST_SKIP: &[&str] = &[ // It claims QEMU's virtio NIC, which the T14 has none of: `bar_map_again` // runs it. "bar_map_again", + // It claims the NVMe controller a boot off that disk starts no server + // for, and reads the inventory: `block_grants_reach_their_partitions` + // runs it on tests/blockgrantcase. + "partition_grant", + // It installs the image its machine test staged into a second slot on + // the disk diskserver drives: `update_writes_the_idle_slot_through_the_block_service` + // runs it on tests/slotscase. + "update_idle_slot", // It claims the xHCI controller `xhci-leave=` leaves alone, which no other // boot's kernel does: `usbd_drives_the_spare` and the // `usbd_drives_the_type_c_controller` metal row run it. @@ -378,6 +386,21 @@ const MACHINE_TESTS: &[&str] = &[ // reads it have no host build, and the T14 boots from a stick beside an // NVMe disk that is another system's. "nvme_disk_keeps_log_and_home", + // A badge the kernel stamps on a connection to diskserver, judged by + // diskserver on the disk it drives: the grant's decisions are host-tested + // in diskserver and toyos-blockring, but whether the badge diskserver + // reads is the one a connector was minted with, and whether a refused + // open stays refused against a partition nothing else holds, is the + // kernel's port and a claimed controller, which have no host build; and + // the T14 boots from a stick, so no disk diskserver drives holds a + // partition the loader named. + "block_grants_reach_their_partitions", + // The supervisor reading the slot table through a block session and + // minting `update` the idle slot's partitions, which `update` writes and + // marks through diskserver: the supervisor, the launcher and diskserver + // have no host build, and the T14's slots are on its stick, which only + // the kernel drives until usbd. + "update_writes_the_idle_slot_through_the_block_service", // usbd on a controller the kernel leaves alone, with two devices on it, // and a transfer aimed past its claim's grants: usbd is one binary that // owns its controller, with no host build; `toyos-xhci`'s host tests and @@ -3562,6 +3585,8 @@ fn run_machine_test(name: &str, test_config: &Path) -> Result<(), String> { "bar_map_again" => bar_map_again(test_config), "console_image_boots" => console_image_boots(), "nvme_disk_keeps_log_and_home" => nvme_disk_keeps_log_and_home(test_config), + "block_grants_reach_their_partitions" => block_grants_reach_their_partitions(), + "update_writes_the_idle_slot_through_the_block_service" => update_writes_the_idle_slot_through_the_block_service(), "usbd_drives_the_spare" => usbd_drives_the_spare(test_config), "usb_keyboard_rollover" => usb_keyboard_rollover(test_config), other => Err(format!("unknown machine test {other}")), @@ -3600,6 +3625,11 @@ fn served_by_diskserver(qemu: &mut QemuInstance, console: &mut String) -> Result "diskserver opened sessions on {sessions:?}, and DATA, the log and the slot's volume are three partitions\n{console}" )); } + // The boot volume's grant does not write, and only that one. + let read_only = said.text().lines().filter(|line| line.contains("diskserver: session ") && line.contains(", read-only)")).count(); + if read_only != 1 { + return Err(format!("diskserver opened {read_only} read-only sessions, and only the slot's volume is granted one\n{console}")); + } Ok(()) } @@ -3685,6 +3715,148 @@ fn nvme_disk_keeps_log_and_home(test_config: &Path) -> Result<(), String> { Ok(()) } +/// What `partition_grant` says, a line per grant it was refused through. +const GRANTS_SAID: [&str; 5] = [ + "partition_grant: the log's grant opened the log and was refused the boot volume NotGranted", + "partition_grant: DATA's grant opened DATA and was refused the log NotGranted", + "partition_grant: the boot volume's read-only grant read it and was refused a write ReadOnly", + "partition_grant: the port's own connector was refused a listing and an open NotGranted", + "partition_grant: the running ROOT was refused Held to the grant naming it", +]; + +/// A connection to diskserver opens only what the badge on its connector +/// grants, on the NVMe disk the machine booted from: `partition_grant` starts +/// diskserver itself and mints each grant, and diskserver names each refusal. +fn block_grants_reach_their_partitions() -> Result<(), String> { + const JOB: &str = "partition_grant"; + let bin = qemu::build_toyos_bin(qemu::SUITE_ARCH, &compile::repo_root().join("tests/toyos-rust-tests"), JOB); + let case = compile::repo_root().join("tests/blockgrantcase"); + let options = BootOptions { profile: qemu::Profile::HeadlessNoUsb, ..Default::default() }; + let mut qemu = QemuInstance::boot_with_options(&case, &[], &[(JOB.to_string(), bin)], options); + let result = qemu.run_test("test_rs_partition_grant", Duration::from_secs(60)); + if let Some(why) = &result.error { + return Err(format!("{why}\nthe job said:\n{}", result.stdout)); + } + if result.exit_code != Some(0) { + return Err(format!("the job ended {:?}:\n{}", result.exit_code, result.stdout)); + } + let said = serial::Serial::named("the job", result.stdout); + for line in GRANTS_SAID { + eprintln!(" [grant] {}", said.must_say(line)?.trim()); + } + // diskserver's own word for each refusal, beside the job's. + for refused in ["refused: NotGranted", "refused: Held"] { + if !said.text().lines().any(|l| l.contains("diskserver: an open of ") && l.trim_end().ends_with(refused)) { + return Err(format!("diskserver never said an open was {refused}:\n{}", said.text())); + } + } + Ok(()) +} + +/// The update image `update_writes_the_idle_slot_through_the_block_service` +/// stages: a ROOT of one file, signed with this run's key at a version past +/// any a build signs; the ROOT's length, which the second slot is made to +/// hold exactly, so the host mounts the partition whole; its file; and the +/// three files the slot's FAT volume is to carry, each with its bytes. +struct StagedUpdate { + image: Vec, + version: u64, + root_bytes: u64, + marker: Vec, + volume: [(&'static str, Vec); 3], +} + +/// Where the staged ROOT's one file is, and the path the guest reads the +/// update image at. +const UPDATE_MARKER: &str = "etc/update-test"; +const UPDATE_IMAGE: &str = "share/update-test.img"; + +fn staged_update() -> StagedUpdate { + let since = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).expect("this host's clock is past 1970"); + let marker = format!("update {} {}\n", std::process::id(), since.as_nanos()).into_bytes(); + let root = toyos_build::image::create_root_image(&[(UPDATE_MARKER.to_string(), marker.clone())], &[], true); + // Past every version a build signs, which is its Unix time. + let version = u64::from(u32::MAX) << 8; + let signing = toyos_build::image::Signing { key: toyos_build::signing::key(), version }; + let kernel = b"update test kernel"; + let image = toyos_build::image::update_image(kernel, &root, "", signing); + // The image is the signed header, the kernel, the boot parameter and + // ROOT, end to end (`toyos_update::image`). + let signed = toyos_update::image::SIGNED_BYTES; + let volume = [ + (toyos_update::slots::SIGNED_FILE, image[..signed].to_vec()), + (toyos_update::slots::KERNEL_FILE, kernel.to_vec()), + (toyos_update::slots::CMDLINE_FILE, image[signed + kernel.len()..image.len() - root.len()].to_vec()), + ]; + StagedUpdate { image, version, root_bytes: root.len() as u64, marker, volume } +} + +/// `update` on a machine booted off its NVMe disk: the supervisor reads the +/// slot table through a session on diskserver and grants `update` the idle +/// slot's partitions as connectors minted for each, and `update` writes the +/// signed image there and marks it — read back off the disk by the host, and +/// its FAT volume judged by toyos-fat32-check, which shares no code with it. +fn update_writes_the_idle_slot_through_the_block_service() -> Result<(), String> { + const JOB: &str = "update_idle_slot"; + let staged = staged_update(); + let bin = qemu::build_toyos_bin(qemu::SUITE_ARCH, &compile::repo_root().join("tests/toyos-rust-tests"), JOB); + let case = compile::repo_root().join("tests/slotscase"); + let options = BootOptions { + profile: qemu::Profile::HeadlessNoUsb, + extra_root_files: vec![(UPDATE_IMAGE.to_string(), staged.image.clone())], + second_slot: Some(staged.root_bytes), + ..Default::default() + }; + let mut qemu = QemuInstance::boot_with_options(&case, &[], &[(JOB.to_string(), bin)], options); + let result = qemu.run_test("test_rs_update_idle_slot", Duration::from_secs(120)); + if let Some(why) = &result.error { + return Err(format!("{why}\nthe job said:\n{}", result.stdout)); + } + if result.exit_code != Some(0) { + return Err(format!("the job ended {:?}:\n{}", result.exit_code, result.stdout)); + } + let said = serial::Serial::named("the job", result.stdout); + eprintln!( + " [update] {}", + said.must_say("supervisor: the idle slot is B, granted with the slot table read through the block service")?.trim() + ); + eprintln!(" [update] {}", said.must_say(&format!("update: installed version {} in slot B", staged.version))?.trim()); + + let mut disk = fs::File::open(qemu.boot_image()).map_err(|e| format!("{}: {e}", qemu.boot_image().display()))?; + let table = toyos_build::image::slot_table_of(&mut disk)?; + let marked = table.slot(table.marked).ok_or("the slot table marks a slot it does not carry")?; + if table.marked.letter() != 'B' || marked.version != staged.version { + return Err(format!("after the update the slot table marks {} at version {}, not B at {}", table.marked.letter(), marked.version, staged.version)); + } + let (_, read_back) = toyos_build::image::root_file_on(qemu.boot_image(), UPDATE_MARKER)?; + if read_back != staged.marker { + return Err(format!("slot B's ROOT carries {:?} at {UPDATE_MARKER}, and the update carried {:?}", String::from_utf8_lossy(&read_back), String::from_utf8_lossy(&staged.marker))); + } + eprintln!(" [update] the disk's slot table marks B at version {}, and B's ROOT mounts carrying the update's {UPDATE_MARKER}", staged.version); + + use std::io::{Read as _, Seek as _}; + // B's FAT volume: judged whole by the checker that is no part of the + // driver `update` wrote it with, then each file read back byte for byte. + let (at, len) = toyos_build::image::partition_extent(&mut disk, marked.boot)?; + let mut volume = vec![0u8; usize::try_from(len).map_err(|_| format!("a {len}-byte volume"))?]; + disk.seek(std::io::SeekFrom::Start(at)) + .and_then(|_| disk.read_exact(&mut volume)) + .map_err(|e| format!("slot B's volume at byte {at}: {e}"))?; + let complaints = toyos_fat32_check::check(&volume); + if !complaints.is_empty() { + return Err(format!("toyos-fat32-check refuses slot B's volume:\n{}", toyos_fat32_check::describe(&complaints))); + } + for (name, staged) in &staged.volume { + let read = toyos_build::image::read_file_on(&mut disk, marked.boot, name) + .map_err(|why| format!("slot B's volume: {why}"))?; + if read != *staged { + return Err(format!("slot B's {name} is {} bytes that are not the {} the update carried", read.len(), staged.len())); + } + } + eprintln!(" [update] slot B's volume checks out, carrying the update's {} byte for byte", staged.volume.iter().map(|(name, _)| *name).collect::>().join(", ")); + Ok(()) +} + /// Run `command` as a job of the boot, to exit 0: everything said in its /// window, the kernel's records with the program lines. fn job_window(qemu: &mut QemuInstance, command: &str) -> Result { diff --git a/toyos-blockring/src/client.rs b/toyos-blockring/src/client.rs index 191cf9c3004..7f69ca411d1 100644 --- a/toyos-blockring/src/client.rs +++ b/toyos-blockring/src/client.rs @@ -60,6 +60,8 @@ pub enum Outcome { Durable, /// The server refused it as malformed. Invalid, + /// A write the session's grant does not make, refused unissued. + ReadOnly, /// The device did not do it. A write answered this may or may not have /// reached the medium; a flush answered this left writes the device would /// not keep. @@ -224,12 +226,18 @@ impl Client { let outcome = match status { Status::Invalid => Outcome::Invalid, Status::Device => Outcome::Device, - // A read or a write answered `Lost` is a server that does - // not know which op it was. - Status::Lost | Status::Ok => return Err(Violation::Entry), + Status::ReadOnly if matches!(q.op, Op::Write { .. }) => Outcome::ReadOnly, + // A read or a write answered `Lost`, or a read answered + // `ReadOnly`, is a server that does not know which op it + // was. + Status::ReadOnly | Status::Lost | Status::Ok => return Err(Violation::Entry), }; self.answers.push_back((ticket, outcome)); } + // A write the session took once, refused by the same grant on its + // way again, or a flush refused as a write: a server that does + // not know what it granted. + (Kind::Reissue(_) | Kind::Flush(_), Status::ReadOnly) => return Err(Violation::Entry), (Kind::Reissue(mut acked), Status::Ok) => { acked.seq = self.bump(); // A loss since it went out: an earlier write it overlaps may @@ -521,6 +529,25 @@ mod tests { assert_eq!(client.take_answers().collect::>(), [(3, Outcome::Device)]); } + /// A write refused by its grant is the caller's `ReadOnly` and gives its + /// arena blocks back; the word on a read or a flush is a server that does + /// not know which op it answered. + #[test] + fn read_only_answers_a_write_alone() { + let mut client: Client = Client::new(); + client.session_started(); + client.submit(1, Op::Write { run: run(0, 1), lba: 0 }); + let w = client.next_request().unwrap(); + answer(&mut client, w, Status::ReadOnly); + assert_eq!(client.take_answers().collect::>(), [(1, Outcome::ReadOnly)]); + assert_eq!(client.take_released().collect::>(), [run(0, 1)]); + for op in [Op::Read { run: run(1, 1), lba: 0 }, Op::Flush] { + client.submit(2, op); + let r = client.next_request().unwrap(); + assert_eq!(client.complete(Completion { tag: r.tag, status: Status::ReadOnly }), Err(Violation::Entry)); + } + } + #[test] fn what_was_on_the_wire_at_the_end_is_refused_and_what_was_not_waits() { let mut client: Client = Client::new(); diff --git a/toyos-blockring/src/entry.rs b/toyos-blockring/src/entry.rs index f39788b4c00..a6fcf280543 100644 --- a/toyos-blockring/src/entry.rs +++ b/toyos-blockring/src/entry.rs @@ -100,6 +100,8 @@ pub enum Status { Ok, /// Refused unread: the request was malformed ([`Refused`]). Invalid, + /// Refused unissued: a write on a session whose grant does not write. + ReadOnly, /// The device did not do it, or was reset under it. A write answered this /// may or may not have reached the medium. Device, @@ -110,12 +112,15 @@ pub enum Status { } impl Status { + const ALL: [Self; 5] = [Self::Ok, Self::Invalid, Self::Device, Self::Lost, Self::ReadOnly]; + const fn word(self) -> u32 { match self { Self::Ok => 0, Self::Invalid => 1, Self::Device => 2, Self::Lost => 3, + Self::ReadOnly => 4, } } } @@ -139,8 +144,7 @@ impl Completion { if !reserved.iter().all(|word| word.is(0)) { return None; } - let status = - [Status::Ok, Status::Invalid, Status::Device, Status::Lost].into_iter().find(|s| status.is(s.word()))?; + let status = Status::ALL.into_iter().find(|s| status.is(s.word()))?; Some(Self { tag: opaque(tag), status }) } } @@ -207,11 +211,11 @@ mod tests { #[test] fn a_completion_survives_its_words_and_refuses_what_it_does_not_define() { - for status in [Status::Ok, Status::Invalid, Status::Device, Status::Lost] { + for status in Status::ALL { let c = Completion { tag: 9, status }; assert_eq!(Completion::decode(peer(c.encode())), Some(c)); } - assert_eq!(Completion::decode(peer([1, 4, 0, 0])), None); + assert_eq!(Completion::decode(peer([1, 5, 0, 0])), None); assert_eq!(Completion::decode(peer([1, 0, 1, 0])), None); } } diff --git a/toyos-blockring/src/lib.rs b/toyos-blockring/src/lib.rs index db67c0e94d0..42fb150ad31 100644 --- a/toyos-blockring/src/lib.rs +++ b/toyos-blockring/src/lib.rs @@ -50,6 +50,6 @@ pub use entry::{Completion, Op, Request, Status}; pub use toyos_transport::Run; pub use layout::{BLOCK_BYTES, DEPTH, MAX_REQUEST_BLOCKS, SESSION_BYTES}; -/// The name a block service is served under. A holder of its connector may -/// open any partition the service has. +/// The name a block service is served under. A connection opens only what its +/// connector's badge grants ([`wire::Grant`]). pub const PORT: &str = "block"; diff --git a/toyos-blockring/src/model.rs b/toyos-blockring/src/model.rs index 39c2a60dfaf..90d6a58b609 100644 --- a/toyos-blockring/src/model.rs +++ b/toyos-blockring/src/model.rs @@ -273,7 +273,7 @@ fn start(failures: Failures) -> World { fn connect(world: &mut World) { let mut holds = Holds::new(); holds.hold(0, BLOCKS as u64, 1).expect("a fresh server holds nothing"); - world.server = Some(Server { session: ServerSession::new(0, BLOCKS as u64), holds }); + world.server = Some(Server { session: ServerSession::new(0, BLOCKS as u64, true), holds }); world.alive = true; world.losses = 0; world.client.session_started(); diff --git a/toyos-blockring/src/server.rs b/toyos-blockring/src/server.rs index 24ca1703bf2..ef94f3e3737 100644 --- a/toyos-blockring/src/server.rs +++ b/toyos-blockring/src/server.rs @@ -31,6 +31,9 @@ pub struct ServerSession { blocks: u64, /// The span of the device this session holds, which is its writer. first: u64, + /// Its grant lets it write; a write on a session whose grant does not is + /// answered `ReadOnly` and never reaches the device. + writes: bool, /// Each request in flight, in the order it was taken: a tag is only ever /// compared for equality. inflight: Vec<(u32, Op)>, @@ -47,9 +50,9 @@ pub enum Taken { impl ServerSession { /// A session over the partition at device block `first`, `blocks` long, - /// which `holds` already holds for it. - pub fn new(first: u64, blocks: u64) -> Self { - Self { blocks, first, inflight: Vec::new() } + /// which `holds` already holds for it, taking writes if `writes`. + pub fn new(first: u64, blocks: u64, writes: bool) -> Self { + Self { blocks, first, writes, inflight: Vec::new() } } /// The writer this session's writes and flushes are accounted to. @@ -74,9 +77,10 @@ impl ServerSession { /// Decide what one entry the client published is. /// - /// A malformed entry, and a tag already in flight, are answered at once - /// and never reach the device: the second would make one tag two - /// requests, and the client could not tell which answer was whose. + /// A malformed entry, a tag already in flight, and a write its grant does + /// not let it make, are answered at once and never reach the device: the + /// second would make one tag two requests, and the client could not tell + /// which answer was whose. pub fn take(&mut self, words: [Untrusted; SQE_WORDS]) -> Taken { let request = match Request::decode(words, self.blocks) { Ok(request) => request, @@ -87,6 +91,9 @@ impl ServerSession { if self.inflight.iter().any(|&(tag, _)| tag == request.tag) { return Taken::Answer(Completion { tag: request.tag, status: Status::Invalid }); } + if !self.writes && matches!(request.op, Op::Write { .. }) { + return Taken::Answer(Completion { tag: request.tag, status: Status::ReadOnly }); + } self.inflight.push((request.tag, request.op)); Taken::Issue(request) } @@ -145,7 +152,7 @@ mod tests { fn a_reset_answers_once_and_the_late_device_answer_is_dropped() { let mut holds: Holds = Holds::new(); holds.hold(10, 20, 1).unwrap(); - let mut session = ServerSession::new(10, 10); + let mut session = ServerSession::new(10, 10, true); assert!(matches!(session.take(write(1)), Taken::Issue(_))); assert_eq!(session.abort_all(), [Completion { tag: 1, status: Status::Device }]); assert_eq!(session.complete(1, true, &mut holds, 1), None); @@ -155,7 +162,7 @@ mod tests { /// tags' values: nothing orders a tag but its arrival. #[test] fn a_reset_answers_in_the_order_taken() { - let mut session = ServerSession::new(0, 10); + let mut session = ServerSession::new(0, 10, true); for tag in [9, 4] { assert!(matches!(session.take(write(tag)), Taken::Issue(_))); } @@ -165,7 +172,7 @@ mod tests { #[test] fn a_tag_in_flight_twice_is_refused_unissued() { - let mut session = ServerSession::new(0, 10); + let mut session = ServerSession::new(0, 10, true); assert!(matches!(session.take(write(4)), Taken::Issue(_))); assert_eq!(session.take(write(4)), Taken::Answer(Completion { tag: 4, status: Status::Invalid })); assert_eq!(session.inflight().len(), 1); @@ -177,7 +184,7 @@ mod tests { fn a_flush_after_a_loss_answers_lost_once() { let mut holds: Holds = Holds::new(); holds.hold(0, 10, 1).unwrap(); - let mut session = ServerSession::new(0, 10); + let mut session = ServerSession::new(0, 10, true); session.take(write(1)); assert_eq!(session.complete(1, true, &mut holds, 0).unwrap().status, Status::Ok); session.take(flush(2)); @@ -185,4 +192,16 @@ mod tests { session.take(flush(3)); assert_eq!(session.complete(3, true, &mut holds, 1).unwrap().status, Status::Ok); } + + /// A session whose grant does not write answers a write `ReadOnly` and + /// holds nothing for the device, and still reads and flushes. + #[test] + fn a_read_only_session_refuses_a_write_unissued() { + let mut session = ServerSession::new(0, 10, false); + assert_eq!(session.take(write(1)), Taken::Answer(Completion { tag: 1, status: Status::ReadOnly })); + assert_eq!(session.inflight().len(), 0); + let read = Request { op: Op::Read { run: ARENA.run(0, 1).unwrap(), lba: 0 }, tag: 2 }; + assert!(matches!(session.take(read.encode().map(Untrusted::new)), Taken::Issue(_))); + assert!(matches!(session.take(flush(3)), Taken::Issue(_))); + } } diff --git a/toyos-blockring/src/wire.rs b/toyos-blockring/src/wire.rs index 71236c917bf..0092c1c02f1 100644 --- a/toyos-blockring/src/wire.rs +++ b/toyos-blockring/src/wire.rs @@ -6,6 +6,12 @@ //! [`Refusal`]. After `MSG_OPENED` the connection carries nothing but doorbell //! bytes, each way. //! +//! **What a connection may open is its [`Grant`]**, the badge the holder of +//! the service's acceptor minted its connector with, which the kernel stamps +//! on every connection through it: a listing names what the grant admits and +//! nothing else, an open of anything else is refused [`Refusal::NotGranted`], +//! and a connection with no grant reaches nothing. +//! //! **The answer comes with the server's ends of the page.** A client looks at //! the page the moment it hears, and one that opens the same region again //! sends the cursors its last server left on it. So [`Opened::over`] makes a @@ -17,14 +23,15 @@ use toyos_transport::Word; use crate::layout::{self, ServerRings, RING_WORDS}; /// Open the partition whose unique GUID is the payload, over the region sent -/// with it. Handles: the region. +/// with it, if the connection's [`Grant`] admits it. Handles: the region. pub const MSG_OPEN: u32 = 1; /// The session is open; the payload is [`Opened`]. pub const MSG_OPENED: u32 = 2; /// Refused; the payload is a [`Refusal`]'s word. pub const MSG_REFUSED: u32 = 3; -/// What partitions the service serves, for a client that finds its own by -/// type: no payload, no handles; answered [`MSG_LISTED`]. +/// What partitions the service serves that the connection's [`Grant`] +/// admits, for a client that finds its own by type or holds one partition's +/// grant: no payload, no handles; answered [`MSG_LISTED`]. pub const MSG_LIST: u32 = 4; /// The answer to [`MSG_LIST`]: one [`Listed`] after another. pub const MSG_LISTED: u32 = 5; @@ -91,10 +98,10 @@ impl Opened { } /// One partition of the table a service drives, as [`MSG_LISTED`] carries -/// it: its unique and type GUIDs as the table stores them. Every entry is -/// listed, one the service will not open among them, so a client that finds -/// its partition by type learns why from the open's refusal rather than -/// taking the partition for missing. +/// it: its unique and type GUIDs as the table stores them. Every entry the +/// grant admits is listed, one the service will not open among them, so a +/// client that finds its partition by type learns why from the open's refusal +/// rather than taking the partition for missing. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Listed { pub unique: [u8; GUID_BYTES], @@ -124,6 +131,67 @@ impl Listed { } } +/// What a connector to a block service reaches: the badge it was minted with +/// (`SYS_PORT_MINT`), whose bytes are [`Grant::encode`]'s. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Grant { + pub scope: Scope, + /// A session it opens takes writes; one that does not answers every + /// write `ReadOnly`, unissued. + pub writes: bool, +} + +/// Which partitions a [`Grant`] reaches. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Scope { + /// The one partition whose unique GUID this is. + Unique([u8; GUID_BYTES]), + /// Every partition whose type GUID this is: a role found by type. + Kind([u8; GUID_BYTES]), +} + +impl Grant { + pub const BYTES: usize = 2 + GUID_BYTES; + + pub fn encode(&self) -> [u8; Self::BYTES] { + let (tag, guid) = match self.scope { + Scope::Unique(guid) => (1, guid), + Scope::Kind(guid) => (2, guid), + }; + let mut out = [0u8; Self::BYTES]; + out[0] = tag; + out[1] = u8::from(self.writes); + out[2..].copy_from_slice(&guid); + out + } + + /// `None` for bytes no minter of this protocol stamps. + pub fn decode(bytes: &[u8]) -> Option { + let bytes: &[u8; Self::BYTES] = bytes.try_into().ok()?; + let guid: [u8; GUID_BYTES] = bytes[2..].try_into().ok()?; + let scope = match bytes[0] { + 1 => Scope::Unique(guid), + 2 => Scope::Kind(guid), + _ => return None, + }; + let writes = match bytes[1] { + 0 => false, + 1 => true, + _ => return None, + }; + Some(Self { scope, writes }) + } + + /// Whether it reaches the partition whose unique GUID is `unique` and + /// whose type GUID is `kind`. + pub fn admits(&self, unique: [u8; GUID_BYTES], kind: [u8; GUID_BYTES]) -> bool { + match self.scope { + Scope::Unique(guid) => guid == unique, + Scope::Kind(guid) => guid == kind, + } + } +} + /// Why an open was refused. #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum Refusal { @@ -144,6 +212,11 @@ pub enum Refusal { /// service its claim: nothing on it is served, and a listing is refused /// the same. ClaimRefused, + /// The connection's grant does not reach that partition, whether or not + /// the service has it. A connection with no grant, or with bytes no + /// minter of this protocol stamps, reaches none, and its listing is + /// refused the same. + NotGranted, } impl Refusal { @@ -155,6 +228,7 @@ impl Refusal { Self::Malformed => 4, Self::Exhausted => 5, Self::ClaimRefused => 6, + Self::NotGranted => 7, } } @@ -166,6 +240,7 @@ impl Refusal { 4 => Some(Self::Malformed), 5 => Some(Self::Exhausted), 6 => Some(Self::ClaimRefused), + 7 => Some(Self::NotGranted), _ => None, } } @@ -207,6 +282,7 @@ mod tests { Refusal::Malformed, Refusal::Exhausted, Refusal::ClaimRefused, + Refusal::NotGranted, ] { assert_eq!(Refusal::decode(&r.encode()), Some(r)); } @@ -214,4 +290,33 @@ mod tests { assert_eq!(Refusal::decode(&[1, 0, 0]), None); assert_eq!(guid(&[0; 15]), None); } + + /// A unique grant reaches its one partition and a kind grant every + /// partition of its type, never one that only shares the other GUID's + /// bytes; and a badge decodes only as a minter of this protocol stamps it. + #[test] + fn a_grant_reaches_its_own_partitions_and_decodes_only_whole() { + let (own, other, data) = ([1; GUID_BYTES], [2; GUID_BYTES], [9; GUID_BYTES]); + let unique = Grant { scope: Scope::Unique(own), writes: false }; + assert!(unique.admits(own, data)); + assert!(!unique.admits(other, data)); + assert!(!unique.admits(data, own), "a unique grant read as a type"); + let kind = Grant { scope: Scope::Kind(data), writes: true }; + assert!(kind.admits(own, data) && kind.admits(other, data)); + assert!(!kind.admits(data, own), "a kind grant read as a unique GUID"); + + for grant in [unique, kind] { + assert_eq!(Grant::decode(&grant.encode()), Some(grant)); + assert_eq!(Grant::decode(&grant.encode()[1..]), None); + let mut long = grant.encode().to_vec(); + long.push(0); + assert_eq!(Grant::decode(&long), None); + } + let mut bad = kind.encode(); + bad[0] = 3; + assert_eq!(Grant::decode(&bad), None); + bad = kind.encode(); + bad[1] = 2; + assert_eq!(Grant::decode(&bad), None); + } } diff --git a/toyos-manifest/src/lib.rs b/toyos-manifest/src/lib.rs index 13ffce90806..e8b5c0c45ad 100644 --- a/toyos-manifest/src/lib.rs +++ b/toyos-manifest/src/lib.rs @@ -20,7 +20,7 @@ //! receive a connector in this program's namespace //! device a claim the supervisor mints and endows //! syscap a right on the SysCap dup the supervisor endows -//! slots the idle slot's partitions and the slot table, claimed by the supervisor +//! slots the idle slot's partitions and the slot table //! service a system service: its `HOME` is `/state/`, not the session's //! role a file server for ``: one process of it per role //! restart the supervisor starts it again when it ends @@ -239,9 +239,8 @@ pub struct Program { /// the system may enter the RT band, mint a device claim, read the machine /// log, list every process in the machine, or power the machine off. pub syscap: Vec, - /// The machine's idle slot, granted as claims: the slot table's partition - /// and the idle slot's FAT volume and ROOT, which the supervisor resolves against - /// the ROOT the kernel holds and mints (`toyos_update::slots`). The + /// The machine's idle slot: the slot table's partition and the idle + /// slot's FAT volume and ROOT (`toyos_update::slots`). The /// authority to write the next image and nothing else: the slot a boot /// runs is never among them. One program holds it — `src/build.rs` gates /// which. diff --git a/toyos-update/src/slots.rs b/toyos-update/src/slots.rs index 77cc13f8a5e..a2245996ffd 100644 --- a/toyos-update/src/slots.rs +++ b/toyos-update/src/slots.rs @@ -183,6 +183,33 @@ pub fn current(copies: [&[u8; BLOCK]; 2]) -> Result<(Table, usize), Unreadable> } } +/// [`current`] of a slot table's partition whose first [`COPIES`] blocks +/// `fill` reads, whatever reaches the partition. +pub fn read( + fill: impl FnOnce(&mut [[u8; BLOCK]; COPIES as usize]) -> Result<(), E>, +) -> Result<(Table, usize), NotRead> { + let mut copies = [[0; BLOCK]; COPIES as usize]; + fill(&mut copies).map_err(NotRead::Read)?; + current([&copies[0], &copies[1]]).map_err(NotRead::Unreadable) +} + +/// Why [`read`] has no table. +#[derive(Debug)] +pub enum NotRead { + /// The blocks would not read, in the reader's word. + Read(E), + Unreadable(Unreadable), +} + +impl core::fmt::Display for NotRead { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + Self::Read(why) => write!(f, "the slot table would not read: {why:?}"), + Self::Unreadable(why) => write!(f, "the slot table's partition holds {why}"), + } + } +} + /// What a writer puts where, to make `next` the table: the copy that is not /// `current`'s, one sequence past it. pub fn next_write(current: (Table, usize), mut next: Table) -> (usize, [u8; BLOCK]) { @@ -192,7 +219,7 @@ pub fn next_write(current: (Table, usize), mut next: Table) -> (usize, [u8; BLOC /// The labels `/system/bin/supervisor` endows a `slots` grant under, and /// `/system/bin/update` takes it by: the slot table's partition, and the idle -/// slot's FAT volume and ROOT, each a partition claim. +/// slot's FAT volume and ROOT. pub const TABLE_LABEL: &str = "slots:table"; pub const BOOT_LABEL: &str = "slots:boot"; pub const ROOT_LABEL: &str = "slots:root"; @@ -205,7 +232,7 @@ pub const HOLDER: &str = "update"; pub enum NoIdle { /// The table carries one slot, and that is the one running. OneSlot, - /// Neither slot's ROOT is the one the kernel holds: this table is not + /// Neither slot's ROOT is the one this boot runs: this table is not /// the one this boot came from. NotThisBoot, /// The idle slot names a partition that is no idle slot's. @@ -262,7 +289,7 @@ pub struct Kinds { pub root: [u8; 16], } -/// The idle slot a grant may claim, given the ROOT the kernel holds +/// The idle slot a grant may claim, given the ROOT this boot runs /// (`running`) and every partition the machine lists. /// /// **The table is the grantee's to write**, so nothing it names is taken on @@ -290,7 +317,7 @@ pub fn grant(table: &Table, running: &Listed, listed: &[Listed], kinds: Kinds) - Ok((idle, slot)) } -/// The slot the machine is not running, given the ROOT the kernel holds — +/// The slot the machine is not running, given the ROOT this boot runs — /// **what makes the running slot unwritable by construction**: the grant is /// only ever the other one. pub fn idle(table: &Table, running_root: &[u8; 16]) -> Result<(Which, Slot), NoIdle> { diff --git a/userland/fileserver/src/disk.rs b/userland/diskserver/src/disk.rs similarity index 73% rename from userland/fileserver/src/disk.rs rename to userland/diskserver/src/disk.rs index c7c6386ac33..e3979fe8760 100644 --- a/userland/fileserver/src/disk.rs +++ b/userland/diskserver/src/disk.rs @@ -1,5 +1,6 @@ -//! Where a volume's blocks come from: a partition diskserver serves, a partition -//! the kernel's own disk serves through a claim, or memory. +//! A partition as a program that reads and writes its blocks holds it: one a +//! block service serves ([`Served`]), or one the kernel's own disk serves +//! through a claim ([`Claimed`]), until usbd serves the stick. //! //! **A block is 4 KiB here, on every source.** A partition that is not whole //! 4 KiB blocks is refused before it is served (diskserver's table read, the @@ -12,9 +13,7 @@ //! because a block write names its whole destination and means the same thing //! twice. -use std::collections::BTreeMap; - -use diskserver::{Error as SessionError, Outcome, Session}; +use crate::{Error as SessionError, Outcome, Session}; use toyos::PartitionDev; use toyos_abi::part::{Block, MAX_BLOCKS_PER_CALL}; use toyos_blockring::MAX_REQUEST_BLOCKS; @@ -31,6 +30,8 @@ pub enum DiskError { Gone, /// Past the end of the partition. Range, + /// A write the block service's grant to this holder does not make. + ReadOnly, } /// A partition, a block at a time or several. @@ -44,8 +45,10 @@ pub trait Disk { fn flush(&mut self) -> Result<(), DiskError>; } -fn span(first: u64, len: usize, blocks: u64) -> Result { - assert!(len % BLOCK == 0, "fileserver: a transfer of {len} bytes is no whole blocks"); +/// How many blocks `len` bytes from `first` are, or `Range` past the end of +/// a partition `blocks` long. +pub fn span(first: u64, len: usize, blocks: u64) -> Result { + assert!(len % BLOCK == 0, "a transfer of {len} bytes is no whole blocks"); let count = (len / BLOCK) as u64; match first.checked_add(count) { Some(end) if end <= blocks => Ok(count), @@ -53,50 +56,6 @@ fn span(first: u64, len: usize, blocks: u64) -> Result { } } -/// A volume in memory: the DATA role on a machine with no DATA partition, as -/// the kernel's tmpfs was. Sparse, so what nothing wrote costs nothing. -pub struct Ram { - blocks: u64, - written: BTreeMap>, -} - -impl Ram { - pub fn new(blocks: u64) -> Self { - Self { blocks, written: BTreeMap::new() } - } -} - -impl Disk for Ram { - fn blocks(&self) -> u64 { - self.blocks - } - - fn read(&mut self, first: u64, out: &mut [u8]) -> Result<(), DiskError> { - span(first, out.len(), self.blocks)?; - for (i, chunk) in out.chunks_exact_mut(BLOCK).enumerate() { - match self.written.get(&(first + i as u64)) { - Some(block) => chunk.copy_from_slice(&block[..]), - None => chunk.fill(0), - } - } - Ok(()) - } - - fn write(&mut self, first: u64, data: &[u8]) -> Result<(), DiskError> { - span(first, data.len(), self.blocks)?; - for (i, chunk) in data.chunks_exact(BLOCK).enumerate() { - let mut block = Box::new([0u8; BLOCK]); - block.copy_from_slice(chunk); - self.written.insert(first + i as u64, block); - } - Ok(()) - } - - fn flush(&mut self) -> Result<(), DiskError> { - Ok(()) - } -} - /// A partition the kernel's own disk serves: the stick, until usbd serves it. pub struct Claimed { claim: PartitionDev, @@ -173,9 +132,9 @@ impl Served { Ok(answer) => return Ok(answer), Err(SessionError::Ended) if reconnects < MAX_RECONNECTS => { reconnects += 1; - println!("fileserver: the block service ended; reconnecting ({reconnects} of {MAX_RECONNECTS})"); + println!("the block service ended; reconnecting ({reconnects} of {MAX_RECONNECTS})"); if let Err(why) = self.session.reconnect() { - println!("fileserver: the block service would not take the session back: {why:?}"); + println!("the block service would not take the session back: {why:?}"); return Err(DiskError::Gone); } } @@ -211,14 +170,15 @@ impl Disk for Served { let per = MAX_REQUEST_BLOCKS as usize; for (i, chunk) in data.chunks(BLOCK * per).enumerate() { let at = first + (i * per) as u64; - match self.asked(|s| s.write(at, chunk))? { + let mut outcome = self.asked(|s| s.write(at, chunk))?; + // On the wire when the session ended: the write names its whole + // destination, so it is written again rather than guessed at. + if outcome == Outcome::Refused { + outcome = self.asked(|s| s.write(at, chunk))?; + } + match outcome { Outcome::Done => {} - // On the wire when the session ended: the write names its whole - // destination, so it is written again rather than guessed at. - Outcome::Refused => match self.asked(|s| s.write(at, chunk))? { - Outcome::Done => {} - _ => return Err(DiskError::Device), - }, + Outcome::ReadOnly => return Err(DiskError::ReadOnly), _ => return Err(DiskError::Device), } } diff --git a/userland/diskserver/src/lib.rs b/userland/diskserver/src/lib.rs index 31b787ac9a5..e4d6c10ecd5 100644 --- a/userland/diskserver/src/lib.rs +++ b/userland/diskserver/src/lib.rs @@ -1,9 +1,11 @@ //! What a program that opens a partition through a block service links — the -//! session region as a process sees it ([`region`]) and the session itself -//! ([`session`]) — and the NVMe driver the service runs ([`nvme`]), which is a +//! session region as a process sees it ([`region`]), the session itself +//! ([`session`]), and a partition's blocks however it is held ([`disk`]) — +//! and the NVMe driver the service runs ([`nvme`]), which is a //! library so a test can aim the controller at an address itself. The service //! is `src/main.rs` beside this. +pub mod disk; pub mod nvme; pub mod region; pub mod session; diff --git a/userland/diskserver/src/main.rs b/userland/diskserver/src/main.rs index 818cab622ce..31e78b53685 100644 --- a/userland/diskserver/src/main.rs +++ b/userland/diskserver/src/main.rs @@ -32,6 +32,14 @@ //! starter names it (`--running `, the ROOT the loader read), because a //! writer there changes the image under the kernel that booted from it. //! +//! **A connection reaches what its grant says** ([`wire::Grant`]): the badge +//! the supervisor minted its connector with on this service's port, which the +//! kernel stamps on it and answers this acceptor alone. A listing names the +//! partitions the grant admits, an open of any other is refused `NotGranted` +//! whether or not the table carries it, a connection with no grant reaches +//! nothing, and a session whose grant does not write answers every write +//! `ReadOnly` before the device sees it. +//! //! **A client may ask what is served** ([`wire::MSG_LIST`]) before it opens //! anything, which is how a file server finds its role's partition by type. //! @@ -52,12 +60,12 @@ use toyos::ipc::{self, Connection, RxStep}; use toyos::poller::{Poller, READABLE}; use toyos::AsHandle; use toyos_abi::part::{PartGuid, GUID_TEXT_LEN}; -use toyos_abi::syscall::{DEV_PREFIX, SyscallError}; +use toyos_abi::syscall::{DEV_PREFIX, MAX_BADGE, SyscallError}; use toyos_blockhold::Holds; use toyos_blockring::entry::{Completion, Op}; use toyos_blockring::layout::{ServerRings, DEPTH}; use toyos_blockring::server::{ServerSession, Taken}; -use toyos_blockring::wire::{self, Opened, Refusal}; +use toyos_blockring::wire::{self, Grant, Opened, Refusal}; use toyos_blockring::{BLOCK_BYTES, PORT, SESSION_BYTES}; /// How long a command may go unanswered before the controller is reset to @@ -172,6 +180,47 @@ fn read_table(ctrl: &mut Controller) -> Vec { parts } +/// The partitions a listing through `grant` names: every one it admits, of +/// `parts`, the table of a drive or why it has none. **The grant is asked +/// first**, so a connection with none learns nothing of the drive. +fn listed(parts: Result<&[Part], Refusal>, grant: Option) -> Result, Refusal> { + let grant = grant.ok_or(Refusal::NotGranted)?; + Ok(parts? + .iter() + .filter(|p| grant.admits(p.unique, p.kind)) + .flat_map(|p| wire::Listed { unique: p.unique, kind: p.kind }.encode()) + .collect()) +} + +/// Where an open of `guid` through `grant` is served, before anything is +/// held for it; or its refusal. **The grant is asked first**, so a +/// connection learns nothing of a partition it does not reach, not even +/// whether the table carries it; then the drive, then the table, then the +/// partition the machine runs from. +fn admitted( + parts: Result<&[Part], Refusal>, + running: Option<[u8; 16]>, + guid: [u8; 16], + grant: Option, +) -> Result<(u64, u64), Refusal> { + let grant = grant.ok_or(Refusal::NotGranted)?; + let part = parts?.iter().find(|p| p.unique == guid && guid != [0; 16]); + // A partition the table does not carry has no type: only a unique grant + // naming it reaches it, to be told `NotFound`. + if !grant.admits(guid, part.map_or([0; 16], |p| p.kind)) { + return Err(Refusal::NotGranted); + } + let span = match part.map(|p| &p.span) { + None => return Err(Refusal::NotFound), + Some(Err(_)) => return Err(Refusal::Unusable), + Some(Ok(span)) => *span, + }; + if running == Some(guid) { + return Err(Refusal::Held); + } + Ok(span) +} + /// A connection that has not opened a session yet. struct Pending { conn: Connection, @@ -223,6 +272,18 @@ impl Drive { } } + /// The table a listing and an open are judged against: none on a drive + /// that is absent, and a drive this service cannot use, or was refused, + /// refused by that word. + fn parts(&self) -> Result<&[Part], Refusal> { + match self { + Drive::Up(_, parts) => Ok(parts), + Drive::Absent => Ok(&[]), + Drive::Unusable => Err(Refusal::Unusable), + Drive::ClaimRefused => Err(Refusal::ClaimRefused), + } + } + fn oldest(&self) -> Option { match self { Drive::Up(ctrl, _) => ctrl.oldest(), @@ -251,6 +312,7 @@ struct Opening { opened: Opened, device_addr: u64, first: u64, + writes: bool, } impl Service { @@ -258,35 +320,27 @@ impl Service { Self { ctrl, holds: Holds::new(), losses: 0, sessions: BTreeMap::new(), next_id: 0, running } } - /// What a listing answers: every partition the table names. - fn listing(&self) -> Result, Refusal> { - match &self.ctrl { - Drive::Up(_, parts) => { - Ok(parts.iter().flat_map(|p| wire::Listed { unique: p.unique, kind: p.kind }.encode()).collect()) - } - Drive::Absent => Ok(Vec::new()), - Drive::Unusable => Err(Refusal::Unusable), - Drive::ClaimRefused => Err(Refusal::ClaimRefused), - } + /// What a listing through `grant` answers: every partition of the table + /// it admits. + fn listing(&self, grant: Option) -> Result, Refusal> { + listed(self.ctrl.parts(), grant) } - /// The span an open of `guid` is served on, held for it; or its refusal. - fn place(&mut self, guid: [u8; 16]) -> Result<(u64, u64), Refusal> { - let parts = match &self.ctrl { - Drive::Up(_, parts) => parts, - Drive::Absent => return Err(Refusal::NotFound), - Drive::Unusable => return Err(Refusal::Unusable), - Drive::ClaimRefused => return Err(Refusal::ClaimRefused), - }; - let part = parts.iter().find(|p| p.unique == guid && guid != [0; 16]); - let (first, blocks) = match part.map(|p| &p.span) { - None => return Err(Refusal::NotFound), - Some(Err(_)) => return Err(Refusal::Unusable), - Some(Ok(span)) => *span, - }; - if self.running == Some(guid) { - return Err(Refusal::Held); + /// The span an open of `guid` through `grant` is served on, held for it; + /// or its refusal. + /// + /// **A client that hung up holds nothing**: every session's end its client + /// has already made is read before the open is judged, so an open made + /// after another holder closed its connection is never refused `Held` for + /// a holder that is gone — only for one whose commands are still on the + /// device. + fn place(&mut self, guid: [u8; 16], grant: Option) -> Result<(u64, u64), Refusal> { + let (first, blocks) = admitted(self.ctrl.parts(), self.running, guid, grant)?; + let ids: Vec = self.sessions.keys().copied().collect(); + for id in ids { + self.hear(id); } + self.retire(); if self.sessions.len() >= MAX_SESSIONS { return Err(Refusal::Exhausted); } @@ -298,13 +352,14 @@ impl Service { /// What an open answers: a session to admit, or its refusal. `region` is /// the client's and is consumed either way. - fn open(&mut self, guid: [u8; 16], region: toyos::RawHandle) -> Result { + fn open(&mut self, guid: [u8; 16], region: toyos::RawHandle, grant: Option) -> Result { let region = Region::adopt(region).map_err(|_| Refusal::Malformed)?; - let (first, blocks) = self.place(guid)?; + let (first, blocks) = self.place(guid, grant)?; + let writes = grant.expect("placed through a grant").writes; match self.ctrl.up().claim().dma_map(region.handle()) { Ok(mapping) if mapping.bytes == SESSION_BYTES as u64 => { let (rings, opened) = Opened::over(region.words(), blocks, guid); - Ok(Opening { region, rings, opened, device_addr: mapping.device_addr, first }) + Ok(Opening { region, rings, opened, device_addr: mapping.device_addr, first, writes }) } // A region longer than a session would spend the claim's bound on // the kernel's side for every other client: refused whole. @@ -336,7 +391,7 @@ impl Service { region: opening.region, device_addr: opening.device_addr, rings: opening.rings, - state: ServerSession::new(opening.first, opening.opened.blocks()), + state: ServerSession::new(opening.first, opening.opened.blocks(), opening.writes), unique: opening.opened.unique(), closing: false, requests: 0, @@ -454,6 +509,21 @@ impl Service { } } + /// Consume session `id`'s doorbell bytes; once its client has hung up, + /// the session is closing. + fn hear(&mut self, id: u64) { + let s = self.sessions.get_mut(&id).expect("a session"); + let mut sink = [0u8; 64]; + while !s.closing { + match s.conn.read_nonblock(&mut sink) { + Ok(0) => s.closing = true, + Ok(_) => {} + Err(SyscallError::WouldBlock) => return, + Err(_) => s.closing = true, + } + } + } + /// End every session that is closing and has nothing on the device: its /// region leaves the controller's domain, and its partition is free. fn retire(&mut self) { @@ -648,7 +718,7 @@ fn serve(service: &mut Service, acceptor: &toyos::port::Acceptor) -> ! { } RxStep::Frame { msg_type, payload_len } => { let p = pending.remove(i); - handshake(service, p, msg_type, payload_len); + handshake(service, acceptor, p, msg_type, payload_len); } } } @@ -656,10 +726,7 @@ fn serve(service: &mut Service, acceptor: &toyos::port::Acceptor) -> ! { let ids: Vec = service.sessions.keys().copied().collect(); for id in ids { if ready.contains(&(TOKEN_SESSION + id)) { - let s = service.sessions.get_mut(&id).expect("listed"); - if !doorbells(&s.conn) { - s.closing = true; - } + service.hear(id); } service.pull(id); } @@ -668,26 +735,25 @@ fn serve(service: &mut Service, acceptor: &toyos::port::Acceptor) -> ! { } } -/// Consume a session's doorbell bytes; `false` once its client has hung up. -fn doorbells(conn: &Connection) -> bool { - let mut sink = [0u8; 64]; - loop { - match conn.read_nonblock(&mut sink) { - Ok(0) => return false, - Ok(_) => continue, - Err(SyscallError::WouldBlock) => return true, - Err(_) => return false, - } +/// The grant `conn`, accepted from `acceptor`, was minted with; `None` for a +/// connection through an unbadged connector or with bytes no minter stamps. +fn grant(acceptor: &toyos::port::Acceptor, conn: &Connection) -> Option { + let mut badge = [0u8; MAX_BADGE]; + match acceptor.badge(conn, &mut badge) { + Ok(bytes) => Grant::decode(bytes), + Err(SyscallError::NotFound) => None, + Err(why) => panic!("diskserver: its own acceptor would not say a connection's badge: {why:?}"), } } /// Answer one connection's first frame: a listing, or an open. -fn handshake(service: &mut Service, p: Pending, msg_type: u32, payload_len: usize) { +fn handshake(service: &mut Service, acceptor: &toyos::port::Acceptor, p: Pending, msg_type: u32, payload_len: usize) { let refuse = |conn: &Connection, why: Refusal| { let _ = conn.try_send_bytes(wire::MSG_REFUSED, &why.encode()); }; + let grant = grant(acceptor, &p.conn); if msg_type == wire::MSG_LIST && payload_len == 0 { - match service.listing() { + match service.listing(grant) { Err(why) => refuse(&p.conn, why), // One frame, and the connection is done with: a table larger than // a frame is one this service lists no part of rather than half of. @@ -708,19 +774,20 @@ fn handshake(service: &mut Service, p: Pending, msg_type: u32, payload_len: usiz refuse(&p.conn, Refusal::Malformed); return; }; - match service.open(guid, region) { + match service.open(guid, region, grant) { Err(why) => { println!("diskserver: an open of {} refused: {why:?}", guid_text(guid)); refuse(&p.conn, why); } Ok(opening) => { - let opened = opening.opened; + let (opened, writes) = (opening.opened, opening.writes); if p.conn.try_send_bytes(wire::MSG_OPENED, &opened.encode()).is_err() { service.abandon(opening); return; } let id = service.admit(opening, p.conn); - println!("diskserver: session {id} opened {} ({} blocks)", guid_text(guid), opened.blocks()); + let access = if writes { "" } else { ", read-only" }; + println!("diskserver: session {id} opened {} ({} blocks{access})", guid_text(guid), opened.blocks()); } } } @@ -728,6 +795,7 @@ fn handshake(service: &mut Service, p: Pending, msg_type: u32, payload_len: usiz #[cfg(test)] mod tests { use super::*; + use toyos_blockring::wire::Scope; /// A controller this service cannot use, or one whose claim was refused, /// is a disk there and never a machine without one: its listing and its @@ -736,14 +804,77 @@ mod tests { #[test] fn an_unusable_or_unclaimed_controller_is_refused_and_an_absent_one_lists_nothing() { let guid = [7; 16]; + let grant = Some(Grant { scope: Scope::Unique(guid), writes: true }); let mut unusable = Service::new(Drive::Unusable, None); - assert_eq!(unusable.listing(), Err(Refusal::Unusable)); - assert_eq!(unusable.place(guid), Err(Refusal::Unusable)); + assert_eq!(unusable.listing(grant), Err(Refusal::Unusable)); + assert_eq!(unusable.place(guid, grant), Err(Refusal::Unusable)); let mut unclaimed = Service::new(Drive::ClaimRefused, None); - assert_eq!(unclaimed.listing(), Err(Refusal::ClaimRefused)); - assert_eq!(unclaimed.place(guid), Err(Refusal::ClaimRefused)); + assert_eq!(unclaimed.listing(grant), Err(Refusal::ClaimRefused)); + assert_eq!(unclaimed.place(guid, grant), Err(Refusal::ClaimRefused)); let mut absent = Service::new(Drive::Absent, None); - assert_eq!(absent.listing(), Ok(Vec::new())); - assert_eq!(absent.place(guid), Err(Refusal::NotFound)); + assert_eq!(absent.listing(grant), Ok(Vec::new())); + assert_eq!(absent.place(guid, grant), Err(Refusal::NotFound)); + for mut service in [unusable, unclaimed, absent] { + assert_eq!(service.listing(None), Err(Refusal::NotGranted)); + assert_eq!(service.place(guid, None), Err(Refusal::NotGranted)); + } + } + + const LOG: [u8; 16] = [1; 16]; + const BOOT: [u8; 16] = [2; 16]; + const DATA: [u8; 16] = [3; 16]; + const ROOT: [u8; 16] = [4; 16]; + const LOG_KIND: [u8; 16] = [0x10; 16]; + const BOOT_KIND: [u8; 16] = [0x20; 16]; + const DATA_KIND: [u8; 16] = [0x30; 16]; + const ROOT_KIND: [u8; 16] = [0x40; 16]; + + fn table() -> Vec { + [(LOG, LOG_KIND), (BOOT, BOOT_KIND), (DATA, DATA_KIND), (ROOT, ROOT_KIND)] + .into_iter() + .enumerate() + .map(|(i, (unique, kind))| Part { unique, kind, span: Ok((i as u64 * 100, 100)) }) + .collect() + } + + fn unique(guid: [u8; 16]) -> Option { + Some(Grant { scope: Scope::Unique(guid), writes: true }) + } + + /// A unique grant opens its one partition and is refused every other of + /// the same table by name, as is a connection with no grant; a kind grant + /// opens every partition of its type and no other; and the partition the + /// machine runs from is held whatever reaches it. + #[test] + fn an_open_reaches_only_what_its_grant_admits() { + let parts = table(); + assert_eq!(admitted(Ok(&parts), None, LOG, unique(LOG)), Ok((0, 100))); + for other in [BOOT, DATA, ROOT] { + assert_eq!(admitted(Ok(&parts), None, other, unique(LOG)), Err(Refusal::NotGranted)); + assert_eq!(admitted(Ok(&parts), None, other, None), Err(Refusal::NotGranted)); + } + let data = Some(Grant { scope: Scope::Kind(DATA_KIND), writes: true }); + assert_eq!(admitted(Ok(&parts), None, DATA, data), Ok((200, 100))); + assert_eq!(admitted(Ok(&parts), None, LOG, data), Err(Refusal::NotGranted)); + // Nothing of a GUID the table does not carry, but to the grant that names it. + assert_eq!(admitted(Ok(&parts), None, [9; 16], data), Err(Refusal::NotGranted)); + assert_eq!(admitted(Ok(&parts), None, [9; 16], unique([9; 16])), Err(Refusal::NotFound)); + assert_eq!(admitted(Ok(&parts), Some(ROOT), ROOT, unique(ROOT)), Err(Refusal::Held)); + assert_eq!(admitted(Ok(&parts), Some(ROOT), ROOT, unique(LOG)), Err(Refusal::NotGranted)); + } + + /// A listing names exactly what its grant admits, and nothing to a + /// connection with none. + #[test] + fn a_listing_names_only_what_its_grant_admits() { + let parts = table(); + let decoded = |bytes: Vec| -> Vec<[u8; 16]> { + wire::Listed::decode_all(&bytes).expect("whole entries").map(|l| l.unique).collect() + }; + assert_eq!(listed(Ok(&parts), unique(BOOT)).map(decoded), Ok(vec![BOOT])); + let roots = Some(Grant { scope: Scope::Kind(ROOT_KIND), writes: false }); + assert_eq!(listed(Ok(&parts), roots).map(decoded), Ok(vec![ROOT])); + assert_eq!(listed(Ok(&parts), unique([9; 16])).map(decoded), Ok(vec![])); + assert_eq!(listed(Ok(&parts), None), Err(Refusal::NotGranted)); } } diff --git a/userland/fileserver/src/cache.rs b/userland/fileserver/src/cache.rs index 6306c35fb78..021e2aef5e0 100644 --- a/userland/fileserver/src/cache.rs +++ b/userland/fileserver/src/cache.rs @@ -17,7 +17,7 @@ use std::cell::RefCell; use std::collections::{BTreeMap, VecDeque}; -use crate::disk::{Disk, DiskError, BLOCK}; +use diskserver::disk::{Disk, DiskError, BLOCK}; /// Clean blocks kept: 64 MiB. pub const CLEAN_LIMIT: usize = 16 * 1024; @@ -232,11 +232,11 @@ impl Clone for Shared { impl bcachefs::BlockIO for Shared { fn read_block(&self, block: bcachefs::BlockNum, buf: &mut bcachefs::BlockBuf) -> Result<(), bcachefs::DeviceError> { - self.0.read(block.raw(), buf.as_bytes_mut()).map_err(|e| bcachefs::DeviceError::classify(&e)) + self.0.read(block.raw(), buf.as_bytes_mut()).map_err(|_| bcachefs::DeviceError::classify(&Attempted)) } fn write_block(&self, block: bcachefs::BlockNum, buf: &bcachefs::BlockBuf) -> Result<(), bcachefs::DeviceError> { - self.0.write(block.raw(), buf.as_bytes()).map_err(|e| bcachefs::DeviceError::classify(&e)) + self.0.write(block.raw(), buf.as_bytes()).map_err(|_| bcachefs::DeviceError::classify(&Attempted)) } fn block_count(&self) -> u64 { @@ -244,12 +244,15 @@ impl bcachefs::BlockIO for Shared { } fn sync(&self) -> Result<(), bcachefs::DeviceError> { - self.0.flush().map_err(|e| bcachefs::DeviceError::classify(&e)) + self.0.flush().map_err(|_| bcachefs::DeviceError::classify(&Attempted)) } } -/// Every refusal here was attempted: nothing in this server refuses on a clock. -impl bcachefs::TransferError for DiskError { +/// What every [`DiskError`] is to bcachefs: attempted, since nothing in this +/// server refuses on a clock. +struct Attempted; + +impl bcachefs::TransferError for Attempted { fn refused_before_attempt(&self) -> bool { false } @@ -258,7 +261,7 @@ impl bcachefs::TransferError for DiskError { #[cfg(test)] mod tests { use super::*; - use crate::disk::Ram; + use crate::ram::Ram; /// A disk that counts what reaches it. struct Counting { diff --git a/userland/fileserver/src/data.rs b/userland/fileserver/src/data.rs index 3fe12928e23..76901f44288 100644 --- a/userland/fileserver/src/data.rs +++ b/userland/fileserver/src/data.rs @@ -38,7 +38,7 @@ use bcachefs::{Extent, Formatted, FsError, Mounted, ReadWrite}; use toyos_abi::syscall::SyscallError; use crate::cache::{Cache, Shared}; -use crate::disk::{Disk, DiskError, BLOCK}; +use diskserver::disk::{Disk, DiskError, BLOCK}; use crate::volume::{join, parent, Kind, Meta, Node, OpenHow, Out, Volume}; /// The longest symlink target read back: the wire's path bound. @@ -150,6 +150,7 @@ fn disk_word(e: DiskError) -> SyscallError { match e { DiskError::Device | DiskError::Range => SyscallError::Io, DiskError::Gone => SyscallError::Gone, + DiskError::ReadOnly => SyscallError::PermissionDenied, } } @@ -773,7 +774,7 @@ mod tests { use std::collections::HashMap; use super::*; - use crate::disk::Ram; + use crate::ram::Ram; use crate::volume::Buf; fn clock() -> u64 { diff --git a/userland/fileserver/src/fat.rs b/userland/fileserver/src/fat.rs index 49d695ff466..1b827260db8 100644 --- a/userland/fileserver/src/fat.rs +++ b/userland/fileserver/src/fat.rs @@ -24,7 +24,7 @@ use toyos_abi::syscall::SyscallError; use toyos_fat32::{BlockAccess, Error, Fat32, FatTime, IoError}; use crate::cache::Cache; -use crate::disk::{Disk, BLOCK}; +use diskserver::disk::{Disk, BLOCK}; use crate::volume::{parent, Kind, Meta, Node, OpenHow, Out, Volume, NANOS_PER_SEC}; /// The most entries one directory listing materialises. @@ -37,6 +37,11 @@ pub struct Bytes { len: u64, } +// The disk's `ReadOnly` is `Device` here because it never arrives: the one +// read-only grant is the BOOT server's, whose volume is mounted unwritable, +// whose clients are refused every changing operation before it, and whose +// close and sync write nothing. `toyos-fat32` has the one device word since it +// reads every refused write as of unknown outcome. impl BlockAccess for Bytes { fn capacity(&self) -> u64 { self.len @@ -438,7 +443,8 @@ mod tests { use super::*; use crate::cache::CLEAN_LIMIT; - use crate::disk::{DiskError, Ram}; + use crate::ram::Ram; + use diskserver::disk::DiskError; /// A disk that refuses every read of one block, after the fixture's own /// bytes are on it. @@ -579,6 +585,47 @@ mod tests { assert_eq!(v.node_meta(a), Err(SyscallError::Io)); } + /// A disk whose grant does not write: every write and flush is refused. + struct Granted(Ram); + + impl Disk for Granted { + fn blocks(&self) -> u64 { + self.0.blocks() + } + fn read(&mut self, first: u64, out: &mut [u8]) -> Result<(), DiskError> { + self.0.read(first, out) + } + fn write(&mut self, _: u64, _: &[u8]) -> Result<(), DiskError> { + panic!("a read-only volume wrote to its disk") + } + fn flush(&mut self) -> Result<(), DiskError> { + panic!("a read-only volume flushed its disk") + } + } + + /// A volume mounted unwritable never asks its disk to write or flush, so + /// the read-only grant's refusal never reaches [`Bytes`]. + #[test] + fn a_read_only_volume_never_writes_its_disk() { + const OPEN: OpenHow = OpenHow { create: false, create_new: false, truncate: false }; + let spec = spec_volume::fixture(); + let blocks = spec.bytes.len().div_ceil(BLOCK); + let mut ram = Ram::new(blocks as u64); + let mut image = spec.bytes.clone(); + image.resize(blocks * BLOCK, 0); + ram.write(0, &image).unwrap(); + let mut v = FatVolume::mount(Granted(ram), false, || 1_717_245_296 * NANOS_PER_SEC).unwrap(); + + assert_eq!(v.lstat("short.txt").unwrap().size, 100); + v.list("sub").unwrap(); + let n = v.open("short.txt", OPEN).unwrap(); + let mut out = vec![0u8; 100]; + assert_eq!(v.read(n, 0, &mut crate::volume::Buf(&mut out)), Ok(100)); + assert!(out == spec.bytes[spec_volume::cluster_offset(spec.at("short.txt").first)..][..100], "the file reads as the fixture wrote it"); + v.close(n).unwrap(); + assert_eq!(v.sync(), Ok(Vec::new())); + } + /// What the driver says of a volume that stopped answering is `Io`, which /// no caller takes for a name that is not there. #[test] diff --git a/userland/fileserver/src/lib.rs b/userland/fileserver/src/lib.rs index 0c29712db4d..c8eb2f0d62b 100644 --- a/userland/fileserver/src/lib.rs +++ b/userland/fileserver/src/lib.rs @@ -1,4 +1,5 @@ -//! A file server's decisions: where its blocks come from ([`disk`]), the one +//! A file server's decisions: the blocks memory stands in for a partition +//! with ([`ram`]) — a partition's own are `diskserver::disk`'s — the one //! cache every byte of its volume passes through ([`cache`]), the volumes it //! can serve ([`data`] for the bcachefs DATA role, [`fat`] for FAT32's LOG and //! BOOT, [`absent`] for a role with no volume this boot), and the resolver that @@ -16,8 +17,8 @@ pub mod absent; pub mod cache; pub mod data; -pub mod disk; pub mod fat; +pub mod ram; pub mod resolve; pub mod rights; pub mod volume; diff --git a/userland/fileserver/src/main.rs b/userland/fileserver/src/main.rs index 662115a1631..19c0453f413 100644 --- a/userland/fileserver/src/main.rs +++ b/userland/fileserver/src/main.rs @@ -3,8 +3,10 @@ //! //! **What it holds**: the acceptor of its role's one port, endowed by the //! supervisor under `serve:fs:`; for its volume, a claim on each partition -//! of its role a disk the kernel drives carries, and diskserver's `block` -//! connector in its namespace; and nothing else of the machine. DATA is one +//! of its role a disk the kernel drives carries, and `block` in its namespace, +//! a connector to diskserver minted for its role's partition alone — every +//! DATA partition, or the one the loader named, read-only for the boot +//! volume; and nothing else of the machine. DATA is one //! partition counted over both, and two are refused by name, never guessed //! between (`fileserver::data::find`). Argv is the role, and for LOG and BOOT the //! unique GUID of the partition the loader named for it when no claim on it @@ -42,7 +44,8 @@ use std::time::{Duration, Instant}; use fileserver::absent::Absent; use fileserver::data::{DataVolume, Located, Probed}; -use fileserver::disk::{Claimed, Disk, Ram, Served}; +use diskserver::disk::{Claimed, Disk, Served}; +use fileserver::ram::Ram; use fileserver::fat::FatVolume; use fileserver::resolve::{self, Found, Refusal as Escape, Resolved}; use fileserver::rights; diff --git a/userland/fileserver/src/ram.rs b/userland/fileserver/src/ram.rs new file mode 100644 index 00000000000..f146f74a619 --- /dev/null +++ b/userland/fileserver/src/ram.rs @@ -0,0 +1,50 @@ +//! The blocks memory stands in for a partition with. + +use std::collections::BTreeMap; + +use diskserver::disk::{span, Disk, DiskError, BLOCK}; + +/// A volume in memory: the DATA role on a machine with no DATA partition, as +/// the kernel's tmpfs was. Sparse, so what nothing wrote costs nothing. +pub struct Ram { + blocks: u64, + written: BTreeMap>, +} + +impl Ram { + pub fn new(blocks: u64) -> Self { + Self { blocks, written: BTreeMap::new() } + } +} + +impl Disk for Ram { + fn blocks(&self) -> u64 { + self.blocks + } + + fn read(&mut self, first: u64, out: &mut [u8]) -> Result<(), DiskError> { + span(first, out.len(), self.blocks)?; + for (i, chunk) in out.chunks_exact_mut(BLOCK).enumerate() { + match self.written.get(&(first + i as u64)) { + Some(block) => chunk.copy_from_slice(&block[..]), + None => chunk.fill(0), + } + } + Ok(()) + } + + fn write(&mut self, first: u64, data: &[u8]) -> Result<(), DiskError> { + span(first, data.len(), self.blocks)?; + for (i, chunk) in data.chunks_exact(BLOCK).enumerate() { + let mut block = Box::new([0u8; BLOCK]); + block.copy_from_slice(chunk); + self.written.insert(first + i as u64, block); + } + Ok(()) + } + + fn flush(&mut self) -> Result<(), DiskError> { + Ok(()) + } +} + diff --git a/userland/supervisor/Cargo.toml b/userland/supervisor/Cargo.toml index 710f43ca183..c912c2b8cae 100644 --- a/userland/supervisor/Cargo.toml +++ b/userland/supervisor/Cargo.toml @@ -17,8 +17,11 @@ toyos-swap = { path = "../../toyos-swap" } toyos-update = { path = "../../toyos-update" } # The partition types that name the running ROOT and the slot table. toyos-gpt = { path = "../../toyos-gpt" } -# The block service's port name, which marks a storage row. +# The block service's port name, which marks a storage row, and the grant +# every connector to it is minted with. toyos-blockring = { path = "../../toyos-blockring" } +# The slot table, read through a session where the block service serves it. +diskserver = { path = "../diskserver" } # How long a stop is prepared before the kernel is asked for it. toyos-quiesce = { path = "../../toyos-quiesce" } diff --git a/userland/supervisor/src/main.rs b/userland/supervisor/src/main.rs index 62fcba2c79c..55658eb162f 100644 --- a/userland/supervisor/src/main.rs +++ b/userland/supervisor/src/main.rs @@ -92,6 +92,7 @@ use toyos::shm::SharedMemory; use toyos::syscap::SysCap; use toyos::{AsHandle, Pipe}; use toyos_abi::Rights; +use toyos_blockring::wire::{Grant as BlockGrant, Scope}; use toyos_logstream::{ Registration, Tag, ALIVE, CONSOLE, FLUSH, FLUSHED, LOGKEEPER, MAX_TAG, ORIGINS, REGISTER, RESUME, STOPPING, }; @@ -420,7 +421,7 @@ fn main() { syscap: &syscap, acceptors, connectors, - grants: Grants { roles: Vec::new() }, + grants: Grants { roles: Vec::new(), block: None }, sessions: Sessions::default(), files, services: Vec::new(), @@ -511,7 +512,12 @@ impl Worker { /// file server. Started before every other row, and never made a home, which /// would be a directory on the volume it serves. fn is_storage(program: &Program) -> bool { - !program.roles.is_empty() || program.serves.iter().any(|s| s == toyos_blockring::PORT) + !program.roles.is_empty() || is_block(program) +} + +/// A row serving the block port. +fn is_block(program: &Program) -> bool { + program.serves.iter().any(|s| s == toyos_blockring::PORT) } /// Everything the supervisor's loop acts on, for the machine's life. @@ -644,7 +650,7 @@ impl<'a> Service<'a> { 0 => Served::Keep(&kept.acceptors), _ => Served::Restart { acceptors: &kept.acceptors, owed }, }; - let storage = storage_endowment(self.program, self.role, syscap)?; + let storage = storage_endowment(self.program, self.role, syscap, grants)?; let (child, devices) = start( Command::new(path), self.program, @@ -763,16 +769,19 @@ impl Flight { impl<'a> Supervisor<'a> { /// Start `[boot] start`, kept for the machine's life: the supervisor is the only /// thing that can kill a daemon, and there is no other way back to a - /// process it started. The storage rows go first — every other start may - /// make a directory, and a directory is a file server's — and the + /// process it started. The storage rows go first, the block service + /// before the file servers its grants are minted on — every other start + /// may make a directory, and a directory is a file server's — and the /// session's home is made before the first row that is not one. fn boot(&mut self, role_acceptors: &mut BTreeMap<&str, Acceptor>, wake: toyos::RawHandle) { let system = self.system; - let storage_first = system - .start - .iter() - .filter(|n| system.program(n).is_some_and(is_storage)) - .chain(system.start.iter().filter(|n| system.program(n).is_none_or(|p| !is_storage(p)))); + let rank = |name: &&String| match system.program(name) { + Some(p) if is_block(p) => 0, + Some(p) if is_storage(p) => 1, + _ => 2, + }; + let mut storage_first: Vec<&String> = system.start.iter().collect(); + storage_first.sort_by_key(rank); let mut homes_made = false; for name in storage_first { let program = system @@ -810,6 +819,10 @@ impl<'a> Supervisor<'a> { if let Some(role) = role { self.grants.roles.push((role, Arc::clone(&service.kept))); } + if is_block(program) { + assert!(self.grants.block.is_none(), "supervisor: two rows start a block service"); + self.grants.block = Some(Arc::clone(&service.kept)); + } let started = service.spawn(&program.path, &[], system, self.syscap, &self.connectors, &self.grants, &self.launcher, &mut self.log); match started { @@ -872,9 +885,70 @@ impl<'a> Supervisor<'a> { } } - /// `work`, a call into the file servers, made on [`Worker`], and its - /// answer — with every server that ends meanwhile started again, so a call - /// its end left waiting in the port's queue goes on to the new process. + /// The idle slot, for the one program whose row asks for it + /// (`toyos_update::slots`): minted here, so the slot this boot runs is + /// never among what is endowed. A machine with none says so and the + /// program starts holding nothing, which it refuses by name. + fn slot_storage(&mut self, program: &Program) -> Storage { + if !program.slots { + return Storage::default(); + } + self.slot_grant().unwrap_or_else(|why| { + say!("supervisor: {}: no slot to grant: {why}", program.name); + Storage::default() + }) + } + + /// **Which slot is idle is the loader's word, not the table's**: the + /// running ROOT is the one the loader read, the table is the one on that + /// ROOT's disk, and the idle slot is the one whose ROOT is not it — and + /// its two partitions are reached only as `toyos_update::slots::grant` + /// admits them: claimed on a disk the kernel drives, and on one the block + /// service serves, through connectors minted for each. + fn slot_grant(&mut self) -> Result { + use toyos_abi::inventory::{Record, Role}; + use toyos_update::slots::{BOOT_LABEL, ROOT_LABEL, TABLE_LABEL}; + let records = inventory(self.syscap)?; + let running = loaded(&records, Role::Root).ok_or("the loader named no ROOT")?; + let parts: Vec<_> = records + .iter() + .filter_map(|r| match r { + Record::Partition(p) => Some(*p), + _ => None, + }) + .collect(); + if let Some(root) = parts.iter().find(|p| p.unique_guid == running) { + return claimed_slots(self.syscap, &parts, *root); + } + let mint = |grants: &Grants<'_>, grant: BlockGrant| { + grants + .partitions(grant)? + .ok_or_else(|| "the ROOT this boot runs is on no disk the kernel drives, and no block service runs".to_string()) + }; + let kind = |guid: toyos_gpt::Guid| BlockGrant { scope: Scope::Kind(guid.0), writes: false }; + let tables = mint(&self.grants, kind(toyos_gpt::Guid::TOYOS_SLOTS))?; + let boots = mint(&self.grants, kind(toyos_gpt::Guid::TOYOS_BOOT))?; + let roots = mint(&self.grants, kind(toyos_gpt::Guid::TOYOS_ROOT))?; + let (table, table_guid, listed) = self.files("the slot table", move || served_slots(tables, boots, roots))??; + let Some(runs) = listed.iter().find(|p| p.unique_guid == running && p.type_guid == SLOT_KINDS.root) else { + return Err("the ROOT this boot runs is on no disk the kernel or the block service serves".into()); + }; + let (idle, slot) = toyos_update::slots::grant(&table, runs, &listed, SLOT_KINDS).map_err(|why| why.to_string())?; + let mut ports = Vec::new(); + for (label, guid) in [(TABLE_LABEL, table_guid), (BOOT_LABEL, slot.boot), (ROOT_LABEL, slot.root)] { + ports.push((label.to_string(), mint(&self.grants, BlockGrant { scope: Scope::Unique(guid), writes: true })?)); + } + say!( + "supervisor: the idle slot is {}, granted with the slot table read through the block service", + idle.letter() + ); + Ok(Storage { ports, ..Storage::default() }) + } + + /// `work`, a call into the file servers or the block service, made on + /// [`Worker`], and its answer — with every server that ends meanwhile + /// started again, so a call its end left waiting in the port's queue goes + /// on to the new process. /// `Err` is the call still unanswered at [`FILES_BOUND`], which the worker /// goes on waiting for alone, or the worker still waiting on an earlier /// one. @@ -1642,6 +1716,7 @@ impl Supervisor<'_> { return; } } + let storage = self.slot_storage(program); let started = start( command, program, @@ -1651,7 +1726,7 @@ impl Supervisor<'_> { &self.connectors, &self.grants, &extras, - Storage::default(), + storage, (&self.launcher, session), Output::Launch { log: &mut self.log, slots: &caller_slots }, ); @@ -1785,71 +1860,96 @@ fn resolve<'a, V>(system: &'a Manifest, path: &str, judge: impl FnOnce(Target<'_ Resolved::Package(row, verdict) } -/// The slot table's partition and the idle slot's two, claimed: the grant a -/// `slots` row is endowed (`toyos_update::slots`). -/// -/// **Which slot is idle is the kernel's word, not the table's**: the running -/// ROOT is the TOYOS-ROOT partition the kernel holds, the table is the one on -/// that ROOT's disk, and the idle slot is the one whose ROOT is not it — and -/// its two partitions are claimed only as `toyos_update::slots::grant` admits -/// them. -fn slot_grant(syscap: &SysCap) -> Result<[(&'static str, toyos::Device); 3], String> { - use toyos_abi::inventory::{PartState, Partition, Record}; - let parts: Vec = inventory(syscap)? - .into_iter() - .filter_map(|r| match r { - Record::Partition(p) => Some(p), - _ => None, - }) +/// The slot table's partition and the idle slot's two, claimed on a disk the +/// kernel drives: the grant a `slots` row is endowed (`toyos_update::slots`), +/// where the ROOT this boot runs, `running`, is one of `parts`. +fn claimed_slots( + syscap: &SysCap, + parts: &[toyos_abi::inventory::Partition], + running: toyos_abi::inventory::Partition, +) -> Result { + use toyos_abi::inventory::Partition; + let tables: Vec<&Partition> = parts + .iter() + .filter(|p| p.device == running.device && p.type_guid == toyos_gpt::Guid::TOYOS_SLOTS.0) .collect(); - let one = |what: &str, found: Vec<&Partition>| match found[..] { - [p] => Ok(*p), - _ => Err(format!("the machine has {} {what}, and a grant needs one", found.len())), + let [table_part] = tables[..] else { + return Err(format!("the machine has {} slot tables on the running ROOT's disk, and a grant needs one", tables.len())); }; - let running = one( - "ROOT partitions the kernel holds", - parts - .iter() - .filter(|p| p.type_guid == toyos_gpt::Guid::TOYOS_ROOT.0 && p.state == PartState::Kernel) - .collect(), - )?; - let table_part = one( - "slot tables on the running ROOT's disk", - parts - .iter() - .filter(|p| p.device == running.device && p.type_guid == toyos_gpt::Guid::TOYOS_SLOTS.0) - .collect(), - )?; let claim = |guid: [u8; 16], what: &str| { syscap .claim_partition::(toyos_abi::part::PartGuid(guid)) .map_err(|e| refused(&format!("the {what}"), e)) }; let table_claim = claim(table_part.unique_guid, "slot table")?; - let mut copies: [toyos_abi::part::Block; 2] = [[0; toyos_abi::part::BLOCK_BYTES]; 2]; - toyos_abi::syscall::partition_read(table_claim.as_handle(), 0, &mut copies) - .map_err(|e| format!("the slot table would not read: {e:?}"))?; - let (table, _) = toyos_update::slots::current([&copies[0], &copies[1]]) - .map_err(|why| format!("the slot table's partition holds {why}"))?; - // The table is the grantee's to write, so what it names is held to the - // inventory before anything is claimed. + let (table, _) = + toyos_update::slots::read(|copies| toyos_abi::syscall::partition_read(table_claim.as_handle(), 0, copies)) + .map_err(|why| why.to_string())?; let listed = |p: &Partition| toyos_update::slots::Listed { device: p.device, type_guid: p.type_guid, unique_guid: p.unique_guid, }; - let kinds = toyos_update::slots::Kinds { boot: toyos_gpt::Guid::TOYOS_BOOT.0, root: toyos_gpt::Guid::TOYOS_ROOT.0 }; let all: Vec<_> = parts.iter().map(listed).collect(); - let (idle, slot) = - toyos_update::slots::grant(&table, &listed(&running), &all, kinds).map_err(|why| why.to_string())?; + let (idle, slot) = toyos_update::slots::grant(&table, &listed(&running), &all, SLOT_KINDS).map_err(|why| why.to_string())?; let boot = claim(slot.boot, "idle slot's volume")?; let root = claim(slot.root, "idle slot's ROOT")?; say!("supervisor: the idle slot is {}, granted with the slot table", idle.letter()); - Ok([ - (toyos_update::slots::TABLE_LABEL, table_claim), - (toyos_update::slots::BOOT_LABEL, boot), - (toyos_update::slots::ROOT_LABEL, root), - ]) + let claims = vec![ + (toyos_update::slots::TABLE_LABEL.to_string(), table_claim), + (toyos_update::slots::BOOT_LABEL.to_string(), boot), + (toyos_update::slots::ROOT_LABEL.to_string(), root), + ]; + Ok(Storage { claims, ..Storage::default() }) +} + +/// The types a slot's two partitions carry. +const SLOT_KINDS: toyos_update::slots::Kinds = + toyos_update::slots::Kinds { boot: toyos_gpt::Guid::TOYOS_BOOT.0, root: toyos_gpt::Guid::TOYOS_ROOT.0 }; + +/// What the block service serves of the slots: the slot table, read through +/// a session on `tables`, its unique GUID, and every partition `tables`, +/// `boots` and `roots` list — each a connector minted for one type, read-only. +/// Its session on the table ends when this returns, and diskserver reads +/// that end before it judges `update`'s open of the same partition. +/// +/// **Made on the supervisor's file worker** ([`Supervisor::files`]): a call into a +/// service the supervisor restarts, from its loop alone, would wait in the +/// port's queue for a process only the loop can start. +fn served_slots( + tables: Connector, + boots: Connector, + roots: Connector, +) -> Result<(toyos_update::slots::Table, [u8; 16], Vec), String> { + use diskserver::disk::Disk as _; + let names = |connector: &Connector| { + namespace::build() + .add(toyos_blockring::PORT, connector) + .finish() + .map_err(|e| format!("no namespace for the block service: {e:?}")) + }; + let list = |connector: &Connector| { + diskserver::list(&names(connector)?, toyos_blockring::PORT) + .map_err(|why| format!("the block service would not list the slots' partitions: {why:?}")) + }; + let table_parts = list(&tables)?; + let [table_part] = table_parts[..] else { + return Err(format!("the block service serves {} slot tables, and a grant needs one", table_parts.len())); + }; + let session = diskserver::Session::open(names(&tables)?, toyos_blockring::PORT, table_part.unique) + .map_err(|why| format!("the slot table would not open: {why:?}"))?; + let mut disk = diskserver::disk::Served::new(session); + let (table, _) = + toyos_update::slots::read(|copies| disk.read(0, copies.as_flattened_mut())).map_err(|why| why.to_string())?; + let mut listed = table_parts; + listed.extend(list(&boots)?); + listed.extend(list(&roots)?); + // One block service is one disk. + let listed = listed + .into_iter() + .map(|p| toyos_update::slots::Listed { device: 0, type_guid: p.kind, unique_guid: p.unique }) + .collect(); + Ok((table, table_part.unique, listed)) } /// What the supervisor says about a device it could not mint a claim for. @@ -1924,8 +2024,9 @@ fn start<'a>( launcher: (&Acceptor, Session), output: Output<'_>, ) -> std::io::Result<(Child, Vec)> { + let Storage { args, claims, ports } = storage; // A storage row's own arguments first: a file server's role leads its argv. - command.args(&storage.args); + command.args(&args); command.args(&program.args); let booting = matches!(output, Output::Boot(_)); @@ -1945,12 +2046,14 @@ fn start<'a>( // acceptor is gone can never be served again. let mut taken: Vec<(&'a str, Acceptor)> = Vec::new(); - for (label, claim) in storage.claims { + for (label, claim) in claims { let raw = claim.into_raw(); command.endow(&label, raw.0); held.0.push(raw); } - if let Some(ns) = build_namespace(program, system, connectors, grants.view(program, launcher.1), extras)? { + let mut view = grants.view(program, launcher.1); + view.extend(ports); + if let Some(ns) = build_namespace(program, system, connectors, view, extras)? { let raw = ns.into_raw(); command.endow(SVC_LABEL, raw.0); held.0.push(raw); @@ -2089,28 +2192,12 @@ fn start<'a>( say!("supervisor: {}: {}", program.name, refused(name, e)); // A block service is told, so the partitions on a controller // the machine has are refused and never taken for none. - if e != SyscallError::NotFound && program.serves.iter().any(|s| s == toyos_blockring::PORT) { + if e != SyscallError::NotFound && is_block(program) { command.args(["--claim-refused", name.as_str()]); } } } } - // The idle slot, for the one program whose row asks for it: minted here, - // against the ROOT the kernel holds, so the slot this boot runs is never - // among what is endowed. A machine with none says so and the program - // starts holding nothing, which it refuses by name. - if program.slots { - match slot_grant(syscap) { - Ok(claims) => { - for (label, claim) in claims { - let raw = claim.into_raw(); - command.endow(label, raw.0); - held.0.push(raw); - } - } - Err(why) => say!("supervisor: {}: no slot to grant: {why}", program.name), - } - } // Nothing was spawned, so everything minted goes back with `held`. if let Some(unpaid) = unpaid { return Err(unpaid); @@ -2230,7 +2317,8 @@ enum Output<'l> { Launch { log: &'l mut Log, slots: &'l [(u32, toyos::RawHandle)] }, } -/// The namespace this program's `receives` names, [`toyos_swap::PORT`] apart. +/// The namespace this program's `receives` names, [`toyos_swap::PORT`] and +/// [`toyos_blockring::PORT`] apart, with `view`'s. /// /// A name some program *provides* rather than serves is not the supervisor's to give: it /// is one port per instance, made by whoever spawns the holder, and it reaches @@ -2246,9 +2334,14 @@ fn build_namespace( view: Vec<(String, Connector)>, extras: &[(&str, Connector)], ) -> std::io::Result> { - // Never the swap port: [`swap_namespace`] says why. - let receives: Vec<&String> = - program.receives.iter().filter(|name| *name != toyos_swap::PORT).collect(); + // Never the swap port: [`swap_namespace`] says why. Never the block + // port's own connector, which reaches nothing: a holder of `block` holds + // one minted for what it reaches, in `view`. + let receives: Vec<&String> = program + .receives + .iter() + .filter(|name| *name != toyos_swap::PORT && *name != toyos_blockring::PORT) + .collect(); if receives.is_empty() && extras.is_empty() && view.is_empty() { return Ok(None); } @@ -2281,7 +2374,7 @@ fn build_namespace( } /// What each program's directory capabilities are minted from: each -/// file-server role's port. +/// file-server role's port; and what its partitions are, the block service's. /// /// **Each start is minted grants naming its session's share** (`toyos::fs::Grant`, /// [`Session::share`]), one per directory of its row's view: a service has a @@ -2289,12 +2382,34 @@ fn build_namespace( /// child it spawns directly and every launch made from it that opens no /// session against one share, and a login session's processes against one /// more. A role whose ports closed for good is minted nothing. +/// +/// **A partition is reached through a connector minted for it** +/// ([`toyos_blockring::wire::Grant`]), never through the block port's own, +/// which no program is handed. struct Grants<'a> { /// Each role's service, whose kept acceptor is the role's port. roles: Vec<(&'a str, Arc>)>, + /// The block service the supervisor started, whose kept acceptor is + /// [`toyos_blockring::PORT`]. + block: Option>>, } impl Grants<'_> { + /// A connector to the block service reaching what `grant` admits; `None` + /// on a machine whose boot starts no block service, and `Err` once its + /// port has closed for good. + fn partitions(&self, grant: BlockGrant) -> Result, String> { + let Some(kept) = &self.block else { return Ok(None) }; + let kept = kept.lock().expect("supervisor: a service's state is poisoned"); + let Some((_, acceptor)) = kept.acceptors.iter().find(|(name, _)| name == toyos_blockring::PORT) else { + return Err("the block service's port has closed for good".into()); + }; + acceptor + .mint(&grant.encode()) + .map(Some) + .map_err(|e| format!("no connector to the block service could be minted: {e:?}")) + } + /// The directory capabilities `program` is endowed for one start in /// `session`, by namespace name: its row's view (`Program::view`), but /// that a storage row sees none, since a file server resolving a path of @@ -2382,13 +2497,15 @@ fn swapped(service: &str, word: Word, detail: &str) { say!("{}", toyos_swap::said(service, word, detail)); } -/// What a storage row's process is started with beside its row: its -/// arguments, and the claims on the partition a file server's role is on -/// where a disk the kernel drives carries it. +/// What a process is started with beside its row for the partitions it +/// reaches: its arguments, the claims endowed by label on the partitions a +/// disk the kernel drives carries, and the connectors in its namespace by +/// name to those the block service serves, each minted for what it reaches. #[derive(Default)] struct Storage { args: Vec, claims: Vec<(String, toyos::Device)>, + ports: Vec<(String, Connector)>, } /// Every record the kernel's inventory answers. @@ -2411,25 +2528,31 @@ fn guid_text(guid: [u8; 16]) -> String { toyos_abi::part::PartGuid(guid).write_text(&mut buf).to_string() } -/// A storage row's arguments and claims for one start. +/// A storage row's arguments, claims and block connector for one start. /// /// A block service is told the ROOT the machine runs from, which it serves no /// session on. A file server is told its role, and gets a claim on every /// partition of its role a disk the kernel drives carries — the stick, until -/// usbd serves it — and otherwise the partition's GUID, which it opens through -/// the block service; DATA it finds by type there itself, and counts with its -/// claims. A log or boot role the loader named no partition for, and a claim -/// the kernel refuses, each refuse the start: the role is then absent, never -/// served from memory. -fn storage_endowment(program: &Program, role: Option<&str>, syscap: &SysCap) -> Result { +/// usbd serves it — and otherwise the partition's GUID and `block` minted for +/// that partition alone, writing for the log and reading for the boot volume; +/// DATA it finds by type, through `block` minted for every DATA partition, +/// and counts with its claims. A log or boot role the loader named no +/// partition for, a claim the kernel refuses, and a block port closed for +/// good each refuse the start: the role is then absent, never served from +/// memory. +fn storage_endowment( + program: &Program, + role: Option<&str>, + syscap: &SysCap, + grants: &Grants<'_>, +) -> Result { use toyos_abi::inventory::{Record, Role}; let mut storage = Storage::default(); - let is_block = program.serves.iter().any(|s| s == toyos_blockring::PORT); - if !is_block && role.is_none() { + if !is_block(program) && role.is_none() { return Ok(storage); } let records = inventory(syscap).map_err(|why| StartError::Other(std::io::Error::other(why)))?; - if is_block { + if is_block(program) { if let Some(root) = loaded(&records, Role::Root) { storage.args.extend(["--running".to_string(), guid_text(root)]); } @@ -2446,14 +2569,18 @@ fn storage_endowment(program: &Program, role: Option<&str>, syscap: &SysCap) -> }) .collect() }; - let (kernel, named) = match role { - "data" => (on_kernel_disk(&|p| p.type_guid == toyos_gpt::Guid::TOYOS_DATA.0), None), + let (kernel, named, served) = match role { + "data" => { + let kind = toyos_gpt::Guid::TOYOS_DATA.0; + (on_kernel_disk(&|p| p.type_guid == kind), None, BlockGrant { scope: Scope::Kind(kind), writes: true }) + } "log" | "boot" => { let which = if role == "log" { Role::Log } else { Role::Boot }; let Some(guid) = loaded(&records, which) else { return Err(StartError::Partition(format!("the loader named no `{role}` partition"))); }; - (on_kernel_disk(&|p| p.unique_guid == guid), Some(guid)) + let grant = BlockGrant { scope: Scope::Unique(guid), writes: role == "log" }; + (on_kernel_disk(&|p| p.unique_guid == guid), Some(guid), grant) } other => panic!("supervisor: `{other}` is no role; the build refuses it"), }; @@ -2466,8 +2593,15 @@ fn storage_endowment(program: &Program, role: Option<&str>, syscap: &SysCap) -> Err(e) => return Err(StartError::Partition(refused(&name, e))), } } - if let (Some(guid), []) = (named, kernel.as_slice()) { - storage.args.push(guid_text(guid)); + // DATA is counted over both; a named partition no claim reached is the + // block service's. + if named.is_none() || kernel.is_empty() { + if let Some(guid) = named { + storage.args.push(guid_text(guid)); + } + if let Some(port) = grants.partitions(served).map_err(StartError::Partition)? { + storage.ports.push((toyos_blockring::PORT.to_string(), port)); + } } Ok(storage) } diff --git a/userland/update/Cargo.toml b/userland/update/Cargo.toml index 1ebadd4d574..d6ebd3b4bbb 100644 --- a/userland/update/Cargo.toml +++ b/userland/update/Cargo.toml @@ -7,7 +7,9 @@ license = "MIT OR Apache-2.0" [dependencies] toyos = { path = "../../toyos" } -toyos-abi = { path = "../../toyos-abi" } +# A slot's partitions, claimed on a disk the kernel drives or opened through +# the block service. +diskserver = { path = "../diskserver" } # The format, the verifier and the slot table the loader reads the same way. toyos-update = { path = "../../toyos-update" } # A slot's volume is FAT, written with the driver the kernel mounts FAT with. @@ -16,4 +18,4 @@ toyos-fat32 = { path = "../../toyos-fat32" } toyos-sha2 = { path = "../../toyos-sha2" } [package.metadata.toyos.host] -exempt.manages = "ToyOS's boot slots: it writes the idle one, onto the partitions the supervisor claims for it" +exempt.manages = "ToyOS's boot slots: it writes the idle one, onto the partitions the supervisor grants it" diff --git a/userland/update/src/main.rs b/userland/update/src/main.rs index bae5a717108..4d6e996de40 100644 --- a/userland/update/src/main.rs +++ b/userland/update/src/main.rs @@ -7,9 +7,12 @@ //! signature over them is the whole of its authority to install anything. //! //! **What it holds is the whole of what it can write** (`slots` in its -//! `system.toml` row): the supervisor claims the slot table's partition and the idle -//! slot's FAT volume and ROOT and endows them, and the slot this boot runs is -//! never among them (`toyos_update::slots::idle`). So it writes, in order: +//! `system.toml` row): the slot table's partition and the idle slot's FAT +//! volume and ROOT, as claims the supervisor endows under their labels on a +//! disk the kernel drives, or as connectors in its namespace under the same +//! names, each minted for its one partition on the block service's port; the +//! slot this boot runs is never among them (`toyos_update::slots::idle`). So +//! it writes, in order: //! //! 1. nothing, until the signed header's signature is this machine's key's and //! its version is newer than the running image and no older than the idle @@ -18,7 +21,7 @@ //! the header's hash once whole; //! 3. the kernel, its boot parameter and the signed header onto the idle FAT //! volume, each held to its hash before it is written; -//! 4. an fsync of each claim, which answers for these writes and no other +//! 4. a flush of each partition, which answers for these writes and no other //! process's; //! 5. the slot table, marking the idle slot — the copy that is not current, //! so a torn write leaves the old mark — and its fsync. @@ -30,12 +33,11 @@ use std::io::Read; use std::time::Instant; -use toyos::endow::Endowments; +use diskserver::disk::{Claimed, Disk, Served, BLOCK}; +use toyos::endow::{self, Endowments}; use toyos::PartitionDev; -use toyos_abi::part::{Block, BLOCK_BYTES, MAX_BLOCKS_PER_CALL}; use toyos_update::image::{Header, HEADER_BYTES, SIGNED_BYTES}; -use toyos_update::slots::{self, Table, Which}; -use toyos_fat32::BlockAccess as _; +use toyos_update::slots::{self, Which}; use toyos_update::{policy, sig}; /// The key an image must be signed with: the same the loader embeds. @@ -55,24 +57,39 @@ fn main() { } } -/// The three claims the supervisor endowed, or why this process holds none. -fn grant() -> Result<(PartitionDev, PartitionDev, PartitionDev), String> { - let take = |label: &str| { - Endowments::get() - .take::(label) - .ok_or_else(|| format!("this process holds no `{label}`: the supervisor grants the idle slot to one update at a time, and says why where it grants none")) +/// One partition of the grant: its blocks, and its unique GUID. +struct Held { + disk: Box, + unique: [u8; 16], +} + +/// The partition endowed under `label`: a claim, or a session through the +/// connector of that name, which reaches one partition and lists it. +fn held(label: &str) -> Result { + let none = || format!("this process holds no `{label}`: the supervisor grants the idle slot to one update at a time, and says why where it grants none"); + if let Some(claim) = Endowments::get().take::(label) { + let unique = claim.describe().map_err(|e| format!("the `{label}` claim: {e:?}"))?.unique_guid; + let disk = Claimed::new(claim).map_err(|e| format!("the `{label}` claim: {e:?}"))?; + return Ok(Held { disk: Box::new(disk), unique }); + } + let names = endow::namespace().ok_or_else(none)?; + let own = toyos::namespace::build().keep(names, &[label]).finish().map_err(|_| none())?; + let listed = diskserver::list(&own, label).map_err(|why| format!("`{label}` would not list its partition: {why:?}"))?; + let [one] = listed[..] else { + return Err(format!("`{label}` lists {} partitions, and is minted for one", listed.len())); }; - Ok((take(slots::TABLE_LABEL)?, take(slots::BOOT_LABEL)?, take(slots::ROOT_LABEL)?)) + let session = diskserver::Session::open(own, label, one.unique) + .map_err(|why| format!("`{label}`'s partition would not open: {why:?}"))?; + Ok(Held { disk: Box::new(Served::new(session)), unique: one.unique }) } fn run(began: Instant) -> Result { - let (table_claim, boot, root) = grant()?; - let (table, current) = read_table(&table_claim)?; - let boot_guid = boot.describe().map_err(|e| format!("the idle volume's claim: {e:?}"))?.unique_guid; - let root_info = root.describe().map_err(|e| format!("the idle ROOT's claim: {e:?}"))?; + let (mut table_part, mut boot, mut root) = (held(slots::TABLE_LABEL)?, held(slots::BOOT_LABEL)?, held(slots::ROOT_LABEL)?); + let (table, current) = + slots::read(|copies| table_part.disk.read(0, copies.as_flattened_mut())).map_err(|why| why.to_string())?; let idle = [Which::A, Which::B] .into_iter() - .find(|&w| table.slot(w).is_some_and(|s| s.boot == boot_guid && s.root == root_info.unique_guid)) + .find(|&w| table.slot(w).is_some_and(|s| s.boot == boot.unique && s.root == root.unique)) .ok_or("the partitions this process holds are no slot the table names")?; let running = table.slot(idle.other()).ok_or("the table carries no running slot")?; @@ -84,12 +101,12 @@ fn run(began: Instant) -> Result { sig::verify(&KEY, header_bytes, &toyos_update::image::signature_of(&signed)).map_err(|why| why.to_string())?; let idle_version = table.slot(idle).map(|s| s.version).filter(|&v| v != 0); policy::installable(header.version, running.version, idle_version).map_err(|why| why.to_string())?; - if header.root().len > root_info.blocks * BLOCK_BYTES as u64 { + if header.root().len > root.disk.blocks() * BLOCK as u64 { return Err(format!( "ROOT is {} bytes and slot {}'s ROOT partition holds {}", header.root().len, idle.letter(), - root_info.blocks * BLOCK_BYTES as u64 + root.disk.blocks() * BLOCK as u64 )); } println!( @@ -103,7 +120,7 @@ fn run(began: Instant) -> Result { let kernel = take(&mut input, header.kernel().len, header.kernel().sha256, "kernel")?; let cmdline = take(&mut input, header.cmdline().len, header.cmdline().sha256, "cmdline")?; let streamed = Instant::now(); - stream_root(&mut input, &root, header.root().len, header.root().sha256)?; + stream_root(&mut input, &mut *root.disk, header.root().len, header.root().sha256)?; let root_ms = streamed.elapsed().as_millis(); let mut rest = [0u8; 1]; match input.read(&mut rest) { @@ -112,9 +129,9 @@ fn run(began: Instant) -> Result { Err(e) => return Err(format!("the input's end would not read: {e}")), } - write_volume(&boot, &kernel, &cmdline, &signed)?; - root.sync().map_err(|e| format!("slot {}'s ROOT is not durable: {e:?}", idle.letter()))?; - boot.sync().map_err(|e| format!("slot {}'s volume is not durable: {e:?}", idle.letter()))?; + write_volume(&mut *boot.disk, &kernel, &cmdline, &signed)?; + root.disk.flush().map_err(|e| format!("slot {}'s ROOT is not durable: {e:?}", idle.letter()))?; + boot.disk.flush().map_err(|e| format!("slot {}'s volume is not durable: {e:?}", idle.letter()))?; let mut next = table; next.marked = idle; @@ -122,10 +139,11 @@ fn run(began: Instant) -> Result { slot.version = header.version; next.slots[idle.index()] = Some(slot); let (copy, block) = slots::next_write((table, current), next); - table_claim - .write(copy as u64, &[block]) + table_part + .disk + .write(copy as u64, &block) .map_err(|e| format!("the slot table's copy {copy} would not write: {e:?}"))?; - table_claim.sync().map_err(|e| format!("the slot table is not durable: {e:?}"))?; + table_part.disk.flush().map_err(|e| format!("the slot table is not durable: {e:?}"))?; Ok(format!( "{INSTALLED} version {} in slot {} ({} bytes of ROOT in {root_ms} ms, {} ms in all); it boots at the next reboot", @@ -136,13 +154,6 @@ fn run(began: Instant) -> Result { )) } -/// The slot table and which copy of it is current. -fn read_table(claim: &PartitionDev) -> Result<(Table, usize), String> { - let mut copies: [Block; 2] = [[0; BLOCK_BYTES]; 2]; - claim.read(0, &mut copies).map_err(|e| format!("the slot table would not read: {e:?}"))?; - slots::current([&copies[0], &copies[1]]).map_err(|why| format!("the slot table's partition holds {why}")) -} - /// Exactly `len` bytes of the input, held to `sha256`. fn take(input: &mut impl Read, len: u64, sha256: toyos_update::Digest, section: &str) -> Result, String> { let mut bytes = vec![0u8; len as usize]; @@ -153,20 +164,22 @@ fn take(input: &mut impl Read, len: u64, sha256: toyos_update::Digest, section: Ok(bytes) } -/// ROOT onto the idle ROOT partition as it arrives, a call's worth of blocks -/// at a time, and held to `sha256` once whole. -fn stream_root(input: &mut impl Read, root: &PartitionDev, len: u64, sha256: toyos_update::Digest) -> Result<(), String> { +/// The blocks of ROOT [`stream_root`] reads and writes at a time. +const STREAM_BLOCKS: usize = 32; + +/// ROOT onto the idle ROOT partition as it arrives, [`STREAM_BLOCKS`] at a +/// time, and held to `sha256` once whole. +fn stream_root(input: &mut impl Read, root: &mut dyn Disk, len: u64, sha256: toyos_update::Digest) -> Result<(), String> { let mut hasher = toyos_sha2::Sha256::new(); - let mut run: Vec = vec![[0; BLOCK_BYTES]; MAX_BLOCKS_PER_CALL]; - let blocks = len / BLOCK_BYTES as u64; + let mut run = vec![0u8; STREAM_BLOCKS * BLOCK]; + let blocks = len / BLOCK as u64; let mut at = 0u64; while at < blocks { - let n = (blocks - at).min(MAX_BLOCKS_PER_CALL as u64) as usize; - for block in &mut run[..n] { - input.read_exact(block).map_err(|e| format!("the input ended inside ROOT, at block {at}: {e}"))?; - hasher.update(&block[..]); - } - root.write(at, &run[..n]).map_err(|e| format!("ROOT's blocks from {at} would not write: {e:?}"))?; + let n = (blocks - at).min(STREAM_BLOCKS as u64) as usize; + let bytes = &mut run[..n * BLOCK]; + input.read_exact(bytes).map_err(|e| format!("the input ended inside ROOT, at block {at}: {e}"))?; + hasher.update(&*bytes); + root.write(at, bytes).map_err(|e| format!("ROOT's blocks from {at} would not write: {e:?}"))?; at += n as u64; } if hasher.finalize() != sha256 { @@ -177,9 +190,8 @@ fn stream_root(input: &mut impl Read, root: &PartitionDev, len: u64, sha256: toy /// The kernel, its boot parameter and the signed header onto the idle slot's /// FAT volume, replacing whatever was there. -fn write_volume(boot: &PartitionDev, kernel: &[u8], cmdline: &[u8], signed: &[u8]) -> Result<(), String> { - let blocks = boot.describe().map_err(|e| format!("the idle volume's claim: {e:?}"))?.blocks; - let mut fs = toyos_fat32::Fat32::mount(volume::Cached::new(boot, blocks)) +fn write_volume(boot: &mut dyn Disk, kernel: &[u8], cmdline: &[u8], signed: &[u8]) -> Result<(), String> { + let mut fs = toyos_fat32::Fat32::mount(volume::Cached::new(boot)) .map_err(|e| format!("the idle slot's volume does not mount: {e:?}"))?; let now = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) @@ -198,8 +210,8 @@ fn write_volume(boot: &PartitionDev, kernel: &[u8], cmdline: &[u8], signed: &[u8 fs.write(&mut file, 0, bytes).map_err(|e| format!("writing {path}: {e:?}"))?; fs.flush_meta(&mut file, time).map_err(|e| format!("recording {path}: {e:?}"))?; } - fs.sync().map_err(|e| format!("the idle volume's metadata: {e:?}"))?; - fs.into_device().flush().map_err(|e| format!("the idle volume's blocks: {e:?}")) + // Writes every block the volume holds (`Cached::flush`). + fs.sync().map_err(|e| format!("the idle volume's blocks: {e:?}")) } mod volume; diff --git a/userland/update/src/volume.rs b/userland/update/src/volume.rs index 67cad4703dc..f8274c57942 100644 --- a/userland/update/src/volume.rs +++ b/userland/update/src/volume.rs @@ -1,39 +1,37 @@ -//! A partition claim as `toyos-fat32` reads one: byte-addressed, over 4 KiB +//! A partition as `toyos-fat32` reads one: byte-addressed, over 4 KiB //! blocks, with every write held until [`Cached::flush`] writes the blocks it -//! touched in runs of whole calls. +//! touched in runs. //! //! **Held, because FAT writes a cluster at a time**, and a slot volume's //! clusters are smaller than a block: written through, a kernel image would be //! a read and a write of one block per 512 bytes. Held, the volume's writes are -//! as many calls as the blocks it touched need, and the partition's own fsync -//! is what makes them durable — which is the caller's, after this. +//! as many requests as the blocks it touched need, and the partition's own +//! flush is what makes them durable — which is the caller's, after this. use std::collections::BTreeMap; -use toyos::PartitionDev; -use toyos_abi::part::{Block, BLOCK_BYTES, MAX_BLOCKS_PER_CALL}; +use diskserver::disk::{Disk, BLOCK}; use toyos_fat32::{BlockAccess, IoError}; pub struct Cached<'a> { - claim: &'a PartitionDev, - blocks: u64, + disk: &'a mut dyn Disk, /// Every block written since the last flush, by block number. - dirty: BTreeMap>, + dirty: BTreeMap>, } impl<'a> Cached<'a> { - pub fn new(claim: &'a PartitionDev, blocks: u64) -> Self { - Self { claim, blocks, dirty: BTreeMap::new() } + pub fn new(disk: &'a mut dyn Disk) -> Self { + Self { disk, dirty: BTreeMap::new() } } /// Block `n` as the volume now reads: the held write, or the device's. - fn block(&self, n: u64) -> Result { + fn block(&mut self, n: u64) -> Result<[u8; BLOCK], IoError> { if let Some(held) = self.dirty.get(&n) { return Ok(**held); } - let mut one = [[0u8; BLOCK_BYTES]]; - self.claim.read(n, &mut one).map_err(|_| IoError::Device)?; - Ok(one[0]) + let mut one = [0u8; BLOCK]; + self.disk.read(n, &mut one).map_err(|_| IoError::Device)?; + Ok(one) } /// Each block `[offset, offset + len)` touches, with the part of it that @@ -46,9 +44,9 @@ impl<'a> Cached<'a> { let mut out = Vec::new(); let mut at = offset; while at < end { - let n = at / BLOCK_BYTES as u64; - let from = (at % BLOCK_BYTES as u64) as usize; - let to = BLOCK_BYTES.min(from + (end - at) as usize); + let n = at / BLOCK as u64; + let from = (at % BLOCK as u64) as usize; + let to = BLOCK.min(from + (end - at) as usize); out.push((n, from..to, (at - offset) as usize)); at += (to - from) as u64; } @@ -58,7 +56,7 @@ impl<'a> Cached<'a> { impl BlockAccess for Cached<'_> { fn capacity(&self) -> u64 { - self.blocks * BLOCK_BYTES as u64 + self.disk.blocks() * BLOCK as u64 } fn read_at(&mut self, offset: u64, buf: &mut [u8]) -> Result<(), IoError> { @@ -72,31 +70,32 @@ impl BlockAccess for Cached<'_> { fn write_at(&mut self, offset: u64, buf: &[u8]) -> Result<(), IoError> { for (n, range, from) in self.spans(offset, buf.len())? { // A whole block's write needs nothing read under it. - let mut block = if range.len() == BLOCK_BYTES { [0u8; BLOCK_BYTES] } else { self.block(n)? }; + let mut block = if range.len() == BLOCK { [0u8; BLOCK] } else { self.block(n)? }; block[range.clone()].copy_from_slice(&buf[from..from + range.len()]); self.dirty.insert(n, Box::new(block)); } Ok(()) } - /// Write every held block, a run of consecutive ones per call. Durable is - /// the claim's fsync, which the caller asks once every volume is written. + /// Write every held block, a run of consecutive ones per request. Durable + /// is the partition's flush, which the caller asks once every volume is + /// written. fn flush(&mut self) -> Result<(), IoError> { let held = std::mem::take(&mut self.dirty); - let mut run: Vec = Vec::with_capacity(MAX_BLOCKS_PER_CALL); + let mut run: Vec = Vec::new(); let mut first = 0u64; for (n, block) in held { - if !run.is_empty() && (n != first + run.len() as u64 || run.len() == MAX_BLOCKS_PER_CALL) { - self.claim.write(first, &run).map_err(|_| IoError::Device)?; + if !run.is_empty() && n != first + (run.len() / BLOCK) as u64 { + self.disk.write(first, &run).map_err(|_| IoError::Device)?; run.clear(); } if run.is_empty() { first = n; } - run.push(*block); + run.extend_from_slice(&block[..]); } if !run.is_empty() { - self.claim.write(first, &run).map_err(|_| IoError::Device)?; + self.disk.write(first, &run).map_err(|_| IoError::Device)?; } Ok(()) }