Skip to content

Self-update, stage 1: signed A/B slots, ssh … update < image, swap over exec - #530

Merged
Japabu merged 22 commits into
mainfrom
wt/toyos-update
Sep 26, 2026
Merged

Japabu merged 22 commits into
mainfrom
wt/toyos-update

Conversation

@Japabu

@Japabu Japabu commented Sep 26, 2026 •

Copy link
Copy Markdown
Collaborator

The first stage of issues/boot-media/the-machine-updates-itself-without-ubuntu.md, proven in QEMU: a kernel change reaches a running machine as ssh … update < image, is written to the idle slot and boots after a reboot; a slot with a flipped byte, no signature, another key's signature or a version under the floor is refused by name and the other slot boots; a kernel that dies, or hangs before any boot proved it, falls back on its own and the next boot's /log names it; swap runs over a plain exec and sshd's toyos-swap subsystem is gone. Every metal stick boots this loader, so the T14's NVRAM carries its floor: what that does there is under "The floor on the T14".

What changed, per decision

The signed image (toyos-update, new, pure, no_std). A header — magic, format 1, a monotonic u64 version, and for each of kernel, cmdline, root its length and SHA-256 — then a 64-byte Ed25519 signature over that header, then the sections. The sections are fixed in name and order and refused by name otherwise; ROOT is whole 4 KiB blocks. The signature covers the hashes and never the bytes, so a chunked, content-addressed form later is the same signature over the same hashes. The signed message is OpenSSH's SSHSIG signed-data in the toyos-image namespace (PROTOCOL.sshsig), so ssh-keygen -Y sign -n toyos-image makes a signature this verifies and ours verifies under ssh-keygen -Y verify; the namespace stops a signature the owner's key made for anything else passing as an image.

Why ed25519-dalek 2.2: the Ed25519 the Rust ecosystem ships (RustCrypto's ssh-key and russh, which sshd already runs, verify with it), no_std without alloc, and already in the userland lockfile at this version. No default features (no std, no precomputed tables, no zeroize — the machine holds no secret). verify_strict. The loader builds it with curve25519_dalek_backend="serial" and sha2's force-soft, because x86_64-unknown-uefi is soft-float and the default backends reach for AVX2/SSE/SHA-NI registers. New crates in the root and loader lockfiles: ed25519-dalek, ed25519, curve25519-dalek, curve25519-dalek-derive, signature, subtle, fiat-crypto, rustc_version (build) — all MIT/Apache/BSD-3, which the licence gate allows; getrandom 0.3 was already resolved.

Slots. Every image is slotted: the ESP carries the loader and log.guid; a slot table partition (type 94464329-…) names each slot's FAT volume (type 037719D7-…: toyos/kernel.elf, toyos/cmdline, toyos/image.sig) and ROOT by unique GUID, in two copies with a sequence — the writer writes the copy that is not current, so moving the mark survives a torn block. Entry order is ESP, slot table, log, slot A's volume, slot A's ROOT[, slot B's], so the log stays partition 3 where toyos-metal reads it. Images a mode builds (cargo run, --build-only) carry an empty slot B with room for a ROOT twice their own; test images carry slot A alone.

The loader reads the slot table off the disk it was loaded from, tries the marked slot then the other, and boots a slot only once its ROOT partition locates cleanly, its signed header verifies under the embedded key, its image is not recorded dead, its version is at or above the floor, and its kernel, cmdline and ROOT hash to what the header names — each refusal said by name. The kernel is handed boot-slot=<A|B> and, where the marked slot was refused, slot-refused=<slot>:<word>, and logs boot: slot A, because the marked slot B was refused: died, which logd carries to /log. ROOT is found by the slot's partition GUID and read for exactly the header's length; the old pick of ROOT by root= among TOYOS-ROOT candidates is deleted with toyos_rootimage::pick.

Death and fallback, through the existing attempt counter. The attempts file becomes the slots' record (toyos_update::record): the image a pass booted and the images that died. A black-box death (panic, fault, wedge, armed-and-silent) marks the booted image dead; a hang the count catches marks it dead only when its version is above the floor — the count cannot tell a hang from a power cut, and a power cut is no reason to leave a proven image. A death is the one refusal a slot comes back from: where nothing else verifies, the dead slot boots again, so a one-slot machine behaves as it did before slots. The first record a pass writes names no booted image (record::account); the slot it boots is written down only once chosen, so a pass whose second write fails, or which chooses nothing, leaves nothing an earlier pass booted for the next pass to credit.

Anti-rollback: a signed version, in a variable per key. The floor rises only on a boot that hands the machine back on purpose (the black box's DONE), and only to a version the loader verifies in the pass that raises it: the record on the log partition — which the running system can write — names the slot and the digest of the header it booted; the loader reads that slot's signed header again, verifies it under its key, requires its SHA-256 to be the record's digest, and raises to the header's version, or says Anti-rollback floor: not raised, because the proven image is not verified: …. So the running system can raise the floor to no version the owner did not sign, which is no more than update installing that image does. The floor is a UEFI variable the loader creates non-volatile and boot-services-only (nothing after ExitBootServices can reach it), one per signing key (toyos_update::floor): named ToyOSImageFloor-K<fingerprint of the key> for a loader built with the owner's key, which keeps the machine's floor, and ToyOSImageFloor-I<fingerprint of the key and the log partition's GUID> for one built with a throwaway key, which keeps a floor per image — every image mints its log GUID, so each starts at none. A loader deletes the floors it can tell are stale: the owner's every other one, a throwaway's every other image's and never an owner's. A stored floor with runtime access was made after a handoff (UEFI 2.10 §8.2 refuses a runtime SetVariable of a name that exists without it), so it is deleted and the floor is none, and refused where the firmware will not delete it, since every raise would then fail against it; any other shape — the loader's attributes and not 8 bytes, attributes it never writes, a variable firmware will not read — is refused and boots nothing, never read as no floor, naming the variable and how to delete it. Which firmware answer is which is pure (floor::Stored::answered: only EFI_NOT_FOUND is no variable), and so is the delete's (floor::deleted). What it cannot defend, filed as issues/boot-media/the-anti-rollback-floor-is-a-firmware-variable.md: anything firmware boots instead of the loader, a firmware reset of its variables, a machine only ever powered off, and a throwaway-key image whose running system rewrites its log GUID. update's newer-than-running check reads the slot table's advisory versions; the floor is the enforcement.

update (userland/update, manifest slots = true, gated to that one program). init resolves the grant against the kernel's own word: the running ROOT is the TOYOS-ROOT partition the kernel holds, the slot table is the one on that disk, the idle slot is the other (toyos_update::slots::grant): each of the idle slot's partitions must be listed on the running ROOT's disk, of its kind's type (TOYOS_BOOT, TOYOS_ROOT), and neither of the running slot's; otherwise init says no slot to grant: the idle slot's volume is … and update holds nothing; a GUID two listed partitions carry is refused as such. Which volume is the running slot's, init knows only from the table, which update writes, so a table naming the running volume as the idle slot's is granted it: a denial only, since the loader holds that volume to the signature, and filed. update reads the signed header off stdin and writes nothing until the signature is this machine's key's and the version is newer than the running image and no older than the idle slot's; it streams ROOT onto its partition hashing as it goes, writes the volume's three files through toyos-fat32 (a block-caching adapter so FAT's cluster writes are not a read-modify-write per 512 bytes), refuses a kernel or ROOT that is not the bytes the header names and any byte past the last section, fsyncs both claims, and only then writes the table copy that marks the idle slot, and fsyncs it. Slot writes use the partition-claim path; #525 (blockd and partition sessions) landed while this was in review, and moving update onto a session is not this PR's.

swap over exec, and its length is not optional. /system/bin/swap <service> <sha256> <length> replaces sshd's subsystem, which is deleted (userland/sshd/src/swap.rs, the header framing, SUBSYSTEM, sshd_said). The input is exactly <length> bytes and an input that ends sooner is refused by name before init hears of it: a connection that drops mid-upload ends a program's input exactly as the end of a file does, so the short form swap <service>, which read to the end of its input, is deleted, and with it issues/boot-media/swap-without-a-length-cannot-tell-a-cut-input-from-its-end.md. init's side is untouched — only init swaps; the build lets only swap receive the port. After answering, the program holds its input open until the caller closes it — the go, since the service being swapped may carry the caller's connection — or ANSWER_MS. Its own refusals answer unasked <why>, so the host knows init will say nothing. toyos-metal --swap and the swap rehearsals drive it through the harness client, which sends a program's input beside reading its channel.

Keys. Images for QEMU guests, CI and metal-loop sticks are signed with this checkout's throwaway key, minted once 0600 into target/image-signing-throwaway and kept there (never committed; two processes minting at once link one file into place and agree), so an unchanged tree rebuilds nothing that embeds it; its public half reaches the loader and update as TOYOS_IMAGE_KEY at compile time, with TOYOS_IMAGE_FLOOR=image. --owner-key and --update-image <path> sign with the owner's key (TOYOS_IMAGE_FLOOR=machine), read from TOYOS_SIGNING_KEY or ~/.config/toyos/image-signing-key (never a checkout), an unencrypted OpenSSH Ed25519 key refused if its mode lets anyone but its owner read it; a missing one is refused by name before anything builds, naming cargo run -- --signing-key-new, which mints one 0600 and refuses to replace one. Only the fingerprint is ever printed.

abi-split runs in the merge queue too. It ran on the pull request alone, so two branches that took the same next version each passed against main, git merged their identical bumps without a conflict, and the second would ship its change under the first's version — this branch and #525 were that pair. In a merge group it now judges the branch against the base the queue built it on (sdkversion::judge_queued: the group's tip is one merge onto its first parent), which holds every branch queued or landed ahead of it. This branch merged main after #525 and again after #531, which each took the versions this branch had, and bumped past both: toyos-abi 0.15.0, toyos 0.16.0, toyos-window 0.17.0, every pin and tracked lockfile with them.

toyos-metal may name the NVMe (owner ruling): Node is a whole SCSI or NVMe disk (/dev/nvme<n>n<m>, partitions spelled p<k>), and the doc and issue text stating "the NVMe is never written" are changed. The stick identity check stays.

Tests (tests/common/update.rs, tests/updatecase), each on a machine with its own writable OVMF variable store (BootOptions::firmware_vars), all seven nightly (each 9–83 s of boots at the head):

  • update_boots_the_new_kernel — the exit; then the slots' record is forged at the reboot (slot B, a digest not its header's, version 2^63) and the floor stays where the verified boot put it; then a plain reboot from B raises the floor from 100 to 200, and slot A, marked again, is refused as its version 100 is below 200 while B boots.
  • update_refused_pass_credits_no_image — slot A boots once and the record names it; then a byte of A's kernel is flipped and B holds no image, so the pass panics after its first write, and the record it leaves names no booted image.
  • update_refusals_boot_the_other_slot — update refuses another key, an older version, a flipped kernel byte, a flipped ROOT byte and one appended byte, each by name with status 1; the record is forged at the reboot (slot A's own digest, version 2^63) and the floor rises to 100, the version the loader verified; then four slots the loader refuses boot the other.
  • update_falls_back_from_a_dying_kernel — unchanged.
  • update_hang_kills_an_unproven_image — slot B cut at the loader's handoff, the retry marks it dead, slot A boots.
  • update_grant_refuses_a_stray_partition — a table naming the ESP, the log partition, the running slot's volume or its ROOT as slot B's is refused by init by name, and update holds nothing.
  • update_floor_is_the_images_own — the owner's floor and another image's, both u64::MAX, planted in the variable store: this image boots, the other image's floor is deleted and the owner's stays, and the reboot raises this image's own to 100; then this image's own floor planted as nine bytes is refused and nothing boots.

The forged record is written while QEMU holds the machine at its kernel's reset (qemu::QmpHold: set-action reboot=shutdown,shutdown=pause, then system_reset and cont), after that kernel's last write and before the loader's first read: a forge written under a running kernel is overwritten by its cache writing back the 4 KiB block the record's cluster shares (measured: the first run of the forged-digest arm raised the floor to 200, the true digest). The variable store is read and planted by EDK2's VariableFormat.h layout (tests/common/update.rs's vars), not by anything the loader shares. swap_refusals also sends half of netd under its whole length and digest, and with no length, each refused as unasked with netd untouched.

