Repository navigation
Nothing ships for tests alone: the five virt probes leave the shipped toybox, and two owner rulings enter reviewer.md - #636
Conversation
CLAUDE.md's Principles gains a bullet after "Fail fast, trust nothing.": nothing ships for tests alone. The orchestrator's brief gives it as the owner's ruling of 2026-09-30, "i dont want to write kernel functionality just for tests thats never used in production", confirmed with "yes". Six accepted process rules go where the agent that applies them reads them. Every addition is a pure insertion. - reviewer.md, Tests: a check that a judge refuses something plants a sibling the judge must accept; a mutation counts once it is shown to build; a pull request body names a gate only under the head it ran at. - reviewer.md, Growth: the pull request that meets a track milestone's exit deletes that milestone from its track. - reviewer.md, Prose: a removal's sweep deletes the present-tense, forward-looking and instructive statements of what it removed and leaves dated, commit- or pull-request-anchored records of runs. - orchestrator.md, Agents: a brief asks for the agent's own attribution lines, never pastes the orchestrator's. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
The owner's rulings of 2026-09-30, given to the orchestrator:
"the agents should be able to talk to everyone and collaborate. move
cpu heavy stuff as requests to you the orchestrator you can then queue
them and bundle them and tell an agent maybe that it doesnt make sense.
i dont like useless overtesting it just slows us down. we do one full
test run if the pr is done anyways."
On negative-control checks, to the orchestrator: "its not your job to
verify or confirm stuff concerning point 3".
CI already runs the host checks on every push, so agents do not repeat
that gate locally.
The owner then asked for these edits to be retried ("try to let the agent
make the edits again") after the permission check refused them on the
previous round.
- implementer.md gains SendMessage in its tools: without it an
implementer cannot message the orchestrator or an overlapping agent.
- implementer.md, "The brief is a fence": an agent may message any agent
whose work overlaps its own.
- implementer.md, "Measure, build, test": CI runs the host checks; an
agent runs only targeted checks, and minutes of CPU beyond that are a
request to the orchestrator, who queues, bundles or declines it. Test to
prove the change, not to cover the tree.
- orchestrator.md, Agents: every brief names the agents on overlapping
code. Land: one full guest run of a finished pull request and its metal
runs are the orchestrator's; negative controls are the implementer's
and the reviewer's.
Deleted, because the rulings contradict them:
- implementer.md: the "Host tests only: `cargo run -- --ci host`, and
`cargo run -- --build-only` at most ..." bullet.
- CLAUDE.md: "An agent verifies with host tests and builds the image at
most;" ("Agents never run QEMU." and the orchestrator's clause stay).
- orchestrator.md: the mutation-loop sentences, "A mutation loop, guest
or metal, starts on a clean worktree ... None runs while an agent edits
that worktree."
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
|
Review of 6c11159. CI BLOCKER
NOTE
REMOVE
SEND BACK |
Main's edits to the four instruction files merged without conflict and every hunk of both sides stands: implementer.md keeps main's "never run `git submodule` in a linked worktree" beside this branch's bullets; orchestrator.md keeps main's "a landing that changes how agents work" beside this branch's Land line; reviewer.md keeps main's Instructions and Arrivals bullets; CLAUDE.md keeps main's Dependencies rewrite and its pointer to Arrivals beside this branch's principle and QEMU line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
The owner ruled that nothing ships for tests alone and that the five toybox applets `tests/virtjobcase` and `tests/virtsmpcase` run move out of the shipped image, so the rule is true when it lands. Code: - `userland/kernelprobe` is a new userland workspace member, named only by the two virt cases' `system.toml`: `debug_refused`, `first_entry`, `fp_isolation`, `preempt` and `unmap_touch`, moved from toybox by `git mv` unchanged, dispatched on argv[0], and an unknown name panics. - Toybox loses the five and its `arch` module. The x86-64 half of that module is deleted rather than moved: it existed so the shipped x86-64 toybox compiled, and no x86-64 config names kernelprobe, so nothing would compile it. kernelprobe built for x86-64 fails at its imports. - `issues/design-debt/the-x86-toybox-ships-two-applets-that-only-panic.md` is closed: the x86-64 toybox answers neither name. No citation named it. - `issues/design-debt/the-shipped-image-carries-three-test-programs.md` files what the principle still finds in the shipped `system.toml`: `proctest`, `input-test` and toybox's `spin`. Rules (root `CLAUDE.md`, `.claude/agents/*.md`): - The principle is the owner's sentence. Its "tests exercise what ships" clause and the fault-injection sentence the review found permissive are gone. - Dependencies gains the owner's LLVM-and-Rust sentence; the firmware paragraph admits the CPU's own microcode, which the kernel loads. - implementer.md: CI's host check runs on every push to a pull request marked ready, not on a draft (`.github/workflows/ci.yml`'s `host` condition). "One targeted check per claim" now sits beside the two checks for high-risk code and the review's mutations. Negative controls are the implementer's and the reviewer's to design and judge. Guest and T14 runs and compiler or LLVM builds go through one channel, the request file and a final `RUN REQUESTED:` line; SendMessage-to-main as a second channel is gone, and a sysroot build stays the implementer's so an ABI brief can build. An agent may message any agent. Attribution lines are the agent's own, a standing rule moved here from orchestrator.md, where it contradicted "a brief carries only the task". - orchestrator.md: runs the guest and T14 runs requested and judges none; the mutation-loop procedure deleted in round one is back, since the orchestrator still runs those loops. - reviewer.md: the rationale tails and the `-D warnings` example go; the removal-sweep sentence defers to Instructions and keeps no run record that cites a deleted document. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
Review round 1 on 7eb4428. - The microcode ruling orders the kernel to load what root CLAUDE.md's firmware rule still bars: it admits only device firmware that never executes on the CPU. PR #636 amends that sentence to admit the CPU's own microcode, which the kernel loads, and this branch lands after it. The defect regains the deleted question's pointer at that rule, as one line saying the rule admits the microcode once #636 lands. - Step 6 of the small-kernel track loses its heading "The scheduler knows nothing about devices": step 5 now keeps the pass painting the panel, a device the pass reaches. - The child-process track's stage headings lose " (ruled)", and stage 3 loses "ruled; ": every stage carried it, so it distinguished nothing. The track is 35 bytes longer than on main (18608 -> 18643), at 260 lines. - doom.jpg's row loses "he keeps it, and rules that": the committed file shows it is kept, and "no licence question applies" carries the ruling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
…table is tested, and Data Size 0 is refused Review round 1 of #653 found a mutant (`is_none_or` -> `is_some_and` at the extended-table size check) under which a 4..16-byte table that sums to 0 panicked at `table[EXT_HEADER..]`. The file is untrusted input, so the fix is structural rather than one more test: - The crate denies `clippy::indexing_slicing`, `arithmetic_side_effects`, `unwrap_used`, `expect_used`, `panic`, `panic_in_result_fn`, `unreachable`, `todo` and `unimplemented`, so `--clippy` refuses any future site that could panic on input. The tests allow them back: a test fails by panicking. - Every length is taken by a checked split whose failure is a `Refusal`: the header by `split_first_chunk`, data and table by `split_at_checked`, the extended table's header by `split_first_chunk`, so its entries exist only as the remainder of a table that held a header. The size check compares `count.checked_mul(12)` with the entries' length. - Header fields are read by destructuring a fixed `[u32; N]` out of a fixed `[u8; 4N]`; a mismatched N does not build. `Update` keeps the parsed revision, signature, flags, data and entries, so nothing re-reads bytes. Intel's `intel-ucode/06-cc-02` at microcode-20260925 (bdc92abe), unmodified, 165,888 bytes, sha256 4e43bb4d..., is committed with its NOTICE and COMMITTED_FILES rows: one update, revision 0x11c, flags 0x94, with four extended signatures. A test selects its last, 0xe0652, to Load below 0x11c. `build()` writes entry checksums with the formula the parser checks, so only Intel's bytes test that formula. Data Size 0 stands for 2000 bytes of data in updates for CPUs from long before ToyOS's 2020 floor; the arm and `DEFAULT_DATA` go, and Data Size 0 is refused as `ZeroDataSize`. An empty file is refused as truncated rather than answering `NoMatch`: a file with no update is not an Intel file. New tests kill the four survivors the review named: a table shorter than its header, a count naming fewer entries than the table holds, the T14's entry first in the extended table, and a Total Size of 2560. A valid 3072-byte update kills a 2048-byte granule. NOTICE: one section for `toyos-microcode/intel-ucode/*`; the copy of the notice and disclaimer goes (licenses/ carries it), as does the paragraph that restated the crate's doc. The issue loses the line about #636 and the sentence that pointed at that copy; `src/licence.rs` loses the comment that restated its row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L
The four conflicts are the four prompts: CLAUDE.md, implementer.md, orchestrator.md and reviewer.md. Each is taken from main whole. #678 rewrote them from the owner's later decisions, which override every prompt rule this branch wrote on 2026-09-30; none of this branch's prompt hunks survives. Everything else merged without a conflict: the kernelprobe move, the two case configs, the closed issue and the filed one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
…; one Dependencies sentence is said directly
reviewer.md, Fit: "Nothing ships for tests alone", the owner's ruling of
2026-09-30, as the sentence a reviewer applies to a program, applet or kernel
path a diff adds.
reviewer.md, Forks: the owner's "LLVM and Rust are never changed to suit a
ToyOS tool; a tool that cannot build them unchanged is the one that goes."
implementer.md: the wait recipe was "under an explicit `timeout`". macOS has
no `timeout` command (`command -v timeout` exits 1 on this host), and three
agents reported it on 2026-10-02. The recipe is now a counted `until` loop,
run to its bound and to its condition under /bin/sh, bash, zsh and dash here.
CLAUDE.md, Dependencies: the packaging-mirror sentence read as a
contradiction ("cannot build without changing it" beside "unmodified"). One
sentence replaces it with the same intent, as the owner approved: source is
used unmodified and pinned by hash wherever only packaging has to change, and
a fork is for source changes only.
The filed issue cites the ruling where it now lives, and its `spin` row is
made true of main after #660: the two guest cases that ran `spin` are gone,
and the one use the tree names is a metal row the guest-suite track owes.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
|
Evidence for the body at The check ( #!/bin/sh
# Exit 0: no image named carries a kernel probe. Exit 1: one does.
# Exit 2: an image holds no toybox, so the check read nothing.
status=0
for img in "$@"; do
seen=$(LC_ALL=C grep -a -c -F -e 'toybox: unknown command' "$img")
echo "$img: toybox's own refusal line: $seen"
[ "$seen" -ge 1 ] || { echo "$img: no toybox in it, so the check read nothing"; exit 2; }
for needle in debug_refused first_entry fp_isolation unmap_touch 'preempt: the counting thread'; do
n=$(LC_ALL=C grep -a -c -F -e "$needle" "$img")
echo "$img: $needle: $n"
[ "$n" -eq 0 ] || status=1
done
done
exit $statusGreen arm, on the images Negative control ( #!/bin/sh
# The whole change reverted onto the head the green arm was measured on: the
# tree becomes origin/main's. Builds both shipped images there, runs the image
# check, restores, rebuilds and checks again.
S=/Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/636-recut
cd /Users/jan/Dev/jan/toyos-rulesbatch || exit 9
echo "head: $(git rev-parse HEAD)"
echo "status before: [$(git status --porcelain --ignore-submodules=none)]"
git apply --check $S/revert-to-main.patch; echo "APPLY_CHECK=$?"
git apply $S/revert-to-main.patch; echo "APPLY=$?"
git diff --quiet origin/main; echo "TREE_DIFFERS_FROM_ORIGIN_MAIN=$?"
cargo run -- --build-only > $S/nc-build-x86.log 2>&1; echo "REVERTED_BUILD_X86=$?"
cargo run -- --build-only --arch aarch64 > $S/nc-build-aarch64.log 2>&1; echo "REVERTED_BUILD_AARCH64=$?"
sh $S/shipped-check.sh target/bootable.img target/bootable.aarch64.img; echo "REVERTED_CHECK=$?"
git apply -R $S/revert-to-main.patch; echo "RESTORE=$?"
echo "status after: [$(git status --porcelain --ignore-submodules=none)]"
cargo run -- --build-only > $S/nc-rebuild-x86.log 2>&1; echo "RESTORED_BUILD_X86=$?"
cargo run -- --build-only --arch aarch64 > $S/nc-rebuild-aarch64.log 2>&1; echo "RESTORED_BUILD_AARCH64=$?"
sh $S/shipped-check.sh target/bootable.img target/bootable.aarch64.img; echo "RESTORED_CHECK=$?"
echo "status at end: [$(git status --porcelain --ignore-submodules=none)]"Its log. The reverted working tree is The revert patch is revert-to-main.patchdiff --git a/.claude/agents/implementer.md b/.claude/agents/implementer.md
index 447fb5868..c88fb33d7 100644
--- a/.claude/agents/implementer.md
+++ b/.claude/agents/implementer.md
@@ -32,10 +32,9 @@ guess. Then build, then test on root `CLAUDE.md`'s tiers before anyone reviews:
- A result is the command's own exit code: `<cmd> > <file> 2>&1; echo EXIT=$?`. A grepped
`test result` line is not one, and a gate you did not run is a gate you do not claim.
- Long commands run in the background with output to a file under the scratchpad the brief names.
- Stay inside one turn while anything runs: block in the foreground on `n=0; until <it is done> ||
- [ $((n+=1)) -gt 50 ]; do sleep 2; done`, which its count bounds, print a line, repeat; a long
- `sleep` is refused. Ten minutes of silence kills you, and ending a turn to announce a wait strands
- the work.
+ Stay inside one turn while anything runs: block in the foreground on `until <it is done>; do sleep
+ 2; done` under an explicit `timeout`, print a line, repeat; a long `sleep` is refused. Ten minutes
+ of silence kills you, and ending a turn to announce a wait strands the work.
- Nothing a pull request's evidence rests on, mutation patches and run logs included, lives only in
a temporary directory: `/tmp` is wiped when the CLI restarts. Post mutation patches to the pull
request as a comment.
diff --git a/.claude/agents/reviewer.md b/.claude/agents/reviewer.md
index 40ea29dd4..6d1883b3d 100644
--- a/.claude/agents/reviewer.md
+++ b/.claude/agents/reviewer.md
@@ -50,8 +50,6 @@ if it meets the bar above; otherwise it is a NOTE.
`arch::aarch64` in no generic kernel code; and none of them in a crate whose manifest
`description` says pure.
A `4096` in the kernel that means a page is a private copy of `mm::PAGE_SIZE`.
- Nothing ships for tests alone: a program, an applet or a kernel path whose only user is a test
- lives in a test image or a test kernel, and one a shipped image carries is a BLOCKER.
- **Hosts.** A BLOCKER: a change that makes an app build on fewer of Linux under Wayland, macOS
and Windows, or answers a host build failure by making the app ToyOS-only — by a split, a `cfg`
that compiles what it does out of a host, or an `exempt` in its manifest — instead of fixing it
@@ -129,8 +127,6 @@ if it meets the bar above; otherwise it is a NOTE.
- **Forks.** A broken rule of `implementer.md`'s "A fork" is a BLOCKER, and so is a lockfile or
gitlink left naming a fork branch whose consumed commit the pull request changes. A search for
callers that skipped the fork clones and `~/.cargo/git/checkouts/` searched part of the tree.
- LLVM and Rust are never changed to suit a ToyOS tool; a tool that cannot build them unchanged is
- the one that goes, and a diff that changes either for one is a BLOCKER.
## Prose
diff --git a/CLAUDE.md b/CLAUDE.md
index f2a665b4d..4c8de6657 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -55,7 +55,7 @@ There are no spec documents. A rule a reader can check lives in an agent's promp
## Dependencies
-**Rust** and **QEMU** for development, on any host OS and architecture — the development machine is nothing special. Beside them, where no Rust tool does the job, only C or C++ tools ToyOS can one day build and run (Python, Perl, CMake, make), each declared where `reviewer.md`'s Arrivals says. No binary for one host OS alone: a macOS binary is a hard no, and "only for tests" does not soften it. ToyOS's own code is Rust; it writes no Python, Perl or shell of its own. Only general and widely used crates — one that does *our* job we write ourselves, and a driver crate never; third-party crates are used as published, and a fork carries a change written to upstream quality and goes when upstream has it. No upstream pull request is sent for now. Third-party source is used unmodified, pinned by hash, wherever only its packaging has to change; a fork is for source changes only. The north star is **self-hosting**: ToyOS rebuilds itself on ToyOS and reproduces the host's bytes, and nothing — build, test, or verification — rests on a host binary; a bootstrap from source with no binary seed is out of scope. Ask of anything new: could this ever run inside ToyOS?
+**Rust** and **QEMU** for development, on any host OS and architecture — the development machine is nothing special. Beside them, where no Rust tool does the job, only C or C++ tools ToyOS can one day build and run (Python, Perl, CMake, make), each declared where `reviewer.md`'s Arrivals says. No binary for one host OS alone: a macOS binary is a hard no, and "only for tests" does not soften it. ToyOS's own code is Rust; it writes no Python, Perl or shell of its own. Only general and widely used crates — one that does *our* job we write ourselves, and a driver crate never; third-party crates are used as published, and a fork carries a change written to upstream quality and goes when upstream has it. No upstream pull request is sent for now. A third-party source ToyOS cannot build without changing it is carried as an unmodified-source packaging mirror with a byte-identity gate, not as a fork. The north star is **self-hosting**: ToyOS rebuilds itself on ToyOS and reproduces the host's bytes, and nothing — build, test, or verification — rests on a host binary; a bootstrap from source with no binary seed is out of scope. Ask of anything new: could this ever run inside ToyOS?
Vendor firmware a device or CPU verifies by its maker's signature may be shipped: pinned by version and hash, redistributable unmodified, recorded in `NOTICE`. A device's is loaded only by its own driver through its IOMMU domain and never executes on the CPU; CPU microcode is loaded by the kernel. `NOTICE` names every committed third-party file with its hash, upstream and licence.
diff --git a/issues/design-debt/the-shipped-image-carries-three-test-programs.md b/issues/design-debt/the-shipped-image-carries-three-test-programs.md
deleted file mode 100644
index f43b8c0cc..000000000
--- a/issues/design-debt/the-shipped-image-carries-three-test-programs.md
+++ /dev/null
@@ -1,26 +0,0 @@
----
-status: open
-kind: defect
-opened: 2026-09-30
----
-
-# The shipped image carries three test programs
-
-"Nothing ships for tests alone" (`.claude/agents/reviewer.md`, Fit) is untrue
-of the shipped `system.toml` in three rows:
-
-- `proctest = {}`: `userland/proctest/README.md` calls it a test harness, and
- nothing in the tree runs `/system/bin/proctest` but `proctest` itself.
-- `input-test = {}`: a test utility by its README, and nothing runs
- `/system/bin/input-test`; `toyos-symbols/tests/real.rs` reads a frozen copy
- under `toyos-symbols/tests/fixtures/`, not the shipped binary.
-- `"bin/spin" = "/system/bin/toybox"`: `userland/toybox/src/spin.rs` loops
- forever, and nothing in the tree runs it; its one named use is a test, the
- metal `sshd_exec` row
- `issues/build/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md` owes.
-
-Found by `git grep -n -w -e proctest -e input-test` and
-`git grep -n -w -e spin -- system.toml tests userland issues/build`.
-
-**Exit condition**: the shipped `system.toml` names none of the three, and a
-test that needs one carries it in its own image alone.
diff --git a/issues/design-debt/the-x86-toybox-ships-two-applets-that-only-panic.md b/issues/design-debt/the-x86-toybox-ships-two-applets-that-only-panic.md
new file mode 100644
index 000000000..7dae8903a
--- /dev/null
+++ b/issues/design-debt/the-x86-toybox-ships-two-applets-that-only-panic.md
@@ -0,0 +1,18 @@
+---
+status: open
+kind: defect
+opened: 2026-09-29
+---
+
+# The x86-64 toybox ships two applets that only panic
+
+`userland/toybox/src/main.rs` puts `fp_isolation` and `first_entry` in one
+`commands!` list for every architecture, so the x86-64 toybox that ships in
+every image answers both names, and `userland/toybox/src/arch/x86_64.rs`'s
+body for each is a `panic!` naming where x86-64's probe lives or is owed
+(`test_rs_fpu_isolation`,
+`issues/isolation/a-new-x86-thread-enters-ring-3-holding-kernel-register-values.md`).
+A shipping binary carries two commands whose only behaviour is to die.
+
+**Exit condition**: the x86-64 toybox answers neither name with a panic —
+each is left out of its `commands!`, or runs a probe of x86-64's own.
diff --git a/tests/toyos.rs b/tests/toyos.rs
index 4011139c0..db3d987a6 100644
--- a/tests/toyos.rs
+++ b/tests/toyos.rs
@@ -1365,7 +1365,7 @@ fn judge_virt_job(mut qemu: QemuInstance, job: &str, said: &str) -> Result<Strin
Ok(serial)
}
-/// What `unmap_touch` says once every read of a page just unmapped,
+/// What toybox's `unmap_touch` says once every read of a page just unmapped,
/// on the unmapping thread and on another, ended its process.
const UNMAP_TOUCH_SAID: &str =
"unmap_touch: 4 reads of a page just unmapped on the unmapping thread, and 4 on another";
@@ -1732,7 +1732,7 @@ fn run_screen_test(name: &str, profile: qemu::Profile, test_config: &Path) -> Re
Ok(())
}
"virt_timer_preempts" => {
- // Spelled in `userland/kernelprobe/src/preempt.rs`.
+ // Spelled in `userland/toybox/src/preempt.rs`.
virt_job(profile, "preempt", "preempt: the counting thread was preempted twice")
}
"virt_fp_isolation" => virt_job(profile, "fp_isolation", "fp_isolation: v0-v31, FPCR and FPSR survived"),
diff --git a/tests/virtjobcase/system.toml b/tests/virtjobcase/system.toml
index 838f3dda1..190d1b33a 100644
--- a/tests/virtjobcase/system.toml
+++ b/tests/virtjobcase/system.toml
@@ -1,4 +1,4 @@
-# One CPU and the AArch64 guest's jobs, each a kernelprobe applet or, last because
+# One CPU and the AArch64 guest's jobs, each a toybox applet or, last because
# a kernel it fails on rewrites the clock page every reader asserts on, the
# suite's `test_rs_abuse_readonly_copyout`, which the harness puts on ROOT;
# each job's line comes only if the kernel kept what that job asks about.
@@ -15,11 +15,11 @@ receives = ["power"]
# CPU, and a job past the bound reboots the guest with the job named.
args = ["--bound-ms=240000", "preempt", "fp_isolation", "first_entry", "unmap_touch", "debug_refused", "test_rs_abuse_readonly_copyout"]
-[programs.kernelprobe]
+[programs.toybox]
[symlinks]
-"bin/preempt" = "/system/bin/kernelprobe"
-"bin/fp_isolation" = "/system/bin/kernelprobe"
-"bin/first_entry" = "/system/bin/kernelprobe"
-"bin/unmap_touch" = "/system/bin/kernelprobe"
-"bin/debug_refused" = "/system/bin/kernelprobe"
+"bin/preempt" = "/system/bin/toybox"
+"bin/fp_isolation" = "/system/bin/toybox"
+"bin/first_entry" = "/system/bin/toybox"
+"bin/unmap_touch" = "/system/bin/toybox"
+"bin/debug_refused" = "/system/bin/toybox"
diff --git a/tests/virtsmpcase/system.toml b/tests/virtsmpcase/system.toml
index bd0e9c302..e7dfb0bc4 100644
--- a/tests/virtsmpcase/system.toml
+++ b/tests/virtsmpcase/system.toml
@@ -13,7 +13,7 @@ receives = ["power"]
# the bound reboots the guest with the job named.
args = ["--bound-ms=240000", "unmap_touch"]
-[programs.kernelprobe]
+[programs.toybox]
[symlinks]
-"bin/unmap_touch" = "/system/bin/kernelprobe"
+"bin/unmap_touch" = "/system/bin/toybox"
diff --git a/userland/Cargo.lock b/userland/Cargo.lock
index 5aa43186b..b21a2ec8e 100644
--- a/userland/Cargo.lock
+++ b/userland/Cargo.lock
@@ -1886,13 +1886,6 @@ dependencies = [
"rand_core 0.10.0",
]
-[[package]]
-name = "kernelprobe"
-version = "0.1.0"
-dependencies = [
- "toyos-abi",
-]
-
[[package]]
name = "kurbo"
version = "0.13.0"
diff --git a/userland/Cargo.toml b/userland/Cargo.toml
index 5533dec5f..c2aa2e8f8 100644
--- a/userland/Cargo.toml
+++ b/userland/Cargo.toml
@@ -15,7 +15,6 @@ members = [
"init",
"input-test",
"inspect",
- "kernelprobe",
"logd",
"metalprobe",
"netd",
diff --git a/userland/kernelprobe/Cargo.toml b/userland/kernelprobe/Cargo.toml
deleted file mode 100644
index 09c5a5ae8..000000000
--- a/userland/kernelprobe/Cargo.toml
+++ /dev/null
@@ -1,9 +0,0 @@
-[package]
-name = "kernelprobe"
-version = "0.1.0"
-edition = "2021"
-license = "MIT OR Apache-2.0"
-description = "Test-only multi-call binary: the kernel probes the AArch64 guest's virt jobs run, one per name it is invoked by."
-
-[dependencies]
-toyos-abi = { path = "../../toyos-abi" }
diff --git a/userland/kernelprobe/src/arch/mod.rs b/userland/kernelprobe/src/arch/mod.rs
deleted file mode 100644
index 0af20cbff..000000000
--- a/userland/kernelprobe/src/arch/mod.rs
+++ /dev/null
@@ -1,6 +0,0 @@
-//! What the probes must say in assembly, one module per architecture.
-
-#[cfg(target_arch = "aarch64")]
-mod aarch64;
-#[cfg(target_arch = "aarch64")]
-pub use aarch64::*;
diff --git a/userland/kernelprobe/src/main.rs b/userland/kernelprobe/src/main.rs
deleted file mode 100644
index 4728869ff..000000000
--- a/userland/kernelprobe/src/main.rs
+++ /dev/null
@@ -1,20 +0,0 @@
-mod arch;
-mod debug_refused;
-mod preempt;
-mod unmap_touch;
-
-use arch::{first_entry, fp_isolation};
-
-fn main() {
- let mut args = std::env::args();
- let invoked_as = args.next().expect("argv[0] names the probe");
- let args: Vec<String> = args.collect();
- match std::path::Path::new(&invoked_as).file_name().and_then(|n| n.to_str()) {
- Some("debug_refused") => debug_refused::main(args),
- Some("first_entry") => first_entry::main(args),
- Some("fp_isolation") => fp_isolation::main(args),
- Some("preempt") => preempt::main(args),
- Some("unmap_touch") => unmap_touch::main(args),
- other => panic!("kernelprobe: no probe is called {other:?}"),
- }
-}
diff --git a/userland/kernelprobe/src/arch/aarch64.rs b/userland/toybox/src/arch/aarch64.rs
similarity index 100%
rename from userland/kernelprobe/src/arch/aarch64.rs
rename to userland/toybox/src/arch/aarch64.rs
diff --git a/userland/toybox/src/arch/mod.rs b/userland/toybox/src/arch/mod.rs
new file mode 100644
index 000000000..95e0e9d15
--- /dev/null
+++ b/userland/toybox/src/arch/mod.rs
@@ -0,0 +1,11 @@
+//! What toybox's applets must say in assembly, one module per architecture.
+
+#[cfg(target_arch = "aarch64")]
+mod aarch64;
+#[cfg(target_arch = "aarch64")]
+pub use aarch64::*;
+
+#[cfg(target_arch = "x86_64")]
+mod x86_64;
+#[cfg(target_arch = "x86_64")]
+pub use x86_64::*;
diff --git a/userland/toybox/src/arch/x86_64.rs b/userland/toybox/src/arch/x86_64.rs
new file mode 100644
index 000000000..c08523a99
--- /dev/null
+++ b/userland/toybox/src/arch/x86_64.rs
@@ -0,0 +1,51 @@
+//! x86-64's half of `unmap_touch`; its FP and first-entry probes are
+//! elsewhere, or owed.
+
+pub mod fp_isolation {
+ pub fn main(_args: Vec<String>) {
+ panic!("fp_isolation probes AArch64's FP/SIMD switch; x86-64's is test_rs_fpu_isolation");
+ }
+}
+
+pub mod first_entry {
+ pub fn main(_args: Vec<String>) {
+ panic!(
+ "first_entry probes AArch64's first entry to EL0; x86-64's is owed by \
+ issues/isolation/a-new-x86-thread-enters-ring-3-holding-kernel-register-values.md"
+ );
+ }
+}
+
+/// Reads the word at `page`, unmaps the `len` bytes there with `SYS_MUNMAP`
+/// and reads the word again, in one block, so nothing but the unmap itself
+/// can switch the CPU between the reads. Answers the first read, the unmap's
+/// answer and the second read.
+///
+/// # Safety
+/// `page` starts a mapping of `len` bytes that nothing else names. The second
+/// read ends the process unless the unmap was refused or left its
+/// translation standing.
+pub unsafe fn read_unmap_read(page: *const u64, len: usize) -> (u64, u64, u64) {
+ let (first, answer, second): (u64, u64, u64);
+ // SAFETY: the caller's; the two loads name `page` and the `syscall` is
+ // the ABI's (`toyos_abi::syscall`), which clobbers rax, rcx and r11.
+ unsafe {
+ core::arch::asm!(
+ "mov {first}, qword ptr [{page}]",
+ "syscall",
+ "mov {second}, qword ptr [{page}]",
+ page = in(reg) page,
+ first = out(reg) first,
+ second = lateout(reg) second,
+ in("rdi") toyos_abi::syscall::SYS_MUNMAP,
+ in("rsi") page,
+ in("rdx") len,
+ in("r8") 0u64,
+ in("r9") 0u64,
+ out("rax") answer,
+ out("rcx") _,
+ out("r11") _,
+ );
+ }
+ (first, answer, second)
+}
diff --git a/userland/kernelprobe/src/debug_refused.rs b/userland/toybox/src/debug_refused.rs
similarity index 100%
rename from userland/kernelprobe/src/debug_refused.rs
rename to userland/toybox/src/debug_refused.rs
diff --git a/userland/toybox/src/main.rs b/userland/toybox/src/main.rs
index 9e3df6877..449ce6e89 100644
--- a/userland/toybox/src/main.rs
+++ b/userland/toybox/src/main.rs
@@ -1,5 +1,7 @@
+mod arch;
mod cat;
mod cp;
+mod debug_refused;
mod echo;
mod free;
mod grep;
@@ -9,6 +11,7 @@ mod ls;
mod mkdir;
mod mv;
mod net;
+mod preempt;
mod ps;
mod pwd;
mod reboot;
@@ -18,6 +21,7 @@ mod shutdown;
mod spin;
mod stats;
mod tone;
+mod unmap_touch;
macro_rules! commands {
($($name:ident),*) => {
@@ -30,7 +34,9 @@ macro_rules! commands {
};
}
-commands!(cat, cp, echo, free, grep, hexdump, locale, ls, mkdir, mv, net, ps, pwd, reboot, rm, screen, shutdown, spin, stats, tone);
+use arch::{first_entry, fp_isolation};
+
+commands!(cat, cp, debug_refused, echo, first_entry, fp_isolation, free, grep, hexdump, locale, ls, mkdir, mv, net, preempt, ps, pwd, reboot, rm, screen, shutdown, spin, stats, tone, unmap_touch);
fn main() {
let args: Vec<String> = std::env::args().collect();
diff --git a/userland/kernelprobe/src/preempt.rs b/userland/toybox/src/preempt.rs
similarity index 100%
rename from userland/kernelprobe/src/preempt.rs
rename to userland/toybox/src/preempt.rs
diff --git a/userland/kernelprobe/src/unmap_touch.rs b/userland/toybox/src/unmap_touch.rs
similarity index 100%
rename from userland/kernelprobe/src/unmap_touch.rs
rename to userland/toybox/src/unmap_touch.rs
|
|
Review of Earlier BLOCKERs (the review of
Growth. The three briefed edits, against the brief
BLOCKER
NOTE
REMOVE
Read for this review, under SEND BACK |
…refuses another architecture by name Answers #636 (comment). reviewer.md, Fit: "and one a shipped image carries is a BLOCKER" ranked the image's state, so the sentence sent back every branch while system.toml ships the three programs issues/design-debt/the-shipped-image-carries-three-test-programs.md records. It now ranks a diff, as the Forks clause beside it does: "and a diff that ships one is a BLOCKER". The orchestrator briefed the words. userland/kernelprobe/src/arch/mod.rs: the selector refuses any architecture but AArch64 with a compile_error!. Before, a build for x86_64-unknown-toyos died at `unresolved imports arch::first_entry, arch::fp_isolation`, which names what is missing and not why. With the compile_error! rustc stops at it and prints that one error. tests/toyos.rs: the comment over `virt_timer_preempts` named the file its line is spelled in. It is deleted, and the arm is one line like its four siblings. implementer.md: ", which its count bounds" said what the recipe's `-gt 50` says, and is deleted; the paragraph is rewrapped and no other word changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
|
Evidence for the body at The check ( #!/bin/sh
# Exit 0: no image named carries a kernel probe. Exit 1: one does.
# Exit 2: an image holds no toybox, so the check read nothing.
status=0
for img in "$@"; do
seen=$(LC_ALL=C grep -a -c -F -e 'toybox: unknown command' "$img")
echo "$img: toybox's own refusal line: $seen"
[ "$seen" -ge 1 ] || { echo "$img: no toybox in it, so the check read nothing"; exit 2; }
for needle in debug_refused first_entry fp_isolation unmap_touch 'preempt: the counting thread'; do
n=$(LC_ALL=C grep -a -c -F -e "$needle" "$img")
echo "$img: $needle: $n"
[ "$n" -eq 0 ] || status=1
done
done
exit $statusGreen arm, on the images On the two toybox binaries themselves, Negative control ( #!/bin/sh
# The whole change reverted onto the head the green arm was measured on: the
# tree becomes origin/main's. Builds both shipped images there, runs the image
# check, restores, rebuilds and checks again.
S=/Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/636-r2
cd /Users/jan/Dev/jan/toyos-rulesbatch || exit 9
echo "head: $(git rev-parse HEAD)"
echo "origin/main: $(git rev-parse origin/main)"
echo "status before: [$(git status --porcelain --ignore-submodules=none)]"
git diff --binary HEAD origin/main > $S/revert-to-main.patch; echo "PATCH=$?"
git apply --check $S/revert-to-main.patch; echo "APPLY_CHECK=$?"
git apply $S/revert-to-main.patch; echo "APPLY=$?"
rm -f $S/nc.index
GIT_INDEX_FILE=$S/nc.index git add -A; echo "SCRATCH_ADD=$?"
echo "reverted tree: $(GIT_INDEX_FILE=$S/nc.index git write-tree)"
echo "origin/main tree: $(git rev-parse 'origin/main^{tree}')"
rm -f $S/nc.index
cargo run -- --build-only > $S/nc-build-x86.log 2>&1; echo "REVERTED_BUILD_X86=$?"
cargo run -- --build-only --arch aarch64 > $S/nc-build-aarch64.log 2>&1; echo "REVERTED_BUILD_AARCH64=$?"
sh $S/shipped-check.sh target/bootable.img target/bootable.aarch64.img; echo "REVERTED_CHECK=$?"
git apply -R $S/revert-to-main.patch; echo "RESTORE=$?"
echo "status after: [$(git status --porcelain --ignore-submodules=none)]"
cargo run -- --build-only > $S/nc-rebuild-x86.log 2>&1; echo "RESTORED_BUILD_X86=$?"
cargo run -- --build-only --arch aarch64 > $S/nc-rebuild-aarch64.log 2>&1; echo "RESTORED_BUILD_AARCH64=$?"
sh $S/shipped-check.sh target/bootable.img target/bootable.aarch64.img; echo "RESTORED_CHECK=$?"
echo "status at end: [$(git status --porcelain --ignore-submodules=none)]"
echo "head at end: $(git rev-parse HEAD)"Its log. The revert patch is revert-to-main.patchdiff --git a/.claude/agents/implementer.md b/.claude/agents/implementer.md
index 76e4b54a2..c88fb33d7 100644
--- a/.claude/agents/implementer.md
+++ b/.claude/agents/implementer.md
@@ -32,9 +32,9 @@ guess. Then build, then test on root `CLAUDE.md`'s tiers before anyone reviews:
- A result is the command's own exit code: `<cmd> > <file> 2>&1; echo EXIT=$?`. A grepped
`test result` line is not one, and a gate you did not run is a gate you do not claim.
- Long commands run in the background with output to a file under the scratchpad the brief names.
- Stay inside one turn while anything runs: block in the foreground on `n=0; until <it is done> ||
- [ $((n+=1)) -gt 50 ]; do sleep 2; done`, print a line, repeat; a long `sleep` is refused. Ten
- minutes of silence kills you, and ending a turn to announce a wait strands the work.
+ Stay inside one turn while anything runs: block in the foreground on `until <it is done>; do sleep
+ 2; done` under an explicit `timeout`, print a line, repeat; a long `sleep` is refused. Ten minutes
+ of silence kills you, and ending a turn to announce a wait strands the work.
- Nothing a pull request's evidence rests on, mutation patches and run logs included, lives only in
a temporary directory: `/tmp` is wiped when the CLI restarts. Post mutation patches to the pull
request as a comment.
diff --git a/.claude/agents/reviewer.md b/.claude/agents/reviewer.md
index 63dcca51f..6d1883b3d 100644
--- a/.claude/agents/reviewer.md
+++ b/.claude/agents/reviewer.md
@@ -50,8 +50,6 @@ if it meets the bar above; otherwise it is a NOTE.
`arch::aarch64` in no generic kernel code; and none of them in a crate whose manifest
`description` says pure.
A `4096` in the kernel that means a page is a private copy of `mm::PAGE_SIZE`.
- Nothing ships for tests alone: a program, an applet or a kernel path whose only user is a test
- lives in a test image or a test kernel, and a diff that ships one is a BLOCKER.
- **Hosts.** A BLOCKER: a change that makes an app build on fewer of Linux under Wayland, macOS
and Windows, or answers a host build failure by making the app ToyOS-only — by a split, a `cfg`
that compiles what it does out of a host, or an `exempt` in its manifest — instead of fixing it
@@ -129,8 +127,6 @@ if it meets the bar above; otherwise it is a NOTE.
- **Forks.** A broken rule of `implementer.md`'s "A fork" is a BLOCKER, and so is a lockfile or
gitlink left naming a fork branch whose consumed commit the pull request changes. A search for
callers that skipped the fork clones and `~/.cargo/git/checkouts/` searched part of the tree.
- LLVM and Rust are never changed to suit a ToyOS tool; a tool that cannot build them unchanged is
- the one that goes, and a diff that changes either for one is a BLOCKER.
## Prose
diff --git a/CLAUDE.md b/CLAUDE.md
index f2a665b4d..4c8de6657 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -55,7 +55,7 @@ There are no spec documents. A rule a reader can check lives in an agent's promp
## Dependencies
-**Rust** and **QEMU** for development, on any host OS and architecture — the development machine is nothing special. Beside them, where no Rust tool does the job, only C or C++ tools ToyOS can one day build and run (Python, Perl, CMake, make), each declared where `reviewer.md`'s Arrivals says. No binary for one host OS alone: a macOS binary is a hard no, and "only for tests" does not soften it. ToyOS's own code is Rust; it writes no Python, Perl or shell of its own. Only general and widely used crates — one that does *our* job we write ourselves, and a driver crate never; third-party crates are used as published, and a fork carries a change written to upstream quality and goes when upstream has it. No upstream pull request is sent for now. Third-party source is used unmodified, pinned by hash, wherever only its packaging has to change; a fork is for source changes only. The north star is **self-hosting**: ToyOS rebuilds itself on ToyOS and reproduces the host's bytes, and nothing — build, test, or verification — rests on a host binary; a bootstrap from source with no binary seed is out of scope. Ask of anything new: could this ever run inside ToyOS?
+**Rust** and **QEMU** for development, on any host OS and architecture — the development machine is nothing special. Beside them, where no Rust tool does the job, only C or C++ tools ToyOS can one day build and run (Python, Perl, CMake, make), each declared where `reviewer.md`'s Arrivals says. No binary for one host OS alone: a macOS binary is a hard no, and "only for tests" does not soften it. ToyOS's own code is Rust; it writes no Python, Perl or shell of its own. Only general and widely used crates — one that does *our* job we write ourselves, and a driver crate never; third-party crates are used as published, and a fork carries a change written to upstream quality and goes when upstream has it. No upstream pull request is sent for now. A third-party source ToyOS cannot build without changing it is carried as an unmodified-source packaging mirror with a byte-identity gate, not as a fork. The north star is **self-hosting**: ToyOS rebuilds itself on ToyOS and reproduces the host's bytes, and nothing — build, test, or verification — rests on a host binary; a bootstrap from source with no binary seed is out of scope. Ask of anything new: could this ever run inside ToyOS?
Vendor firmware a device or CPU verifies by its maker's signature may be shipped: pinned by version and hash, redistributable unmodified, recorded in `NOTICE`. A device's is loaded only by its own driver through its IOMMU domain and never executes on the CPU; CPU microcode is loaded by the kernel. `NOTICE` names every committed third-party file with its hash, upstream and licence.
diff --git a/issues/design-debt/the-shipped-image-carries-three-test-programs.md b/issues/design-debt/the-shipped-image-carries-three-test-programs.md
deleted file mode 100644
index f43b8c0cc..000000000
--- a/issues/design-debt/the-shipped-image-carries-three-test-programs.md
+++ /dev/null
@@ -1,26 +0,0 @@
----
-status: open
-kind: defect
-opened: 2026-09-30
----
-
-# The shipped image carries three test programs
-
-"Nothing ships for tests alone" (`.claude/agents/reviewer.md`, Fit) is untrue
-of the shipped `system.toml` in three rows:
-
-- `proctest = {}`: `userland/proctest/README.md` calls it a test harness, and
- nothing in the tree runs `/system/bin/proctest` but `proctest` itself.
-- `input-test = {}`: a test utility by its README, and nothing runs
- `/system/bin/input-test`; `toyos-symbols/tests/real.rs` reads a frozen copy
- under `toyos-symbols/tests/fixtures/`, not the shipped binary.
-- `"bin/spin" = "/system/bin/toybox"`: `userland/toybox/src/spin.rs` loops
- forever, and nothing in the tree runs it; its one named use is a test, the
- metal `sshd_exec` row
- `issues/build/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md` owes.
-
-Found by `git grep -n -w -e proctest -e input-test` and
-`git grep -n -w -e spin -- system.toml tests userland issues/build`.
-
-**Exit condition**: the shipped `system.toml` names none of the three, and a
-test that needs one carries it in its own image alone.
diff --git a/issues/design-debt/the-x86-toybox-ships-two-applets-that-only-panic.md b/issues/design-debt/the-x86-toybox-ships-two-applets-that-only-panic.md
new file mode 100644
index 000000000..7dae8903a
--- /dev/null
+++ b/issues/design-debt/the-x86-toybox-ships-two-applets-that-only-panic.md
@@ -0,0 +1,18 @@
+---
+status: open
+kind: defect
+opened: 2026-09-29
+---
+
+# The x86-64 toybox ships two applets that only panic
+
+`userland/toybox/src/main.rs` puts `fp_isolation` and `first_entry` in one
+`commands!` list for every architecture, so the x86-64 toybox that ships in
+every image answers both names, and `userland/toybox/src/arch/x86_64.rs`'s
+body for each is a `panic!` naming where x86-64's probe lives or is owed
+(`test_rs_fpu_isolation`,
+`issues/isolation/a-new-x86-thread-enters-ring-3-holding-kernel-register-values.md`).
+A shipping binary carries two commands whose only behaviour is to die.
+
+**Exit condition**: the x86-64 toybox answers neither name with a panic —
+each is left out of its `commands!`, or runs a probe of x86-64's own.
diff --git a/tests/toyos.rs b/tests/toyos.rs
index f3da8dfc1..db3d987a6 100644
--- a/tests/toyos.rs
+++ b/tests/toyos.rs
@@ -1365,7 +1365,7 @@ fn judge_virt_job(mut qemu: QemuInstance, job: &str, said: &str) -> Result<Strin
Ok(serial)
}
-/// What `unmap_touch` says once every read of a page just unmapped,
+/// What toybox's `unmap_touch` says once every read of a page just unmapped,
/// on the unmapping thread and on another, ended its process.
const UNMAP_TOUCH_SAID: &str =
"unmap_touch: 4 reads of a page just unmapped on the unmapping thread, and 4 on another";
@@ -1731,7 +1731,10 @@ fn run_screen_test(name: &str, profile: qemu::Profile, test_config: &Path) -> Re
}
Ok(())
}
- "virt_timer_preempts" => virt_job(profile, "preempt", "preempt: the counting thread was preempted twice"),
+ "virt_timer_preempts" => {
+ // Spelled in `userland/toybox/src/preempt.rs`.
+ virt_job(profile, "preempt", "preempt: the counting thread was preempted twice")
+ }
"virt_fp_isolation" => virt_job(profile, "fp_isolation", "fp_isolation: v0-v31, FPCR and FPSR survived"),
"virt_first_entry" => virt_job(profile, "first_entry", "first_entry: x1-x30 were zero"),
"virt_unmap_touch" => virt_job(profile, "unmap_touch", UNMAP_TOUCH_SAID),
diff --git a/tests/virtjobcase/system.toml b/tests/virtjobcase/system.toml
index 838f3dda1..190d1b33a 100644
--- a/tests/virtjobcase/system.toml
+++ b/tests/virtjobcase/system.toml
@@ -1,4 +1,4 @@
-# One CPU and the AArch64 guest's jobs, each a kernelprobe applet or, last because
+# One CPU and the AArch64 guest's jobs, each a toybox applet or, last because
# a kernel it fails on rewrites the clock page every reader asserts on, the
# suite's `test_rs_abuse_readonly_copyout`, which the harness puts on ROOT;
# each job's line comes only if the kernel kept what that job asks about.
@@ -15,11 +15,11 @@ receives = ["power"]
# CPU, and a job past the bound reboots the guest with the job named.
args = ["--bound-ms=240000", "preempt", "fp_isolation", "first_entry", "unmap_touch", "debug_refused", "test_rs_abuse_readonly_copyout"]
-[programs.kernelprobe]
+[programs.toybox]
[symlinks]
-"bin/preempt" = "/system/bin/kernelprobe"
-"bin/fp_isolation" = "/system/bin/kernelprobe"
-"bin/first_entry" = "/system/bin/kernelprobe"
-"bin/unmap_touch" = "/system/bin/kernelprobe"
-"bin/debug_refused" = "/system/bin/kernelprobe"
+"bin/preempt" = "/system/bin/toybox"
+"bin/fp_isolation" = "/system/bin/toybox"
+"bin/first_entry" = "/system/bin/toybox"
+"bin/unmap_touch" = "/system/bin/toybox"
+"bin/debug_refused" = "/system/bin/toybox"
diff --git a/tests/virtsmpcase/system.toml b/tests/virtsmpcase/system.toml
index bd0e9c302..e7dfb0bc4 100644
--- a/tests/virtsmpcase/system.toml
+++ b/tests/virtsmpcase/system.toml
@@ -13,7 +13,7 @@ receives = ["power"]
# the bound reboots the guest with the job named.
args = ["--bound-ms=240000", "unmap_touch"]
-[programs.kernelprobe]
+[programs.toybox]
[symlinks]
-"bin/unmap_touch" = "/system/bin/kernelprobe"
+"bin/unmap_touch" = "/system/bin/toybox"
diff --git a/userland/Cargo.lock b/userland/Cargo.lock
index 5aa43186b..b21a2ec8e 100644
--- a/userland/Cargo.lock
+++ b/userland/Cargo.lock
@@ -1886,13 +1886,6 @@ dependencies = [
"rand_core 0.10.0",
]
-[[package]]
-name = "kernelprobe"
-version = "0.1.0"
-dependencies = [
- "toyos-abi",
-]
-
[[package]]
name = "kurbo"
version = "0.13.0"
diff --git a/userland/Cargo.toml b/userland/Cargo.toml
index 5533dec5f..c2aa2e8f8 100644
--- a/userland/Cargo.toml
+++ b/userland/Cargo.toml
@@ -15,7 +15,6 @@ members = [
"init",
"input-test",
"inspect",
- "kernelprobe",
"logd",
"metalprobe",
"netd",
diff --git a/userland/kernelprobe/Cargo.toml b/userland/kernelprobe/Cargo.toml
deleted file mode 100644
index 09c5a5ae8..000000000
--- a/userland/kernelprobe/Cargo.toml
+++ /dev/null
@@ -1,9 +0,0 @@
-[package]
-name = "kernelprobe"
-version = "0.1.0"
-edition = "2021"
-license = "MIT OR Apache-2.0"
-description = "Test-only multi-call binary: the kernel probes the AArch64 guest's virt jobs run, one per name it is invoked by."
-
-[dependencies]
-toyos-abi = { path = "../../toyos-abi" }
diff --git a/userland/kernelprobe/src/arch/mod.rs b/userland/kernelprobe/src/arch/mod.rs
deleted file mode 100644
index d1bc784fe..000000000
--- a/userland/kernelprobe/src/arch/mod.rs
+++ /dev/null
@@ -1,9 +0,0 @@
-//! What the probes must say in assembly, one module per architecture.
-
-#[cfg(target_arch = "aarch64")]
-mod aarch64;
-#[cfg(target_arch = "aarch64")]
-pub use aarch64::*;
-
-#[cfg(not(target_arch = "aarch64"))]
-compile_error!("kernelprobe is built for AArch64 alone: its probes are the AArch64 guest's");
diff --git a/userland/kernelprobe/src/main.rs b/userland/kernelprobe/src/main.rs
deleted file mode 100644
index 4728869ff..000000000
--- a/userland/kernelprobe/src/main.rs
+++ /dev/null
@@ -1,20 +0,0 @@
-mod arch;
-mod debug_refused;
-mod preempt;
-mod unmap_touch;
-
-use arch::{first_entry, fp_isolation};
-
-fn main() {
- let mut args = std::env::args();
- let invoked_as = args.next().expect("argv[0] names the probe");
- let args: Vec<String> = args.collect();
- match std::path::Path::new(&invoked_as).file_name().and_then(|n| n.to_str()) {
- Some("debug_refused") => debug_refused::main(args),
- Some("first_entry") => first_entry::main(args),
- Some("fp_isolation") => fp_isolation::main(args),
- Some("preempt") => preempt::main(args),
- Some("unmap_touch") => unmap_touch::main(args),
- other => panic!("kernelprobe: no probe is called {other:?}"),
- }
-}
diff --git a/userland/kernelprobe/src/arch/aarch64.rs b/userland/toybox/src/arch/aarch64.rs
similarity index 100%
rename from userland/kernelprobe/src/arch/aarch64.rs
rename to userland/toybox/src/arch/aarch64.rs
diff --git a/userland/toybox/src/arch/mod.rs b/userland/toybox/src/arch/mod.rs
new file mode 100644
index 000000000..95e0e9d15
--- /dev/null
+++ b/userland/toybox/src/arch/mod.rs
@@ -0,0 +1,11 @@
+//! What toybox's applets must say in assembly, one module per architecture.
+
+#[cfg(target_arch = "aarch64")]
+mod aarch64;
+#[cfg(target_arch = "aarch64")]
+pub use aarch64::*;
+
+#[cfg(target_arch = "x86_64")]
+mod x86_64;
+#[cfg(target_arch = "x86_64")]
+pub use x86_64::*;
diff --git a/userland/toybox/src/arch/x86_64.rs b/userland/toybox/src/arch/x86_64.rs
new file mode 100644
index 000000000..c08523a99
--- /dev/null
+++ b/userland/toybox/src/arch/x86_64.rs
@@ -0,0 +1,51 @@
+//! x86-64's half of `unmap_touch`; its FP and first-entry probes are
+//! elsewhere, or owed.
+
+pub mod fp_isolation {
+ pub fn main(_args: Vec<String>) {
+ panic!("fp_isolation probes AArch64's FP/SIMD switch; x86-64's is test_rs_fpu_isolation");
+ }
+}
+
+pub mod first_entry {
+ pub fn main(_args: Vec<String>) {
+ panic!(
+ "first_entry probes AArch64's first entry to EL0; x86-64's is owed by \
+ issues/isolation/a-new-x86-thread-enters-ring-3-holding-kernel-register-values.md"
+ );
+ }
+}
+
+/// Reads the word at `page`, unmaps the `len` bytes there with `SYS_MUNMAP`
+/// and reads the word again, in one block, so nothing but the unmap itself
+/// can switch the CPU between the reads. Answers the first read, the unmap's
+/// answer and the second read.
+///
+/// # Safety
+/// `page` starts a mapping of `len` bytes that nothing else names. The second
+/// read ends the process unless the unmap was refused or left its
+/// translation standing.
+pub unsafe fn read_unmap_read(page: *const u64, len: usize) -> (u64, u64, u64) {
+ let (first, answer, second): (u64, u64, u64);
+ // SAFETY: the caller's; the two loads name `page` and the `syscall` is
+ // the ABI's (`toyos_abi::syscall`), which clobbers rax, rcx and r11.
+ unsafe {
+ core::arch::asm!(
+ "mov {first}, qword ptr [{page}]",
+ "syscall",
+ "mov {second}, qword ptr [{page}]",
+ page = in(reg) page,
+ first = out(reg) first,
+ second = lateout(reg) second,
+ in("rdi") toyos_abi::syscall::SYS_MUNMAP,
+ in("rsi") page,
+ in("rdx") len,
+ in("r8") 0u64,
+ in("r9") 0u64,
+ out("rax") answer,
+ out("rcx") _,
+ out("r11") _,
+ );
+ }
+ (first, answer, second)
+}
diff --git a/userland/kernelprobe/src/debug_refused.rs b/userland/toybox/src/debug_refused.rs
similarity index 100%
rename from userland/kernelprobe/src/debug_refused.rs
rename to userland/toybox/src/debug_refused.rs
diff --git a/userland/toybox/src/main.rs b/userland/toybox/src/main.rs
index 9e3df6877..449ce6e89 100644
--- a/userland/toybox/src/main.rs
+++ b/userland/toybox/src/main.rs
@@ -1,5 +1,7 @@
+mod arch;
mod cat;
mod cp;
+mod debug_refused;
mod echo;
mod free;
mod grep;
@@ -9,6 +11,7 @@ mod ls;
mod mkdir;
mod mv;
mod net;
+mod preempt;
mod ps;
mod pwd;
mod reboot;
@@ -18,6 +21,7 @@ mod shutdown;
mod spin;
mod stats;
mod tone;
+mod unmap_touch;
macro_rules! commands {
($($name:ident),*) => {
@@ -30,7 +34,9 @@ macro_rules! commands {
};
}
-commands!(cat, cp, echo, free, grep, hexdump, locale, ls, mkdir, mv, net, ps, pwd, reboot, rm, screen, shutdown, spin, stats, tone);
+use arch::{first_entry, fp_isolation};
+
+commands!(cat, cp, debug_refused, echo, first_entry, fp_isolation, free, grep, hexdump, locale, ls, mkdir, mv, net, preempt, ps, pwd, reboot, rm, screen, shutdown, spin, stats, tone, unmap_touch);
fn main() {
let args: Vec<String> = std::env::args().collect();
diff --git a/userland/kernelprobe/src/preempt.rs b/userland/toybox/src/preempt.rs
similarity index 100%
rename from userland/kernelprobe/src/preempt.rs
rename to userland/toybox/src/preempt.rs
diff --git a/userland/kernelprobe/src/unmap_touch.rs b/userland/toybox/src/unmap_touch.rs
similarity index 100%
rename from userland/kernelprobe/src/unmap_touch.rs
rename to userland/toybox/src/unmap_touch.rs
The T14 staging run: The check on the staged image, Its one |
|
T14 evidence at
|
|
Review of Earlier BLOCKERs
Earlier NOTEs and REMOVEs, all answered: the four-shells sentence is gone from the body; Growth. What the verdict rests on
The image check as a negative control: sound as posted, and its pattern stays as it is.
The prompt edits against the brief. Forks ( BLOCKER
NOTE
REMOVE
Read for this review, under SEND BACK |
…probe's selector header drops a clause The review of a098330 (#636 (comment)) sent the branch back on one BLOCKER and one REMOVE in the tree. reviewer.md, Fit: the middle clause, "a program, an applet or a kernel path whose only user is a test lives in a test image or a test kernel", was not briefed, and it was what "one" resolved to: the BLOCKER ranked by who uses a thing where the owner's ruling ranks by what a thing is for. Read that way it was true of `mv` and `hexdump` at a098330, which only test binaries run and which have production value. The sentence is now the orchestrator's, as briefed: "Nothing ships for tests alone, and a diff that ships something for a test alone is a BLOCKER." No other word of any prompt changes. userland/kernelprobe/src/arch/mod.rs: ", one module per architecture" is deleted. The selector holds one module and refuses every other architecture. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
|
Review of Earlier BLOCKERs
Earlier NOTEs and the REMOVE, all answered. Growth. What the verdict rests on
BLOCKER None. NOTE
REMOVE None. Read for this review, under LAND AFTER NAMED CHANGES |
#636 moved the five `virt_` probes out of the shipped toybox into `userland/kernelprobe`, and put two rulings in `reviewer.md`. One conflict, `tests/virtsmpcase/system.toml`, every hunk of both sides kept: - main's: `[programs.toybox]` became `[programs.kernelprobe]`, and `bin/unmap_touch` points at `/system/bin/kernelprobe`. Both taken. - this branch's: the header's second sentence, `shutdown` after `unmap_touch` in the job list, and `bin/shutdown` pointing at `/system/bin/toybox`. All three kept, and `[programs.toybox]` stays beside `[programs.kernelprobe]`, because `shutdown` is a shipped toybox applet and no probe. `tests/toyos.rs` merged without conflict: main's two hunks are a doc line and `virt_timer_preempts`'s arm, neither in a function this branch touches. `tests/virtrebootcase/system.toml` names toybox alone, for `reboot`, and is untouched. `cargo test --test toyos-build -- virt_` on the merged tree before this commit: exit 0, 19 of 19. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
No file is changed on both sides. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
#647, #642 and #636's follow-up landed since the last merge. One conflict, in tests/toyos.rs: #647 made `boot_virt_smp` take a `BootOptions` and `judge_virt_job` borrow its guest, and this branch had added `virt_mask_windows` on the old shape. Main's shape is kept whole; `virt_mask_windows` names its kernel build in the options and lends its guest like the others. `tests/virtsmpcase` now ends in `shutdown`, which this test does not wait for: it judges once `unmap_touch` has ended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm
At
ee6f425ba, onmainata31eec595. The branch was written on 2026-09-30 as code plus prompt rules. #678 has since rewritten all four prompts from the owner's later decisions, so every prompt rule this branch wrote is dropped: the merge at10aed2d75takesmain'sCLAUDE.md,implementer.md,orchestrator.mdandreviewer.mdwhole. What remains is the code half and briefed prompt edits in three files.What changed
The five virt probes leave the shipped toybox
The owner's ruling of 2026-09-30: nothing ships for tests alone; test-only programs live in test images, and the kernel's actuators stay, compiled only into test kernels. On
mainthe shipped toybox answersdebug_refused,first_entry,fp_isolation,preemptandunmap_touch, which only the AArch64virt_tests run.userland/kernelprobeis a new userland workspace member that onlytests/virtjobcase/system.tomlandtests/virtsmpcase/system.tomlname. The five probes moved from toybox withgit mv, unchanged.main.rsdispatches on argv[0] and panics on any other name.[programs.kernelprobe], and their symlinks point at/system/bin/kernelprobe.archmodule.fp_isolationandfirst_entrywere panics, and itsread_unmap_readexisted so the shipped x86-64 toybox compiled: no x86-64 test runsunmap_touch.kernelprobeis AArch64 only: itsarchselector refuses any other architecture with acompile_error!(measured below). No x86-64 config names it.tests/toyos.rs: two comments named toybox. The one overvirt_timer_preemptsis deleted, and its arm is one line like its four siblings; the other loses the word.Issues
issues/design-debt/the-x86-toybox-ships-two-applets-that-only-panic.mdis closed. Its exit is met: the x86-64 toybox answers neither name.git grep the-x86-toybox-shipsfinds no citation (exit 1).issues/design-debt/the-shipped-image-carries-three-test-programs.mdis filed for what stays in the shippedsystem.toml:proctest,input-testand toybox'sspin. Itsspinrow is written formainafter The guest suite keeps the 21 tests only a booted machine answers; the rest are metal, host or tracked #660: the two guest cases that ranspinare gone, and its one named use is a metal row the guest-suite track owes.Prompt edits in three files, each briefed
reviewer.md, Fit: "Nothing ships for tests alone, and a diff that ships something for a test alone is a BLOCKER." The orchestrator's sentence, word for word. It ranks a diff, as the Forks clause does, so the three rows the filed issue records send no branch back. It ranks by what a thing is for and not by who runs it:mvandhexdump, which at this head only test binaries run, are not what it names.reviewer.md, Forks: "LLVM and Rust are never changed to suit a ToyOS tool; a tool that cannot build them unchanged is the one that goes, and a diff that changes either for one is a BLOCKER."implementer.md: the wait recipe said "under an explicittimeout". macOS has notimeoutcommand (/bin/sh -c 'command -v timeout'exits 1 on this host, macOS 27.0.1). The recipe is nown=0; until <it is done> || [ $((n+=1)) -gt 50 ]; do sleep 2; done.CLAUDE.md, Dependencies: "A third-party source ToyOS cannot build without changing it is carried as an unmodified-source packaging mirror with a byte-identity gate, not as a fork." becomes "Third-party source is used unmodified, pinned by hash, wherever only its packaging has to change; a fork is for source changes only." The file is 21 bytes shorter.Against
main, the four prompts differ by these edits and nothing else (git diff --numstat origin/main HEAD -- CLAUDE.md .claude:implementer.md3/3,reviewer.md3/0,CLAUDE.md1/1).The review of
a098330c3#636 (comment), answered in
ee6f425ba, which isgit diff --stat a098330c3 ee6f425ba:reviewer.mdanduserland/kernelprobe/src/arch/mod.rs, +2 −3.reviewer.md:53-54: the middle clause is deleted, and the sentence is the one quoted above. No other word of any prompt changes.userland/kernelprobe/src/arch/mod.rs:1: ", one module per architecture" is deleted.Gates
Each on a clean tree. The host suite is at
ee6f425ba; every other row is a reading ofa098330c3and was not run again, because the one commit since changes a prompt sentence and a source comment. Logs are under/Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/: the host suite's in636-r3/, the rest in636-r2/.cargo run -- --ci host("Host: 64 step(s), all green",host.log:7070)ee6f425bacargo run -- --build-onlya098330c3cargo run -- --build-only --arch aarch64a098330c3cargo test --test toyos-build, the whole guest suite, 21 tests, 16 of themvirt_a098330c3cargo build --target x86_64-unknown-toyos --profile toyos -p kernelprobe, which must faila098330c3a098330c3a098330c3cargo test --test toyos-build -- --metal --metal-readback <dir> toybox_file_tools, staging onlya098330c3cargo run --bin toyos-metal -- --image … --readback … --fat32-check, run by the orchestrator; verdictpasseda098330c3kernelprobeand said their lines invirt_timer_preempts,virt_first_entry,virt_fp_isolation,virt_unmap_touch,virt_debug_refused,virt_smp,virt_el1_smpandvirt_failed_ap_leaves_no_hole. No guest test is new and none changes behaviour: the same probes say the same lines from another binary.kernelprobefor x86-64 fails with one error and nothing else:error: kernelprobe is built for AArch64 alone: its probes are the AArch64 guest's, atkernelprobe/src/arch/mod.rs:9:1. Ate9701cdf4the same command died atunresolved imports arch::first_entry, arch::fp_isolation.debug_refused,first_entry,fp_isolationandunmap_touchand the linepreempt: the counting thread. It exits 2 unless it also finds toybox's own refusal line there, so a green cannot be an image it could not read. Ata098330c3every count is 0 intarget/bootable.imgandtarget/bootable.aarch64.img, and in the two toybox binaries themselves.a098330c3, which makes the working treeorigin/main's (tree41253219…both ways). Both images built there, exit 0 each, and the check exits 1: each image holdsdebug_refused3,first_entry2,fp_isolation2,unmap_touch8 and thepreemptline 1. Restored and rebuilt, the check exits 0 again andgit statusis empty.e9701cdf4read that no guest runs a toybox applet, so the x86-64 toybox this diff edits ran nowhere. Thesharedboot with the one jobtest_rs_toybox_file_toolsrunscp,mvandhexdumpthere and ends throughreboot. It was staged ata098330c3(exit 2 is the harness'sVerdict::Staged) and then flashed and booted once by the orchestrator, on the LENOVO 20W0003AMZ: Nothing ships for tests alone: the five virt probes leave the shipped toybox, and two owner rulings enter reviewer.md #636 (comment).636-r2/metal/shared/.verdict.txtreadspassed.kernel.log:696is===TEST_END test_rs_toybox_file_tools exit=0===. The kernel's spawn records inkernel.logcount/system/bin/cp5,/system/bin/mv6,/system/bin/hexdump11 and/system/bin/reboot1, and its last line, 701, is init's power line.t14-636-shared EXIT=0 (75s)insummary.txt, written by the orchestrator's loop. The run's own log,logs/t14-636-shared.log, carries no exit line: it ends atPASS: the machine booted ToyOS in 1165 ms.a098330c3: the change targets no hardware, and nothing was staged or booted atee6f425ba.The check, the control's script and log, the revert patch, the x86-64 build's output and the staging run's log are in #636 (comment).
Not on root
CLAUDE.md's high-risk list: test programs move between images, and no kernel, ABI or shipped behaviour changes but toybox answering five fewer names. The control is the brief's.No new gate is in the tree. The image check is a measurement for this pull request. The rule it measured is now a sentence in
reviewer.md, and what a shipped config or toybox'scommands!names is something a reviewer reads.Growth
git diff --shortstat origin/main...HEAD: 20 files, +91 −105.userland/without its lockfile: +40 −69.tests/toyos.rs: +11 −14.userland/Cargo.lock: +7. Issues: +26 −18. Prompts: +7 −4.Unsure
mainis still untrue of "Nothing ships for tests alone" in three rows, which the filed issue records.first_entry, which is cranelift'sassertion failed: index > 0 || self.at_first_entry().grep -a -c -F at_first_entrycounts 1 intests/toyos-rust-tests/tls-cranelift's builtlibtls_cranelift.so, 1 in the staged image and 0 in each shipped image, and the check countsfirst_entry0 in each toybox binary. The other four counts in the staged image are 0.🤖 Generated with Claude Code
https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm