Repository navigation
A return to userland takes an entry inside the user half, and nothing is placed in its last page - #757
Conversation
The file is the audit's, filed on wt/toyos-audit-kernel (draft pull request 754) and not yet on main. It is carried here unchanged so that it lands with its fix and is deleted by it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
…e stops a page short of its top SYS_THREAD_SPAWN bounded the stack and passed the entry on unread, so the first return to userland of the new thread carried whatever the caller wrote. For an address that is not canonical the architecture faults in the returning instruction itself, in the kernel's ring, where the fatal path halts every CPU. Under QEMU's TCG the emulator completes the return and faults at the fetch in Ring 3, which is why the audit that found this (issues/a-thread-entry-is-unchecked-and-a-noncanonical-one-is-measured-only-under-tcg.md, deleted here) saw only the one process end. toyos_userbound::Entry is an instruction pointer inside the user half, by is_user_addr and by no second definition, and it has one constructor. loader::Start is now the only way to a trampoline: its Process and Thread arms carry an Entry, its Kernel arm a kernel thread's body, and the trampolines are no longer re-exported from loader. sys_thread_spawn refuses what Entry refuses with InvalidArgument; the loader's entry, already inside an image rebase_base placed, is made one the same way. The other paths by which a user-chosen instruction pointer reaches a return: - a process's entry is e_entry, which toyos-elf holds inside the image, and the image is held inside the user half by rebase_base; - no syscall writes a saved user frame: there is no signal delivery, no resumption at a registered address and no context to set, and a saved frame lives on a kernel stack; - the stack pointer is not checked by either return instruction and is the thread's own to fault on; - AArch64 takes a bad ELR_EL1 as an abort from EL0 after the ERET; - an instruction that ends at the last byte of the user half leaves the first address past it, which is not canonical, as its successor: what a syscall placed there returns to through SYSRET, and what an interrupt taken after any instruction there returns to through IRET. Nothing held this. rebase_base allowed an image to end exactly at USER_TOP, and the image is the only executable mapping that can reach it (libraries and anonymous memory are placed below ALLOC_CEILING, and anonymous memory is never executable). It now refuses an image that reaches the last 2 MiB page. This one is read off the code and the architecture's documents; nothing here executed it, and TCG would not show it. Tests: the host table in toyos-userbound holds Entry's edges and the image bound; thread_entry_noncanonical boots tests/testcases alone and runs spawn_noncanonical_entry, which asks for a thread at an address that is not canonical, at the first past the user half and at the first of the kernel half, and wants InvalidArgument for each. The binary is on RUST_SKIP: a kernel that took the entry would take a shared boot with it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
Control and mutation patches, as run against
|
|
Review round 1 of Growth. BLOCKER
NOTE
What was checked, at its lines
Ruling on the KVM reading of the unfixed kernelNot required for landing, and not worth a CI run. The fixed kernel executes no return to an address SEND BACK |
…a window stops where an image does The three entries a thread spawn refuses move into `abuse_thread_table`, which already drives `SYS_THREAD_SPAWN` raw on stacks it mapped and asserts a refusal by name, on the T14's `process_bound_threads` row. The type already keeps an unchecked entry from a trampoline, so what the test holds is the word the caller reads, and a metal row reaches that: the tier before a QEMU guest test. The wait for an accepted thread goes with the standalone binary: a kernel that accepted one is red on the assertion or on the fault, whichever comes first. `spawn_noncanonical_entry`, its `RUST_SKIP` entry, the `MACHINE_TESTS` entry and the arm are deleted. Not `abuse_tls_alloc`: no QEMU boot runs a discovered Rust binary. The suite's QEMU half is `MACHINE_TESTS` and `SCREEN_TESTS`, and `shared_metal` is the only runner of the rest, so that binary would have held the word on the T14 as well, under the shared boot's name and not a row's. `IMAGE_TOP` becomes `PLACED_TOP` and `Window::new` asserts its ceiling against it. The claim that nothing executable reaches the last page of the user half rested on the kernel's window happening to end at `STACK_BASE`; libraries are placed through that window and are executable. It is now one declaration, and the kernel's window is a `const`, so one above it does not compile. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
Round 1 mutation patches, as run against
|
|
T14 at head
|
|
Review round 2 of Growth. Round 1's BLOCKERs
Round 1's NOTEs
Ruling 1: which T14 boots the landing is owedNone beyond the one that ran. The QEMU suite above plus
For the record, should the orchestrator want the named binaries read anyway: unfiltered, the shipping set is two flashes, Ruling 2: the
|
|
Review round 3 of Growth. Round 2's BLOCKER
Round 2's NOTEs
BLOCKERNone. NOTE
LAND |
…nd the SDK resolve together (#746) Stage 3 of `issues/the-tree-says-who-uses-each-thing.md`. The root, `kernel/`, `bootloader/`, `userland/` and `toyos/` were five Cargo resolutions; they are one workspace with one `Cargo.lock`, one `[profile.toyos]`, one `[patch]` table and one tracked `.cargo/config.toml`. No directory moves. Head `dd12c0b32`, on `origin/main` `6f87cdb9c` (#749; none of #757, #759 or #762 had landed when it was merged and measured). It is `9ef866436`, where the CI readings of the fold itself were taken, plus two merges of `main` and the close of the stage's issue. Everything owed at the merged head is in the next section, measured at `dd12c0b32`. ## The merge of #749, measured at `dd12c0b32` #749 wrote its new dependency edges into `kernel/Cargo.lock` and `userland/Cargo.lock`, which this branch deletes. Both modify/delete conflicts are resolved by deleting the file and re-resolving the root lock. Git merged the root `Cargo.lock` without a conflict into a lock that is wrong, as the review found: `cargo metadata --locked` on it exits 101 (`cannot update the lock file … because --locked was passed`). It had `toyos-userbound`'s edges to `toyos-abi` and `toyos-bootmap`, which #749 also wrote into the root lock, and not `acpiserver`'s to `toyos-acpi` and `toyos-aml`, which #749 wrote into userland's alone. `cargo metadata --offline` re-resolved it. `diff` of git's merged lock against the re-resolved one is those two lines under `acpiserver` and nothing else: no package added, no version moved. | Owed | Command | Result | |---|---|---| | The lock resolves as committed | `cargo metadata --locked --format-version 1` | exit 0 at this head by the host suite's step "the licences of what ships", which runs `cargo metadata --locked` for every shipped crate's manifest and is green in `host.log` and in run 37757675374; the hand run's empty stderr (`metadata-locked.err`) predates the merge commit and recorded no exit | | The lock's (name, version) pairs are the union of `main`'s five | `pairs.sh <worktree> 6f87cdb`: the pairs of `Cargo.lock`, `kernel/`, `bootloader/`, `userland/` and `toyos/Cargo.lock` at `6f87cdb9c`, `sort -u`, against the root lock's | 692 against 692, `diff` exit 0 (`pairs.out`) | | The folded kernel and loader are the control's bytes | `prove.sh 6f87cdb dd12c0b …`, the round 3 script unchanged | exit 0; all four `control vs fold` byte rows `cmp` exit 0; every row in "The checks" below (`prove.out`) | | No new reader of a compiled-in path | `git diff -U0 e3bdff8 dd12c0b -- tests src toyos-blackbox toyos-symbols userland/symbolize`, its added lines searched for `\.rs`, `taken at`, `panicked at`, `Location`, `file()`, `PREVIOUS_PANIC`, `strip_prefix`, `src/`, `pure/` | the merge touches five files there, all under `tests/`; 13 hits, of which 4 are diff headers and 9 the field `info.rsdp`; none reads a path. `tests/common/power.rs:429` is still the one reader outside fixtures, and reads `taken at kernel/src/hardlockup/probe.rs` (`readers-merge.diff`, `readers-hits.txt`) | | `cargo run -- --ci host`, once, on the development machine | at `dd12c0b32`, `cargo run -- --ci host > host.log 2>&1; echo EXIT=$?` | exit 0; the log ends `[ci] Host: 77 step(s), all green`; 1-minute load 26.60 when it started (`host.log`) | The logs are in the round's scratch directory (`orch/oneworkspace-r4/`), which a reader of this pull request cannot reach; `prove.out` and `pairs.sh` are in the round 4 comment. ## What changed, per decision - **Members.** `kernel`, `bootloader`, `toyos` and userland's 40 packages join the root `[workspace]`. `userland/Cargo.toml`, four locks, three `rust-toolchain.toml` and three per-directory `.cargo/config.toml` are deleted. The toolchain files chose nothing the build read: every guest `cargo` already runs under `RUSTUP_TOOLCHAIN` naming its sysroot. - **Flags.** `build.target` is gone, since every guest build already passes `--target`. The root config holds one `[target.<triple>]` table per guest triple, six, each with the flags its directory's config gave it. A host build takes none, as before. Two things do change: - The guest crates outside the workspace that are built from their own directory for a ToyOS triple (`tests/toyos-rust-tests` and its `tls-*` crates) now take `-Dwarnings`, because cargo reads the tracked root config from above them. On a checkout without a local config they took no flags. - The one build that sets `RUSTFLAGS` itself (the test binaries linked against a `cdylib`, `src/build.rs`) takes none of the table: the variable replaces it. - **Profiles.** The root's `[profile.dev]` is `opt-level = 2`. The kernel library's host tests, its model controls, the SDK's tests and every surveyed userland crate's host tests used to resolve in their own workspaces and ran at `opt-level = 0`; they now run at 2. - **What stays apart** (the root manifest's `exclude` says why at each entry): `rust/`; `tests/toyos-rust-tests` and its `tls-*` crates and `tests/ssh-client-host`, because `[patch]` is workspace-wide and they patch or refuse what the root patches; and `userland/libc`. libc keeps its own lock because that lock is an input of the sysroot key: as a member it would be resolved by the root lock, and every dependency change of any member would move the key and rebuild every sysroot. The price is a sixth resolution of `toyos`, `toyos-abi`, `toyos-elf`, `toyos-osrelease` and `dlmalloc` that nothing holds to the root's (both carry `dlmalloc` 0.2.13 today). - **The lock** is every `[[package]]` of the five locks, deduplicated and resolved by `cargo metadata`. Nothing was `cargo update`d. Since the lock reviewed at `88bcbf4d3` it has changed by #749's four edges alone (see the section above); `cargo metadata --locked` exits 0 at this head. - **One target directory.** Every guest is built at the root with `-p` into `target/`. A stale sysroot used to `cargo clean` a crate's own target; that would now empty the build system's own, so `Stale::All` removes `target/toyos` and the guest triples' directories instead, and the `cargo clean` path, its member assertion and its test are deleted. - **Kernel and loader build one after the other.** They were built on two threads. In one target directory cargo serialises them anyway (measured in round 1: the second prints `Blocking waiting for file lock on artifact directory`), so the thread scope is deleted. - **The host suite** can no longer be `--workspace`: the kernel binary, the loader and most of userland do not build for a host. `src/hostws.rs` says which members a host tests, and the workspace test and clippy runs `--exclude` the rest by package name. - **Fork clones.** The tracked config includes the gitignored `.cargo/local.toml` when it exists, and `implementer.md` names it. - **The merge of #745.** `src/sysroot.rs` keeps both sides: `SYSROOT_SOURCES` carries `"sdk/std"` and `SYSROOT_MANIFESTS` ends in `".cargo/config.toml"`; its test keeps both loops; `issues/toyos-has-its-own-allocator.md` keeps `sdk/std/sys/alloc.rs` and "from the kernel's graph in `Cargo.lock`". Git merged all three without a conflict. - **The host's own apps build where the userland tests build.** `src/ci.rs`'s apps step passes `--target` only where it checks another host's triple. On `main` the `userland/*` test steps and the host's apps step both named the host triple and shared `userland/target/<host triple>`. The fold took `--target` off the test steps, which no longer need it to keep a guest triple out, and left it on the apps step: the tests filled `target/debug`, the apps `target/<host triple>`, and every dependency was compiled twice. That is what made the sealed tree larger than `main`'s (see CI). - **The merges of `main`.** #747, #748, #750, #751, #753 and #755 merged without a conflict. #749 did not: see the section above. - **The merge of #752.** Git merged it without a conflict: both workflows' `host` jobs keep `CARGO_PROFILE_DEV_DEBUG: line-tables-only` in `env:`, and `carry()` no longer sets it. No manifest and no lock moved in the merge. - **Issues.** `issues/the-tree-resolves-in-five-cargo-locks-not-one.md` is deleted: the one thing it named as left, the T14's run of the metal profile on the folded build, ran green at `db55db96a`, and review round 2 ruled no boot owed for what followed on two conditions, both in the section above. Stages 2 and 3 of `issues/the-tree-says-who-uses-each-thing.md` now read "Landed in #724, #732 and #738" and "Landed in #746". What the file carried that stays true is the root manifest's `exclude`, which says why each excluded directory keeps its own resolution; the deleting commit's message carries the rest (libc's second resolution of five crates, and `miniz_oxide` 0.8.9 beside 0.9.1 until `png` takes 0.9). `issues/cargo-run-inside-kernel-loom-or-kernel-sim-builds-for-a-bare-target.md` is closed on the two in-directory runs at `9ef866436`. `issues/the-sdk-is-linted-by-no-clippy-run.md` is filed and names its owner, the build system. ## The fold changed the kernel's source paths, and the proof did not see it The T14's run of the whole metal profile at `88bcbf4d3` exited 1: 295 passed, 1 failed, 30 boots. The red row was `hard_lockup_ends_a_deaf_cpu`. Its judge looked for `taken at src/hardlockup/probe.rs` in the previous boot's panic record, and the readback's loader log says `taken at kernel/src/hardlockup/probe.rs:145:29`. **What changed in the kernel's strings.** Cargo hands rustc a workspace member's source by its path from the workspace root, and rustc writes that path into every panic and `Location`. The kernel's root was `kernel/`; it is now the repository. So `src/...` became `kernel/src/...`, and a path dependency outside the old root, which the base named by the checkout's absolute path, is now named from the repository root (`toyos-abi/src/...`). Read from the actuator kernel staged at this head: 254 distinct `.rs` paths, 158 under `kernel/`, 38 under a `toyos-*` crate or `bcachefs`, none bare `src/` or `pure/`, none naming the worktree. **Why the proof did not see it.** Both of its oracles were blind to it by construction: - The byte row compared the fold against a control that is the base with its workspace root moved up. The control moved the root too, so it carries the same new paths and the bytes agree. - The `rustc`-lines row compared base against fold after a `sed` that rewrites `(kernel/|bootloader/)?(src|pure)/x.rs` and `ROOT/<crate>/src/lib.rs` to one form. That rewrite is needed, or every path crate's line differs and the row can show nothing else; but it absorbed the change without reporting it. `prove.sh` now reports what that rewrite absorbs: per artifact, how many crates' source arguments were renamed, and a diff of the `.rs` paths the artifact carries, base against fold and control against fold. The script is in the round 3 comment and has run twice since, at `9ef866436` and at this head. **Every reader of a compiled-in path.** I searched the harness, the guest tests, the build system, `toyos-blackbox`, `toyos-symbols`, `userland/symbolize`, the loader and the kernel's panic path for path literals, prefix strips and `Location` readers. One reader matches a compiled-in path by its prefix: `tests/common/power.rs`, the red row's judge, now fixed to the path the kernel records. Everything else is prefix-blind (`panicked at`, a file name with its line) or a synthetic fixture. The kernel's panic slot keeps the last 96 bytes of a path; the longest kernel path is 46, so nothing is cut. Userland's panic sites gain a `userland/` prefix the same way; no test reads one. **The record rows.** The judging asked to record three `boot.testcases-bounds.*` rows. They are not this change's: that boot was already staged on the base and unrecorded, and `main` recorded it in #745. They arrive with the merge and nothing is committed here. ## What the fold changes in what is built The lock row was measured at `dd12c0b32` against `6f87cdb9c`, the `rustc` row by `prove.sh` at the same pair; the two `cargo tree` rows at `88bcbf4d3`, and were not taken again. | Measured | Result | |---|---| | Lock: (name, version) pairs, the fold's against the union of `origin/main`'s five | identical, 692 pairs, `diff` exit 0 | | Lock: sources | registry `getrandom` 0.2.17, 0.3.4, 0.4.2 are gone; the forks at the same versions remain | | Kernel and loader, both arches: every `rustc` command line of `cargo build -v`, base against fold, path and cargo's path-derived hashes taken out | identical, `diff` exit 0: 31 units per kernel, 55 and 37 per loader | | Userland, both triples: `cargo tree -e features` over every program, base against fold | identical, `cmp` exit 0 | | Host members: the same | `diff` exit 1, on `getrandom`'s source alone | So one resolved crate changes: the build system and the other host members compile the ToyOS forks of `getrandom` 0.2.17, 0.3.4 and 0.4.2 instead of the registry's, same versions, same features. And every source path compiled into the kernel and the loader changes, as above. ## The checks (high-risk: build system) Measured at `dd12c0b32` against `origin/main` `6f87cdb9c` by `prove.sh` (the script of the round 3 comment, unchanged), exit 0; its output is in the round 4 comment. Round 3 measured the same rows at `9ef866436` against `b432ed21c`, round 1 at `88bcbf4d3` against `e7010129f`. **Negative control.** The whole change reverted is the base. A second control is the base with only its workspace root moved up, keeping the crate's own base lock and its profile. It is given the fold's root `.cargo/config.toml`, so the control does not hold the flags: the one row that does is base against fold on normalised `rustc` lines. **Oracle.** Bytes and cargo's own command lines. Each cell is its own `cmp` or `diff` exit: | | kernel x86_64 | kernel AArch64 | loader x86_64 | loader AArch64 | |---|---|---|---|---| | control vs fold, bytes | 0 | 0 | 0 | 0 | | fold vs fold rebuilt, bytes | 0 | 0 | 0 | 0 | | base vs fold, bytes | 1 | 1 | 1 | 1 | | base vs fold, `rustc` lines normalised | 0 | 0 | 0 | 0 | | control vs fold, `rustc` lines verbatim | 0 | 0 | 0 | 0 | What the normalisation absorbs, reported by the three `paths` rows: base against fold, cargo hands rustc another source path for 29 of 31 crates of each kernel and for 35 of 50 and 13 of 35 crates of the loaders (`diff` exit 1 each, as expected); the `.rs` paths the x86-64 kernel carries are 250 on both sides, of which the base has 39 under the tree's absolute path and 150 from the crate's own root and the fold none of either (`diff` exit 1); control against fold the artifacts' paths are identical (`diff` exit 0, all four). **Mutations**, each on a fresh copy of the fold: M1 (drop the `[target.x86_64-unknown-uefi]` table) loader build exit 101; M2 (lock `dlmalloc` at 0.2.12) `cmp` exit 1 and lines `diff` exit 1; M3 (select `bcachefs` beside the kernel in one `cargo`) kernel build exit 101. ## Gates The rows of the section "The merge of #749" were read at `dd12c0b32`. Every row below was read at `9ef866436` unless it says otherwise, each once, the narrowest that judges it. `ci.yml` runs on the push of `dd12c0b32`; its result is not in this body. | Gate | Result | |---|---| | `cargo run -- --ci host` | at `dd12c0b32`, development machine: exit 0, `[ci] Host: 77 step(s), all green`. Linux runner at `9ef866436`: `ci.yml` run 37740454881 `host` success; cold inside `--ci seal`, nightly run 37740449787: `[ci] Seal: 80 step(s), all green`; on macOS, the same nightly's `portability-macos`: success | | `cargo test --lib ci::tests` (the changed step's own test) | exit 0, 13 passed | | The images and the guest suite | at `9ef866436`, run 37740454881: `toolchain / build` and `guest / suite` success (KVM); run 37740449787: `toolchain / build` and `tcg / suite` success. At `e3bdff8af`, run 37755369755: `host` and `toolchain / build` success, `guest / suite` still running when read. Not run locally, and not read at `dd12c0b32` | | `prove.sh 6f87cdb dd12c0b …` | exit 0; every row as in the table above | | `cargo test` inside `kernel/loom` | exit 0 | | `cargo test` inside `kernel/sim` | exit 0 | | Cold wall clock, x86-64 kernel and loader (`wall.sh`, one run) | base, two cargos side by side: 24 s, 1-minute load 34.92 before it. Fold, one after the other: 20 s, load 42.23. Other agents' builds were running, so the two are not a controlled pair; the fold was not slower | | Metal profile | every row green at `db55db96a` (comment 6048782042). Since then the branch changed `src/ci.rs` and the root lock's two `acpiserver` edges; the kernel sources that moved are `main`'s own landings (#747, #748, #749), merged in. Review round 2, ruling (3), owes no boot for the merge of #749 on two conditions, both met above | | `cargo test --manifest-path userland/acpiserver/aml/Cargo.toml` at `e3bdff8af` | exit 0 | | `git status --porcelain --ignore-submodules=none` at `dd12c0b32` | empty | `issues/cargo-run-inside-kernel-loom-or-kernel-sim-builds-for-a-bare-target.md`'s close now stands on the two in-directory runs at `9ef866436`. The logs of these rows are files in the scratch directory of the round that took them (`orch/oneworkspace-r3/`), which a reader of this pull request cannot reach; the proof's and the measurements' outputs are in the round 3 comment. ## CI **Why the sealed tree was larger than `main`'s, measured.** A `workflow_dispatch` of `nightly.yml` at `88bcbf4d3` (run 37685714260) sealed `9348536345 B in 20064 files`, red. Units compiled per step, counted from that log and from `main`'s nightly at `b432ed21c` (run 37717000719, sealed `18708 files, 7664839895 B`): | step | `88bcbf4d3` | `main` | |---|---|---| | the driver's own build | 203 | 203 | | the build system | 207 | 207 | | the workspace's host members | 99 | 99 | | clippy, warnings denied | 412 | 417 | | the controls | 56 | 56 | | `userland/*` | 225 | 289 | | the apps for linux | 282 | 47 | | the apps for macos | 248 | 248 | | the apps for windows | 239 | 239 | Of the 256 distinct crates the apps-for-linux step compiled at `88bcbf4d3`, 226 had been compiled by an earlier step of the same run; 30 by none. The cause is the target directory: the test steps built without `--target` into `target/debug`, the apps step with `--target x86_64-unknown-linux-gnu` into `target/x86_64-unknown-linux-gnu`. Profile, features and `RUSTFLAGS` are the same in both. **The fix, measured once on the development machine** (`share.sh` in the round 3 comment; cold, a target of its own, `aarch64-apple-darwin`): after the fourteen test steps' builds (271 units), the ten apps with `--target <host>` compile 270 units and add 616,616 KiB under `target/<host>` and 135,064 KiB under `target/debug`; the same ten without `--target` then compile 47 units and add 107,856 KiB. The 47 are the same crates `main`'s step compiles on the runner. **The seal at `9ef866436`, nightly run 37740449787** (conclusion success: `host`, `portability-linux`, `portability-macos`, `toolchain / build`, `tcg / suite`): ``` the cache entry, read by content: none restored: the run is cold the apps for linux: 10 app(s) pass `cargo build`; … the tree, sealed as the host cache's entry: 17010 files, 7493968284 B of the 8000000000 B an entry may hold; sealed: 2496 sources, built on Linux X64 ubuntu24 20261004.327.1, every target dated as built [ci] Seal: 80 step(s), all green ``` Units per step in its log, against the two columns above: | step | `9ef866436` | `88bcbf4d3` | `main` | |---|---|---|---| | `userland/*` | 225 | 225 | 289 | | the apps for linux | 42 | 282 | 47 | | the apps for macos | 264 | 248 | 248 | | the apps for windows | 240 | 239 | 239 | Every other step compiles what it did at `88bcbf4d3`. I expected 47 for the Linux apps: it is `main`'s 47 less `crc32fast`, `log`, `memchr`, `smallvec` and `toyos-keymap`, which an earlier step had compiled. I did not expect the macOS step's 16 more: all are host-side units (`syn`, `thiserror-impl`, `tokio-macros`, `futures-macro`, `autocfg` and the like), which the Linux step's `--target` build used to compile for the host and which the first `--target` step now compiles instead. The four steps together compile 771 units against 994 at `88bcbf4d3` and 823 on `main`. - Margin: 506,031,716 B under the limit, 6.3 %, on image 20261004.327.1. `main`'s 7,664,839,895 B was sealed on 20260927.320.1. The one pair of figures there is for the two images is `f260e0b98`, built with full debuginfo before #752 cut it to line tables: 8,540,783,725 B on 20260927.320.1 (run 37292450697) against 8,182,940,473 B on 20261004.327.1 (run 37601225884), 357,843,252 B or 4.2 % less on the newer. So this head's figure and `main`'s are not a pair, and this head is unmeasured on the older image. - Against the same branch before the fix and before #752: 9,348,536,345 B at `88bcbf4d3` on 20260927.320.1. **`ci.yml` run 37740454881 at `9ef866436`:** `host`, `toolchain / build` and `guest / suite` success. After this lands every `host` check runs cold until the first nightly on `main` seals and saves: the path list is the cache's version. **Toolchain keys.** The fold moves the sysroot key once: `userland/.cargo/config.toml` was one of its inputs and `.cargo/config.toml` replaces it. From now on a change to any guest triple's flags moves that key. The merges of #747 and #749 moved `toyos-abi` and `toyos`, so the sysroot key moved with `main`; the key at this head was not read here. ## No new gate, test or dependency No guest test is added or changed. No dependency is added. The proof is a one-off script because its subject is this one change against its base. ## Size `git diff --shortstat origin/main...HEAD` at `dd12c0b32`: 53 files, +5454 −7765. Without the locks: 48 files, +446 −704. `src/`, `tests/toyos.rs` and `tests/common/`: 12 files, +237 −360, of which tests are roughly +65 −115 by my reading of the hunks (an estimate, not a count). `issues/`: 16 files, +49 −115. ## What I am unsure of - **The seal on the older runner image.** GitHub serves two; this branch was sealed on the newer one, at `9ef866436`. The only pair of figures for the two is `f260e0b98` with full debuginfo (above): the older image sealed it 357,843,252 B larger. Carried unscaled onto this tree that leaves 148,188,464 B under the limit on the older image; scaled by the pair's ratio, about 178 MB. Both are arithmetic, not a run. - **`dd12c0b32` itself:** `ci.yml` run 37757675374 has `host` and `toolchain / build` success at it; its `guest / suite` is what the landing waits on. #757 (`b6bcb9691`) landed on `main` after this head was merged and measured: ten source files under `kernel/src`, `toyos-abi/src`, `toyos-userbound/src` and `tests/toyos-rust-tests`, no manifest and no lock; `git merge-tree --write-tree dd12c0b b6bcb96` exits 0. Nothing here was measured with it in. It differs from the sealed head by `main`'s #749, #750, #753 and #755 and the root lock's two edges; the seal's byte count at this head is unmeasured. - **Build wall clock.** One run under load; the fold was not slower. - **Clippy reaches further.** The bare-target shapes now also lint the kernel's and the loader's path dependencies for those targets. - **The licence gate reads a superset.** `--all-features` for the kernel now turns on every member's features. It judges more than ships. - **The runner's cargo.** The nightly's `host` at `88bcbf4d3` parsed the optional `include`, so the runner's cargo accepts it. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A
The defect
SYS_THREAD_SPAWNbounded the stack and passed the entry on unread, so the new thread's first return to userland carried whatever address the caller wrote. What that does on a real processor is read, not seen: Intel's SDM hasIRETto an address that is not canonical fault in the returning instruction, in the kernel's ring, andfatal_exceptionhalts every CPU on a fault taken there, so one process could take the machine. What was seen is the unfixed kernel under TCG, which completes the return and faults at the fetch in Ring 3: the caller ends and nothing else does. RootCLAUDE.md: input that crossed a trust boundary is refused.What changed, per decision
toyos_userbound::Entryis an instruction pointer inside the user half. It is decided byis_user_addr, the tree's one notion of a user address, and has one constructor.loader::Startis the only way to a trampoline.alloc_kernel_stacktook a trampoline and three bare words; it now takesStart::{Process, Thread, Kernel}, whose two userland arms carry anEntry, andloaderno longer re-exports the trampolines. An unchecked address cannot be handed to a return to userland without namingarch::entrydirectly.sys_thread_spawnrefuses whatEntryrefuses withInvalidArgument. That covers the kernel half too, which is canonical and only ever ended the caller, because one definition is cheaper than two.PLACED_TOPis that bound, one declaration with two readers:rebase_baserefuses an image that reaches past it, andWindow::newasserts a placement window's ceiling against it. The kernel's window is aconst, so one above it does not compile. Before round 1 the window's half of this rested onALLOC_CEILINGhappening to beSTACK_BASE.Every other path by which a user-chosen instruction pointer reaches a return
e_entry)toyos-elfholds it inside the image andrebase_baseholds the image belowPLACED_TOP. It is now made anEntryas well, because the type asks.ELR_EL1is taken as an abort from EL0 after theERET.Entryapplies there all the same.kernel/src/arch/x86_64/syscall.rsrunssysretqafterpop rsp, so on an Intel processor the fault a noncanonicalrcxraises is taken in Ring 0 on the stack pointer the caller chose. That is a kernel exception frame written wherever a process points, which is more than a halt. Read from the SDM and the code, executed by nobody. Executable memory comes from two places, an image's segments and a library placed through the kernel's window, andPLACED_TOPnow bounds both;sys_mmapmakes no executable mapping.Round 1
spawn_noncanonical_entry.rs, itsRUST_SKIPentry, theMACHINE_TESTSentry and the arm. The three refusals are intests/toyos-rust-tests/src/bin/abuse_thread_table.rs, the T14'sprocess_bound_threadsrow.abuse_tls_alloc, which the brief preferred so that CI would hold the word. No QEMU boot runs a discovered Rust binary. The suite's QEMU half isMACHINE_TESTSandSCREEN_TESTS(select,tests/toyos.rs), andshared_metalis the only runner of the rest. Measured at the round's first commit:cargo test --test toyos-build -- abuse_tls_allocexits 1 withNo test matches filter Some("abuse_tls_alloc"), and the same forabuse_elf_loader,std_t,std_sync,124_atomic_counterand202_errno_per_thread. So either binary holds the word on the T14 and neither in CI;abuse_thread_tablehas a row of its own to ask for by name, and its boot carries three jobs where a shared boot carries a chunk.process_bound_threadsis owed for them. What CI's suite reaches is every boot's process and thread spawns, throughrebase_baseandalloc_kernel_stack, on both architectures:guest / suiteat this head is green, 30 of 30 (run 37752723738, job 113230606172; x86-64 under KVM, AArch64 emulated).Gates, at
b00560c4bcargo run -- --ci hostHost: 77 step(s), all green)cargo run -- --build-onlycargo test -p toyos-userboundcargo test --test toyos-build -- machine_shutdownmachine_shutdown,machine_shutdown_short_stop)cargo test --test toyos-build -- --metal --metal-readback <dir> process_bound_threadstestcases-bounds), then booted on the T14 by the orchestrator at this head: boot exit 0, judge exit 0,[metal] 1 passed, 0 failed, 1 boot(s),===TEST_END test_rs_abuse_thread_table exit=0===, image sha256db1f156d…47e10b07(#757 (comment))b00560c4bhostsuccess (job 113229625347),toolchain / buildsuccess (113229626035),guest / suitesuccess (113230606172):test result: ok. 30 passed, 30 total,[ci] Guest: 5 step(s), all greenWhere the moved assertions have run: on the T14, in the
process_bound_threadsrow above, on the fixed kernel. The whole QEMU suite ran in CI at this head, not locally. Themachine_shutdownfilter is the one local boot of the fixed kernel at this head: x86-64 under TCG,tests/testcases, every server spawned.The checks of high-risk code (the syscall ABI, a security boundary)
Negative control, measured at
a45d98269with the standalone binary and standing as the record of the defect. The whole fix reverted (kernel/,toyos-userbound/,toyos-abi/back toorigin/main, the tests kept), as a checked patch applied and restored by one script:cargo test --test toyos-build -- thread_entry_noncanonical --nocaptureEntry::newaccepts every address (m1)Host mutations, at
b00560c4b, each checked, applied, run and reversed by one script, the tree clean after (git status --porcelainempty):cargo test -p toyos-userboundm1:Entry::newaccepts every addressan_entry_is_an_address_of_the_user_half_and_nothing_elsem2:rebase_basebounds byUSER_TOPan_image_that_reaches_the_last_page_of_the_user_half_is_refusedm3:Window::newadmits a ceiling ofUSER_TOPa_window_reaching_the_last_page_of_the_user_half_is_a_kernel_bugm1was not booted on the T14, and round 2's review ruled it is not owed.m1reds on the host at this head, and removing the check fromsys_thread_spawndoes not compile, sincespawn_threadtakes anEntry. On the T14 anm1kernel is the removed defect on an Intel processor; the likely reading is a halted machine, which measures the severity of what was removed and nothing about this change.Independent oracle. Intel's SDM, for
IRETand forSYSRET. CI's runner is not an instrument for either claim: the workflow does not choose its processor's vendor, and where a return to a noncanonical address faults is vendor behaviour, documented so forSYSRET. The machine that could execute the dangerous half is the T14, which the issue named, withcontrol-revert.patchapplied. The review rules that reading is not required to land, since the fixed kernel executes no return to an addressEntryrefuses.Why the behaviour is on a metal row
The type holds that no unchecked entry reaches a trampoline, and the host table holds the predicate. Neither reaches the word the syscall answers its caller:
kernel/src/syscall/links into no host test, and the refusal, as opposed to a kill or another error word, is whatstdandlibcwill read. A metal row reaches it, in a binary that already spawns raw threads on mapped stacks and asserts a refusal by name, on a boot the table already has. No guest test is added or changed.What I am unsure of
rebase_baseandWindow::newon the host and nowhere else.Entry::newrefusal is unreachable whiletoyos-elfandrebase_basehold. It is there because the type has no unchecked constructor, and it refuses instead of panicking because the number is the file's.The issue
issues/a-thread-entry-is-unchecked-and-a-noncanonical-one-is-measured-only-under-tcg.mdwas filed onwt/toyos-audit-kernel(pull request #754, since closed without landing) and is not onmain. This branch carries it unchanged in its first commit and deletes it in the fix, with its one durable line atsys_thread_spawn. No other file cites the slug.Growth
git diff --shortstat origin/main...b00560c4b: 10 files changed, 123 insertions(+), 40 deletions(-). Tests are +38 −7 (span.rstests +18 −3,place.rstests +7 −3,abuse_thread_table.rs+13 −1); production is the rest, +85 −33: the type and the bound intoyos-userbound,Startin the loader, the refusal.🤖 Generated with Claude Code
https://claude.ai/code/session_01RvnWQFcMuGqTHYhvSnTe8A