Gates

Every row ran at eefac4fb: main at 17eb66a4 (#531) merged in, and the bump past it. The head, 513be66d, adds one line to an issue file.

gate exit
cargo test -p toyos-update (at 811afc01, the last commit to touch the crate) 0
cargo run -- --ci abi-split 0 — bumped: toyos-abi 0.14.0 -> 0.15.0, toyos 0.15.0 -> 0.16.0, toyos-window 0.16.0 -> 0.17.0
GITHUB_EVENT_NAME=merge_group cargo run -- --ci abi-split on a group of 17eb66a4 + eefac4fb (git commit-tree, first parent main) 0
the same, on a group of 17eb66a4 + de9369d8 (the merge before the bump, still 0.14.0) 1 — toyos-abi changed and its version did not: toyos-abi/Cargo.toml still says 0.14.0, and the next one is 0.15.0
cargo test --test toyos-build -- --nightly update_ (7 tests) 0
cargo test --test toyos-build -- swap (12 tests) 0
cargo run -- --ci host (43 steps) 0
fast tier, cargo test

High-risk: the two checks

Negative controls. Each a checked patch (the substitution matched exactly once), the mutated tree shown to build (exit 0), the test run, the file restored and the tree clean. Guest controls ran at 6d196f98 (B3a and B8a again at 56aa8182, after the floor test was reordered so each reds at its own stage); the code they mutate is unchanged since. Round 3's (B12, B13, and the second N2, N4, N5) ran at 811afc01, before the merge; of the files they mutate or run, the merge changed only tests/updatecase/system.toml (three service = true rows), and both tests are green at the head.

finding mutation test exit red at
B1 raise to the record's version update_refusals_boot_the_other_slot 1 the loader raised the floor to 9223372036854775808, refused slot A (its version 100 is below …) and said no slot verifies — the review's brick, reproduced
B1 drop the digest comparison in slot::proven update_boots_the_new_kernel 1 not raised … never said; the floor rose to 200 on a forged digest
B2 account keeps the last pass's booted cargo test -p toyos-update 101 the_first_write_credits_no_image
B3 the loader reads the machine-scoped name update_floor_is_the_images_own 1 the owner's u64::MAX floor refused slot A, nothing booted
B3 a throwaway loader deletes every other floor update_floor_is_the_images_own 1 the variable store holds ToyOSImageFloor-K03f7… as []
B3 the same, pure cargo test -p toyos-update 101 a_loader_deletes_the_stale_floors_of_its_scope_and_never_the_owners
B4 slots::grant back to slots::idle update_grant_refuses_a_stray_partition 1 init never refused the ESP by type
B4 the type check dropped, pure cargo test -p toyos-update 101 a_grant_claims_only_an_idle_slot_on_the_running_disk
B5 the short form restored swap_refusals 1 swap netd with half of netd was taken and the connection died (nothing answered in 120s)
B5 a short read accepted (read for read_exact) swap_refusals 1 init refused it for its hash instead: refused the binary hashes to …
B6 ROOT's hash comparison in stream_root deleted update_refusals_boot_the_other_slot 1 root-flipped.update ended Some(0) and installed
B7 take's hash comparison deleted same 1 kernel-flipped.update ended Some(0)
N5 the trailing-byte refusal deleted same 1 appended.update ended Some(0)
B8 the wrong size read as floor 0 update_floor_is_the_images_own 1 the boot never said the refusal and booted
B8 the same, pure cargo test -p toyos-update 101 only_the_loaders_own_variable_is_a_floor_and_nothing_else_is_none
B8 an unreadable variable read as floor 0 cargo test -p toyos-update 101 same test (no guest can make firmware fail a read)
round-1 N4 (None, true) => Ended::Unknown update_hang_kills_an_unproven_image 1 died on its last boot… never said; slot B booted again
round-1 N2 the mode check off cargo test --lib signing:: 101 an_owner_key_wider_than_0600_is_refused
B12 the loader's first record write carries booted: previous.as_ref().ok().and_then(|p| p.booted) update_refused_pass_credits_no_image 1 a pass that booted nothing left a record naming slot A's image, red again alone
B13 slot::proven reads table.slot(Which::A) update_boots_the_new_kernel 1 the plain reboot from B said not raised … slot B's signed header is d7a8…, and the record names ffd5…, never 200, raised from 100; red again alone
N2 Stored::answered maps every failure to Absent cargo test -p toyos-update 101 only_not_found_is_no_variable
N4 floor::deleted reads an undeletable variable as floor 0 cargo test -p toyos-update 101 a_runtime_variable_that_stays_is_refused
N5 a duplicated GUID reported as OtherDisk again cargo test -p toyos-update 101 a_grant_claims_only_an_idle_slot_on_the_running_disk
B9 — a simulated collision: another branch taking toyos-abi 0.13.0 and adding one item, this branch's pre-merge head merged onto it (git commit-tree), checked out queue arm 1 (toyos-abi changed and its version did not: … still says 0.13.0, and the next one is 0.14.0); the pull-request arm, all the queue had before, 0 on the same commit
B9 the same collision, in-tree sdkversion::tests::two_branches_that_take_one_version_are_refused_in_the_queue 0 at head; each branch alone passes, the group reds, the branch bumped past passes
B9 (#531) the merge of #531 without the bump (de9369d8, 0.14.0), as a queue group on 17eb66a4 GITHUB_EVENT_NAME=merge_group cargo run -- --ci abi-split 1 still says 0.14.0, and the next one is 0.15.0; the bumped head's group: 0

Every guest control was red again on the harness's alone re-run, the same way for all but B5's two, which the harness calls different failures: the short form's differs only in the forwarded port its message names, and the short read's re-run went red at the wrong-digest case, which the partial read also breaks.

B10, rebuild cost (/usr/bin/time -p cargo run -- --build-only, two consecutive runs on an unchanged tree after a priming build, this host under other agents' load): main (7345e3fe, no key at all) 2.45 s, 2.09 s, 0 crates compiled; this head 2.86 s, 6.41 s, then 2.62 s, 2.39 s, 2.33 s, 0 crates compiled; the control — the per-process key restored as a checked patch at this head — 5.31 s, 3.33 s, the loader and update recompiled on every run. ROOT's in-process memo is the same on both arms.

Independent oracles — RFC 8032 §7.1 vectors 1–3 and toyos-update/tests/sshsig_oracle.rs, a header signed by OpenSSH 10.3's ssh-keygen -Y sign (fixture committed; the throwaway private key deleted); by hand once, a signature this tree made verified under ssh-keygen -Y verify and failed over a bent header, and --signing-key-new's key reads under ssh-keygen -y with the same -l fingerprint. The variable store is read and written by EDK2's published layout against OVMF's own firmware, which accepts the planted variables and writes the floor the test then reads back. The T14 run below is the hardware oracle for the floor's life on real NVRAM.

The floor on the T14

Every metal stick is built with the orchestrator checkout's throwaway key, so its loader keeps an image-scoped floor. On the T14's NVRAM, each stick's first pass lists the variables under vendor 33BE3D4A-30E6-49F5-8050-F169D93A20FB, deletes every ToyOSImageFloor* but its own and any ToyOSImageFloor-K… — every earlier stick's floor — and reads its own ToyOSImageFloor-I<key, log GUID>, which a fresh flash has not written: floor 0, said as Anti-rollback floor: ToyOSImageFloor-I… (image scope) holds 0. The next flash has a new log GUID, so its loader deletes that variable as stale and starts at 0. So the T14 holds at most one ToyOS floor variable at a time, no stick is held to a floor another stick raised — a stick built earlier, or a bisect, boots — and no owner floor exists there until an owner-signed image is installed, which no stick reads or deletes. Ubuntu cannot see or change the variable (boot-services-only). The one way a stick boots nothing is its own variable in a shape its loader never writes, which only a broken loader or something booted in its place could leave; it is refused by name on the stick's loader.log, and clearing it needs a UEFI shell (dmpstore -d) or the firmware's variable reset.

T14 run 148 (the orchestrator's, at a97708ae's staged image; its reading is on this PR): metal_device_probe PASS, slot A's signed header verified under the loader's key, ROOT hashed in 662,100,857 TSC cycles, and the floor lines of its loader.log, verbatim:

3:  Anti-rollback floor: ToyOSImageFloor-I48b74aaec69b6a65 (image scope) holds 0
8:  Slots: the table marks A (sequence 1); slot A present, slot B absent; the floor is 0
46: Anti-rollback floor: ToyOSImageFloor-I48b74aaec69b6a65 (image scope) holds 0
48: Anti-rollback floor: 1790444037, raised from 0 by the boot that proved it

So the chain-ending pass created and raised the image floor on the T14's NVRAM. That the next stick deletes it (… is no floor this image loader keeps; deleted) is owed by the next metal run of a different image: one is staged from the head 513be66d: cargo test --test toyos-build -- --metal --metal-readback <scratch>/update530c/metal metal_device staged metaldevicecase (5 jobs), its image and toyos-metal invocation in that directory's request.txt. No T14 reading of it is claimed here.

Measured

At 01a91713, not re-run since (the paths measured are unchanged):

  • ssh … update < image (32.8 MB: 5.6 MB kernel, 27 MB ROOT) to slot B's ready marker: 18,541–24,352 ms over five runs (22,190, 18,541, 22,583, 23,940, 24,352); of that, update itself took 1,834–4,043 ms (1,834, 4,043, 2,306, 3,673, 3,921), and the rest is the old kernel's shutdown, the loader pass that proves it, the pass that boots B, and B's boot.
  • The loader hashes that 27 MB ROOT in 314–530 M TSC cycles on the guest's ~1 GHz TSC (0.3–0.5 s, TCG); the shipped 114 MB ROOT scales to ~1.3–2 s under TCG, and is a native SHA-256 on the T14.

Unsure

  • The unreadable-variable arm of B8 is pure, now with the answer's mapping (Stored::answered): OVMF will not fail a read on request.
  • N4's refusal is pure too. A floor planted with runtime access and time-based authenticated write (0x27) was measured on OVMF: the firmware deleted it on the loader's plain delete (it is deleted, and the floor is 0), so no guest here reaches the refusal. The refusal means a kernel that plants such a variable before the first raise, on firmware that keeps it, stops every boot until the firmware's variables are reset; that is a denial a kernel could already cause, and the loader names the variable.
  • N3 is not answered here: CI's target cache still carries the throwaway seed; it signs only CI guests with image-scoped floors.
  • The pass that raises the floor reads the slot table and the slot's signed header once more (one FAT file read and one Ed25519 verify on the chain-ending pass); not measured on the T14.
  • A signed image's own cmdline could carry boot-slot=; the kernel takes the loader's, which is appended last.
  • The floor rises only on a clean reboot; a boot that confirms itself healthy is a later stage.
  • partition_claim_departure went red once beside other guests earlier in this branch's life and green alone — issues/boot-media/partition-claim-departure-told-no-flush-beside-other-guests.md, not this diff.

What toyos-metal still runs Ubuntu for (the next PR's list)

  1. Writing the image: wipefs --all and dd of=/dev/sda conv=fsync under the sudoers rule.
  2. Choosing the next boot: efibootmgr --create-only, --delete-bootnum, --bootnext.
  3. Reading a boot's verdict: dd if=/dev/sda3 and mount -o ro/umount of the log partition.
  4. Liveness and control: reboot, true, date -u +%s, the /sys/class/block identity reads of the stick, and waiting for Ubuntu's sshd after every ToyOS boot.
  5. The runner key and ssh t14 reach Ubuntu's sshd.

Filed

  • issues/boot-media/the-anti-rollback-floor-is-a-firmware-variable.md
  • issues/boot-media/the-running-slots-volume-is-claimable.md — the kernel holds the running ROOT but not the same slot's FAT volume, and update can be granted it through the table it writes; a denial only.
  • The track itself now carries stage 2 (the T14, NVMe, no Ubuntu) and the later stages the owner named.

🤖 Generated with Claude Code

https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK

Japabu and others added 11 commits September 26, 2026 15:58
The crate every later piece of the self-update track reads one format
through: the loader, `/system/bin/update` and the Mac's signing step.

- image: a header naming a monotonic version and the SHA-256 of each
  section (kernel, cmdline, root), then an Ed25519 signature over the
  header, then the sections. The signature covers the hashes and never
  the bytes, so a later chunked form is the same signature over the
  same hashes.
- sig: Ed25519 (ed25519-dalek, verify_strict) over PROTOCOL.sshsig's
  signed data in the `toyos-image` namespace, so an image signed by
  `ssh-keygen -Y sign` verifies here and ours verifies under
  `ssh-keygen -Y verify`.
- slots: the slot table, two copies with a sequence, a writer writing
  the copy that is not current, so moving the mark survives a torn block.
- record: what the loader keeps between passes: the image it booted
  last and the images that died.
- policy: the anti-rollback floor, the updater's newer-than-running
  rule, and every refusal by the word the kernel is handed.

Oracles: RFC 8032 §7.1 vectors 1-3 through the verifier this crate
calls, and a header signed by OpenSSH 10.3's ssh-keygen (fixture: its
signature and public key, the throwaway private key deleted) verifying,
with a bent byte and another key refused. The reverse direction was run
once by hand: a signature this crate made verified under
`ssh-keygen -Y verify` (exit 0) and failed over a bent header (exit 255).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
…ver exec

The first stage of the self-update track, proven in QEMU.

Image and loader
- Every image is slotted: the ESP carries the loader and `log.guid`; a
  slot table partition names each slot's FAT volume (kernel.elf, cmdline,
  image.sig) and ROOT. Images a mode builds carry an empty slot B with
  room for a ROOT twice their own; test images carry slot A alone.
- The loader reads the slot table off its own disk, boots the marked
  slot, and holds every byte it hands the kernel to the slot's signed
  header: signature under the embedded key, kernel/cmdline/ROOT hashes,
  and the anti-rollback floor. A refused marked slot falls back to the
  other; the kernel is told `boot-slot=` and `slot-refused=` and logs
  `boot: slot A, because the marked slot B was refused: <word>`.
- The attempts file becomes the slots' record: the image booted last,
  and images that died (a black-box death, or a hang of an image above
  the floor). A death is the one refusal a slot comes back from where
  nothing else verifies, so a one-slot machine behaves as before.
- The floor is `ToyOSImageFloor`, NV + boot-services-only, raised on a
  boot that hands the machine back on purpose; what it cannot defend is
  filed.
- The old pick of ROOT by `root=` among TOYOS-ROOT candidates is gone,
  with toyos-rootimage's `pick`.

Userland
- `update` (manifest `slots = true`, init claims the slot table and the
  idle slot's two partitions against the ROOT the kernel holds): reads a
  signed image on stdin, verifies before writing, streams ROOT, writes
  the FAT volume through toyos-fat32, fsyncs, then moves the mark.
- `swap <service> [<sha256> <length>]` over plain exec replaces sshd's
  `toyos-swap` subsystem, which is deleted; init's side is unchanged.

Build and keys
- A throwaway key per build process signs everything that stays on the
  Mac; `--owner-key` and `--update-image <path>` sign with the owner's
  key (TOYOS_SIGNING_KEY or ~/.config/toyos/image-signing-key, minted by
  `--signing-key-new`, refused by name where missing). The public key
  reaches the loader and `update` as TOYOS_IMAGE_KEY at compile time.

Tests
- update_boots_the_new_kernel, update_refusals_boot_the_other_slot,
  update_falls_back_from_a_dying_kernel (tests/updatecase), with a
  per-test writable OVMF variable store.
- The ROOT-pick tests are reworked for signed slots; the two whose claim
  was the superblock the loader no longer reads are deleted.
- toyos-metal names an NVMe disk: the owner lifted "the T14's NVMe is
  never written".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
- `VALUED_PARAMS` names `boot-slot=` and `slot-refused=`, which
  `params::claims` matches, as it names the black box's word: the loader
  appends all three.
- The kernel takes the last of each slot word, so the loader's own,
  appended after the slot's signed parameter, is the one it logs.
- `ALL_CONFIGS` covers tests/updatecase.
- The swap rehearsal's headers say `swap` carries the ask, not sshd.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
- root_chunk_refused waits on the read's own line: QEMU's failing stick
  answers the loader's next `loader.log` write with nothing, so the
  refusal after it never reaches the console.
- root_candidate_overlaps overlaps the partition before ROOT, and the
  host reads an extent as its table states it (`toyos_gpt::list`), not
  through `locate`'s range and overlap checks, which are the loader's
  to refuse. The loader locates a slot's ROOT before it reads a byte of
  the slot, so the refusal names the overlap.
- log_partition_layout asserts the five-partition layout, the three
  ToyOS types read with the kernel's parser.
- A dead slot booted because nothing else verifies still tells the
  kernel which marked slot was refused, where it is not the marked one.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
It waits for slot A's fallback record or for slot B booting a second
time, and names the second as the finding, so a loader that boots the
dead slot again is red on an assertion rather than a stalled wait.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
The upper bound was the count of loader lines between the two markers,
and the root bridges' descriptor dump alone is 410 characters: at the
metal profile's 1920-pixel mode, 240 eight-pixel columns, it is two
rows. The fast tier's red "a growth of 16, where the loader printed 15
lines" is that wrap arriving inside the window once the loader's slot
verification moved how far it had got when the first dump landed. The
bound is now the rows each line takes at the mode's columns
(`EFI_GLYPH_WIDTH`, eight), read off the loader's own GOP line.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
…h a key

`toyos_abi::boot` gained `SLOT_PARAM` and `SLOT_REFUSED_PARAM`, so the
published crate's minor moves and every in-tree pin and lockfile with
it (the abi-split judge's rule).

`cargo run -- --clippy` hands clippy a throwaway `TOYOS_IMAGE_KEY`: the
loader does not compile without the key it embeds. Six invocations
clean.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
Each carries a pin that moved, so each is a changed published crate.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
Where a lockfile holds the registry's older toyos-abi beside the path
one, its dependency lists spell the version, and cargo rewrote them at
the first build after the bump.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
main brought #529 (every test's scratch goes with it). Conflicts, each
hunk accounted for:
- userland/sshd/Cargo.toml: this branch deleted the swap subsystem's
  `toyos` and `toyos-swap` dependencies; main added the
  `toyos-tmpdir` dev-dependency for sshd's tests. Both kept.
- Cargo.lock: the root package's list takes main's `toyos-tmpdir` and
  this branch's `toyos-update`.
- userland/Cargo.lock: sshd's list is main's without the two deleted
  dependencies; main's `toyos-tmpdir` package and this branch's
  `toyos-update` both kept.
The image test this branch added takes its scratch through
`toyos_tmpdir::TempDir`, as main now requires of every unit test.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
After a reset the loader's passes speak on the 16550 alone, one of them
hashing ROOT, so the harness's console-only liveness read a machine
working through a panic's hold and two passes as one gone quiet: the
dying-kernel test went red on a loaded host with the fallback still on
its way. The update tests now wait on both channels, with the
harness's own quiet and wedged bounds.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
@Japabu
Japabu marked this pull request as ready for review September 26, 2026 15:52
@Japabu

Japabu commented Sep 26, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #530 at 01a9171. CI: ci run 36253436515 at this head, abi-split success (host is skipped outside the merge queue). Branch tests: the gate table in the body, cargo test -- update_ exit 0. Hardware: this branch has no T14 reading, and the loader it changes runs on every stick the metal loop flashes (see B3).

Net lines (git diff --shortstat origin/main...HEAD): 83 files, +5134 / -1568. Split: production (with inline unit tests) +3810 / -1122 = +2688; tests/ +814 / -355 = +459; lockfiles +289; issues +130. The growth pays for a new subsystem, and that reason is accepted. Deleted: sshd's swap subsystem and toyos_rootimage::pick. Dead code: see N6.

BLOCKER

  • B1 toyos-update/src/record.rs:83, bootloader/src/main.rs:878,917 — the anti-rollback floor rises to record.booted.version. That value comes from attempts on the log partition, which the running OS writes (logd owns /log). A logd or kernel that writes attempts with booted.version = u64::MAX and then reboots cleanly (black box DONE) makes the next pass set ToyOSImageFloor = u64::MAX. From then on every slot is BelowFloor, slot::choose panics on every boot, and only a firmware NVRAM reset recovers. This breaks "the kernel never crashes from userland", and a userland process bricks the machine. The floor must rise only to a version this pass verifies itself: re-verify the booted slot's signed header, require its SHA-256 to equal record.booted.digest, and raise to that header's version. Test: in a firmware_vars rig, the host rewrites attempts on the image between two boots with version 2^63 and a matching slot letter. The next boot's floor line must name the verified version, and slot A must still boot.
  • B2 bootloader/src/main.rs:887,982 — the first record write carries booted over from the last pass (..accounted.record). If write_chosen then fails, which the code logs and boots through, the disk still names the last pass's image. Example: A=v100 and B=v200. B dies and is marked dead, then A boots but write_chosen fails. A reboots cleanly and the floor rises to 200. A is now BelowFloor, B is dead, so the rescue boots B, which dies, again and again: a crash loop with no way out. Fix: the first write sets booted: None. B1's re-verification also closes this.
  • B3 bootloader/src/floor.rs, src/image.rs:19 (version_now) — the floor is one variable per machine, shared by every key, and versions are build-time Unix seconds. On the T14 (real NVRAM) every metal job's clean reboot raises it. Any stick image built before the high-water mark is then refused: an agent that built earlier but gets the metal lock later, or a bisect. Test images carry slot A alone, so the loader panics. Ubuntu cannot delete a boot-services-only variable. The body's "Nothing here touches the T14" is false: every metal stick now boots this loader. Scope the floor to the embedded key, for example with the key's fingerprint in the variable name, and delete every other ToyOSImageFloor* under the vendor GUID so NVRAM holds one. Then bring one T14 metal reading through the new loader: the slot verify line, the ROOT hash cycles, and the floor line.
  • B4 userland/init/src/main.rs:1111-1113 — init claims the idle slot's boot and root GUIDs exactly as the slot table names them. update, the grantee, writes that table. So a compromised update names any unheld partition in its own next grant: another disk's (the T14's NVMe), a non-ToyOS type, or the running slot's volume. That lets a process widen its own authority, which the capability model forbids. Fix: before claiming, require from the inventory that the partition is on running.device, has type TOYOS_BOOT or TOYOS_ROOT respectively, and is neither of the running slot's partitions. Test: the host stages a table whose slot B boot is the log partition's GUID. init must say "no slot to grant" and update must refuse holding nothing. With this check in place, the filed running-volume issue is a denial only and is not a blocker for this stage.
  • B5 userland/sshd/src/main.rs:573 (channel_close), :556 (INPUT_STALL), session drop — sshd closes a program's input (EOF) when the client closes the channel without CHANNEL_EOF, when input stalls, or when the connection drops. The last one races the kill in end. swap <service> (short form, userland/swap/src/main.rs:95-105) treats that EOF as the end of the binary and asks init. A cut that falls in the trailing sections (symbols, section headers) leaves an ELF that spawns and passes probation. So a truncated binary can be swapped in, and the old length-framed subsystem did not allow that. Fix: sshd gives EOF only on the client's CHANNEL_EOF, and any other end of the input kills the program before its stdin closes. Otherwise, delete the short form. Test: cut an upload mid-way (close the channel without EOF). init's log must show no swap and netd's binary must be unchanged.
  • B6 userland/update/src/main.rs:173-175 — surviving mutation: delete the ROOT hash comparison in stream_root. No test turns red, because every hash refusal is staged by the host (stage_slot) and never sent through update. Test: in update_refusals_boot_the_other_slot's first boot, update < next with one ROOT byte flipped must end status 1 saying "ROOT is not the bytes", and the reboot must still log boot: slot A, the one the slot table marks.
  • B7 userland/update/src/main.rs:150-152 — surviving mutation: delete take's hash check. Same test, with the kernel byte flipped (the existing flipped bytes), sent through update.
  • B8 bootloader/src/floor.rs:39-58 — a ToyOSImageFloor longer than 8 bytes makes get_variable return BUFFER_TOO_SMALL. That lands in the Err(e) arm: the floor is 0, the variable is not deleted, and raise then fails forever on the attribute mismatch. So one runtime-created 9-byte variable, planted before the first DONE, turns off anti-rollback permanently. This contradicts the module's own contract ("not read, and it is replaced"). Fix: handle BUFFER_TOO_SMALL like the foreign-variable arm (delete, then state it).
  • B9 ABI collision with Storage stage 3, steps 1–4: toyos-blockring, SYS_DEVICE_DMA_MAP, blockd and partition sessions #525 — both branches move toyos-abi 0.12.0→0.13.0, toyos 0.13.0→0.14.0 and toyos-window 0.14.0→0.15.0 with identical text. Git merges identical hunks without a conflict, and abi-split does not run in the merge queue. So the second branch to land ships a different ABI under the same versions, silently. Required resolution: whichever lands second merges main and bumps again (toyos-abi 0.14.0, toyos 0.15.0, toyos-window 0.16.0, with every lockfile), and reruns cargo run -- --ci abi-split at that head before it enters the queue.
  • B10 src/signing.rs:107, src/build.rs (GuestEnv image_key, key_hash(&[PROFILE, &env.image_key])) — a key minted per process reaches the loader and update through env!. Every cargo run and cargo test process therefore recompiles both, restages the loader, and reassembles ROOT. The branch did not measure this, against "iteration speed beats feature count". Measure two consecutive cargo run -- --build-only on an unchanged tree on main and on this branch. If the second run grows by more than a few seconds, the throwaway key has to live for the checkout, not the process.

NOTE

  • N1 tests/toyos.rs:815-817 — the three update tests (25-86 s) are over the 10 s line in src/tiers.rs. Move all three to Tier::Nightly, which is a one-word edit each.
  • N2 src/signing.rs:131-143 — the owner key is read whatever its mode is. Refuse a key file readable by group or others, as ssh does.
  • N3 bootloader/src/floor.rs:58 — any other read error fails open with floor 0. At least delete and recreate, as in B8, so the next DONE restores the floor.
  • N4 bootloader/src/main.rs:873-876 — wiring mutation (None, true) => Ended::Unknown (a hang is never a death) is caught only by the pure unit test, and no guest test holds the wiring.
  • N5 userland/update/src/main.rs:108-113 — surviving mutation: delete the trailing-byte refusal. Add a case with one byte appended to the refusals test's first boot.
  • N6 toyos-update/src/image.rs:234 — Parts::mismatched has no production caller (only tests): delete it or use it in stage_slot.
  • N7 Answers to the brief, for the record. verify_strict over the header is sound: each section's length and hash is signed, the sections are fixed in name and order, update refuses appended bytes, and a cut ends in read_exact. The loader hashes the exact buffer it boots (kernel Vec, cmdline Vec, ROOT pages) and does not re-read. SSHSIG framing matches PROTOCOL.sshsig. The RFC 8032 vectors and the ssh-keygen fixture are independent oracles. Walking every crash point in update and the table found none that bricks, since the mark moves last and the loader re-verifies. Neither key crosses: CI and test images use throwaway keys only, and a throwaway loader refuses an owner-signed image.

REMOVE

  • toyos-update/src/policy.rs:10 — "the disk holds nothing the floor rests on" is false: the raised value is read from attempts on the log partition.
  • PR body — "Nothing here touches the T14." is false: every metal stick boots this loader and raises the T14's NVRAM floor.

SEND BACK

Japabu and others added 6 commits September 26, 2026 18:45
The floor rises only to the version of the proven slot's signed header,
verified again in the pass that raises it and held to the digest the record
names; the record on the log partition is the running system's to write, so
its version is never read for the floor (B1). The first record a pass writes
names no booted image, so a failed second write credits nothing (B2).

The floor is one firmware variable per signing key (toyos_update::floor): an
owner-key loader keeps the machine's, a throwaway-key loader one per image,
keyed by the log partition's GUID, so no stick or bisect is locked out and no
throwaway image reads or deletes the owner's floor (B3). A stored floor the
loader did not write is refused and boots nothing, never read as none (B8).

init claims only an idle slot's partitions on the running disk, of their
kind's type, and neither of the running slot's (B4). swap requires its
length and digest; the short form, which could not tell a cut input from its
end, is deleted with its issue (B5). abi-split runs in the merge queue too,
judging a group against the base the queue built it on (B9's gate hole).
The throwaway key lives per checkout under target/, so an unchanged tree
rebuilds nothing (B10); an owner key wider than 0600 is refused (N2).

Guest tests: update_refusals sends a flipped kernel, a flipped ROOT and an
appended byte through `update` (B6, B7, N5) and forges the record at the
reboot; update_boots forges the digest; update_hang_kills_an_unproven_image,
update_grant_refuses_a_stray_partition and update_floor_is_the_images_own are
new; every update test is nightly (N1). Parts::mismatched is deleted (N6).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
…rol reds at its own assertion

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
#525 landed with the same version bumps this branch carries (toyos-abi
0.13.0, toyos 0.14.0, toyos-window 0.15.0); the next commit moves past them.
The two lockfile conflicts were each side's own packages: this branch's side
taken, and cargo added main's (blockd, toyos-blockhold, toyos-blockring and
what toyos-rust-tests now links); sshd's toyos-swap edge stays gone with the
subsystem this branch deleted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
#525 landed toyos-abi 0.13.0, toyos 0.14.0 and toyos-window 0.15.0, the
versions this branch had taken for its own change (toyos_abi::boot's two
slot words). The second to land bumps again: every manifest pin and every
tracked lockfile moves with it, re-resolved by cargo.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
Clippy's type_complexity refused the tuple `vars::walk` returned; it is a
struct now, and nothing else moves.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
…10 on

Found by this branch's fast tier: lan_mdns_answer red in lane 10 on a
104-byte socket path, green alone; nothing here touches how the path is
built, so it is filed for its owner rather than fixed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
@Japabu

Japabu commented Sep 26, 2026

Copy link
Copy Markdown
Collaborator Author

Hand-back for the review of 01a91713. Head: eaa49e38.

Each control is a checked patch: the substitution matched exactly once, the mutated tree built (exit 0), the test ran, the file was restored, and the tree was clean. Red means the test's own exit code. Guest controls ran at 6d196f98; B3 and B8 ran again at 56aa8182.

finding what was done control: red green at head
B1 The floor rises only to the version of the proven slot's signed header. The loader re-verifies that header in the pass that raises the floor, and requires its SHA-256 to equal the record's digest. The record's version is never read for the floor. update_refusals_boot_the_other_slot with "raise to the record's version": 1. The floor went to 9223372036854775808, slot A was refused, and the loader said no slot verifies, which is the brick the review described. update_boots_the_new_kernel with the digest comparison dropped: 1, and the floor rose to 200 on a forged digest. 0 (both). The record is forged while QMP holds the machine at its kernel's reset. A forge written under a running kernel gets overwritten by that kernel's 4 KiB block writeback: the first run raised the floor to 200 on the true digest.
B2 The first record a pass writes names no booted image (record::account). cargo test -p toyos-update with account keeping booted: 101 (the_first_write_credits_no_image). 0. The test is pure, because no guest can make the second write fail without a hook in the loader.
B3 One floor variable per signing key (toyos_update::floor). An owner-key loader keeps ToyOSImageFloor-K<key>; a throwaway-key loader keeps ToyOSImageFloor-I<key, log GUID>, a floor per image. A throwaway loader never reads or deletes an owner floor. The body's "The floor on the T14" states what the T14's NVRAM holds: at most one image floor at a time, each flash starting at 0, and no owner floor until an owner-signed install. The false sentence is deleted. update_floor_is_the_images_own with the machine-scoped name read: 1, because the owner's u64::MAX refused slot A. With a throwaway loader deleting every other floor: 1, because the owner's variable was gone from the store. Pure form: 101. 0
B4 init claims an idle slot's partition only when it is on the running disk, of its kind's type, and not one of the running slot's partitions (slots::grant). update_grant_refuses_a_stray_partition with grant swapped back to idle: 1. Pure form with the type check dropped: 101. 0. The test covers the ESP, the log partition, the running volume and the running ROOT.
B5 swap <service> <sha256> <length> only. A short input is refused as unasked … the input ended before the N bytes it promised. The short form is deleted, and so is its issue. swap_refusals with the short form restored: 1. Half of netd was swapped in and the connection died. With a short read accepted: 1. 0
B6 / B7 / N5 A flipped ROOT byte, a flipped kernel byte and one appended byte, each sent through update. update_refusals… with the ROOT hash check deleted, the take hash check deleted, and the trailing-byte refusal deleted: 1 each. Each input installed with status Some(0). 0
B8 A stored floor with the loader's attributes and not 8 bytes, attributes it never writes, or a variable firmware will not read is a hard refusal that names the variable, and nothing boots. A variable with runtime access is deleted and said. Wrong size read as 0: guest 1 (the boot went ahead), pure 101. Unreadable read as 0: pure 101. There is no guest for that arm, because OVMF will not fail a read on request. 0
B9 abi-split now runs in the merge queue and judges a group against its base (judge_queued). #525 landed; I merged origin/main and bumped to toyos-abi 0.14.0, toyos 0.15.0 and toyos-window 0.16.0 with every lockfile. Simulated collision (another branch on 0.13.0, with this branch merged onto it): the queue arm 1, the old pull-request arm 0 on the same commit. The in-tree test two_branches_that_take_one_version_are_refused_in_the_queue passes (0). --ci abi-split 0 at a97708ae, and the queue arm on its group 0
B10 The throwaway key is kept per checkout, in target/image-signing-throwaway. Per-process key restored at head: 5.31 s and 3.33 s, with the loader and update recompiled on every run. Head: 2.86 / 6.41 / 2.62 / 2.39 / 2.33 s with 0 compiled. main (7345e3fe): 2.45 / 2.09 s with 0 compiled.
N1 All six update tests are in Tier::Nightly. — —
N2 An owner key whose mode is wider than 0600 is refused. cargo test --lib signing:: with the check off: 101 0
N3 Answered by B8's hard refusal. — —
N4 New update_hang_kills_an_unproven_image. With (None, true) => Ended::Unknown: 1 0
N6 Parts::mismatched deleted. — —
REMOVE policy.rs:10 rewritten to say where the floor's value comes from, and the PR-body sentence deleted. — —

Gates: --ci host 0 at 669fbc02. Its first run, at a97708ae, was 1 on clippy's type_complexity in the new variable-store reader, which I fixed. --clippy 0. --ci abi-split 0. update_ 0 (6 tests), swap 0 (12 tests).

Fast tier: 1, with 395 passed. The one red is lan_mdns_answer: its lane-10 tap socket path is 104 bytes, over SUN_LEN. It was green on the alone re-run and --known-red says it is not quarantined. This diff touches no part of that path, so I filed it as issues/build/a-lane-socket-path-overruns-sun-len.md.

T14: a metal_device case built with this loader is staged in the orchestrator's scratch directory (update530b/metal/request.txt). No T14 reading is claimed.

@Japabu

Japabu commented Sep 26, 2026

Copy link
Copy Markdown
Collaborator Author

T14 run 148 (orchestrator) at the staged image: metal_device_probe PASS (judge exit 0). Loader: slot A marked, its signed header verifies under the loader's key, ROOT hashed in 662,100,857 TSC cycles, the image-scoped floor variable ToyOSImageFloor-I48b74aaec69b6a65 holds 0 before and after — the per-image floor on real NVRAM behaves as designed and leaves no machine-wide floor. Loader 2937 ms (ROOT read 1155 ms) against 2270 ms on main's run 145; the ~0.67 s is the ROOT hash and signature check. Kernel to Boot: complete 1214 ms.

@Japabu

Japabu commented Sep 26, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #530, round 2, at eaa49e3. CI: ci run 36261109164 at this head (pull_request): abi-split success, host skipped (it runs only in the merge queue). Branch tests: the body's gate table and the hand-back's control table. Hardware: the orchestrator's T14 run 148.

Net lines (git diff --shortstat origin/main...HEAD): 87 files, +6326 / -1599. Production +4537 / -1145 = +3392, which is +704 since round 1: floor.rs, slots::grant, the signing key file, judge_queued. tests/ +1267 / -359 = +908. Lockfiles +285. issues/ +142. The growth answers round 1's findings, and I accept that reason.

Round-1 BLOCKERs

  • B1 CLOSED. The only raise is main.rs:929, which uses slot::proven. proven re-verifies the named slot's signed header, requires the header's digest to equal the record's, and raises to the header's version. The record's version is never read for the floor. The two mutations went red (exit 1 each), and the forged-version and forged-digest arms are in the guest tests. What the running system can still do is raise the floor to an owner-signed version it wrote into the idle slot without booting it, and the body states that.
  • B2 CLOSED in the decision, not in the wiring. account clears booted, and the_first_write_credits_no_image went red (101) with the mutation. The loader line that uses it has no test (new B12 below).
  • B3 CLOSED in code, OPEN on hardware.
    • Code: the name carries the key's fingerprint and scope, and the scope follows Whose. Throwaway always builds Image, so a throwaway loader writes only -I… names and never a -K… one. It cannot raise or delete the owner's floor. The guest control and the pure control are red.
    • NVRAM accumulation is bounded. Every pass deletes every other -I floor, whatever its key, and an owner loader deletes every other floor. The store holds at most one -I floor plus the -K floors of owner keys that have booted since the last owner pass. The one way it grows is a pass whose firmware refuses GetNextVariableName, which the log states.
    • Hardware: run 148 reports only holds 0, "before and after". The body says a raised from 0 line is owed on the chain-ending pass (B11).
  • B4 CLOSED as asked. slots::grant checks the disk, the type and the running slot. The guest control went red with 1, the pure one with 101. The running slot's volume, though, is known only from the table the grantee writes (N1).
  • B5 CLOSED. swap/src/main.rs:80-95 accepts the length form only, and read_exact refuses a short input before init hears of it. Init's digest check stands behind that. Both controls are red.
  • B6, B7, N5 CLOSED. The three update refusals are sent through update itself, and each mutation went red (exit 1).
  • B8 CLOSED for the arm that was reported. A 9-byte floor with the loader's attributes is refused. Guest control 1, pure control 101. The unreadable arm is pure-only (N2).
  • B9 OPEN. The mechanism is correct, but the versions collide again.
    • Mechanism: if: is gone, so the job runs on merge_group. abi_split sees GITHUB_EVENT_NAME=merge_group and judges HEAD against HEAD^1, the group's base, which holds every entry ahead. fetch-depth: 0 keeps HEAD^1 local. The name is still the required check's. The in-tree test and the simulated collision are both measured (queue arm 1, pull-request arm 0). This assumes the queue's merge method makes the group tip one two-parent merge onto its base. judge_queued refuses any other shape by name, so it fails closed.
    • Versions: Where everything lives: the layout as ruled, and its first stage #531 landed at 18:06 (17eb66a) with toyos-abi 0.14.0, toyos 0.15.0 and toyos-window 0.16.0. Those are exactly this branch's versions, and git merge-tree origin/main HEAD now conflicts in 8 files (see B10).
  • B10 CLOSED. The key is kept in target/image-signing-throwaway, 0600, placed by a hard link so two processes that mint at once agree. The control recompiled the loader and update on every run; the head recompiled 0 crates.
    • It can never be the owner's key. The owner's key is read only from TOYOS_SIGNING_KEY or ~/.config/toyos/image-signing-key, must be in OpenSSH format, and must be 0600. The throwaway file is bare hex, which that path refuses.
    • It is not committed: target/ is ignored.
    • It is published once, see N3.

BLOCKER

  • B9/B10 (rebump)
    • Where: toyos-abi/Cargo.toml, toyos/Cargo.toml, userland/toyos-window/Cargo.toml, and the conflicts in src/build.rs, system.toml, tests/{e1000talk,flrswap,lantalk,swap}case/system.toml, toyos-manifest/src/lib.rs and userland/Cargo.lock.
    • What: Where everything lives: the layout as ruled, and its first stage #531 took 0.14/0.15/0.16 and moved the layout these files name.
    • Fix: merge origin/main (17eb66a) and bump to toyos-abi 0.15.0, toyos 0.16.0 and toyos-window 0.17.0 with every pin and lockfile. Rerun --ci abi-split on both arms at the merged head, and rerun update_ and swap there, because the merge moves the paths swap, init and the update rig use.
  • B11
    • Where: T14 run 148, owed by round-1 B3.
    • What: the reading gives only holds 0 "before and after". The chain-ending pass's floor line is not quoted: raised from 0, stays, not raised … or firmware refused. That pass is the only one that runs SetVariable of an NV|BS variable on Lenovo NVRAM, and the only proof the next stick deletes it. "Holds 0 after" reads either as a raise that failed on the target hardware, which would leave anti-rollback inert there, or as a line nobody quoted.
    • Fix: quote run 148's loader.log floor lines from every pass. On the next stick, quote the … is no floor this image loader keeps; deleted line.
  • B12
    • Where: bootloader/src/main.rs:898 (let mut record = Record { count: next, ..accounted.record };).
    • Surviving mutation: ..accounted.record → booted: previous.as_ref().ok().and_then(|p| p.booted), ..accounted.record. This restores round-1 B2 in the loader, and no test sees it, because the pure test holds account and not its caller.
    • Why "no guest can make the second write fail" does not hold: the first write is observable on its own.
    • Test: in a Rig, boot slot A once. Then stage_slot A with a flipped kernel byte on a one-slot table, or both slots bent, so choose panics after the first write. The host decodes attempts and requires booted == None.
  • B13
    • Where: bootloader/src/slot.rs, proven (let slot = table.slot(booted.slot)).
    • Surviving mutation: table.slot(booted.slot) → table.slot(Which::A). Every test that expects a raise proves slot A, so the arms stay green. The forged-digest arm names slot B, and the letter printed comes from booted.slot, so it passes too. Under the mutation, the floor never rises past slot A's version.
    • The claim this leaves untested is the one anti-rollback exists for: after a proven update to B, A is below the floor. No test shows the floor rising to an updated image's version.
    • Test: in update_boots_the_new_kernel, after the forged reboot, one plain reboot from B. It must say Anti-rollback floor: {NEXT}, raised from {BASE}. Then stage_slot A at BASE, and the next boot must refuse it as version if B is also refused.

NOTE

  • N1 toyos-update/src/slots.rs:271: runs.boot is the table's word, and the table is the grantee's to write.
    • The attack: a compromised update rewrites slot A as {decoy TOYOS_BOOT, running ROOT} and slot B as {running volume, idle ROOT}. The next grant then hands it the running slot's volume.
    • The effect is a denial only. The loader signature-checks that volume, and the table grant already lets update refuse every boot.
    • The filed issue's exit closes it: the loader hands the kernel the booted volume's GUID, and the kernel holds it.
  • N2 bootloader/src/floor.rs:71: the wiring mutation Err(e) => Stored::Unreadable(e.status().0) → Err(_) => Stored::Absent survives, because the pure test holds judge and not this map. There is no guest oracle. Move the status-to-Stored map into toyos_update::floor so the pure test covers it.
  • N3 .github/workflows/nightly.yml caches target as host-*, so CI's throwaway seed is in an Actions cache that pull-request workflows restore. It signs only CI guests with image-scoped floors, so it is harmless. It is still a published private key: exclude target/image-signing-throwaway from the cache paths, or accept it in the key's header.
  • N4 bootloader/src/floor.rs:78-87: this is the fail-open that round-1 B8 closed, in another shape.
    • A runtime-made floor whose delete the firmware refuses (for example one created with time-based authenticated write, NV|BS|RT|0x20) reads as 0. Every later raise then fails on the attribute mismatch.
    • Only a kernel running before the first raise can plant it, and the line is logged.
    • Refuse as for Refused::Attributes when the delete fails, or record it in the-anti-rollback-floor-is-a-firmware-variable.md.
  • N5 toyos-update/src/slots.rs:281: a GUID that two listed partitions carry is reported as Stray::OtherDisk. It is a duplicate, not another disk: name it.

REMOVE

  • issues/boot-media/the-running-slots-volume-is-claimable.md: "/system/bin/update cannot (init grants it the idle slot and never this one)". False: see N1.
  • PR body, "update": "nothing it names is claimed on its word". False for the running slot's volume (N1).
  • PR body, "The floor on the T14": "The ToyOS boot's clean reboot is read by the next pass, which … creates that one variable". No hardware reading of that exists (B11).

SEND BACK

@Japabu

Japabu commented Sep 26, 2026

Copy link
Copy Markdown
Collaborator Author

B11, the loader.log floor lines from T14 run 148 (orchestrator), verbatim:

3:  Anti-rollback floor: ToyOSImageFloor-I48b74aaec69b6a65 (image scope) holds 0
8:  Slots: the table marks A (sequence 1); slot A present, slot B absent; the floor is 0
46: Anti-rollback floor: ToyOSImageFloor-I48b74aaec69b6a65 (image scope) holds 0
48: Anti-rollback floor: 1790444037, raised from 0 by the boot that proved it

So creating and raising the image floor happened on the T14's real NVRAM. Deleting a stale image floor is read on the next metal run of a different image (its pass must remove I48b74aaec69b6a65); the orchestrator records that reading on this PR.

Japabu and others added 4 commits September 26, 2026 20:21
…pure crate

- update_refused_pass_credits_no_image (B12): a pass that refuses every
  slot panics after its first record write, and the record it leaves names
  no booted image. The loader's first write carrying the last pass's
  `booted` is the mutation it reds on.
- update_boots_the_new_kernel (B13): one more plain reboot from slot B
  raises the floor from 100 to 200, and slot A at 100, marked again, is
  refused under it while B boots. `slot::proven` reading slot A whatever
  the record names is the mutation it reds on.
- toyos_update::floor::Stored::answered (N2): the map from GetVariable's
  answer to `Stored` is pure, so an unreadable floor read as absent reds on
  the host.
- toyos_update::floor::deleted (N4): a runtime-made floor the firmware will
  not delete is refused, where it was read as floor 0 and every raise then
  failed against its attributes. OVMF deletes one planted with time-based
  authenticated write, so this arm is pure only.
- Stray::Duplicate (N5): a GUID two listed partitions carry is named so,
  not called another disk.
- issues: the running slot's volume is claimable by `update` too, through
  the table it writes (N1); the sentence saying it cannot is deleted.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
#531 (17eb66a) gave daemons a `service` row and moved their state under
/state; this branch gave `update` a `slots` row and `swap` its own row. Every
conflict is the two additions side by side:

- src/build.rs, toyos-manifest/src/lib.rs: `ProgramConfig` and `Program`
  carry both `slots` and `service`; `render` and `parse` write and read both
  records, and the doc table lists both.
- system.toml and tests/{e1000talk,flrswap,lantalk,swap}case/system.toml:
  sshd is `service = true` (main) and receives `netd` and `launcher` only,
  since `swap` is its own row here, which receives `swap`.
- userland/Cargo.lock: init keeps `toyos-gpt` (this branch); sshd depends on
  neither `toyos` nor `toyos-swap` (its subsystem is deleted here, and
  main's sshd change uses neither).
- tests/updatecase/system.toml, new on this branch: logd, netd and sshd
  marked `service = true` as main marks them in every other config.
- issues/build/a-lane-socket-path-overruns-sun-len.md deleted: main filed
  the same defect as a-lane-s-tap-socket-path-is-past-sun-len-on-the-dev-host.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
#531 landed toyos-abi 0.14.0, toyos 0.15.0 and toyos-window 0.16.0, the
versions this branch had taken for its change to toyos_abi::boot. The
second to land bumps again: every manifest pin moves, and every tracked
lockfile carrying them is re-resolved with `cargo update -w --offline` in its
own workspace, which moved those three crates' versions and nothing else.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
…very boot

The loader now refuses a runtime-made floor it cannot delete, where it read
one as floor 0 and then failed every raise. That trades a silent loss of
anti-rollback for a denial a kernel booted before the first raise can
cause; recorded where the floor's other limits are.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
@Japabu

Japabu commented Sep 26, 2026

Copy link
Copy Markdown
Collaborator Author

Hand-back for the review of eaa49e38. Head: 513be66d.

Each control is a checked patch: the substitution matched exactly once, the mutated tree built with cargo run -- --build-only (exit 0) for the guest controls, the test ran, the file was restored byte for byte, and the tree was clean. Controls ran at 811afc01. Red means the test's own exit code.

finding what was done control: red green at head
B12 New update_refused_pass_credits_no_image. Slot A boots once and the record names it. Then a byte of A's kernel is flipped with B empty, so the pass panics after its first record write. The record it leaves must name no booted image. The review's mutation at bootloader/src/main.rs (booted: previous…p.booted): 1, a pass that booted nothing left a record naming slot A's image, red again alone 0
B13 update_boots_the_new_kernel: after the forged reboot, one plain reboot from B must say Anti-rollback floor: 200, raised from 100. Then slot A is marked again and must be refused as its version 100 is below 200 while B boots. table.slot(Which::A) in slot::proven: 1, not raised … slot B's signed header is d7a8…, and the record names ffd5…, red again alone 0
B11 Closed by the orchestrator's run-148 reading, which is now quoted in the body. The next case is staged from 513be66d: …/scratchpad/update530c/metal/request.txt (metaldevicecase, 5 jobs). — —
B9 Merged origin/main (17eb66a4, #531). Every conflict was two additions side by side (slots beside service; sshd's own row beside swap's), each hunk is listed in de9369d8's message, and tests/updatecase gained main's service = true rows. Bumped to toyos-abi 0.15.0, toyos 0.16.0, toyos-window 0.17.0 (eefac4fb). Every pin moved and 8 lockfiles were re-locked; they moved only those three crates' versions. Queue arm on 17eb66a4 + de9369d8 (the merge before the bump): 1, still says 0.14.0, and the next one is 0.15.0 PR arm 0, queue arm on 17eb66a4 + eefac4fb: 0
N2 The map from the firmware's answer to Stored is now toyos_update::floor::Stored::answered, and the loader calls it. Every failure read as Absent: 101 (only_not_found_is_no_variable) 0
N4 Fixed: a runtime-made floor that the firmware will not delete is now refused (floor::deleted, Refused::Undeletable), where before it was read as floor 0. The denial this makes possible is recorded in the-anti-rollback-floor-is-a-firmware-variable.md. A guest arm was tried: OVMF deleted a variable planted as 0x27 (time-based authenticated write), so no guest reaches the refusal, and the arm was dropped. Undeletable read as floor 0: 101 (a_runtime_variable_that_stays_is_refused) 0
N5 Stray::Duplicate: "names a GUID two listed partitions carry". Back to OtherDisk: 101 0
N1 Filed: the-running-slots-volume-is-claimable.md now names update's route through the table it writes. — —
REMOVE Deleted: the issue's sentence, the body's "nothing it names is claimed on its word" and the body's T14 sentence. The body's "Filed" line said update is never granted the volume, which was false too, and it is corrected. — —

N3 (the Actions cache) was not in my brief and is not answered. The body says so.

Gates at eefac4fb. The head adds one issue line.

  • --ci abi-split: 0.
  • --ci host (43 steps): 0.
  • --nightly update_ (7 tests): 0.
  • swap (12 tests): 0.
  • cargo test -p toyos-update: 0, at 811afc01.

Fast tier: exit 1 on every run.

  • lan_mdns_answer was red 4 of 4 on both arms. That is main's filed SUN_LEN issue. This branch's duplicate of it is deleted in the merge.
  • metal_job_reboot was red 3 of 4 on main and 1 of 4 on the branch. It is filed.
  • quiesce_leaves_the_volume_whole was red 1 of 4 on the branch and 0 of 4 on main, where two of its quiesce siblings were red once each. This red is not settled. The branch touches nothing that test runs through.

@Japabu

Japabu commented Sep 26, 2026

Copy link
Copy Markdown
Collaborator Author

T14 run 149 (orchestrator) of the image staged from 513be66: booted clean, all five jobs ran, no panic. The stale-floor deletion B11 asked for, verbatim from loader.log:

3:  Anti-rollback floor: ToyOSImageFloor-I48b74aaec69b6a65 is no floor this image loader keeps; deleted
4:  Anti-rollback floor: ToyOSImageFloor-I4b85af454ae284e7 (image scope) holds 0
49: Anti-rollback floor: 1790449401, raised from 0 by the boot that proved it

So on the T14's real NVRAM a new image deletes the previous image's floor and creates its own: the store holds one image floor at a time. (The automated judge could not compile in the worktree mid-merge; the verdict is read from the logs.)

#528 (e4e540d) published toyos 0.16.0 and toyos-window 0.17.0 for the iced/
winit work, the same versions this branch had already set for its own ABI
change, so the two collide on crates.io. toyos-abi 0.15.0 is still free (main
never moved it past 0.14.0), so it stays; toyos moves to 0.17.0 and
toyos-window to 0.18.0, past main's landed versions, with every in-tree pin
and lockfile re-locked behind them (`cargo update -w` in toyos/, userland/,
userland/libc/, tests/toyos-rust-tests/ and tests/iced-counter/ — the five
workspaces a tracked lockfile of theirs names toyos or toyos-window).

Conflicts, each the two branches' additions side by side:

- src/build.rs: `ALL_CONFIGS` gained `tests/toolkitcase/system.toml` (main)
  and `tests/updatecase/system.toml` (this branch); both kept, alphabetical.
- tests/toyos-rust-tests/Cargo.lock, userland/Cargo.lock: re-locked rather
  than resolved by hand, so `swap`/`update` (this branch's new programs) and
  main's iced/winit additions both land, and the stale `toyos-abi` 0.1.0/
  0.2.0 registry entries main's fork-pin fix retired stay gone.

`rust`'s gitlink is unchanged (`git ls-tree HEAD rust` still names
80ea645f, main's own) — this branch never moved it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
@Japabu

Japabu commented Sep 26, 2026

Copy link
Copy Markdown
Collaborator Author

Merged origin/main (e4e540d, #528's iced/winit work) at a3bcbfa, resolving the version collision it created: #528 also landed toyos 0.16.0 and toyos-window 0.17.0, so this branch's own bump to those numbers now names the same versions main already published, and toyos-abi 0.15.0 (unmoved by main, still at 0.14.0 there) stays free. Versions now: toyos-abi 0.15.0 (unchanged), toyos 0.16.0 -> 0.17.0, toyos-window 0.17.0 -> 0.18.0, every pin and every tracked lockfile that names either re-locked with cargo update -w in its own workspace (toyos/, userland/, userland/libc/, tests/toyos-rust-tests/, tests/iced-counter/) rather than picked from one side of the merge. Gates: --check-forks EXIT=0 (23 branches asked, all current, no third-party drift); --ci abi-split pull-request arm EXIT=0 (bumped: toyos-abi 0.14.0 -> 0.15.0, toyos 0.16.0 -> 0.17.0, toyos-window 0.17.0 -> 0.18.0, against origin/main); --ci abi-split merge-queue arm EXIT=0, exercised by rehearsing the real merge-queue shape in a detached scratch (main as parent 1, this branch as parent 2) since the pushed merge commit's parents run the other way; --ci host EXIT=0 (43 steps, all green); cargo test --test toyos-build -- --nightly update_ EXIT=0 (7 passed); cargo test --test toyos-build -- swap EXIT=0 (12 passed). rust's gitlink is untouched (git ls-tree HEAD rust still names main's 80ea645f); this branch never moved it.

@Japabu

Japabu commented Sep 26, 2026

Copy link
Copy Markdown
Collaborator Author

Round-2 BLOCKERs closed and judged: B12 and B13 each pinned by a guest test red under the review's mutation, B11 read on the T14 (runs 148 and 149: the floor raised on real NVRAM, and a new image deletes the previous image's floor), B9 settled by rebumping after #528 (toyos-abi 0.15.0, toyos 0.17.0, toyos-window 0.18.0; both abi-split arms exit 0, the merge-queue arm rehearsed on a main-first merge). --ci host, update_ 7/7 and swap 12/12 exit 0 at a3bcbfa. The one unsettled fast-tier red (quiesce_leaves_the_volume_whole 1/4 against 0/4, with related quiesce reds on main) runs through nothing this branch changes. Landing.

@Japabu
Japabu added this pull request to the merge queue Sep 26, 2026
Merged via the queue into main with commit fd62f56 Sep 26, 2026
2 checks passed
Japabu added a commit that referenced this pull request Sep 26, 2026
…528)

Conflicts, each resolved to carry both sides:
- bootloader/src/main.rs: the boot map's ROOT_* names with #530's update imports.
- src/image.rs: main's slotted layout, with `arch` threaded into
  create_boot_image and create_esp_volume for the removable loader's path.
- src/build.rs: main's Parts, shipped_parts and signing, keyed per
  architecture: loader_key takes the arch, the embedded key and the floor
  scope; root_image_key the plan (config and arch) and the key; build()
  writes image_for(plan.arch).
- tests/common/qemu.rs: a test's own firmware variables file where it names
  one, and otherwise the architecture's pflash.
- toyos-window is 0.19.0, past main's 0.18.0, since its scanout drain moved
  into arch modules here; both lockfiles take main's and that version.
- tests/common/update.rs names the x86-64 plan and image.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
Japabu added a commit that referenced this pull request Sep 26, 2026
main landed #528 (iced, `toyos::wake`) and #530 (self-update), which took
toyos-abi 0.15.0, toyos 0.17.0 and toyos-window 0.18.0.

- SDK versions: past main's, to toyos-abi 0.16.0, toyos 0.18.0 and
  toyos-window 0.19.0; every tracked lockfile re-locked in its own workspace
  with `cargo update -w`, tests/iced-counter's among them.
- toyos/src/lib.rs: both `power` (this branch) and `wake` (main).
- userland/logd/src/main.rs: main's hunks move logd's `Own`/`Theirs` bell
  onto `toyos::wake`; this branch deleted that machinery (logd's own lines
  are records in its ring), so they have nothing to land on and this side is
  kept. `toyos::wake` stays for its other users.
- userland/soundd/src/say.rs: deleted here; main's hunk moves its wake pipe
  onto `toyos::wake`, and soundd's mix thread writes a lane here instead.
- rust: main's pin is still 80ea645f83b, which this branch's pin already
  merges.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
Japabu added a commit that referenced this pull request Sep 27, 2026
…, #531, #534) into the Netstack3 netd

Two conflicts. tests/common/origin.rs: main added `volumes` to the imports
this branch had split to name `segment`'s NEIGHBOUR and NEIGHBOUR_MAC; both
kept. userland/Cargo.lock: main's lock taken whole and re-locked against this
branch's manifests (`cargo metadata`), which adds the mirrored crates and their
dependencies and drops smoltcp and its defmt.

The rust gitlink fast-forwarded to main's fbf6ad143d8; this branch's pin
(7a809b7591f) is its ancestor, and the branch made no fork commit of its own.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014iqcj4jDKpaiDX8B7CMvmK
@Japabu
Japabu deleted the wt/toyos-update branch September 28, 2026 09:46
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant