From 9015e612daed99823c88ca3255db9d3a14f4b158 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 28 Sep 2026 19:39:55 +0200 Subject: [PATCH 1/6] The loader-slimming track, and the firmware rule in CLAUDE.md The owner ruled on the loader audit (2026-09-28): slim the loader per the audit; the hardware clock is always UTC; keep "no firmware calls after the handover" and write it down; redo #539 inside this track rather than land it; end at least as secure. - CLAUDE.md gains the one briefed paragraph: no firmware code runs on the CPU after ExitBootServices; every firmware call ToyOS makes is the loader's. - issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md is the track: seven stages in dependency order, each with an exit a test or gate checks, and #539's pieces placed or deleted (--boot-next, the panic handler's fall and its two issues, the bootvars::state line). - The anti-rollback floor stage keeps the audit's shrink in a form that is argued not to weaken anything: one floor per key (Scope::Machine's) for every loader, so the owner's floor is unchanged and a throwaway key's floor stops being resettable by a write of the log partition's GUID; stale-floor deletion goes, since it never touched a loader's own floor. - issues/boot-media/the-machine-updates-itself-without-ubuntu.md loses what the new track owns: stage 2's Ubuntu items and exit, and the A/B-rollback later stage. The NVMe install stays there. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j --- CLAUDE.md | 2 + ...oes-only-what-must-precede-the-handover.md | 159 ++++++++++++++++++ ...e-machine-updates-itself-without-ubuntu.md | 27 +-- 3 files changed, 166 insertions(+), 22 deletions(-) create mode 100644 issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md diff --git a/CLAUDE.md b/CLAUDE.md index f64b2c9ddf0..afcf94c14c8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,6 +50,8 @@ A subdirectory `CLAUDE.md` loads when a file in that subtree is `Read`, and not **CPU state** — a CPU's control registers come from one declaration, applied by the BSP and by every AP and asserted on each; no read-modify-write decides what either holds. +**Firmware** — no firmware code runs on the CPU after ExitBootServices; every firmware call ToyOS makes is the loader's. + **Input** — the kernel delivers key *transitions*, never what one types; a surface turns one into the other. Translation, layouts, dead keys and escape sequences live in userland, one translator per surface. **POSIX** — the kernel ABI and SDK are Rust-native and capability-shaped. POSIX lives in `userland/libc` (ours, not a fork) with explicitly relaxed rules. That layer may be ugly; the kernel may not. diff --git a/issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md b/issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md new file mode 100644 index 00000000000..e428691a307 --- /dev/null +++ b/issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md @@ -0,0 +1,159 @@ +--- +status: open +kind: track +opened: 2026-09-28 +--- + +# The loader does only what must happen before the handover + +About a third of `bootloader/src` loads no kernel. It serves the T14's +Ubuntu-hosted loop (`BootNext` back to the loader, a pass that boots nothing +and only reports, a reset into Ubuntu, which reads the stick) and jobs that +landed in the loader because the kernel never maps UEFI runtime services. The +owner's rulings (2026-09-28) set the bounds: + +- the loader is slimmed; +- the hardware clock keeps UTC; +- no firmware code runs after `ExitBootServices` (root `CLAUDE.md`); +- ToyOS ends at least as secure as it starts. + +The audit estimates, without measuring, that about 1,150 of the loader's 3,519 +lines go and that `toyos-update/src/record.rs` (236 lines) becomes a few fields +of the slot table. + +PR #539 does not land. Its pieces are placed in the stages below. Three of its +pieces are deleted: + +- `update --boot-next `; +- the panic handler's fall to the next boot entry, together with the two issues + it filed (`a-failed-pass-can-fall-to-an-entry-the-firmware-never-boots-from-its-order` + and `the-loaders-power-off-with-no-entry-behind-it-is-proven-by-nothing`); +- the `bootvars::state` line. + +Each stage lands on its own, in this order. + +1. **Deletions that wait on nothing.** + - Delete the loader's TCO arm (`bootloader/src/watchdog.rs`, `arch::pio`) + and `loader_watchdog_arms`. This leaves the span from the jump to + `arch::watchdog::init` with no bound on a machine whose TCO counts. On the + T14 nothing bounds it today either + (`issues/hardware/an-armed-tco-has-never-reset-the-t14.md`, whose evidence + moves to the kernel's read-back). + - Delete the time-zone read (`rtc_utc_offset`), `KernelArgs`' two zone + fields, the `rtc_zone_east` actuator and `wall_clock_zone`. The kernel + reads the RTC as UTC. Removing the fields is an ABI change and lands in + this stage. + - Delete `rootbridge.rs`'s hex dump. + - Move `arch::counter_origin` (`IA32_TSC_ADJUST` on x86-64) into the + kernel. + - Delete the audit's ten worst comments where their files stay. + + **Exit**: `bootloader/src` holds no TCO access, no `counter_origin` and no + zone. The Fast tier and `wall_clock_*` pass. Putting a zone back into + `clock::init_wall` makes a `wall_clock_*` test fail. + +2. **One HARDDRIVE rule.** #539's `toyos_update::entry::partition` is taken + by `boot_partition`, `rootimage::boot_disk` and `bootnext.rs`. + + **Exit**: the loader loses lines. #539's host tests + `a_path_names_the_partition_of_its_one_hard_drive_node` and + `a_partitions_disk_is_the_path_before_its_hard_drive_node` pass, and fail + under #539's two mutations: cutting the disk before the path's last node, + and taking a second HARDDRIVE node. The oracle is UEFI 2.10 §10.3.5.1. + +3. **One floor per key.** `floor::Scope` goes. Every loader keeps the floor + `Scope::Machine` names, named for its key alone. `stale` and its deletions + go. + + Why nothing is weaker: + - The owner's floor keeps its name, attributes and judgement unchanged. + - A throwaway key's floor is named today for the key and the log + partition's GUID, so a write of that GUID resets it. After this stage, + that write does not. + - `stale` never deleted a loader's own floor, so removing it lowers no + floor. What stays behind is one variable per key that has booted the + machine, and only a loader, before the handover, writes one. + + The cost: a machine refuses an image older than one its key has already + booted there, as it already does for the owner's key. + + **Exit**: the guest tests run the scope the owner's machine runs. + `update_floor_is_the_images_own` is rewritten to check two things: this + key's floor, planted above the image's version, refuses the image under a + fresh log GUID; and another key's floor holds the image to nothing and + stays. Putting the log GUID back into the name makes the test fail. + +4. **Tries and a good flag per slot in the slot table.** + - They replace the attempts file (`attempt.rs`, `toyos-update/src/record.rs` + and `main.rs`' accounting), the dead-slot retry and `slot::proven`. + - The loader counts a slot's tries down before it hands over, and boots no + slot that is out of tries and was never marked good. The running system + marks its own slot good. + - The floor rises on a pass that boots a good slot, to the version of the + signed header that pass verified and never to a version the table names. + That meets the second half of the exit in + `issues/boot-media/the-anti-rollback-floor-is-a-firmware-variable.md`. + - `update --once` sets tries = 1 on the idle slot, and that slot is never + marked good. The kept slot boots next, and no floor rises. + - `update --boot-first` is the only boot-variable write. The loader writes + its own `HD(…)/File(…)` entry and puts it first, once, from a request it + takes off the table before acting on it. + + **Exit**: the `update_*` guest tests, rewritten for tries, pass. + `update_falls_back_from_a_dying_kernel` and + `update_hang_kills_an_unproven_image` hold as they do today. #539's + `update_boot_first_puts_the_loader_first` passes, and so do its host tests + of the entry rules that survive. + + Negative controls: + - Skipping the countdown makes `update_hang_kills_an_unproven_image` fail. + - Raising the floor on a slot not yet good makes an `update_*` test fail. + - Acting on the request before taking it off the table makes + `update_boot_first_puts_the_loader_first` fail. + + Oracles: the slot table the host reads off the disk after each boot, and + OVMF's boot manager booting the entry the loader wrote. + +5. **The T14 bench.** + - It is built from #539's `src/metalbench.rs`, the bench path of + `src/metal.rs`, `tests/common/bench.rs`, `tests/bench*case` and + `--bench-image`. `--via-ubuntu` stays as the old path. + - The loader names no hash of its own file. The bench reads the file off + the ESP. + - The tested pass's `loader.log` is kept as `loader-previous.log` by a + rename (`SetInfo`), not by #539's copy. + - It runs over the cable that + `issues/hardware/the-t14-answers-only-through-a-usb-stick.md` owns. + - #539's issues `a-loader-change-reaches-a-machine-only-by-writing-its-stick`, + `the-bench-reads-no-quiescent-log-volume`, + `the-bench-runs-with-no-bound-on-its-own-boot`, + `the-bench-sometimes-comes-back-two-minutes-late` and + `the-benchs-cable-is-read-by-the-driver-under-test` land here, each only + as far as it is true of what lands. + + **Exit**: `bench_loop_drives_a_toyos_machine` passes in QEMU. On the T14, + with Ubuntu never started, three things hold: a kernel change boots; a slot + with a flipped byte, no signature or a lower version is refused and the + other boots; and a boot that dies falls back on its own. + +6. **Ubuntu leaves the loop.** Delete `toyos-metal`'s `--via-ubuntu` path and + `bootloader/src/bootnext.rs`. After a reset the firmware comes back to the + loader because ToyOS's entry is first (stage 4). + + **Exit**: no path in `src/metal*.rs` reaches Ubuntu. On the T14, a panic's + reset reaches the loader with no `BootNext` set. + +7. **The crash report belongs to the kernel.** The loader hands the last + boot's record to the kernel instead of decoding it. The kernel logs it, and + `logd` carries it to `/log`. The report pass goes, taking with it: + - `end_this_pass`; + - the chain constants; + - `loaderlog`'s chain lines; + - `armed_at`'s `GetTime`; + - everything else in `bootloader/src/blackbox.rs` except the claim and the + arm; + - the chained-pass item of `issues/hardware/the-t14-boots-toyos-unattended.md`. + + **Exit**: `blackbox_panic_chain` reads the previous boot's panic out of + `/log`, with none of it in `loader.log`, and every pass boots a kernel. + Withholding the handover makes the test fail. diff --git a/issues/boot-media/the-machine-updates-itself-without-ubuntu.md b/issues/boot-media/the-machine-updates-itself-without-ubuntu.md index 404ebff7983..0f4efc82f7a 100644 --- a/issues/boot-media/the-machine-updates-itself-without-ubuntu.md +++ b/issues/boot-media/the-machine-updates-itself-without-ubuntu.md @@ -36,34 +36,17 @@ stdin — and the machine installs nothing the owner did not sign. (`--owner-key`, `--update-image`, minted by `--signing-key-new`) for an image installed on the owner's machine, whose loader keeps the machine's floor. -## Stage 2 — the T14 installs ToyOS on its NVMe and updates without Ubuntu +## Stage 2 — the T14 installs ToyOS on its NVMe -What `toyos-metal` still runs Ubuntu for, each of which this stage replaces: - -1. **Writing the image** — `wipefs` and `dd of=/dev/sda` under a sudoers rule. - Replaced by the machine booting the stick and installing onto its own NVMe, - then `ssh t14 update < image` for every change after. -2. **Choosing the next boot** — `efibootmgr --create-only`, `--delete-bootnum`, - `--bootnext`. The loader already points `BootNext` at itself; an install - writes its own entry once. -3. **Reading a boot's verdict** — `dd if=/dev/sda3` and `mount -o ro` of the log - partition. Replaced by `logd`'s record stream and `ssh … cat`. -4. **Reboots and liveness** — `reboot`, `true`, `date -u +%s`, the `/sys` - identity reads of the stick, and the loop's wait for Ubuntu's sshd to come - back after every ToyOS boot. -5. **The runner key and `ssh t14`** themselves reach Ubuntu's sshd, not ToyOS's. - -**Exit**: a kernel change reaches the T14 and boots with Ubuntu never started; -a slot with a flipped byte, no signature or a lower version is refused and the -other boots; a boot that dies falls back on its own — each on the T14. +The machine boots the stick and installs onto its own NVMe, then takes +`ssh t14 update < image` for every change after. Taking Ubuntu out of +`toyos-metal`'s loop is +`issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md`'s. ## Later stages — the end state, which no earlier stage may block - **An image-based, read-only system**: `/system` is the signed ROOT and nothing else, and nothing the machine runs is outside an image or a package. -- **A/B slots with automatic rollback**: stage 1's fallback, plus a boot that - confirms itself healthy before the floor rises (today the floor rises only on - a boot that hands the machine back on purpose). - **A verified boot chain**: UEFI Secure Boot with the owner's key over the loader. Until then the loader is the one binary no signature covers, and a writable ESP is the gap. From 97e8fb3c6de8fd81fe8e3348a9b5b856ee2b8cbb Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 28 Sep 2026 19:49:43 +0200 Subject: [PATCH 2/6] Firmware rule made true of SMM, and the anti-rollback floor's cost ruled accepted CLAUDE.md's Firmware paragraph scoped to ToyOS's own calls, since the old wording was false of SMM and the xHCI legacy-ownership handshake. Stage 3 of the loader track now records the owner's ruling on the per-key floor's cost as the stage's decision, rather than the track's own argument for it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j --- CLAUDE.md | 2 +- .../the-loader-does-only-what-must-precede-the-handover.md | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index afcf94c14c8..48b07851651 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,7 +50,7 @@ A subdirectory `CLAUDE.md` loads when a file in that subtree is `Read`, and not **CPU state** — a CPU's control registers come from one declaration, applied by the BSP and by every AP and asserted on each; no read-modify-write decides what either holds. -**Firmware** — no firmware code runs on the CPU after ExitBootServices; every firmware call ToyOS makes is the loader's. +**Firmware** — ToyOS calls no firmware after ExitBootServices; every firmware call ToyOS makes is the loader's. **Input** — the kernel delivers key *transitions*, never what one types; a surface turns one into the other. Translation, layouts, dead keys and escape sequences live in userland, one translator per surface. diff --git a/issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md b/issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md index e428691a307..c86228569af 100644 --- a/issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md +++ b/issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md @@ -74,8 +74,9 @@ Each stage lands on its own, in this order. floor. What stays behind is one variable per key that has booted the machine, and only a loader, before the handover, writes one. - The cost: a machine refuses an image older than one its key has already - booted there, as it already does for the owner's key. + The owner's ruling: one floor per signing key; the cost — that an older + image from the same checkout is refused on a machine that has booted a + newer one until the floor is deliberately reset — is accepted. **Exit**: the guest tests run the scope the owner's machine runs. `update_floor_is_the_images_own` is rewritten to check two things: this From 75f3c95e0c168afc41ca22c2b676bf095924c1d6 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 28 Sep 2026 20:33:04 +0200 Subject: [PATCH 3/6] Round 1 of #582: the firmware rule names UEFI calls, and the track is rebuilt on the owner's rulings CLAUDE.md: ToyOS calls no UEFI runtime service; every UEFI call is the loader's. PSCI on ARM64 and the xHCI legacy-ownership handshake are not UEFI calls, which closes both CLAUDE.md blockers. The track: - Stage 1 is what #583 lands: UTC, the hex dump, ten comments, the KernelArgs layout identity, IA32_TSC_ADJUST read by the kernel. The loader's TCO arm stays; wall_clock_utc is the test a zone reds. - Stage 2 scopes every volume lookup to the boot disk with exactly one match, and asks firmware once per pass. - Stage 3 compares one floor per key on a signed security version; the accepted cost of refusing older builds is gone with the build time. - Stage 4 is new: current uefi, the loader's own panic handler, the unsound allocations and relocation unsafes gone, typed KernelArgs, one CRC32. - Stage 5 adopts the Android/libabr tries rules as pure host-tested toyos-update decisions: fresh slot A untried with 3 tries, no bootable slot powers off, the good flag set only past a health gate, and controls for a good flag left set and a floor raised to the table's version. - Stage 8 is new: the kernel arms the TCO before mm::init and takes the read-back the TCO issue's exit needs; only then does the loader's arm go. - Stage 9 rewrites the wedged-report issue's exit and gives the kernel harvest's stale-record check. - Every #539 piece is placed or listed as deleted. the-machine-updates-itself-without-ubuntu.md: stage 2 gets its exit back, and names its wait on the track's --boot-first. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j --- CLAUDE.md | 2 +- ...oes-only-what-must-precede-the-handover.md | 347 ++++++++++++------ ...e-machine-updates-itself-without-ubuntu.md | 10 +- 3 files changed, 243 insertions(+), 116 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 48b07851651..27da23cd231 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,7 +50,7 @@ A subdirectory `CLAUDE.md` loads when a file in that subtree is `Read`, and not **CPU state** — a CPU's control registers come from one declaration, applied by the BSP and by every AP and asserted on each; no read-modify-write decides what either holds. -**Firmware** — ToyOS calls no firmware after ExitBootServices; every firmware call ToyOS makes is the loader's. +**Firmware** — ToyOS calls no UEFI runtime service; every UEFI call ToyOS makes is the loader's. **Input** — the kernel delivers key *transitions*, never what one types; a surface turns one into the other. Translation, layouts, dead keys and escape sequences live in userland, one translator per surface. diff --git a/issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md b/issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md index c86228569af..9f61bc9b024 100644 --- a/issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md +++ b/issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md @@ -6,121 +6,210 @@ opened: 2026-09-28 # The loader does only what must happen before the handover -About a third of `bootloader/src` loads no kernel. It serves the T14's -Ubuntu-hosted loop (`BootNext` back to the loader, a pass that boots nothing -and only reports, a reset into Ubuntu, which reads the stick) and jobs that -landed in the loader because the kernel never maps UEFI runtime services. The -owner's rulings (2026-09-28) set the bounds: +The owner's bounds: -- the loader is slimmed; +- the loader does only what must precede the handover; - the hardware clock keeps UTC; -- no firmware code runs after `ExitBootServices` (root `CLAUDE.md`); -- ToyOS ends at least as secure as it starts. +- ToyOS calls no UEFI runtime service (root `CLAUDE.md`); +- ToyOS ends at least as secure as it starts; +- the anti-rollback floor counts a signed security version, raised only by a + release that fixes a security hole. -The audit estimates, without measuring, that about 1,150 of the loader's 3,519 -lines go and that `toyos-update/src/record.rs` (236 lines) becomes a few fields -of the slot table. +PR #539 does not land. Its pieces: -PR #539 does not land. Its pieces are placed in the stages below. Three of its -pieces are deleted: - -- `update --boot-next `; -- the panic handler's fall to the next boot entry, together with the two issues - it filed (`a-failed-pass-can-fall-to-an-entry-the-firmware-never-boots-from-its-order` +- stage 2 takes `toyos_update::entry::partition` and its two host tests; +- stage 5 takes the rest of `toyos_update::entry` and its host tests, + `update --boot-first`, `update --once` and + `update_boot_first_puts_the_loader_first`; +- stage 6 takes the bench: `src/metalbench.rs`, `tests/common/bench.rs`, + `tests/bench*case`, `--bench-image`, toybox `date`, `Ssh::probe` and + `Ssh::fetch` with `ssh-client-host`'s `probe` and `fetch`, + `build::AUTHORIZED_ON_ROOT`, and `src/bootlog.rs`' `LOADER_PREVIOUS_LOG`, + `MOUNTED_FROM_MEMORY` and `BOOT_PARAMETER`; +- deleted: `update --boot-next ` with `Guid::parse`, its only reader; the + panic handler's fall to the next boot entry, with the two issues it filed + (`a-failed-pass-can-fall-to-an-entry-the-firmware-never-boots-from-its-order` and `the-loaders-power-off-with-no-entry-behind-it-is-proven-by-nothing`); -- the `bootvars::state` line. + the `bootvars::state` line; `SLOT_ONCE`, because stage 4's `KernelArgs` + carries a once boot as a field; `LOADER_IS`, because the loader names no hash + of its own file. Each stage lands on its own, in this order. -1. **Deletions that wait on nothing.** - - Delete the loader's TCO arm (`bootloader/src/watchdog.rs`, `arch::pio`) - and `loader_watchdog_arms`. This leaves the span from the jump to - `arch::watchdog::init` with no bound on a machine whose TCO counts. On the - T14 nothing bounds it today either - (`issues/hardware/an-armed-tco-has-never-reset-the-t14.md`, whose evidence - moves to the kernel's read-back). - - Delete the time-zone read (`rtc_utc_offset`), `KernelArgs`' two zone - fields, the `rtc_zone_east` actuator and `wall_clock_zone`. The kernel - reads the RTC as UTC. Removing the fields is an ABI change and lands in - this stage. - - Delete `rootbridge.rs`'s hex dump. - - Move `arch::counter_origin` (`IA32_TSC_ADJUST` on x86-64) into the - kernel. - - Delete the audit's ten worst comments where their files stay. - - **Exit**: `bootloader/src` holds no TCO access, no `counter_origin` and no - zone. The Fast tier and `wall_clock_*` pass. Putting a zone back into - `clock::init_wall` makes a `wall_clock_*` test fail. - -2. **One HARDDRIVE rule.** #539's `toyos_update::entry::partition` is taken - by `boot_partition`, `rootimage::boot_disk` and `bootnext.rs`. - - **Exit**: the loader loses lines. #539's host tests - `a_path_names_the_partition_of_its_one_hard_drive_node` and - `a_partitions_disk_is_the_path_before_its_hard_drive_node` pass, and fail - under #539's two mutations: cutting the disk before the path's last node, - and taking a second HARDDRIVE node. The oracle is UEFI 2.10 §10.3.5.1. - -3. **One floor per key.** `floor::Scope` goes. Every loader keeps the floor - `Scope::Machine` names, named for its key alone. `stale` and its deletions - go. +1. **What PR #583 lands.** + - The RTC keeps UTC. The loader's `GetTime` zone read, `KernelArgs`' two + zone fields, the kernel's `UTC_OFFSET_SECS` and its local/UTC split, the + `rtc-zone-east` actuator, `wall_clock_zone` and logd's zone recovery go. + FAT stamps, `SYS_CLOCK_REALTIME` and `SYS_CLOCK_EPOCH` read + `clock::utc_secs`. + - `rootbridge.rs`' hex dump goes, with `toyos-acpi`'s `Walk::bytes`. + - Ten loader and `toyos-update` comments go, each named in #583's commits. + - `KernelArgs` carries a layout identity the kernel checks first. `update` + never replaces the ESP loader, so a slot's kernel can meet a loader built + to another layout; it is refused by name. + - The kernel reads `IA32_TSC_ADJUST` beside its power-on report. aarch64's + `counter_origin` is deleted: the kernel reads `CNTFRQ_EL0` itself. + - The loader's TCO arm stays until stage 8. + - This stage is an ABI change. + + **Exit**: `bootloader/src` holds no zone and no `counter_origin`. + `wall_clock_utc` judges the log file's name, its FAT stamp, the probe's + `local=` and `SYS_CLOCK_EPOCH` against the `-rtc base=` instant. It fails + under a kernel that reads the RTC as local time two hours east, and under + `SYS_CLOCK_REALTIME` patched to `utc_secs + 7200`. A mixed pair, the loader + at the base and the kernel at the head, is refused by the identity check's + name. `boot_from_power_on` fails where the kernel's `IA32_TSC_ADJUST` line is + missing. + +2. **One boot disk.** + - `toyos_update::entry::partition` is the one HARDDRIVE rule, taken by + `boot_partition`, `rootimage::boot_disk` and `bootnext.rs`. + - Every volume the loader opens (the slot's FAT partition, the log + partition and the attempts file on it) is found on the boot disk, where + exactly one match is taken. `loaderlog::volume_handle`'s machine-wide + first match goes. + - A pass asks firmware once: one `LoadedImage` open, one device-path walk, + one `Disk::open` and slot-table read, and one file reader. + `load_file_bytes`, `MAX_ESP_FILE` and the unsound `alloc_uninit` go. + + **Exit**: `a_path_names_the_partition_of_its_one_hard_drive_node` and + `a_partitions_disk_is_the_path_before_its_hard_drive_node` pass on the host, + and fail under two mutations: cutting the disk before the path's last node, + and taking a second HARDDRIVE node. `root_named_twice` plants a twin whose + bytes differ, so a loader that reads the twin fails it. A second disk + carrying the boot disk's log partition GUID leaves the log on the boot disk. + The oracle is UEFI 2.10 §10.3.5.1. + +3. **One floor per key, on a security version.** + - The signed header's `version` is the security version: a number the tree + holds, raised only by the change that fixes a security hole. + `image::version_now` goes. The header's `FORMAT` rises, so a header + carrying a build time is refused by name. + - `floor::Scope` goes. Every loader keeps one floor per signing key, named + for the key alone and under a name no build-time floor carries. `stale` + and its deletions go. + - No loader deletes another key's floor, because a deletion lowers it. The + store keeps one 8-byte variable per key that has booted the machine. + - `policy::installable` admits an image at or above the running image's + and the idle slot's security version. + - This stage deletes the throwaway-key bullet of + `issues/boot-media/the-anti-rollback-floor-is-a-firmware-variable.md` and + the per-image floor in stage 1 of + `issues/boot-media/the-machine-updates-itself-without-ubuntu.md`. The floor + issue's TPM 2.0 NV counter stays its exit: `TPM2_NV_Increment` moves one + step, and so does a security version. Why nothing is weaker: - - The owner's floor keeps its name, attributes and judgement unchanged. - - A throwaway key's floor is named today for the key and the log - partition's GUID, so a write of that GUID resets it. After this stage, - that write does not. - - `stale` never deleted a loader's own floor, so removing it lowers no - floor. What stays behind is one variable per key that has booted the - machine, and only a loader, before the handover, writes one. - - The owner's ruling: one floor per signing key; the cost — that an older - image from the same checkout is refused on a machine that has booted a - newer one until the floor is deliberately reset — is accepted. + - The floor refuses every image below the highest security version a boot + has proven, which is every image the owner has declared holed. + - A throwaway key's floor no longer resets when the log partition's GUID + is written. + - Only a loader, before the handover, writes a floor. **Exit**: the guest tests run the scope the owner's machine runs. - `update_floor_is_the_images_own` is rewritten to check two things: this - key's floor, planted above the image's version, refuses the image under a - fresh log GUID; and another key's floor holds the image to nothing and - stays. Putting the log GUID back into the name makes the test fail. - -4. **Tries and a good flag per slot in the slot table.** - - They replace the attempts file (`attempt.rs`, `toyos-update/src/record.rs` - and `main.rs`' accounting), the dead-slot retry and `slot::proven`. - - The loader counts a slot's tries down before it hands over, and boots no - slot that is out of tries and was never marked good. The running system - marks its own slot good. - - The floor rises on a pass that boots a good slot, to the version of the - signed header that pass verified and never to a version the table names. - That meets the second half of the exit in - `issues/boot-media/the-anti-rollback-floor-is-a-firmware-variable.md`. - - `update --once` sets tries = 1 on the idle slot, and that slot is never - marked good. The kept slot boots next, and no floor rises. - - `update --boot-first` is the only boot-variable write. The loader writes - its own `HD(…)/File(…)` entry and puts it first, once, from a request it - takes off the table before acting on it. - - **Exit**: the `update_*` guest tests, rewritten for tries, pass. - `update_falls_back_from_a_dying_kernel` and - `update_hang_kills_an_unproven_image` hold as they do today. #539's + `update_floor_is_the_images_own` checks three things. This key's floor, + planted above the image's security version, refuses the image under a + fresh log GUID. An older build at the floor's security version boots. + Another key's floor holds the image to nothing and stays. Putting the log + GUID back into the name makes it fail. The oracle is the vars store as the + host reads it (`vars::live` in `tests/common/update.rs`): each floor's name + and its UEFI 2.10 §8.2 attributes. + +4. **The loader on current `uefi`, sound, with a typed handover.** + - `uefi` and `uefi-raw` move to their current releases, and `uefi-services` + goes. The unsafe `BlockIO` media cast in `rootimage.rs` goes with the old + layout. + - The loader's own `#[panic_handler]` writes the panic through `loaderlog`, + then resets the machine. + - `alloc_kernel_memory` stops building a `Vec` over a 2 MiB-aligned + allocation. Both relocation unsafes go. `blackbox::Page` is not `Copy`, so + `bytes()` mints no second `&'static mut`. + - `KernelArgs` is typed: `repr(C)` sub-structs, and `#[repr(C, u32)]` enums + for the optional parts in place of `u32` presence flags and zero + sentinels. The slot booted, the slot refused and why, and the black-box + page are fields, not `boot-slot=`, `slot-refused=` and `blackbox=` text; + `params.rs`' last-one-wins and kernel `main.rs`' branch for a format never + emitted go. `kernel_stack_addr` is renamed for the offset it holds, which + closes `issues/kernel/the-kernel-reserves-its-stack-offset-as-a-physical-region.md`. + x86's `_start` reads by `const offset_of!`, and the hand offset asserts + go. The layout identity changes. This stage is an ABI change. + - `toyos-update`'s slot table takes `toyos-gpt`'s CRC32 and loses its own. + + **Exit**: `bootloader/Cargo.toml` names no `uefi-services`. A guest test + boots an image whose signed kernel is no ELF, and finds the loader's panic + in `loader.log` and a reset; under `uefi::helpers`' handler it fails. The + tests that read `KernelArgs` pass: `boot_from_power_on`, + `bar_placement_is_proven`, `root_withheld_refused`, `blackbox_*` and + `update_*`. + +5. **Tries and a good flag per slot, decided on the host.** Each slot in the + table carries a priority, the tries it has left and a good flag, in the + word the format reserves; the table's format rises. They replace the + attempts file (`attempt.rs`, `toyos-update/src/record.rs` and `main.rs`' + accounting), the dead-slot retry and `slot::proven`. + - A freshly built image's slot A starts untried with 3 tries. An install + writes its slot untried with 3 tries and the good flag clear, whatever + the slot held before. + - A pass boots the highest-priority slot that verifies and is good or has + tries left. A slot out of tries and never good is not booted again. + - The loader spends a try of an untried slot before it hands over. That + write, with sealing `ARMED`, is the pass's last, after every refusal + `start_kernel` can make, so a loader refusal is never booked as the + image's. Where the decrement cannot be persisted, that slot is not booted. + - Where no slot can boot, the loader says so on the panel and in + `loader.log`, and powers the machine off. + - The running system marks its slot good once the image's health gate is + up: the servers its `system.toml` names, which for the shipping image are + `sshd` and `update`'s claims. Init starting is not the gate. + - The floor rises on a pass that boots a good slot, to the security version + of the signed header that pass verified, never to a version the table + names. + - A one-shot request (`update --once`, `update --boot-first`) is cleared + and saved before it is acted on, and ignored where the save fails. + - `update --once` asks for the idle slot once and leaves it at priority 0, + so its good flag is never read. The kept slot boots after it, and no floor + rises for it. + - `update --boot-first` writes the loader's own `HD(…)/File(…)` entry and + puts it first. Once stage 7 deletes `bootnext.rs`, it is the only + boot-variable write. + + The decisions are pure `toyos-update` functions: + `verify(signed, kernel, cmdline, root, key, floor) -> Result` + and `pass::decide(inputs) -> Decision` over the table, the verified slots, + the floor, the request and whether the last write persisted. The loader does + the I/O and carries the decision out. + + **Exit**: `decide`'s host tests hold each rule above, and `verify`'s hold + each refusal; deleting the cmdline hash check or `policy::admits` fails a + host test. The `update_*` tests and `hang_bounded_by_the_stick`, rewritten + for tries, pass. The hang tests boot an image that never reaches its health + gate, instead of killing QEMU on `LOADER_LAST_LINE`. #539's `update_boot_first_puts_the_loader_first` passes, and so do its host tests of the entry rules that survive. Negative controls: - - Skipping the countdown makes `update_hang_kills_an_unproven_image` fail. - - Raising the floor on a slot not yet good makes an `update_*` test fail. - - Acting on the request before taking it off the table makes - `update_boot_first_puts_the_loader_first` fail. - - Oracles: the slot table the host reads off the disk after each boot, and - OVMF's boot manager booting the entry the loader wrote. - -5. **The T14 bench.** - - It is built from #539's `src/metalbench.rs`, the bench path of - `src/metal.rs`, `tests/common/bench.rs`, `tests/bench*case` and - `--bench-image`. `--via-ubuntu` stays as the old path. - - The loader names no hash of its own file. The bench reads the file off - the ESP. + - Skipping the countdown fails `update_hang_kills_an_unproven_image`. + - Raising the floor on a slot not yet good fails an `update_*` test. + - An install that leaves the good flag set fails + `update_over_a_good_slot_starts_it_untried`. It updates into B and boots B + to good, then updates over A, which was good, with a kernel that hangs; A + spends its tries and B boots. + - Raising the floor to the table's version fails + `update_floor_is_the_headers`, which plants a table version above the + header's and reads the floor at the header's. + - Booting after a failed decrement fails a host test of `decide`. + - Acting on a request before taking it off the table fails + `update_boot_first_puts_the_loader_first`. + + Oracles: Android's A/B and Fuchsia libabr's rules, which these are; the + floor the host reads out of the vars store; the slot table the host reads + off the disk after each boot; and OVMF's boot manager booting the entry the + loader wrote. + +6. **The T14 bench.** + - It is built from #539's pieces that stage 6 takes above, and the bench + path of `src/metal.rs`. `--via-ubuntu` stays as the old path. + - The bench reads the loader's file off the ESP. - The tested pass's `loader.log` is kept as `loader-previous.log` by a rename (`SetInfo`), not by #539's copy. - It runs over the cable that @@ -134,26 +223,60 @@ Each stage lands on its own, in this order. **Exit**: `bench_loop_drives_a_toyos_machine` passes in QEMU. On the T14, with Ubuntu never started, three things hold: a kernel change boots; a slot - with a flipped byte, no signature or a lower version is refused and the - other boots; and a boot that dies falls back on its own. + with a flipped byte, no signature or a lower security version is refused + and the other boots; and a slot that dies falls back on its own. -6. **Ubuntu leaves the loop.** Delete `toyos-metal`'s `--via-ubuntu` path and +7. **Ubuntu leaves the loop.** Delete `toyos-metal`'s `--via-ubuntu` path and `bootloader/src/bootnext.rs`. After a reset the firmware comes back to the - loader because ToyOS's entry is first (stage 4). + loader because ToyOS's entry is first (stage 5). **Exit**: no path in `src/metal*.rs` reaches Ubuntu. On the T14, a panic's reset reaches the loader with no `BootNext` set. -7. **The crash report belongs to the kernel.** The loader hands the last +8. **The kernel arms the TCO before anything unbounded.** The loader's arm is + the only bound today from `ExitBootServices` to `deadline::start` on + `boot-deadline=` boots, and to `arch::watchdog::init` on the others: + through `mm::init`, ACPI, interrupts, HPET calibration, the RTC read, PCI + enumeration, `pcidev::publish` and `iommu::init`. + - The kernel arms as its first act, before `mm::init`: `toyos_acpi::ecam_base` + and the bus-0 scan `bootloader/src/watchdog.rs` does, through a boot map + that maps bus 0's ECAM window. What stays unbounded is the loader's code + from `ExitBootServices` to the jump. + - The first feed comes a whole early boot after the arm. The kernel logs + the span from arm to first feed, and it is measured under the bound on + q35 and on the T14. + - The kernel takes the read-back that + `issues/hardware/an-armed-tco-has-never-reset-the-t14.md`'s exit needs: + `TCO_RLD` at the arm and at the first feed (the counting read, with no + stall), `TCO1_STS`, and `no_reboot`, `tco_lock` and `timeout`. That issue's + exit reads the kernel's log in place of `loader.log`. + - Deleted: `bootloader/src/watchdog.rs`, `arch::pio`, the kernel's + on-arrival read with `ARMED_ON_ARRIVAL` and `UNARMED_ON_ARRIVAL`, and + `tests/common/power.rs`' `loader_armed`, `armed_on_arrival` and + `watchdog_quiet`'s loader half. `watchdog_armed` judges the kernel's + read-back, and `loader_watchdog_arms` becomes the kernel's row. + + **Exit**: `bootloader/src` holds no TCO access. On q35, `watchdog_armed` + passes on the kernel's lines (counting, `no_reboot=0`, `timeout=0`), and + `watchdog_resets` passes. A boot that hangs right after the arm, before + `mm::init`, is reset by the TCO; moving the arm back after `pci::enumerate` + makes that test fail. + +9. **The crash report belongs to the kernel.** The loader hands the last boot's record to the kernel instead of decoding it. The kernel logs it, and - `logd` carries it to `/log`. The report pass goes, taking with it: - - `end_this_pass`; - - the chain constants; - - `loaderlog`'s chain lines; - - `armed_at`'s `GetTime`; - - everything else in `bootloader/src/blackbox.rs` except the claim and the - arm; - - the chained-pass item of `issues/hardware/the-t14-boots-toyos-unattended.md`. + `logd` carries it to `/log`. + - The kernel takes `harvest`'s check that a record belongs to the stick + that wrote it (the log partition's GUID). + - The kernel's report carries the record's byte count and + `DROPPED_OPENS_WITH`'s count. + - The report pass goes, taking with it `end_this_pass`, the chain + constants, `loaderlog`'s chain lines, `armed_at`'s `GetTime`, everything + else in `bootloader/src/blackbox.rs` except the claim and the arm, and the + chained-pass item of `issues/hardware/the-t14-boots-toyos-unattended.md`. + - `ENDS_AT_CHAIN` goes, so this stage rewrites the exit of + `issues/diagnostics/a-wedged-reports-newest-records-were-cut-by-the-loaders-own-log-file.md`: + a `deadlinewedge` rerun whose `/log` carries the `WEDGED` report with its + byte count and drop line. **Exit**: `blackbox_panic_chain` reads the previous boot's panic out of `/log`, with none of it in `loader.log`, and every pass boots a kernel. diff --git a/issues/boot-media/the-machine-updates-itself-without-ubuntu.md b/issues/boot-media/the-machine-updates-itself-without-ubuntu.md index 0f4efc82f7a..d4559130bc3 100644 --- a/issues/boot-media/the-machine-updates-itself-without-ubuntu.md +++ b/issues/boot-media/the-machine-updates-itself-without-ubuntu.md @@ -39,9 +39,13 @@ stdin — and the machine installs nothing the owner did not sign. ## Stage 2 — the T14 installs ToyOS on its NVMe The machine boots the stick and installs onto its own NVMe, then takes -`ssh t14 update < image` for every change after. Taking Ubuntu out of -`toyos-metal`'s loop is -`issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md`'s. +`ssh t14 update < image` for every change after. It waits on stage 5 of +`issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md`, +whose `update --boot-first` puts the NVMe loader's entry first; taking Ubuntu +out of `toyos-metal`'s loop is that track's too. + +**Exit**: with the stick pulled, the T14 boots ToyOS off its NVMe, and a +kernel change sent with `ssh t14 update < image` boots at the next reset. ## Later stages — the end state, which no earlier stage may block From 0a76929ae0d81e735f38578e9b50b7ba9ed380b7 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 28 Sep 2026 21:47:08 +0200 Subject: [PATCH 4/6] Round 2 of #582: the firmware rule names the handover, and the track's controls can fail CLAUDE.md's Firmware paragraph now reads as the orchestrator ruled: the kernel calls no UEFI service, and every UEFI call is the loader's, before ExitBootServices. The loader's GetVariable, SetVariable, GetTime and ResetSystem come before the handover, so the old wording was false of it. The track: - Stage 1 matches #583 at 8565cc59: KernelArgs::layout (0x5459_0001), kernel_args_layout_refused with the loader-writes-no-layout actuator, the probe's realtime=, and the kernel's IA32_TSC_ADJUST line. No test fails without that line, and the stage says so. - Stage 3 drops "why nothing is weaker". A security version admits an older build the same key signed at that version. The floor issue records that as the owner's accepted cost. Raising the version is a reviewed PR that edits one constant and names the security fix. The loader deletes the build-time ToyOSImageFloor- variables instead of leaving them behind. - Stage 4's panic handler writes loader.log and powers off, never resets. Its test panics on a floor planted in 9 bytes, a failure the machine causes. KernelArgs' layout word rises to 0x5459_0002, and kernel_args_last_layout_refused fails if it does not. - Stage 5 refuses a signed kernel the loader cannot load inside verify, so the other slot boots instead of the pass bricking the machine. An install's priority rises above the kept slot's. update --good, run by init at the health gate under the slots claim, writes the good flag, and an image without that claim is never good. Each rule gets a named guest control: update_floor_waits_for_good, update_readonly_stick_boots_nothing (red under `let persisted = true;`) and update_unloadable_kernel_boots_the_other_slot. The slot-table oracle is decoded without production code. - Every #539 piece the review listed is placed or deleted. The #539-only issue names and the stack-offset closure (#584's) are gone. Co-Authored-By: Claude Opus 5.5 --- CLAUDE.md | 2 +- ...i-rollback-floor-is-a-firmware-variable.md | 5 + ...oes-only-what-must-precede-the-handover.md | 195 +++++++++++------- 3 files changed, 130 insertions(+), 72 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 2ff922fb934..08f5b49c491 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,7 +50,7 @@ A subdirectory `CLAUDE.md` loads when a file in that subtree is `Read`, and not **CPU state** — a CPU's control registers come from one declaration, applied by the BSP and by every AP and asserted on each; no read-modify-write decides what either holds. -**Firmware** — ToyOS calls no UEFI runtime service; every UEFI call ToyOS makes is the loader's. +**Firmware** — the kernel calls no UEFI service; every UEFI call ToyOS makes is the loader's, before ExitBootServices. **Input** — the kernel delivers key *transitions*, never what one types; a surface turns one into the other. Translation, layouts, dead keys and escape sequences live in userland, one translator per surface. diff --git a/issues/boot-media/the-anti-rollback-floor-is-a-firmware-variable.md b/issues/boot-media/the-anti-rollback-floor-is-a-firmware-variable.md index 87ffd4ff50f..5a89184a74b 100644 --- a/issues/boot-media/the-anti-rollback-floor-is-a-firmware-variable.md +++ b/issues/boot-media/the-anti-rollback-floor-is-a-firmware-variable.md @@ -34,6 +34,11 @@ to the disk can lower it, because the variable is unreachable once refuses rather than boot with no floor, so nothing boots until the firmware's variables are reset. OVMF deletes one made with time-based authenticated write, so no guest reaches that refusal. +- **an older image at the floor's security version** — once stage 3 of + `the-loader-does-only-what-must-precede-the-handover.md` makes the floor + count one, any build the same key signed at that version boots, so a hole + whose fix did not raise the version stays open, which is the cost the + owner accepted for a security version. `/system/bin/update`'s own check — newer than the running image — reads the versions in the slot table, which is on the disk and is advisory; the loader's diff --git a/issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md b/issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md index 9f61bc9b024..0b705da1306 100644 --- a/issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md +++ b/issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md @@ -10,7 +10,8 @@ The owner's bounds: - the loader does only what must precede the handover; - the hardware clock keeps UTC; -- ToyOS calls no UEFI runtime service (root `CLAUDE.md`); +- the kernel calls no UEFI service, and every UEFI call ToyOS makes is the + loader's, before `ExitBootServices` (root `CLAUDE.md`); - ToyOS ends at least as secure as it starts; - the anti-rollback floor counts a signed security version, raised only by a release that fixes a security hole. @@ -18,21 +19,33 @@ The owner's bounds: PR #539 does not land. Its pieces: - stage 2 takes `toyos_update::entry::partition` and its two host tests; -- stage 5 takes the rest of `toyos_update::entry` and its host tests, +- stage 5 takes the rest of `toyos_update::entry` and its host tests; + `slots::Request` with its once slot and its boot-order word; + `bootloader/src/request.rs`, which takes a request off the table before + acting on it; `bootloader/src/bootvars.rs`' own entry and `BootOrder` + write, with `arch::REMOVABLE_PATH`, the file that entry names; `update --boot-first`, `update --once` and - `update_boot_first_puts_the_loader_first`; + `update_boot_first_puts_the_loader_first`, which also takes + `update_boot_next_boots_the_entry_once`'s read-only half; the harness's + `stick_readonly`; and `update_trial_writes_nothing_of_the_kept_slot`, + rewritten for priorities; - stage 6 takes the bench: `src/metalbench.rs`, `tests/common/bench.rs`, - `tests/bench*case`, `--bench-image`, toybox `date`, `Ssh::probe` and + `tests/bench*case`, `--bench-image` with `build::bench_image`, + `src/image.rs`' `update_of`, `tests/common/metal.rs`' `Reach`, `stage` and + bench `invocation`, `--metal-via-ubuntu`, toybox `date`, `Ssh::probe` and `Ssh::fetch` with `ssh-client-host`'s `probe` and `fetch`, `build::AUTHORIZED_ON_ROOT`, and `src/bootlog.rs`' `LOADER_PREVIOUS_LOG`, `MOUNTED_FROM_MEMORY` and `BOOT_PARAMETER`; -- deleted: `update --boot-next ` with `Guid::parse`, its only reader; the - panic handler's fall to the next boot entry, with the two issues it filed - (`a-failed-pass-can-fall-to-an-entry-the-firmware-never-boots-from-its-order` - and `the-loaders-power-off-with-no-entry-behind-it-is-proven-by-nothing`); - the `bootvars::state` line; `SLOT_ONCE`, because stage 4's `KernelArgs` - carries a once boot as a field; `LOADER_IS`, because the loader names no hash - of its own file. +- deleted: `update --boot-next ` with `slots::Next::Esp`, `Guid::parse`, + `bootvars.rs`' `BootNext` and entry-after-its-own writes, and + `update_boot_next_boots_the_entry_once`'s other half; the panic handler's + fall to the next boot entry, with the two issues #539 filed about it, + `update_no_slot_boots_the_recovery_stick`, and the harness's + `recovery_stick` and `RECOVERY_STICK_*`; the `bootvars::state` line; + `policy::order`'s once and `policy::told`, whose rules `pass::decide` takes; + `record.rs`' `once`, with the attempts file; `SLOT_ONCE`, because stage 4's + `KernelArgs` carries a once boot as a field; `LOADER_IS`, because the + loader names no hash of its own file. Each stage lands on its own, in this order. @@ -44,22 +57,27 @@ Each stage lands on its own, in this order. `clock::utc_secs`. - `rootbridge.rs`' hex dump goes, with `toyos-acpi`'s `Walk::bytes`. - Ten loader and `toyos-update` comments go, each named in #583's commits. - - `KernelArgs` carries a layout identity the kernel checks first. `update` - never replaces the ESP loader, so a slot's kernel can meet a loader built - to another layout; it is refused by name. - - The kernel reads `IA32_TSC_ADJUST` beside its power-on report. aarch64's - `counter_origin` is deleted: the kernel reads `CNTFRQ_EL0` itself. + - `KernelArgs::layout`, at offset 164, carries `LAYOUT` (`0x5459_0001`). + The kernel compares it right after arming the panel and before + `blackbox::arm`, and refuses any other value by name. `update` never + replaces the ESP loader, so a slot's kernel can meet a loader built to + another layout. Every field before the word keeps its offset in every + layout. + - The kernel logs `boot: IA32_TSC_ADJUST` beside its power-on report, and + the loader no longer reads it. aarch64's `counter_origin` is deleted. - The loader's TCO arm stays until stage 8. - This stage is an ABI change. - **Exit**: `bootloader/src` holds no zone and no `counter_origin`. - `wall_clock_utc` judges the log file's name, its FAT stamp, the probe's - `local=` and `SYS_CLOCK_EPOCH` against the `-rtc base=` instant. It fails - under a kernel that reads the RTC as local time two hours east, and under - `SYS_CLOCK_REALTIME` patched to `utc_secs + 7200`. A mixed pair, the loader - at the base and the kernel at the head, is refused by the identity check's - name. `boot_from_power_on` fails where the kernel's `IA32_TSC_ADJUST` line is - missing. + **Exit**: the loader applies no zone, and `bootloader/src` holds no + `counter_origin`. `wall_clock_utc` stages a firmware zone of −120 minutes + in PcRtc's `RTC` variable. It judges the log file's name, its FAT stamp, + the probe's `realtime=` and `SYS_CLOCK_EPOCH` against the `-rtc base=` + instant. It fails under a kernel that reads the RTC as local time two + hours east, and under `SYS_CLOCK_REALTIME` patched to `utc_secs + 7200`. + `kernel_args_layout_refused` boots with the `loader-writes-no-layout` + actuator, which makes the loader write 0. It finds the kernel's refusal + naming both words, and no `black box:` record before it. No test fails + where the `IA32_TSC_ADJUST` line is missing. 2. **One boot disk.** - `toyos_update::entry::partition` is the one HARDDRIVE rule, taken by @@ -81,13 +99,18 @@ Each stage lands on its own, in this order. The oracle is UEFI 2.10 §10.3.5.1. 3. **One floor per key, on a security version.** - - The signed header's `version` is the security version: a number the tree - holds, raised only by the change that fixes a security hole. + - The signed header's `version` is the security version: one constant in + the tree. It is raised by a reviewed PR that edits that constant alone, + and the reviewer checks that the PR names the security fix it ships; + this stage adds that check to `.claude/agents/reviewer.md`. `image::version_now` goes. The header's `FORMAT` rises, so a header carrying a build time is refused by name. - `floor::Scope` goes. Every loader keeps one floor per signing key, named - for the key alone and under a name no build-time floor carries. `stale` + for the key alone, under a prefix no build-time floor carries. `stale` and its deletions go. + - The loader deletes every variable under the build-time prefix + `ToyOSImageFloor-`. Only a loader older than this stage reads one, and a + loader firmware boots in place of this one is already outside the floor. - No loader deletes another key's floor, because a deletion lowers it. The store keeps one 8-byte variable per key that has booted the machine. - `policy::installable` admits an image at or above the running image's @@ -97,48 +120,53 @@ Each stage lands on its own, in this order. the per-image floor in stage 1 of `issues/boot-media/the-machine-updates-itself-without-ubuntu.md`. The floor issue's TPM 2.0 NV counter stays its exit: `TPM2_NV_Increment` moves one - step, and so does a security version. - - Why nothing is weaker: - - The floor refuses every image below the highest security version a boot - has proven, which is every image the owner has declared holed. - - A throwaway key's floor no longer resets when the log partition's GUID - is written. - - Only a loader, before the handover, writes a floor. + step, and so does a security version. The cost the owner accepted, an + older image admitted at the floor's security version, is on the floor + issue's list. **Exit**: the guest tests run the scope the owner's machine runs. `update_floor_is_the_images_own` checks three things. This key's floor, planted above the image's security version, refuses the image under a fresh log GUID. An older build at the floor's security version boots. - Another key's floor holds the image to nothing and stays. Putting the log - GUID back into the name makes it fail. The oracle is the vars store as the - host reads it (`vars::live` in `tests/common/update.rs`): each floor's name - and its UEFI 2.10 §8.2 attributes. + Another key's floor holds the image to nothing and stays, and a planted + `ToyOSImageFloor-` variable is gone. Putting the log GUID back into the + name makes it fail. The oracle is the vars store as the host reads it + (`fwvars::live` in `tests/common/fwvars.rs`): each floor's name and its + UEFI 2.10 §8.2 attributes. 4. **The loader on current `uefi`, sound, with a typed handover.** - `uefi` and `uefi-raw` move to their current releases, and `uefi-services` goes. The unsafe `BlockIO` media cast in `rootimage.rs` goes with the old layout. - The loader's own `#[panic_handler]` writes the panic through `loaderlog`, - then resets the machine. + then powers the machine off. It never resets: a panic with a fixed cause + would reset into itself. - `alloc_kernel_memory` stops building a `Vec` over a 2 MiB-aligned allocation. Both relocation unsafes go. `blackbox::Page` is not `Copy`, so `bytes()` mints no second `&'static mut`. - `KernelArgs` is typed: `repr(C)` sub-structs, and `#[repr(C, u32)]` enums for the optional parts in place of `u32` presence flags and zero - sentinels. The slot booted, the slot refused and why, and the black-box - page are fields, not `boot-slot=`, `slot-refused=` and `blackbox=` text; - `params.rs`' last-one-wins and kernel `main.rs`' branch for a format never - emitted go. `kernel_stack_addr` is renamed for the offset it holds, which - closes `issues/kernel/the-kernel-reserves-its-stack-offset-as-a-physical-region.md`. - x86's `_start` reads by `const offset_of!`, and the hand offset asserts - go. The layout identity changes. This stage is an ABI change. + sentinels. Every field before the layout word keeps its offset. The + slot booted and why (the one the table chose, the other one after a + refusal and its reason, or once on the running system's request) and the + black-box page are fields, not `boot-slot=`, `slot-refused=` and + `blackbox=` text. `params.rs`' last-one-wins and kernel `main.rs`' branch + for a format never emitted go. `kernel_stack_addr` is renamed for the + offset it holds. x86's `_start` reads by `const offset_of!`, and the hand + offset asserts go. + - `LAYOUT` rises to `0x5459_0002`. `LAST_LAYOUT` pins `0x5459_0001` as a + literal, and the `loader-writes-the-last-layout` actuator makes the + loader write it. This stage is an ABI change. - `toyos-update`'s slot table takes `toyos-gpt`'s CRC32 and loses its own. - **Exit**: `bootloader/Cargo.toml` names no `uefi-services`. A guest test - boots an image whose signed kernel is no ELF, and finds the loader's panic - in `loader.log` and a reset; under `uefi::helpers`' handler it fails. The - tests that read `KernelArgs` pass: `boot_from_power_on`, + **Exit**: `bootloader/Cargo.toml` names no `uefi-services`. + `loader_panic_powers_off` plants this key's floor in 9 bytes. It finds the + loader's panic in `loader.log`, and QEMU reports `guest-shutdown`. It fails + under `uefi::helpers`' handler, which writes no `loader.log`. + `kernel_args_last_layout_refused` boots with + `loader-writes-the-last-layout` and finds the kernel's refusal naming both + words before any `black box:` record. Leaving `LAYOUT` at `0x5459_0001` + makes it fail. The tests that read `KernelArgs` pass: `boot_from_power_on`, `bar_placement_is_proven`, `root_withheld_refused`, `blackbox_*` and `update_*`. @@ -148,10 +176,13 @@ Each stage lands on its own, in this order. attempts file (`attempt.rs`, `toyos-update/src/record.rs` and `main.rs`' accounting), the dead-slot retry and `slot::proven`. - A freshly built image's slot A starts untried with 3 tries. An install - writes its slot untried with 3 tries and the good flag clear, whatever - the slot held before. + writes its slot untried with 3 tries, the good flag clear and a priority + above the kept slot's, whatever the slot held before. - A pass boots the highest-priority slot that verifies and is good or has tries left. A slot out of tries and never good is not booted again. + - A signed kernel the loader cannot load (not an ELF, another machine's, or + carrying a relocation the loader does not apply) is refused inside + `verify`, like a bad signature, and the pass falls to the other slot. - The loader spends a try of an untried slot before it hands over. That write, with sealing `ARMED`, is the pass's last, after every refusal `start_kernel` can make, so a loader refusal is never booked as the @@ -159,8 +190,17 @@ Each stage lands on its own, in this order. - Where no slot can boot, the loader says so on the panel and in `loader.log`, and powers the machine off. - The running system marks its slot good once the image's health gate is - up: the servers its `system.toml` names, which for the shipping image are - `sshd` and `update`'s claims. Init starting is not the gate. + up. Init runs `update --good` once every service the image's + `system.toml` names is up; it holds the `slots` claim's table and writes + the running slot's good flag and nothing else. Init starting is not the + gate. + - An image whose `system.toml` grants no program the `slots` claim is never + good, so each boot spends a try and it boots three times. Today only + `system.toml` and `tests/updatecase/system.toml` grant it. + - `update_refusals_boot_the_other_slot` boots slot A five times and + `update_grant_refuses_a_stray_partition` four. Both run `updatecase`, and + each boot waits for the good flag before the guest is dropped. Every + other test hands one image's kernel at most three times. - The floor rises on a pass that boots a good slot, to the security version of the signed header that pass verified, never to a version the table names. @@ -169,9 +209,11 @@ Each stage lands on its own, in this order. - `update --once` asks for the idle slot once and leaves it at priority 0, so its good flag is never read. The kept slot boots after it, and no floor rises for it. - - `update --boot-first` writes the loader's own `HD(…)/File(…)` entry and - puts it first. Once stage 7 deletes `bootnext.rs`, it is the only - boot-variable write. + - `update --boot-first` writes only the request. The loader writes its own + `HD(…)/File(…)` entry and puts it first. Once stage 7 deletes + `bootnext.rs`, that is the only boot-variable write. + - This stage deletes the floor issue's "a floor that never rises" bullet + and its exit's second half. The decisions are pure `toyos-update` functions: `verify(signed, kernel, cmdline, root, key, floor) -> Result` @@ -189,22 +231,33 @@ Each stage lands on its own, in this order. Negative controls: - Skipping the countdown fails `update_hang_kills_an_unproven_image`. - - Raising the floor on a slot not yet good fails an `update_*` test. - - An install that leaves the good flag set fails - `update_over_a_good_slot_starts_it_untried`. It updates into B and boots B - to good, then updates over A, which was good, with a kernel that hangs; A - spends its tries and B boots. + - Raising the floor on any pass that boots a slot fails + `update_floor_waits_for_good`. It installs into B an image at the running + security version + 1 whose kernel hangs. B spends its tries and A boots, + and `fwvars::live` reads the floor at A's version. + - An install that leaves the good flag set, or its priority below the kept + slot's, fails `update_over_a_good_slot_starts_it_untried`. It updates + into B and boots B to good, then updates over A, which was good, with a + kernel that hangs. The slot table read after each pass shows A's kernel + booted three times, then B boots. - Raising the floor to the table's version fails `update_floor_is_the_headers`, which plants a table version above the header's and reads the floor at the header's. - - Booting after a failed decrement fails a host test of `decide`. + - `let persisted = true;` in the loader fails + `update_readonly_stick_boots_nothing`. It boots a fresh image off a + read-only stick: nothing boots, the no-bootable-slot line is on the + console, and QEMU reports `guest-shutdown`. + - Turning `verify`'s refusal of an unloadable kernel back into a panic + fails `update_unloadable_kernel_boots_the_other_slot`, which updates B + with a signed non-ELF kernel and finds A booted. - Acting on a request before taking it off the table fails `update_boot_first_puts_the_loader_first`. - Oracles: Android's A/B and Fuchsia libabr's rules, which these are; the - floor the host reads out of the vars store; the slot table the host reads - off the disk after each boot; and OVMF's boot manager booting the entry the - loader wrote. + Oracles: Android's A/B and Fuchsia libabr's rules, except that libabr boots + a slot whose decrement failed; the floor `fwvars::live` reads out of the + vars store; the slot table after each boot, decoded by the test from the + format's bytes and not by `toyos_update::slots::Table::decode`; and OVMF's + boot manager booting the entry the loader wrote. 6. **The T14 bench.** - It is built from #539's pieces that stage 6 takes above, and the bench @@ -226,9 +279,9 @@ Each stage lands on its own, in this order. with a flipped byte, no signature or a lower security version is refused and the other boots; and a slot that dies falls back on its own. -7. **Ubuntu leaves the loop.** Delete `toyos-metal`'s `--via-ubuntu` path and - `bootloader/src/bootnext.rs`. After a reset the firmware comes back to the - loader because ToyOS's entry is first (stage 5). +7. **Ubuntu leaves the loop.** Delete `toyos-metal`'s `--via-ubuntu` path, + `--metal-via-ubuntu` and `bootloader/src/bootnext.rs`. After a reset the + firmware comes back to the loader because ToyOS's entry is first (stage 5). **Exit**: no path in `src/metal*.rs` reaches Ubuntu. On the T14, a panic's reset reaches the loader with no `BootNext` set. From 8e382e0ce775e18feadec903af81015e2c2baa40 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 28 Sep 2026 22:11:04 +0200 Subject: [PATCH 5/6] Round 3 of #582: the prefix is pinned, the panic has its own line, and "up" is a signal - Stage 4 keeps an `offset_of!` assert for the layout word and every field before it, and the typed `KernelArgs` changes only fields after the word, so moving `layout` fails to build. - `loader_panic_powers_off` looks for the handler's own `loader: panicked at` line, and the refused-floor site stops writing its reason to `loader.log` before it panics. - Stage 5 defines the health gate: `[boot] up` names the services, and each writes one byte on a `ready` pipe init endows, since the tree has no readiness signal. `update_dead_service_is_never_good` fails when init marks good right after spawning. - Stage 3 quotes the line it adds to the review prompt, and lists deleting the floor issue's "newer than the running image" line. - Any failure after `verify` in `start_kernel` that the image causes is a refusal of that slot; the unloadable-kernel test also finds the non-ELF refusal of B; the once variant and its `LAYOUT` bump move to stage 5; `Rig::update` is the path that signs at +1. - Removed: "at least as secure as it starts", and the aarch64 RTC header's `GetTime` clause. Co-Authored-By: Claude Opus 5.5 --- ...oes-only-what-must-precede-the-handover.md | 102 +++++++++++------- kernel/src/arch/aarch64/rtc.rs | 4 +- 2 files changed, 65 insertions(+), 41 deletions(-) diff --git a/issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md b/issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md index 0b705da1306..adca0de53b7 100644 --- a/issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md +++ b/issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md @@ -12,7 +12,6 @@ The owner's bounds: - the hardware clock keeps UTC; - the kernel calls no UEFI service, and every UEFI call ToyOS makes is the loader's, before `ExitBootServices` (root `CLAUDE.md`); -- ToyOS ends at least as secure as it starts; - the anti-rollback floor counts a signed security version, raised only by a release that fixes a security hole. @@ -43,7 +42,7 @@ PR #539 does not land. Its pieces: `update_no_slot_boots_the_recovery_stick`, and the harness's `recovery_stick` and `RECOVERY_STICK_*`; the `bootvars::state` line; `policy::order`'s once and `policy::told`, whose rules `pass::decide` takes; - `record.rs`' `once`, with the attempts file; `SLOT_ONCE`, because stage 4's + `record.rs`' `once`, with the attempts file; `SLOT_ONCE`, because stage 5's `KernelArgs` carries a once boot as a field; `LOADER_IS`, because the loader names no hash of its own file. @@ -100,11 +99,17 @@ Each stage lands on its own, in this order. 3. **One floor per key, on a security version.** - The signed header's `version` is the security version: one constant in - the tree. It is raised by a reviewed PR that edits that constant alone, - and the reviewer checks that the PR names the security fix it ships; - this stage adds that check to `.claude/agents/reviewer.md`. - `image::version_now` goes. The header's `FORMAT` rises, so a header - carrying a build time is refused by name. + the tree, `toyos_update::SECURITY_VERSION`. It is raised by a reviewed PR + that edits that constant alone. This stage adds this line to + `.claude/agents/reviewer.md`'s "What to look for": + + ```markdown + - **Security version.** A branch that raises `toyos_update::SECURITY_VERSION` changes nothing else and names in its body the security fix it ships, or it is a BLOCKER. + ``` + + `image::version_now` goes, and `Plan::version` defaults to the constant. + The header's `FORMAT` rises, so a header carrying a build time is + refused by name. - `floor::Scope` goes. Every loader keeps one floor per signing key, named for the key alone, under a prefix no build-time floor carries. `stale` and its deletions go. @@ -115,8 +120,9 @@ Each stage lands on its own, in this order. store keeps one 8-byte variable per key that has booted the machine. - `policy::installable` admits an image at or above the running image's and the idle slot's security version. - - This stage deletes the throwaway-key bullet of - `issues/boot-media/the-anti-rollback-floor-is-a-firmware-variable.md` and + - This stage deletes the throwaway-key bullet and the + "`/system/bin/update`'s own check — newer than the running image" line of + `issues/boot-media/the-anti-rollback-floor-is-a-firmware-variable.md`, and the per-image floor in stage 1 of `issues/boot-media/the-machine-updates-itself-without-ubuntu.md`. The floor issue's TPM 2.0 NV counter stays its exit: `TPM2_NV_Increment` moves one @@ -138,22 +144,25 @@ Each stage lands on its own, in this order. - `uefi` and `uefi-raw` move to their current releases, and `uefi-services` goes. The unsafe `BlockIO` media cast in `rootimage.rs` goes with the old layout. - - The loader's own `#[panic_handler]` writes the panic through `loaderlog`, - then powers the machine off. It never resets: a panic with a fixed cause - would reset into itself. + - The loader's own `#[panic_handler]` writes `loader: panicked at + :: ` through `loaderlog`, then powers the machine + off. It never resets: a panic with a fixed cause would reset into itself. + The refused-floor site stops writing its reason to `loader.log` before + its `panic!`: the handler writes it. - `alloc_kernel_memory` stops building a `Vec` over a 2 MiB-aligned allocation. Both relocation unsafes go. `blackbox::Page` is not `Copy`, so `bytes()` mints no second `&'static mut`. - - `KernelArgs` is typed: `repr(C)` sub-structs, and `#[repr(C, u32)]` enums - for the optional parts in place of `u32` presence flags and zero - sentinels. Every field before the layout word keeps its offset. The - slot booted and why (the one the table chose, the other one after a - refusal and its reason, or once on the running system's request) and the - black-box page are fields, not `boot-slot=`, `slot-refused=` and - `blackbox=` text. `params.rs`' last-one-wins and kernel `main.rs`' branch - for a format never emitted go. `kernel_stack_addr` is renamed for the - offset it holds. x86's `_start` reads by `const offset_of!`, and the hand - offset asserts go. + - The layout word and every field before it keep their offsets, each + pinned by a compile-time `offset_of!` assert, so moving `layout` fails to + build. That prefix stays flat. After the word, `KernelArgs` is typed: + `repr(C)` sub-structs, and `#[repr(C, u32)]` enums for the optional parts + in place of `u32` presence flags and zero sentinels; a field the typing + changes moves after the word. The slot booted and why (the one the table + chose, or the other one after a refusal and its reason) and the black-box + page are fields, not `boot-slot=`, `slot-refused=` and `blackbox=` text. + `params.rs`' last-one-wins and kernel `main.rs`' branch for a format + never emitted go. `kernel_stack_addr` is renamed for the offset it + holds. x86's `_start` reads by `const offset_of!`. - `LAYOUT` rises to `0x5459_0002`. `LAST_LAYOUT` pins `0x5459_0001` as a literal, and the `loader-writes-the-last-layout` actuator makes the loader write it. This stage is an ABI change. @@ -161,12 +170,14 @@ Each stage lands on its own, in this order. **Exit**: `bootloader/Cargo.toml` names no `uefi-services`. `loader_panic_powers_off` plants this key's floor in 9 bytes. It finds the - loader's panic in `loader.log`, and QEMU reports `guest-shutdown`. It fails - under `uefi::helpers`' handler, which writes no `loader.log`. + handler's `loader: panicked at bootloader/src/` line in `loader.log`, which + no other code writes, and QEMU reports `guest-shutdown`. It fails under + `uefi::helpers`' handler, which writes no `loader.log`. `kernel_args_last_layout_refused` boots with `loader-writes-the-last-layout` and finds the kernel's refusal naming both words before any `black box:` record. Leaving `LAYOUT` at `0x5459_0001` - makes it fail. The tests that read `KernelArgs` pass: `boot_from_power_on`, + makes it fail. Moving `layout` after `root_read_tsc` fails to build. The + tests that read `KernelArgs` pass: `boot_from_power_on`, `bar_placement_is_proven`, `root_withheld_refused`, `blackbox_*` and `update_*`. @@ -180,20 +191,24 @@ Each stage lands on its own, in this order. above the kept slot's, whatever the slot held before. - A pass boots the highest-priority slot that verifies and is good or has tries left. A slot out of tries and never good is not booted again. - - A signed kernel the loader cannot load (not an ELF, another machine's, or - carrying a relocation the loader does not apply) is refused inside - `verify`, like a bad signature, and the pass falls to the other slot. + - Any failure after `verify` in `start_kernel` that the image causes is a + refusal of that slot, like a bad signature, and the pass falls to the + other slot. - The loader spends a try of an untried slot before it hands over. That write, with sealing `ARMED`, is the pass's last, after every refusal `start_kernel` can make, so a loader refusal is never booked as the image's. Where the decrement cannot be persisted, that slot is not booted. - Where no slot can boot, the loader says so on the panel and in `loader.log`, and powers the machine off. - - The running system marks its slot good once the image's health gate is - up. Init runs `update --good` once every service the image's - `system.toml` names is up; it holds the `slots` claim's table and writes - the running slot's good flag and nothing else. Init starting is not the - gate. + - The health gate: `system.toml`'s `[boot] up` names the services that + make the image up. The tree has no readiness signal (a swap's probation + only asks whether a process still runs), so this stage adds the smallest + one: init endows each named service the write end of a pipe under the + label `ready`, and the service writes one byte to it once it serves. A + read end that closes with no byte is a service that never came up. + - Init runs `update --good` once every service `[boot] up` names has + written its byte; `update` holds the `slots` claim's table and writes the + running slot's good flag and nothing else. - An image whose `system.toml` grants no program the `slots` claim is never good, so each boot spends a try and it boots three times. Today only `system.toml` and `tests/updatecase/system.toml` grant it. @@ -208,7 +223,9 @@ Each stage lands on its own, in this order. and saved before it is acted on, and ignored where the save fails. - `update --once` asks for the idle slot once and leaves it at priority 0, so its good flag is never read. The kept slot boots after it, and no floor - rises for it. + rises for it. `KernelArgs`' slot field gains the once variant, `LAYOUT` + rises to `0x5459_0003` and `LAST_LAYOUT` to `0x5459_0002`; this stage is + an ABI change. - `update --boot-first` writes only the request. The loader writes its own `HD(…)/File(…)` entry and puts it first. Once stage 7 deletes `bootnext.rs`, that is the only boot-variable write. @@ -233,8 +250,10 @@ Each stage lands on its own, in this order. - Skipping the countdown fails `update_hang_kills_an_unproven_image`. - Raising the floor on any pass that boots a slot fails `update_floor_waits_for_good`. It installs into B an image at the running - security version + 1 whose kernel hangs. B spends its tries and A boots, - and `fwvars::live` reads the floor at A's version. + security version + 1 whose kernel hangs, signed by + `tests/common/update.rs`' `Rig::update` at the version it is handed. B + spends its tries and A boots, and `fwvars::live` reads the floor at A's + version. - An install that leaves the good flag set, or its priority below the kept slot's, fails `update_over_a_good_slot_starts_it_untried`. It updates into B and boots B to good, then updates over A, which was good, with a @@ -247,9 +266,14 @@ Each stage lands on its own, in this order. `update_readonly_stick_boots_nothing`. It boots a fresh image off a read-only stick: nothing boots, the no-bootable-slot line is on the console, and QEMU reports `guest-shutdown`. - - Turning `verify`'s refusal of an unloadable kernel back into a panic - fails `update_unloadable_kernel_boots_the_other_slot`, which updates B - with a signed non-ELF kernel and finds A booted. + - Turning the refusal of an unloadable kernel back into a panic fails + `update_unloadable_kernel_boots_the_other_slot`, which updates B with a + signed non-ELF kernel, finds the loader's refusal of B naming a kernel + that is not an ELF, and finds A booted. + - Running `update --good` right after init spawns `[boot] start` fails + `update_dead_service_is_never_good`. It updates B with an image whose + `[boot] up` names a service that exits at start. B boots three times, + its good flag never set, and then A boots. - Acting on a request before taking it off the table fails `update_boot_first_puts_the_loader_first`. diff --git a/kernel/src/arch/aarch64/rtc.rs b/kernel/src/arch/aarch64/rtc.rs index 9a0c49c0ad3..a5caff16d4b 100644 --- a/kernel/src/arch/aarch64/rtc.rs +++ b/kernel/src/arch/aarch64/rtc.rs @@ -1,5 +1,5 @@ -//! The wall clock at boot: on an ACPI Arm machine, the UEFI runtime's -//! `GetTime` or a PL031 the tables name, the port's stage 6. +//! The wall clock at boot: on an ACPI Arm machine, a PL031 the tables name, +//! the port's stage 6. use core::fmt; From 8a9af8e19725ae5c9b1bf76c83c08b00a589de58 Mon Sep 17 00:00:00 2001 From: japabu Date: Mon, 28 Sep 2026 22:39:03 +0200 Subject: [PATCH 6/6] Round 4 of #582: LAYOUT is derived from KernelArgs' own size, not hand-picked Restates the track's LAYOUT/LAST_LAYOUT literals as `0x5459_0000 | size_of::()`, so a size change moves the word on its own and `kernel_args_last_layout_refused` catches it; names the remaining gap a same-size reorder or equal-size type swap leaves, closed by pinning every field's offset (not only the prefix's) and `assert!(LAYOUT != LAST_LAYOUT)`; states how stage 5's once variant, which keeps the struct's size, still moves the word and how the kernel refuses an unrecognized discriminant by name instead of reading it out of range; gives `update_dead_service_is_never_good`'s image a live service beside the dead one, so "any named service wrote its byte" is distinguished from "every one did"; has stage 5's PR list which `start_kernel` refusals are the image's; and drops the PR body's Unsure bullet on #583's pushed head, now stale. Co-Authored-By: Claude Opus 5.5 --- ...oes-only-what-must-precede-the-handover.md | 57 +++++++++++++------ 1 file changed, 40 insertions(+), 17 deletions(-) diff --git a/issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md b/issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md index adca0de53b7..9ae21c395e8 100644 --- a/issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md +++ b/issues/boot-media/the-loader-does-only-what-must-precede-the-handover.md @@ -56,11 +56,13 @@ Each stage lands on its own, in this order. `clock::utc_secs`. - `rootbridge.rs`' hex dump goes, with `toyos-acpi`'s `Walk::bytes`. - Ten loader and `toyos-update` comments go, each named in #583's commits. - - `KernelArgs::layout`, at offset 164, carries `LAYOUT` (`0x5459_0001`). - The kernel compares it right after arming the panel and before - `blackbox::arm`, and refuses any other value by name. `update` never - replaces the ESP loader, so a slot's kernel can meet a loader built to - another layout. Every field before the word keeps its offset in every + - `KernelArgs::layout`, at offset 164, carries `LAYOUT`: `0x5459_0000 | + size_of::() as u32`, so a size change moves the word with no + literal to remember. At this stage that is `0x5459_0000 | 1272`, the size + PR #583 asserts. The kernel compares it right after arming the panel and + before `blackbox::arm`, and refuses any other value by name. `update` + never replaces the ESP loader, so a slot's kernel can meet a loader built + to another layout. Every field before the word keeps its offset in every layout. - The kernel logs `boot: IA32_TSC_ADJUST` beside its power-on report, and the loader no longer reads it. aarch64's `counter_origin` is deleted. @@ -163,9 +165,17 @@ Each stage lands on its own, in this order. `params.rs`' last-one-wins and kernel `main.rs`' branch for a format never emitted go. `kernel_stack_addr` is renamed for the offset it holds. x86's `_start` reads by `const offset_of!`. - - `LAYOUT` rises to `0x5459_0002`. `LAST_LAYOUT` pins `0x5459_0001` as a - literal, and the `loader-writes-the-last-layout` actuator makes the - loader write it. This stage is an ABI change. + - The derived `LAYOUT` moves on its own once `size_of::()` + does. `LAST_LAYOUT` pins stage 1's derived value, `0x5459_0000 | 1272`, + as a literal, and the `loader-writes-the-last-layout` actuator makes the + loader write it. A size change moves the word, and + `kernel_args_last_layout_refused` catches it; a moved prefix field fails + the prefix `offset_of!` asserts above; a change that keeps the size and + lands after the word — a reorder, a type swap of equal size — moves + neither, so every field after the word keeps its own `offset_of!` assert + too, and `const _: () = assert!(LAYOUT != LAST_LAYOUT);` fails the build + on a same-size stage until it bumps the word itself. This stage is an ABI + change. - `toyos-update`'s slot table takes `toyos-gpt`'s CRC32 and loses its own. **Exit**: `bootloader/Cargo.toml` names no `uefi-services`. @@ -175,9 +185,12 @@ Each stage lands on its own, in this order. `uefi::helpers`' handler, which writes no `loader.log`. `kernel_args_last_layout_refused` boots with `loader-writes-the-last-layout` and finds the kernel's refusal naming both - words before any `black box:` record. Leaving `LAYOUT` at `0x5459_0001` - makes it fail. Moving `layout` after `root_read_tsc` fails to build. The - tests that read `KernelArgs` pass: `boot_from_power_on`, + words before any `black box:` record. Moving `layout` after + `root_read_tsc` fails to build, and so does padding `KernelArgs` back to + 1272 bytes: `size_of::()` is then stage 1's size again, the + derived `LAYOUT` collapses onto the literal `LAST_LAYOUT` + (`0x5459_0000 | 1272`), and `assert!(LAYOUT != LAST_LAYOUT)` fails the + build. The tests that read `KernelArgs` pass: `boot_from_power_on`, `bar_placement_is_proven`, `root_withheld_refused`, `blackbox_*` and `update_*`. @@ -193,7 +206,9 @@ Each stage lands on its own, in this order. tries left. A slot out of tries and never good is not booted again. - Any failure after `verify` in `start_kernel` that the image causes is a refusal of that slot, like a bad signature, and the pass falls to the - other slot. + other slot. Stage 5's PR lists which `start_kernel` refusals count as the + image's — every `load_kernel_elf` refusal of the kernel's bytes — and + which count as the loader's own. - The loader spends a try of an untried slot before it hands over. That write, with sealing `ARMED`, is the pass's last, after every refusal `start_kernel` can make, so a loader refusal is never booked as the @@ -223,9 +238,15 @@ Each stage lands on its own, in this order. and saved before it is acted on, and ignored where the save fails. - `update --once` asks for the idle slot once and leaves it at priority 0, so its good flag is never read. The kept slot boots after it, and no floor - rises for it. `KernelArgs`' slot field gains the once variant, `LAYOUT` - rises to `0x5459_0003` and `LAST_LAYOUT` to `0x5459_0002`; this stage is - an ABI change. + rises for it. `KernelArgs`' slot field gains the once variant. The kernel + reads the field's discriminant as a raw `u32` and refuses one it does not + name, so an unrecognized value is a refusal rather than an out-of-range + `#[repr(C, u32)]` read, which is UB. The once variant keeps the struct's + size and every field's offset, so `size_of::()` alone would + leave the derived `LAYOUT` sitting at stage 4's value; this stage instead + bumps a layout-version component the formula also ORs in, so `LAYOUT` + differs from `LAST_LAYOUT` (stage 4's derived value, pinned as a literal) + and `assert!(LAYOUT != LAST_LAYOUT)` builds. This stage is an ABI change. - `update --boot-first` writes only the request. The loader writes its own `HD(…)/File(…)` entry and puts it first. Once stage 7 deletes `bootnext.rs`, that is the only boot-variable write. @@ -272,8 +293,10 @@ Each stage lands on its own, in this order. that is not an ELF, and finds A booted. - Running `update --good` right after init spawns `[boot] start` fails `update_dead_service_is_never_good`. It updates B with an image whose - `[boot] up` names a service that exits at start. B boots three times, - its good flag never set, and then A boots. + `[boot] up` names a live service beside one that exits at start; the live + service writes its byte and the dead one never does. B boots three + times, its good flag never set because one named service never wrote, + and then A boots. - Acting on a request before taking it off the table fails `update_boot_first_puts_the_loader_first`.