In QEMU, the loader writes boot variables on the running system's request and a failed pass falls to the entry behind its own or powers off; toyos-metal drives a boot through a machine running ToyOS alone - #539
Conversation
…bench replaces Ubuntu The loader writes the firmware's boot variables on a request the running system leaves in the slot table (format 2): `update --boot-first` puts its own entry first in BootOrder, `update --boot-next <esp guid>` sets BootNext to another ESP, and `update --once < image` boots the idle slot once and never marks it. Each request is taken off the table, flushed, before it is acted on. A pass whose every slot is refused falls to the entry after its own in BootOrder instead of dying. A slot booted once raises no floor. toyos-metal drives a T14 running ToyOS alone (the bench): the image goes in with `update --once`, the reboot over ssh, the boot's loader passes and logd files come back over sftp in one session — the loader keeps the last chain's file as loader-previous.log, and the boot's logd file is the one naming its ROOT — and the old judges read them. `--via-ubuntu` names the old path; `--via-ubuntu --resident` hands the machine to a bench image. The bench refuses an image built with another loader, which it names by its file's hash. Measured in QEMU on OVMF: BootOrder was 0003,0002,0000 and the loader's written entry Boot0001 stayed first across the reset after it; the machine stick is Boot0001 beside a recovery stick, which the fall-through reached as Boot0003. tests/updatecase gave toybox `syscap = ["power"]` where power is a connector since the log change, so its `reboot` was refused (exit 1) and every update test's reboot stalled; it receives the connector now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
…L_CONFIGS `as_chunks` for the two-byte walks, a type for a boot entry, no redundant clones, `rfind` for the variable store's last copy; `toyos_tmpdir::TempDir` for the bench's scratch and its tests; tests/benchcase and tests/benchvirtiocase in the list every config gate walks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
After the recovery stick hands the machine back, and again after the machine reboots itself, the wait ends at the machine's own kernel or the recovery stick's a second time; the second is the red, by name, where it used to be the wait's own stall. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
An entry for an ESP is written only where none names it, and only for its removable-media loader; one that names it — the owner's own entry for Ubuntu's ESP, efibootmgr's for a stick — is reused whatever file it boots, and an ESP with neither is refused by name. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
…rule tested `date -u +%s` answers a second on the bench and a bare `date` is refused as 2; `--metal-via-ubuntu` alone is refused as reaching no machine. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
|
Review of Gate. CI at BLOCKER
NOTE
REMOVE
Brief questions
SEND BACK |
One conflict, the ssh client's usage block: main's `fire` answer `exited <n>` and this branch's `probe` line, both kept. `tests/updatecase/system.toml` carries main's side. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
…his boot's, one entry policy The blockers, each with the test that goes red without it: - B1: a boot's kernel log is never a stem whose names `/log` held before the delivery (`metalbench::kernel_log`), so a rerun of one image cannot read the last run's file. - B2: the machine is back only once its `loader.log` differs from the one fetched before the reboot (`rebooted`), in the one wait that fetches `/log`; `keep_previous` deletes `loader-previous.log` first and copies in chunks, so no refusal leaves an earlier chain's file and no bound refuses. - B3: a slot booted once stays on the table as `Next::Trial` (request kind 3) until the next pass that chooses a slot takes it off; while it stands `slots::grant` refuses the running slot's trial, so `update` on the trial holds nothing of the slot the machine keeps. - B4: `bench_image` refuses the owner's key (`bench_keys`), so neither `--owner-key` nor `--update-image` beside `--bench-image` signs a bench. - B5: a pass that cannot take a request off the slot table sets no `BootNext`: the boot-next test runs one pass on a read-only stick (`BootOptions::stick_readonly`) and reads the variable store after it. - B6: the pass after the one that wrote the boot order says no `Request:` and no `is first:` line. - B7: the bench test asserts the report pass's "not raised, because slot B's image was booted once" and no raise to the trial's version. - B8, B9, B10: one enumerator (`bootvars::entries`) and one `BootNext` writer (`bootvars::boot_next`); `bootnext::point_at_us` uses both, and the decisions are `toyos_update::entry::naming` and `entry::after`, each skipping an inactive entry and `after` every entry naming the loader's own ESP, host-tested. - B11: `GuidText` and `entry::parse_guid` are gone; `toyos_gpt::Guid::parse` sits beside the Display it inverts, and `update` uses both. NOTEs: `BootOrder` is written only where it fits `MAX_ORDER` (N3); `entry::after` falls behind the current entry's last place, so a duplicated order ends rather than cycles (N4); a free number is one neither an entry nor the order names (N5); lowercase hex is no `Boot####` (N6); every loader panic falls to the next entry through the loader's own panic handler, and powers off where there is none (N7), which is also the no-slot path now; the chosen slot's once and refusal are set in one place for both loops (N8, N9); the loader line names the removable-media file it hashes (N10); the tests use `build::AUTHORIZED_ON_ROOT` (N11); the restated `update` wording is gone (N12); the table format change is recorded in the loader-change issue (N13); `update`'s header says the two asks are unsigned (N14); the bench's own log is found by the ROOT its pass handed the kernel (N15); the once word is `toyos_abi::boot::SLOT_ONCE`, and the two copies and their gate row are gone (N16); `entry::Unwritable` is gone and the load option is built infallibly (N17); the Ubuntu-flag matrix, the `--metal-via-ubuntu` rule and `date`'s refusal of other forms are gone with their tests (N18). REMOVEs: the duplicated doc and the laptop clause in `bootnext.rs`, the consumed-BootNext clause in `end_this_pass`, the loader-log bound's comment with the bound, the ROOT room's measurement, the registration's duration and the Ubuntu table in `metalbench`'s header. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
`update --boot-next` reads its GUID with `toyos_gpt::Guid::parse`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
…before the exit The grant's mutant went red on the refusal's wording while `update` was stopped by the version rule instead (200 is not newer than 200). An image newer than the trial's leaves the grant as the only thing between it and slot A, and slot A's signed header is read before the exit code is. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
The owner's ruling: stale or false prose is deleted, never corrected, and new prose is the one-clause invariant at the edit site. Every doc and comment this review round rewrote is back to the text it had with only the false part deleted, and every one it added is a clause at the line it guards. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
|
Review round 2 at 22d5e97. CI is red at this head: run 36327751879, job NOT READY FOR REVIEW |
The branch added four lines to toyos-abi/src/boot.rs, and abi-split refused a changed crate republished under its old version. toyos-abi moves to 0.17.0; toyos and toyos-window each name it by version, so both are touched and each takes its own minor bump (0.19.0, 0.21.0), and every lockfile that locks any of the three without a registry source is re-locked with `cargo update --offline -p <crate>`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review of Gate. CI is green at Round-1 BLOCKERs
N8 does not block. Both loops now end in the one BLOCKER
NOTE
REMOVE
Net lines
Round 1 had about +1847 of production. The feature earns the growth. What is still left to delete is the list above: Owed on the T14
SEND BACK |
…and what earns nothing goes
BLOCKERs:
- A flag only Ubuntu carries out (`--install-sudoers`, `--fat32-check`,
`--device`, `--host`, `--resident`) is refused by name without
`--via-ubuntu` rather than dropped by the bench path, so no run returns a
verdict with the outside FAT judge never run.
- `--swap` beside `--via-ubuntu` is refused ("a swap has no Ubuntu half"),
with or without `--image`, and `--resident` beside `--readback`, `--talk`
or `--nic` is refused ("judges no boot"). The three refusals sit after the
swap's own, so `--swap … --fat32-check` is still the swap's refusal and
`FAT32_CHECK` in `NOT_A_SWAP` is live and tested again.
NOTEs:
- `metal::tests`: the swap loop names `--fat32-check` again, and
`--image x.img` with no `--readback` is refused, asserted again.
- `update_no_slot_boots_the_recovery_stick` plants an active
`HD(<the machine's ESP>)/\EFI\BOOT\BOOTX64.EFI` entry right behind the
entry that boots the machine, and holds the pass to one fall, to the entry
behind the planted one.
- `bench_loop_drives_a_toyos_machine` stages a `loader-previous.log` longer
than any chain, ending in a marker line, before the bench's first pass, and
holds the readback's loader text to lacking it (`image::create_file_on`).
- `update_boot_next_boots_the_entry_once` holds the unwritable pass's serial
up to its marker to no `BootNext=Boot`, and the store to no `BootNext`
naming the recovery stick's entry, since the pass goes on past the marker.
- `update --boot-next` refuses by name to replace a slot asked for once
(`slots::Request::boot_next`, host-tested).
- `metalbench::wait_for_the_log` carries what its last ask came to into
`Refusal::Silent`.
- The HARDDRIVE node is chosen in `bootvars::hard_drive` alone (exactly one,
GPT, GUID-signed); `bootnext`'s "first" and `esp`'s "last" are gone.
- `date` refuses every form but `-u +%s`; the bench test asserts `date +%Y`
ends 2.
- `told` is `toyos_update::policy::told`, pure and host-tested with the dead
trial of N8.
Deleted: `machine.txt` with its writer and its readback entry, the bench
configs' `bin/echo` symlink, the second Arm-to-Batch literal (`Batch::of`).
REMOVEs: the benchcase "differ by the one devices row" clause, both "echo is
how the loop asks" clauses, and the installer plan in `metal.rs`'s header.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review of Gate. CI is green at Round-2 findings
Q2, the refusal set. Nothing is refused that the bench needs. By reading, every line the harness writes still parses:
On the bench path Q3. Yes, there is a cheap red, and without it the claim has no test (BLOCKER). Q4. It does not matter for the pin.
Restore it with BLOCKER
NOTE
REMOVE
Net lines
This round's production growth is the refusals,
SEND BACK |
The loader's `bootvars::hard_drive` took exactly one HARDDRIVE node off a uefi `DevicePath`, and `entry::hard_drive_guid` took the first one off an option's bytes for `names`, `naming` and `after`: two rules for which partition a path names, and no test could fail on their disagreeing. `entry::partition` is the one: a walk of a device path's bytes to its end node, refusing a path with no HARDDRIVE node, with two, or with one that is not a 42-byte GPT node signed by GUID, and returning the `Partition` it names. `boot_partition`, `our_partition` and `bootvars::esp` call it on `DevicePath::as_bytes`; `names` calls it on an option's path. The uefi walker and `hard_drive_guid` are deleted. Mutation, taking the first node without looking for a second: `cargo test -p toyos-update` exits 101 (`a_path_names_the_partition_of_its_one_hard_drive_node`); with the fix, exit 0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- `--resident` beside `--fat32-check` is refused, as beside `--readback` and `--nic`: the resident path never ran the check, so the run could come back green with the outside judge never asked. `--resident` joins `ABOUT_A_BOOT`, so `--install-sudoers` beside it is refused rather than installing the rule and dropping it. The resident refusal's `out.talk.is_some()` is gone: `--talk` without `--readback` is refused before it. - `FAT32_CHECK` and `INSTALL_SUDOERS` leave `NOT_A_SWAP`: `UBUNTUS` and the `--via-ubuntu`/`--swap` refusal already refuse both beside a swap. The swap test asserts `--fat32-check` beside it meets the `UBUNTUS` refusal. - `Bench::answers` returns the probe's `Result`, and `Bench::wait` carries the last one into `Refusal::Silent`, so "answer as the delivered boot" and "go down" end with a reason. - One FAT volume round-trip: `image::put_files_on` reads the volume, replaces or removes each named file, syncs and writes it back. `stage_slot` and the bench test use it; `create_file_on` is deleted. - The bench test takes its command line from `metal::invocation` for `Reach::Bench`, the line `request.txt` carries, and `metal::stage` no longer hands back the key that line names. Mutations, each a checked patch, built, restored: - `RESIDENT` out of `ABOUT_A_BOOT`: `cargo test --lib installing_the_rule_is_not_also_a_boot` exits 101. - `out.fat32_check` out of the resident refusal: `cargo test --lib the_ubuntu_path_is_named_and_nothing_falls_back_to_it` exits 101. - `Bench::wait` refusing with `last: None`: `cargo test --lib a_machine_that_never_answers_is_refused_with_the_last_ask` exits 101. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review round 4, at No CI has run at NOT READY FOR REVIEW |
BootDisk::table_at takes #547's scan: `locate_type` fills `[Option<Entry>; 2]`, an entry whose blocks are no partition is refused by name, and the one slot table is then `locate`d and read through `Partition`'s accessors. The SDK manifests take main's side: the publisher assigns their versions (#550). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… under toyos-gpt's lints The merge took main's manifests (#550) but kept this branch's bumped toyos, toyos-abi and toyos-window versions in eight lockfiles; they now say what main says, and userland's keeps only the update binary's toyos-gpt dependency. #547 forbids indexing, `as` and unchecked arithmetic in toyos-gpt outside tests, and `Guid::parse` used all three. It now reads the dashes with `get`, the digits with `char::from`, and a byte with checked arithmetic. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`rootimage::boot_disk` cut the boot device's path at its last node and required that node to be HARDDRIVE, beside `entry::partition`'s "exactly one GPT HARDDRIVE node, GUID-signed", and the two disagreed: `disk/HD/File` was a partition to `partition` and no disk to `boot_disk`, and `disk/HD/HD` or `disk/HD(MBR)` was a disk to `boot_disk` that `partition` refuses. `partition` now hands back the path before its one HARDDRIVE node as the disk's, and `boot_disk` takes the handle whose path is those bytes and the end node. Test: `a_partitions_disk_is_the_path_before_its_hard_drive_node`. Negative controls, each a checked patch built and restored: - the disk cut before the path's last node, the old rule: 101; - the second HARDDRIVE node taken rather than refused: 101. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t of the boot bench_loop_drives_a_toyos_machine went red once on the dev host at be959b5 and green on the harness's alone re-run; the red attempt's sshd accepted twice on the new netd and then no connect reached it for 46 s. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…again A listener is one smoltcp socket that becomes the connection it accepts, so port 22 listens only while that socket is in Listen. netd announced a connection to the owner only when a pass saw the socket Established, and accept took only Established. A peer whose handshake-closing ACK and FIN land in one pass moves the socket SynReceived -> CloseWait, and no pass ever sees it Established. The owner is never woken and never accepts, the socket never listens again, and every later SYN on the port is answered with a reset. Nothing on either side logs it. An ACK and a reset in one pass leave it Closed, with the same result. QEMU's user network produces the first pattern. It takes a host connect at once and finishes the guest's handshake afterwards, and when the host socket is already closed it sends the FIN straight behind the ACK. Measured with a host actuator, applied to tests/ as a checked patch and not committed (40 rounds of one connect closed at once and one closed with a zero linger, then `echo` asked up to ten times a second apart), on 4def9c8's netd with a log line per listener state change: every attempt logged `listener <id> now CloseWait notified=false`, 6 of 6 (3 invocations with their alone re-runs). Without that logging, 5 of 5 invocations were red, 10 of 10 attempts counting the harness's alone re-runs. With this change 5 of 5 were green, `echo` answered on the first ask each time, and sshd logged 31 to 33 connections, the `echo`'s among them. `bench_loop_drives_a_toyos_machine` is where this showed. After the netd swap, port 22 answered every connect with a reset for 46 s and sshd logged nothing. That run was not caught with the state logging, which saw 23 attempts green. With this change the bench test exited 0 in 25 of 25 invocations, none re-run. The trigger is not identified: no dial the bench host is known to make fell in that window. A connect this host closed before its guest handshake finished fits the signature, for example a `probe` cut off by its 10 s bound while the machine rebooted. The rule is now `listen::Listening`, one type used by the pass and by accept. A socket in Established or CloseWait holds a connection its owner is woken for and takes. A Closed one listens again. An accept spends the owner's wake whatever it finds, so a wake written for a connection its peer then reset still leaves the next connection announced. A CloseWait connection handed over reads its data and then EOF, which is how sshd ends such a peer's session. This is main's defect: the branch touches no netd, sshd or network SDK code. Negative controls, each a checked patch on userland/netd/src/listen.rs, shown to build, run, and restored: - CloseWait back with Listen and SynReceived: host suite exit 101 (a_peer_that_closes_with_its_last_ack_is_a_connection) - Closed not listening again: exit 101 (a_peer_that_resets_before_it_is_taken_frees_the_port, a_wake_spent_on_a_reset_connection_announces_the_next) - accept keeping the wake: exit 101 (a_wake_spent_on_a_reset_connection_announces_the_next) A handshake that nobody finishes keeps the socket SynReceived: 600 s were measured. That is filed as issues/hardware/a-handshake-nobody-finishes-holds-a-listeners-port-shut.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Seen across the 48 bench runs this branch's netd investigation made: 7 of them took 126 to 132 s for the bench to give back its /log, and the other 41 took 13 to 34 s. The late runs came with the base netd and with the fixed one alike. The cause is not measured. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Review round 6 of Gate. CI Earlier BLOCKERs
BLOCKER
NOTE
REMOVE
Brief Q5, the checklist
Net lines (brief Q6)
netd is +66 production and +281 tests; it leaves with the split. Nothing else is left to delete beyond the SEND BACK |
The branch continues from a worktree made from main, so main is this merge's first parent and every hash of wt/toyos-install stays reachable. netd takes main's side whole: #559 landed the listener fix and its machine test on its own, which supersedes this branch's copy of listen.rs, its tests and its main.rs wiring. The listener issue takes main's text, which names its owner and `listen::settle`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
Brings #569 (log_ring_keeps_the_owners_slots on the redlist); no conflict. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
`Guid::parse` joins the first `impl Guid` block and loses the `let h = hex` rename. The Stage 2 track drops its "built, and proven in QEMU" bullets, which described the tree and would rot with the next flag rename; the Owed list and the exit are the track. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
|
Review round 7 of Gate. CI Earlier BLOCKERs
BLOCKER
NOTE
REMOVE
SEND BACK |
The loader's panic handler resets at once, so a guest whose ready marker is the loader's own line can have QEMU (-no-reboot) exit before the one-second Timeout arm reads the UART. The Disconnected arm then panicked "QEMU died before ..." with the marker in the file it printed: root_candidate_malformed, root_named_but_absent and root_candidate_overlaps went red 3 of 3. Both arms now ask the one closure whether the UART carries the marker. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
The panic handler derived the loader's own ESP again through an exclusive LoadedImage open. A panic while main held that protocol left it None, and the fall could then take the loader's own entry: the loop the handler exists to prevent. main takes it right after uefi_services::init into a once-set cell; the handler reads the cell, and a pass that failed before the cell was set powers off rather than guessing. The request pass and point_at_us take the same value, so our_partition has one caller. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
An install asked for once replaced a standing Next::Esp with its own slot, silently, while --boot-next refuses to drop a slot asked for once. Request::install is the one rule for what an install leaves asked: a slot asked for once is answered, an ESP's boot is kept, and --once over one is refused by the ESP's GUID before any byte is written. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
update_no_entry_powers_off stages the no-slot machine with every entry behind the stick's in BootOrder made inactive, and holds the pass to saying there is no entry to fall to, QEMU (which takes every reset here) exiting, and exactly one loader banner. A WARM reset in place of SHUTDOWN loops into the same failure and turns it red. Rig::bend_kernel is the one way the tests here bend a slot's kernel. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
Takes #567, #570 and #571 at af817e5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
…l a pass reads none active The setup cleared the entries behind the stick's in the BootOrder the firmware's first boot wrote, a list fixed in advance. At the next boot the firmware wrote an active entry the setup never saw (Boot0003, in the pass's "BootOrder is 0001,0003,0000"), and the failed pass fell to it. The machine now boots good passes until one reads a BootOrder whose every entry behind its own is inactive: after each, the host parses the loader's own "this pass was booted as ...; BootOrder is ..." line, clears every active entry behind the pass's in that order, and boots again. A firmware that still puts an active entry there after four passes reds the test by name, with every pass's order and which of its followers were active. Only then is slot A's kernel bent for the failed pass, which is held, as before, to "there is no entry to fall to", QEMU's exit, and one loader banner. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
…ctive entry behind the stick's The test needed a pass with nothing active behind the stick's entry. Round 9 set this up by clearing, over four passes, every follower the pass had read. The orchestrator's run at 133d5dc measured the result: at each of the 4 passes the firmware had put back an active entry behind the stick's, first Boot0002 and Boot0000, then Boot0003 and Boot0000, and so on. The SHUTDOWN->WARM control arm died on the same setup line, so it measured nothing. EDK2's source at the pinned OVMF's commit (edk2-gf0064ac3af, f0064ac3afa28e1aa3b6b9c22c6cf422a4bb8771) says why. The firmware writes two active entries behind every device at every boot: - The EFI Internal Shell (BdsPlatform.c:1712, LOAD_OPTION_ACTIVE). The firmware matches an existing option on its attributes too (BmLoadOption.c:558), so a Shell entry made inactive is replaced by a new active one. - The Boot Manager Menu, UiApp (BdsEntry.c:991, CATEGORY_APP|ACTIVE|HIDDEN, BmBoot.c:2533). The firmware registers it again whenever BootOrder lists none. SetBootOrderFromQemu then rebuilds BootOrder from the active options alone, the bootindex devices first. That makes Boot0000 UiApp, Boot0001 the stick, and Boot0002 and Boot0003 the Shell. The Shell takes those two numbers in turn because BmGetFreeOptionNumber treats as free a number BootOrder does not list. The firmware owns its entries. Without overriding the firmware's order at every boot, no disk and no variable setup reaches the loader's no-entry arm. So the test goes. The arm is filed as proven by nothing, with its exit. The firmware's order also shows a second finding: entry::after counts a CATEGORY_APP entry, which the firmware's own BootOrder walk skips. That is filed too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
|
Closed in favour of the loader track in #582 ( |
… 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j
…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 8565cc5: 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 <noreply@anthropic.com>
Part of stage 2 of
issues/boot-media/the-machine-updates-itself-without-ubuntu.md, proven in QEMU with ToyOS on an emulated USB stick and claiming nothing of the T14. The loader writes the firmware's boot variables on a request the running system leaves in the slot table. It boots a slot or another ESP once, and when a pass fails it hands the machine to the entry behind its own, or powers it off where there is none; that power-off is proven by nothing, and the section below it says why.toyos-metaldrives a whole metal boot against a machine running ToyOS alone (the bench), with the old path behind--via-ubuntuuntil the installer. The T14's switch-over stays owed in the track, as item 1, and is proven later by the orchestrator's runbook.What changed, per decision
slots::Requestisnext: Option<Next>(a slot once, an ESP by unique GUID, a slot on trial) andfirst: bool; each field is held to the one value a writer leaves, and written away (table rewritten, flushed) before it is acted on. The loader finds the table astoyos_gptproves it:locate_typefor the one TOYOS-SLOTS entry, refused where its blocks are no partition, thenlocate. Entries the loader writes areHD(…)/File(\EFI\BOOT\BOOTX64.EFI)for an ESP it found, never a path the running system names.update --boot-first,update --boot-next <esp guid>(both unsigned),update --once < image.--boot-nextrefuses by name to replace a slot asked for once (Request::boot_next), and--oncerefuses by the ESP's GUID to replace another ESP's boot (Request::install, the one rule for what an install leaves asked), before any byte is written.Next::Trial(kind 3); while it standsslots::grantrefuses the running slot's trial, so nothing on the trial holds the marked slot. The next pass that chooses a slot takes the trial off. A slot booted once raises no floor (record::Booted::once). The kernel logs a once boot fromslot-refused=<marked>:once, whose word istoyos_abi::boot::SLOT_ONCE. What the kernel is told of the chosen slot ispolicy::told, pure, for both of the loader's tries.entry::partitionwalks a device path's bytes: its one HARDDRIVE node (42 bytes, GPT, GUID-signed) or a refusal, and the disk's path, the bytes before that node. The boot partition, the loader's own ESP, an ESP a request names, every optionnamingandafterread, androotimage::boot_disk(the handle whose path is that disk's and the end node) all take it.bootvars::entriesreadsBoot####(an unreadable entry refuses),bootvars::boot_nextwritesBootNext, andbootnext::point_at_ususes both. The other decisions are pure intoyos_update::entry:naming(lowest active entry naming an ESP),after(first active entry afterBootCurrent's last place inBootOrdernot naming the loader's own ESP; never earlier, so no loop),free(a number neither an entry nor the order names),first(refuses an order past 256),number(uppercase only).BootNexttoentry::after's entry and resets, or powers off where there is none. The no-slot path is this path. The loader's own ESP is taken once, right afteruefi_services::initand before anything can panic (bootnext::take_ours), so the handler never opensLoadedImagewhile the pass it interrupted may hold it. A pass that failed before that powers off.loader-previous.logkeeps the last chain: deleted first, then copied in chunks, so no refusal leaves an earlier chain's file. The loader names its ESP's removable-media file by SHA-256, which the bench holds every image's loader to.src/metalbench.rs):update --once < image,reboot,/login one sftp session. A readback is this boot's: the machine is back only onceloader.logdiffers from the one fetched before the reboot; the loader's text isloader-previous.logheld to the image's signed-header digest; the kernel's is thelogdboot naming the image's ROOT with no name/logheld before the delivery. A wait that runs out says what its last ask came to. The bench's own log (a--nicboot's MAC) is found by the ROOT its pass handed the kernel.--install-sudoers,--fat32-check,--device,--hostand--residentreach the machine through Ubuntu and are refused by name without--via-ubuntu;--swapbeside--via-ubuntuis refused, with or without--image;--residentbeside--readback,--nicor--fat32-checkis refused.cargo run -- --bench-image <keys>, andbuild::bench_keysrefuses an owner-signed bench.dateon the bench answers-u +%sand refuses every other form.BootOptions::recovery_stick,BootOptions::stick_readonly,vars::global,vars::plant_globaland an independentEFI_LOAD_OPTIONreader,metal::stage,image::put_files_on,tests/common/bench.rs,Rig::bend_kernel.wait_for_readyreads the UART for its marker when QEMU exits before the console closes, as its timeout arm already did: the loader's panic handler resets at once, and a-no-rebootQEMU then exits before the timeout arm looks.update_no_entry_powers_offis deleted. Its premise was a pass with nothing active behind the stick's entry. The pinned OVMF (edk2-gf0064ac3af) writes two such entries at every boot. The first is its EFI Internal Shell (BdsPlatform.c:1712,LOAD_OPTION_ACTIVE). The firmware matches an existing option on its attributes too (BmLoadOption.c:558), so a Shell entry made inactive is replaced by a new active one. The second is its Boot Manager Menu, UiApp (BdsEntry.c:991,CATEGORY_APP|ACTIVE|HIDDEN,BmBoot.c:2533).SetBootOrderFromQemurebuildsBootOrderfrom active options only: thebootindexdevices first, the firmware's applications behind them. That identifies the log's entries: Boot0000 is UiApp, Boot0001 the stick, and Boot0002 and Boot0003 the Shell, which takes the two numbers in turn becauseBmGetFreeOptionNumbertreats as free a numberBootOrderdoes not list. The orchestrator's run measured it: over 4 passes the firmware wrote both back active each time the test cleared them. So the stick always has the Shell behind it. From the disk or the variables, the no-entry arm is reached only by overriding the firmware's order at every boot. The arm is filed as proven by nothing, with its exit (issues/boot-media/the-loaders-power-off-with-no-entry-behind-it-is-proven-by-nothing.md).userland/netdwhole and carries no netd change.High-risk: the two checks
Negative controls. Each is a checked patch: the mutant was shown to build, then restored, and the tree was left clean.
a_partitions_disk_is_the_path_before_its_hard_drive_node,a_path_names_the_partition_of_its_one_hard_drive_nodeboot_disk's old rulebe959b5dbe959b5d--residentbeside--fat32-checkrefusedthe_ubuntu_path_is_named_and_nothing_falls_back_to_itout.fat32_checkout of the refusal5cd13850installing_the_rule_is_not_also_a_bootRESIDENTout ofABOUT_A_BOOT5cd13850a_machine_that_never_answers_is_refused_with_the_last_askwaitrefusing withlast: None5cd13850the_ubuntu_path_is_named_and_nothing_falls_back_to_it--fat32-check,--device,--host,--resident,--install-sudoersout ofUBUNTUS; the--swaprefusalif false; the resident refusalif false1f576344update_no_slot_boots_the_recovery_stick(an activeHD(<machine ESP>)/\EFI\BOOT\BOOTX64.EFIright behindBootCurrent)after_this_one(rt, None)1f576344loader-previous.logis deleted firstbench_loop_drives_a_toyos_machine(a 384 KiB stale file ending in a marker)1f576344update_boot_next_boots_the_entry_once(read-only stick; serial and store)consumebelow theBootNextwrite1f576344--boot-nextnever replaces a slot asked oncea_boot_next_replaces_an_esp_and_never_a_slot_asked_onceOk1f576344--oncenever replaces another ESP's bootan_install_answers_a_slot_asked_once_and_never_drops_an_esp(Some(Next::Esp(_)), true) => Some(Next::Slot(idle)), the old ruleleft: Ok(Request { next: Some(Slot(B)), first: true }),right: Err([1, …])29ec77faa_machine_that_never_gives_back_its_log_is_refused_with_the_last_asklast: None1f576344dateanswers one formbench_loop_drives_a_toyos_machine(date +%Y)if false1f576344a_trial_is_told_once_and_a_fall_back_is_told_its_refusal(None, None)1f576344a_boots_log_is_the_one_that_names_its_root,a_readback_is_of_a_pass_since_and_of_this_imagebeforeskip inkernel_log; the digest checkif false;rebootedwithout the comparison9f9da32b…4de6c673a_grant_claims_only_an_idle_slot_on_the_running_disk,update_trial_writes_nothing_of_the_kept_slotif false9f9da32b…4de6c673a_bench_is_throwaway_signed_and_authorizes_ed25519_keys_alonebench_keys9f9da32b…4de6c673update_boot_first_puts_the_loader_firstfirst: asked.first9f9da32b…4de6c673bench_loop_drives_a_toyos_machineonce: false9f9da32b…4de6c673the_entry_after_is_later_in_the_order_and_boots_something_else,an_esp_is_booted_by_its_lowest_active_entry,a_number_is_four_uppercase_hex_digits_and_a_free_one_is_named_by_nothing,the_order_puts_ours_first_onceafter;namingwithoutactive; lowercase numbers; a stale order number free; a duplicate order;put_firstpast the bound9f9da32b…4de6c673update_no_slot_boots_the_recovery_stick9f9da32b…4de6c673root_candidate_malformed,root_named_but_absent,root_candidate_overlaps2d6d228f, whose Disconnected arm does not read the UART2d6d228f:QEMU died before Slot A: REFUSED,2d6d228fNo whole-change control: the tests name
slots::Nextand the harness fields this change adds, so they do not compile against the base; each control reverts one decision whole. The handler's no-entry power-off has no control, because no test reaches it.Independent oracles: OVMF's boot manager (it booted the
HD(…)/File(…)entry the loader wrote and the one the test planted, honouredBootNextonce and resumedBootOrder, took the fall to the recovery stick); the variable store read and written by EDK2'sVariableFormat.hlayout, and the load option read and built by the specification's tables in the test, not by the loader's encoder; EDK2's source at the pinned OVMF's commitf0064ac3af, for the entries that firmware writes at every boot; the GPT entry array read by UEFI §5.3.3's layout; QEMU's read-only drive. For the bench: the machine's own kernel record of the once boot, init's refusal of the trial's grant, and the slot table read off the disk after each cycle.Gates
At
b7299abb, the head, the last three rows re-run there;b7299abbchanges onlytests/andissues/from133d5dcd, where the first three ran.133d5dcdis the merge of main atfb34346e, which takes #554, #574, #575 and #576.cargo run -- --build-onlycargo test --libcargo test --workspace --exclude toyos-build(toyos-update,toyos-gptamong them)cargo run -- --clippycargo run -- --ci hostcargo test --test toyos-build -- --list(builds every guest binary, boots nothing)Guest runs by the orchestrator, one
cargo test --test toyos-build -- [--nightly] --jobs 1 <name>each, at29ec77fa. Since then the branch changedupdate_no_entry_powers_off, then deleted it, and took main's merges. At133d5dcdthe Fast tier passed 394 of 394.root_*tests among them.update_*tests the branch keeps,bench_loop_drives_a_toyos_machine, and the nightlies that exercise the loader's report pass or its panic (blackbox_panic_chain,panic_outlives_the_deadline,panic_reboots,watchdog_resets,loader_watchdog_arms,boot_deadline_ends_a_wedge,usb_reset_hands_devices_back,hard_lockup_ends_a_deaf_cpu,usb_reset_records_the_phase_it_cut).usb_transport_breakis disabled on main (Disable four flaky tests behind their filed defects #565), so its run printed only the disabled line.Unsure
mainholdsLoadedImage. That the handler never opens it is shown only by reading the code.boot_disktakes its disk fromentry::partition, but no QEMU test gives the loader a boot path whose HARDDRIVE node is not the last one. OVMF never makes such a path. So the loader's wiring of the rule is held only by reading, and the rule itself by the host test.entry::afteras the loader's own.--resident,take_the_machineandmetalbench::wirehave never run: they run only on the T14.metal::invocation'sReach::ViaUbuntuline is first parsed on the T14.tests/toyos.rsisharness = falsewith no host test site, and only the bench test parsesReach::Bench's line.lan_swapis on the redlist (issues/build/a-swaps-redial-races-a-hard-dial-ceiling-against-an-unbounded-guest-gap.md), so the T14 does not run it. The bench's--swappath is measured only bybench_loop_drives_a_toyos_machine. That test redials the same way after its netd swap, so the redial race that issue names can reach it too.Filed
issues/boot-media/a-loader-change-reaches-a-machine-only-by-writing-its-stick.mdissues/boot-media/the-bench-reads-no-quiescent-log-volume.mdissues/boot-media/the-benchs-cable-is-read-by-the-driver-under-test.mdissues/boot-media/the-bench-runs-with-no-bound-on-its-own-boot.mdissues/boot-media/the-bench-sometimes-comes-back-two-minutes-late.mdissues/boot-media/the-loaders-power-off-with-no-entry-behind-it-is-proven-by-nothing.mdissues/boot-media/a-failed-pass-can-fall-to-an-entry-the-firmware-never-boots-from-its-order.md🤖 Generated with Claude Code
https://claude.ai/code/session_01W6rME2DoqwjcYFStYHHY4j