diff --git a/.claude/agents/implementer.md b/.claude/agents/implementer.md index c88fb33d7bf..76e4b54a29f 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: ` > 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 `until ; 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. + Stay inside one turn while anything runs: block in the foreground on `n=0; until || + [ $((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. - 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 6d1883b3d70..78dda63b29a 100644 --- a/.claude/agents/reviewer.md +++ b/.claude/agents/reviewer.md @@ -50,6 +50,7 @@ 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, and a diff that ships something for a test alone 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 @@ -127,6 +128,8 @@ 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 4c8de665767..f2a665b4d2e 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. 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? +**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? 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 new file mode 100644 index 00000000000..f43b8c0cc7d --- /dev/null +++ b/issues/design-debt/the-shipped-image-carries-three-test-programs.md @@ -0,0 +1,26 @@ +--- +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 deleted file mode 100644 index 7dae8903a8c..00000000000 --- a/issues/design-debt/the-x86-toybox-ships-two-applets-that-only-panic.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -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 db3d987a60e..f3da8dfc11d 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 Re } Ok(()) } - "virt_timer_preempts" => { - // Spelled in `userland/toybox/src/preempt.rs`. - virt_job(profile, "preempt", "preempt: the counting thread was preempted twice") - } + "virt_timer_preempts" => 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 190d1b33acd..838f3dda18a 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 toybox applet or, last because +# One CPU and the AArch64 guest's jobs, each a kernelprobe 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.toybox] +[programs.kernelprobe] [symlinks] -"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" +"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" diff --git a/tests/virtsmpcase/system.toml b/tests/virtsmpcase/system.toml index e7dfb0bc46d..bd0e9c302b0 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.toybox] +[programs.kernelprobe] [symlinks] -"bin/unmap_touch" = "/system/bin/toybox" +"bin/unmap_touch" = "/system/bin/kernelprobe" diff --git a/userland/Cargo.lock b/userland/Cargo.lock index b21a2ec8efa..5aa43186b0d 100644 --- a/userland/Cargo.lock +++ b/userland/Cargo.lock @@ -1886,6 +1886,13 @@ 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 c2aa2e8f806..5533dec5f87 100644 --- a/userland/Cargo.toml +++ b/userland/Cargo.toml @@ -15,6 +15,7 @@ members = [ "init", "input-test", "inspect", + "kernelprobe", "logd", "metalprobe", "netd", diff --git a/userland/kernelprobe/Cargo.toml b/userland/kernelprobe/Cargo.toml new file mode 100644 index 00000000000..09c5a5ae823 --- /dev/null +++ b/userland/kernelprobe/Cargo.toml @@ -0,0 +1,9 @@ +[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/toybox/src/arch/aarch64.rs b/userland/kernelprobe/src/arch/aarch64.rs similarity index 100% rename from userland/toybox/src/arch/aarch64.rs rename to userland/kernelprobe/src/arch/aarch64.rs diff --git a/userland/kernelprobe/src/arch/mod.rs b/userland/kernelprobe/src/arch/mod.rs new file mode 100644 index 00000000000..814151dcf80 --- /dev/null +++ b/userland/kernelprobe/src/arch/mod.rs @@ -0,0 +1,9 @@ +//! What the probes must say in assembly. + +#[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/toybox/src/debug_refused.rs b/userland/kernelprobe/src/debug_refused.rs similarity index 100% rename from userland/toybox/src/debug_refused.rs rename to userland/kernelprobe/src/debug_refused.rs diff --git a/userland/kernelprobe/src/main.rs b/userland/kernelprobe/src/main.rs new file mode 100644 index 00000000000..4728869ff9d --- /dev/null +++ b/userland/kernelprobe/src/main.rs @@ -0,0 +1,20 @@ +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 = 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/toybox/src/preempt.rs b/userland/kernelprobe/src/preempt.rs similarity index 100% rename from userland/toybox/src/preempt.rs rename to userland/kernelprobe/src/preempt.rs diff --git a/userland/toybox/src/unmap_touch.rs b/userland/kernelprobe/src/unmap_touch.rs similarity index 100% rename from userland/toybox/src/unmap_touch.rs rename to userland/kernelprobe/src/unmap_touch.rs diff --git a/userland/toybox/src/arch/mod.rs b/userland/toybox/src/arch/mod.rs deleted file mode 100644 index 95e0e9d154f..00000000000 --- a/userland/toybox/src/arch/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -//! 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 deleted file mode 100644 index c08523a994b..00000000000 --- a/userland/toybox/src/arch/x86_64.rs +++ /dev/null @@ -1,51 +0,0 @@ -//! 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) { - 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) { - 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/toybox/src/main.rs b/userland/toybox/src/main.rs index 449ce6e89d4..9e3df687707 100644 --- a/userland/toybox/src/main.rs +++ b/userland/toybox/src/main.rs @@ -1,7 +1,5 @@ -mod arch; mod cat; mod cp; -mod debug_refused; mod echo; mod free; mod grep; @@ -11,7 +9,6 @@ mod ls; mod mkdir; mod mv; mod net; -mod preempt; mod ps; mod pwd; mod reboot; @@ -21,7 +18,6 @@ mod shutdown; mod spin; mod stats; mod tone; -mod unmap_touch; macro_rules! commands { ($($name:ident),*) => { @@ -34,9 +30,7 @@ macro_rules! commands { }; } -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); +commands!(cat, cp, echo, free, grep, hexdump, locale, ls, mkdir, mv, net, ps, pwd, reboot, rm, screen, shutdown, spin, stats, tone); fn main() { let args: Vec = std::env::args().collect();