From 6cfbaeade401f1270c7a5256901e1242833cf310 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 1 Oct 2026 01:09:05 +0200 Subject: [PATCH 1/7] ABI: a deleted syscall, SYS_DEBUG action or inbox op is simply deleted MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner ruling, 2026-10-01: the ABI is completely unstable until ToyOS is adopted; toyos-abi and the SDK break freely, and nothing is kept for a binary built against an older one. So a number that lost its call is free, and nothing records it. - kernel/src/syscall/dispatch.rs: `retired_syscalls!`, its table of nineteen numbers and its ascending-order assert go. A number no arm claims falls to the one default arm, which answers `InvalidArgument` as it did for every other unassigned number, and now also writes one record, `syscall is unknown`, for every such call. The record is what the retired arm used to write for its nineteen numbers; the two guest tests that need a cheap per-call kernel record read this one. - kernel/src/inbox/mod.rs: the "2 is retired" comment goes; op 2 already refused like any undeclared op. - toyos-abi/src/syscall.rs and toyos-abi/src/inbox.rs: every "formerly …" and "retired and unused" entry goes, with the two doc paragraphs that explained why 76 and 89 kept their numbers, the retired SYS_DEBUG actions 14 and 15, the retired device classes 3 and 4 and the "nic" case of the DeviceRequest parse test that existed for that retirement. No live number moves. - toyos-abi/src/inventory.rs, toyos/src/census.rs: the clauses that named 3 and 4, and CENSUS_TOTAL/BREAKDOWN, retired. - log_hold and panic_halts_first call `u64::MAX`, which no syscall can ever be, rather than 26, which the next syscall may now take; the harness reads `syscall 18446744073709551615 is unknown`. - CLAUDE.md: "a deleted syscall's number is retired, never reused" is replaced by "the ABI is unstable until ToyOS is adopted and breaks freely", and "completely unstable" before it goes as the same fact. - reviewer.md: the BLOCKER on retired ABI names and reused numbers goes. - issues/: every statement of the rule, and every exit condition that asked for a number to be retired, goes. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- .claude/agents/reviewer.md | 6 +- CLAUDE.md | 2 +- ...eregister-fd-leaves-a-pending-poll-live.md | 3 +- ...device-class-answers-for-a-block-device.md | 2 +- issues/diagnostics/no-cyclictest.md | 3 +- .../the-kernel-keeps-nothing-it-enumerates.md | 8 -- ...nt-and-a-parent-takes-its-children-down.md | 7 +- ...time-is-now-a-format-of-sys-clock-epoch.md | 3 +- ...-capability-end-state-is-twelve-answers.md | 7 +- ...ernel-still-parses-what-userland-writes.md | 2 +- kernel/src/inbox/mod.rs | 1 - kernel/src/syscall/dispatch.rs | 57 +------------- tests/common/origin.rs | 4 +- tests/toyos-rust-tests/src/bin/log_hold.rs | 10 +-- .../src/bin/panic_halts_first.rs | 18 ++--- tests/toyos.rs | 4 +- toyos-abi/src/inbox.rs | 6 -- toyos-abi/src/inventory.rs | 2 +- toyos-abi/src/syscall.rs | 78 +------------------ toyos/src/census.rs | 5 +- 20 files changed, 38 insertions(+), 190 deletions(-) diff --git a/.claude/agents/reviewer.md b/.claude/agents/reviewer.md index c780b0dee34..0e6a9f7fbf1 100644 --- a/.claude/agents/reviewer.md +++ b/.claude/agents/reviewer.md @@ -68,11 +68,7 @@ above; otherwise it is a NOTE. A file added to or deleted from `tests/testcases/tinycc/` moves the count `tests/testcases/LICENSE` states in the same diff, and `46_grep.c` never comes back. Nothing else is tracked under `tests/testcases/` but that `LICENSE`, `system.toml` and `hello.c`. -- **What no gate reads.** A BLOCKER each: a diff that declares a retired ABI name or reuses a - retired syscall, `SYS_DEBUG` action or inbox op number (the retired numbers are - `kernel/src/syscall/dispatch.rs`'s `retired_syscalls!` and the "formerly …" and "retired and - unused" entries in `toyos-abi/src/syscall.rs` and `toyos-abi/src/inbox.rs`; the retired names - include `SharedToken` and `services::connect`); a workspace member's `Cargo.toml` declaring `[profile]` or `[patch]`, which +- **What no gate reads.** A BLOCKER each: a workspace member's `Cargo.toml` declaring `[profile]` or `[patch]`, which cargo ignores with only a warning; a new package without a `description` saying what it is. A new cargo feature or `cfg` arm of one, and every arm a changed `src/clippy.rs` shape stops building, is shown linted in the pull request body: a `mem::forget` planted in that arm turns `cargo run -- --clippy` red. - **Growth.** Every line is a responsibility, not an asset. State the branch's net lines diff --git a/CLAUDE.md b/CLAUDE.md index 285817eedf0..acffc08bb80 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,7 +44,7 @@ A subdirectory `CLAUDE.md` loads when a file in that subtree is `Read`, and not **The log is a userland file.** `/system/bin/logd` reads records on a cursor and owns `/log`; the kernel keeps the record ring, the console and the panel, and writes no file. `SYS_FSYNC` reaches the device's cache flush because logd's durability claim rests on it. -**Syscall ABI** — `toyos-abi/`: struct layouts, syscall numbers, typed wrappers; completely unstable, read the code. Never add or change a syscall without discussion; a deleted syscall's number is retired, never reused. `toyos/` builds on it with typed handles, IPC framing, ports, namespaces and `surface` — userland uses `toyos`, the kernel uses `toyos-abi` only. +**Syscall ABI** — `toyos-abi/`: struct layouts, syscall numbers, typed wrappers; read the code. Never add or change a syscall without discussion; the ABI is unstable until ToyOS is adopted and breaks freely. `toyos/` builds on it with typed handles, IPC framing, ports, namespaces and `surface` — userland uses `toyos`, the kernel uses `toyos-abi` only. **Capabilities** — a process holds exactly what its parent moved into it, and among kernel objects there is nothing it can name to get more. No registry, no connect-by-name, no pid-as-authority: `/system/bin/init` builds every program's namespace and device claims from `system.toml` before spawning it, and a handle a process does not hold is a bug in that process — the kernel ends it rather than answering a word it can ignore. **Isolation is non-negotiable, and the filesystem is inside it**: a process names only the paths in the view its parent built for it, the unit of isolation is the program, and a user is the part of the tree a session was handed. Not yet true of files: the kernel still resolves every path against one machine-wide tree until the storage track's per-program views land. diff --git a/issues/build/mio-deregister-fd-leaves-a-pending-poll-live.md b/issues/build/mio-deregister-fd-leaves-a-pending-poll-live.md index 1bcf19f948e..75d98169392 100644 --- a/issues/build/mio-deregister-fd-leaves-a-pending-poll-live.md +++ b/issues/build/mio-deregister-fd-leaves-a-pending-poll-live.md @@ -34,8 +34,7 @@ notification for a resource it was promised was gone. There used to be an ABI op for this. `toyos-abi/src/inbox.rs` op code 2 was `IORING_OP_POLL_REMOVE`, retired in PR #89 (`c41b831`, -"abi: four names retired, and the number each one held") as caller-less. The -retirement's own reasoning is recorded at the site: +"abi: four names retired, and the number each one held") as caller-less: ``` // Op code 2 unused (formerly IORING_OP_POLL_REMOVE). It had no submitter diff --git a/issues/build/no-device-class-answers-for-a-block-device.md b/issues/build/no-device-class-answers-for-a-block-device.md index 083bdf45331..88d71a89da4 100644 --- a/issues/build/no-device-class-answers-for-a-block-device.md +++ b/issues/build/no-device-class-answers-for-a-block-device.md @@ -31,7 +31,7 @@ and none of them is a block device: VirtioSound = 6 => "virtio-sound", PciFunction = 7 => "pci", -(3 and 4 are retired.) `PciFunction` is not the answer either: it names one +`PciFunction` is not the answer either: it names one function by vendor and device id and hands it to a process to drive, and what this gate has to ask is whether a controller *the kernel* binds bound. So no `SYS_DEVICE_CLAIM` can answer for the stuck controller, and diff --git a/issues/diagnostics/no-cyclictest.md b/issues/diagnostics/no-cyclictest.md index c8ee0a7250b..cf63c8cda6d 100644 --- a/issues/diagnostics/no-cyclictest.md +++ b/issues/diagnostics/no-cyclictest.md @@ -21,8 +21,7 @@ the privilege: a `SysCap` carrying `Rights::RT`, granted by a `system.toml` row and not in the scheduler. The gate this file was opened against is gone — `SYS_SET_RT_PRIORITY` (96) demanded a `VirtioSound` or `HdaAudio` device claim, so a latency tool could only reach the band by taking the sound card away from -soundd and measuring a different machine. Number 96 is retired, and -`toyos-abi/src/syscall.rs` says why: "a claim is not a privilege". +soundd and measuring a different machine. **What exists is not a substitute, and each instrument fails differently.** soundd's `max_wake_lat_ns` (`toyos-mixer/src/stats.rs`, printed by diff --git a/issues/diagnostics/the-kernel-keeps-nothing-it-enumerates.md b/issues/diagnostics/the-kernel-keeps-nothing-it-enumerates.md index 77313587a3e..df521d2b9ac 100644 --- a/issues/diagnostics/the-kernel-keeps-nothing-it-enumerates.md +++ b/issues/diagnostics/the-kernel-keeps-nothing-it-enumerates.md @@ -44,14 +44,6 @@ On top of retention, in dependency order: One residual is nearly free: a log-follow tool is a manifest row and about 30 lines, because `LogTail` already exists in the SDK. -**Syscall numbers: the three this work once reserved are all taken, and the -obvious fix is wrong too.** 97 and 98 are the device register read/write pair, -99 is the endowments call. Re-allocating "from 128 up" breaks a compile-time -assert, because 128 is the syscall-profile bin count and 127 is its overflow -bucket. **The first clean numbers are 116, 117, 118** — 113 and 115 are held -reservations, 114 is spent on log reading, and 96 and 107 are retired and never -reused. - The log half of this work landed differently and better, and the difference is worth knowing before anything is rebuilt on the old design: reading is record-shaped rather than byte-shaped, it is authority (`Rights::LOG` on a diff --git a/issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md b/issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md index 8a725701db4..d32950b2916 100644 --- a/issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md +++ b/issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md @@ -45,11 +45,10 @@ directory is `issues/isolation/every-program-sees-only-the-files-it-was-given.md init's `SysCap`, the `reopenable` column, `process::process_object`, `reopen_selftest` and `sched::kthread::open_selftest` with their actuator, guest test, judge and rows — and - `process_lifecycle`'s pid-open arm, its only caller; 110 enters - `retired_syscalls!`. `issues/kernel/the-capability-end-state-is-twelve-answers.md` + `process_lifecycle`'s pid-open arm, its only caller. + `issues/kernel/the-capability-end-state-is-twelve-answers.md` and `issues/diagnostics/the-kernel-keeps-nothing-it-enumerates.md`, which - argue from it, change in the same landing. *Exit*: a call of 110 answers - `NotSupported` and the log names it retired; `git grep` finds no deleted + argue from it, change in the same landing. *Exit*: `git grep` finds no deleted name outside `toyos-symbols/tests/fixtures/input-test.bin`, a frozen binary whose symbols are test data. Negative control: the stage reverted whole, where `sys_process_open` answers 110. Oracle: rustc's name resolution, diff --git a/issues/kernel/sys-clock-realtime-is-now-a-format-of-sys-clock-epoch.md b/issues/kernel/sys-clock-realtime-is-now-a-format-of-sys-clock-epoch.md index 29c87694a98..237254c4634 100644 --- a/issues/kernel/sys-clock-realtime-is-now-a-format-of-sys-clock-epoch.md +++ b/issues/kernel/sys-clock-realtime-is-now-a-format-of-sys-clock-epoch.md @@ -12,8 +12,7 @@ into `h:m:s` (`toyos_abi::syscall::clock_realtime`), which every caller today could derive from `clock_epoch()` instead, with no information `SYS_CLOCK_REALTIME` carries that `SYS_CLOCK_EPOCH` does not. -Exit condition: `SYS_CLOCK_REALTIME`'s number (42) is retired and never -reused, its kernel handler and `toyos_abi::syscall::clock_realtime` are +Exit condition: `SYS_CLOCK_REALTIME`, its kernel handler and `toyos_abi::syscall::clock_realtime` are deleted, `toyos::system::clock_realtime` and both its callers (`userland/compositor/src/render.rs`, `tests/toyos-rust-tests/src/bin/wall_clock_now.rs`) move to `clock_epoch`, all diff --git a/issues/kernel/the-capability-end-state-is-twelve-answers.md b/issues/kernel/the-capability-end-state-is-twelve-answers.md index faf60ebea39..25e7c8a1020 100644 --- a/issues/kernel/the-capability-end-state-is-twelve-answers.md +++ b/issues/kernel/the-capability-end-state-is-twelve-answers.md @@ -111,8 +111,7 @@ beside it" (`toyos-abi/src/syscall.rs:1803`). Tids are process-local names: `SYS_THREAD_JOIN` resolves through `thread_sched(caller, tid)` and `collect_thread_zombie(table, tid, parent_pid)`, both keyed on the caller's own pid (`4a98107f^:kernel/src/arch/syscall.rs:2393`, `kernel/src/process.rs:1412`, `:848`). -Four pid-addressed syscalls were deleted and their numbers retired rather than -reused — 26 `SYS_WAITPID`, 33 `SYS_FIND_PID`, 37 `SYS_GRANT_SHARED`, 65 +Four pid-addressed syscalls were deleted — 26 `SYS_WAITPID`, 33 `SYS_FIND_PID`, 37 `SYS_GRANT_SHARED`, 65 `SYS_KILL` (`4a98107f^:kernel/src/arch/syscall.rs:63`). ## 4. Can a process enumerate objects it lacks authority over? — RULED 2026-08-20, IMPLEMENTED 2026-08-22 @@ -313,9 +312,7 @@ no replace", and a narrower one is a *new* object built from an existing one (`4a98107f^:kernel/src/arch/syscall.rs:1861`); `SYS_NAMESPACE_BUILD` demands `Rights::TRANSFER` on every added connector and resolves kept names against the base before installing anything (`:1754`). A process with no `svc` endowment -resolves no name at all, and there is no registry to fall back to: 85 -`SYS_LISTEN` and 87 `SYS_CONNECT` are retired numbers -(`4a98107f^:kernel/src/arch/syscall.rs:77`, `:78`). The **filesystem** path space is the +resolves no name at all, and there is no registry to fall back to. The **filesystem** path space is the other thing the word could mean, and it is ambient process state (`4a98107f^:kernel/src/arch/syscall.rs:1234`) — questions 2 and 5 hold that half. diff --git a/issues/kernel/the-kernel-still-parses-what-userland-writes.md b/issues/kernel/the-kernel-still-parses-what-userland-writes.md index cc6206ffb9d..b591a5a2ff5 100644 --- a/issues/kernel/the-kernel-still-parses-what-userland-writes.md +++ b/issues/kernel/the-kernel-still-parses-what-userland-writes.md @@ -19,7 +19,7 @@ and it holds the most privileged `unsafe`. No production kernel dynamic-links userland. The end state: the kernel maps the `PT_LOAD`s of a static-PIE image and jumps; everything else runs in a Ring 3 loader inside the target's own address space, holding only the file handles it was endowed, where a crafted -binary can only corrupt itself. Five syscalls retire, numbers never reused, and +binary can only corrupt itself. Five syscalls retire, and the crafted-ELF corpus becomes the negative gate against a kernel that no longer parses most of it. diff --git a/kernel/src/inbox/mod.rs b/kernel/src/inbox/mod.rs index 8e6ed014fd1..a9c02b454b8 100644 --- a/kernel/src/inbox/mod.rs +++ b/kernel/src/inbox/mod.rs @@ -92,7 +92,6 @@ pub enum Op { impl Op { fn from_raw(raw: u8) -> Result { - // 2 is retired (formerly IORING_OP_POLL_REMOVE); it refuses like any undeclared op. match raw { 0 => Ok(Self::Nop), 1 => Ok(Self::Watch), diff --git a/kernel/src/syscall/dispatch.rs b/kernel/src/syscall/dispatch.rs index f3a5ed190e2..d5f3d3542f4 100644 --- a/kernel/src/syscall/dispatch.rs +++ b/kernel/src/syscall/dispatch.rs @@ -52,51 +52,6 @@ use super::proc::{ }; use super::vm::{shared_image, sys_dlopen, sys_dlsym, sys_mmap, sys_munmap, sys_query_modules, sys_tls_alloc_block}; -/// A number a deleted syscall used is retired, never reused. -macro_rules! retired_syscalls { - ($($num:literal => $name:literal),+ $(,)?) => { - const RETIRED_SYSCALLS: &[(u64, &str)] = &[$(($num, $name)),+]; - - const _: () = { - let mut i = 1; - while i < RETIRED_SYSCALLS.len() { - assert!( - RETIRED_SYSCALLS[i - 1].0 < RETIRED_SYSCALLS[i].0, - "the retired-syscall table is not strictly ascending, so a \ - number is retired twice or the list is unreadable", - ); - i += 1; - } - }; - - fn retired_syscall(num: u64) -> Option<&'static str> { - RETIRED_SYSCALLS.iter().find(|(n, _)| *n == num).map(|(_, name)| *name) - } - }; -} - -retired_syscalls! { - 26 => "SYS_WAITPID", - 29 => "SYS_SEND_MSG", - 30 => "SYS_RECV_MSG", - 31 => "SYS_OPEN_DEVICE", - 32 => "SYS_REGISTER_NAME", - 33 => "SYS_FIND_PID", - 36 => "SYS_ALLOC_SHARED", - 37 => "SYS_GRANT_SHARED", - 38 => "SYS_MAP_SHARED", - 39 => "SYS_RELEASE_SHARED", - 65 => "SYS_KILL", - 68 => "SYS_PIPE_OPEN", - 70 => "SYS_PIPE_ID", - 78 => "SYS_NIC_RX_POLL", - 79 => "SYS_NIC_RX_DONE", - 80 => "SYS_NIC_TX", - 85 => "SYS_LISTEN", - 87 => "SYS_CONNECT", - 96 => "SYS_SET_RT_PRIORITY", -} - /// `sched-operation-nesting`'s task half and `sysret-ss-probe`, run once a boot /// on the first syscall: a task's own deadline slot, and a park that switches /// away from it and back. @@ -666,14 +621,10 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> }, SYS_DEVICE_REG_READ => sys_device_reg(RawHandle(a1 as u32), a2, a3, None), SYS_DEVICE_REG_WRITE => sys_device_reg(RawHandle(a1 as u32), a2, a3, Some(a4)), - // Retired, not reused: an old binary is told which call it was, not that the number is nonsense. - _ => match retired_syscall(num) { - Some(name) => { - crate::log!("syscall {num} is retired (formerly {name})"); - SyscallError::NotSupported.to_u64() - } - None => SyscallError::InvalidArgument.to_u64(), - }, + _ => { + crate::log!("syscall {num} is unknown"); + SyscallError::InvalidArgument.to_u64() + } } })(); // The first of the object layer's three drain sites. Here, not at the drop that diff --git a/tests/common/origin.rs b/tests/common/origin.rs index fcc8cd09531..a1eae3af50a 100644 --- a/tests/common/origin.rs +++ b/tests/common/origin.rs @@ -496,7 +496,7 @@ const HOLD_JOB: &str = "test_rs_log_hold"; const HOLD_LINE: &str = "log hold: said after 192 records"; const HOLD_RECORDS: usize = 192; /// The kernel's record of each of those. -const RETIRED: &str = "syscall 26 is retired"; +const UNKNOWN: &str = "syscall 18446744073709551615 is unknown"; /// **A program's line lands between the records written before and after /// it.** `test_rs_log_hold` has the kernel write three batches of records, @@ -518,7 +518,7 @@ pub fn after_records(c_bins: &[(String, Vec)], rust_bins: &[(String, Vec let records: Vec = lines .iter() .enumerate() - .filter(|(_, l)| !toyos_logstream::is_program_line(l) && l.contains(RETIRED)) + .filter(|(_, l)| !toyos_logstream::is_program_line(l) && l.contains(UNKNOWN)) .map(|(i, _)| i) .collect(); if records.len() != HOLD_RECORDS { diff --git a/tests/toyos-rust-tests/src/bin/log_hold.rs b/tests/toyos-rust-tests/src/bin/log_hold.rs index e0ee228e63e..5eea708be44 100644 --- a/tests/toyos-rust-tests/src/bin/log_hold.rs +++ b/tests/toyos-rust-tests/src/bin/log_hold.rs @@ -6,9 +6,9 @@ /// Three times what `logd` asks of the ring at once (`BATCH`, 64). const RECORDS: usize = 192; -/// A retired syscall's number: each call is refused and is one kernel record -/// naming it (`kernel/src/syscall/dispatch.rs`'s `retired_syscall`). -const RETIRED: u64 = 26; +/// No syscall's number: each call is refused and is one kernel record naming +/// it (`kernel/src/syscall/dispatch.rs`'s unknown-number arm). +const UNKNOWN: u64 = u64::MAX; fn main() { for _ in 0..RECORDS { @@ -18,13 +18,13 @@ fn main() { unsafe { core::arch::asm!( "syscall", - in("rdi") RETIRED, + in("rdi") UNKNOWN, lateout("rax") ret, out("rcx") _, out("r11") _, ); } - assert_ne!(ret, 0, "syscall {RETIRED} answered as if it were live"); + assert_ne!(ret, 0, "syscall {UNKNOWN} answered as if it were live"); } println!("log hold: said after {RECORDS} records"); } diff --git a/tests/toyos-rust-tests/src/bin/panic_halts_first.rs b/tests/toyos-rust-tests/src/bin/panic_halts_first.rs index b97eb5403c6..48d598691bf 100644 --- a/tests/toyos-rust-tests/src/bin/panic_halts_first.rs +++ b/tests/toyos-rust-tests/src/bin/panic_halts_first.rs @@ -10,17 +10,17 @@ use std::sync::Arc; #[path = "../arch/mod.rs"] mod arch; -/// A retired syscall's number: each call is refused and is one kernel record -/// naming it. -const RETIRED: u64 = 26; +/// No syscall's number: each call is refused and is one kernel record naming +/// it. +const UNKNOWN: u64 = u64::MAX; /// One per other CPU of the boot that runs this. const SIBLINGS: usize = 3; -fn retired() { - // SAFETY: a retired number, which the kernel refuses without reading any +fn unknown() { + // SAFETY: no syscall's number, which the kernel refuses without reading any // argument; nothing in this process is touched. - let ret = unsafe { arch::bare_syscall(RETIRED) }; - assert_ne!(ret, 0, "syscall {RETIRED} answered as if it were live"); + let ret = unsafe { arch::bare_syscall(UNKNOWN) }; + assert_ne!(ret, 0, "syscall {UNKNOWN} answered as if it were live"); } fn main() { @@ -28,10 +28,10 @@ fn main() { for _ in 0..SIBLINGS { let started = Arc::clone(&started); std::thread::spawn(move || { - retired(); + unknown(); started.fetch_add(1, Ordering::Release); loop { - retired(); + unknown(); } }); } diff --git a/tests/toyos.rs b/tests/toyos.rs index 13739b7a22a..bdc433d6065 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -16069,7 +16069,7 @@ fn the_others_halt_first(mut qemu: QemuInstance, arch: toyos_build::arch::Arch) let fatal = fatal_past_the_stop(&console).expect("awaited above"); let before = &console[..console.find(FATAL_HALT_NONCE).expect("awaited above")]; // Non-vacuity: another CPU was making records up to the fatal one. - if !before.lines().any(|l| l.contains(RETIRED_RECORD) && record_cpu(l).is_some_and(|cpu| cpu != fatal)) { + if !before.lines().any(|l| l.contains(UNKNOWN_RECORD) && record_cpu(l).is_some_and(|cpu| cpu != fatal)) { return Err(format!( "no other CPU made a record before the fatal one on cpu{fatal}, so nothing was running \ to be stopped\n{console}" @@ -16099,7 +16099,7 @@ fn the_others_halt_first(mut qemu: QemuInstance, arch: toyos_build::arch::Arch) } /// The record each sibling of `test_rs_panic_halts_first` makes, over and over. -const RETIRED_RECORD: &str = "syscall 26 is retired"; +const UNKNOWN_RECORD: &str = "syscall 18446744073709551615 is unknown"; /// The CPU a kernel record is stamped with: `[kernel cpu]`. fn record_cpu(line: &str) -> Option { diff --git a/toyos-abi/src/inbox.rs b/toyos-abi/src/inbox.rs index c9f85adf054..8500b391ed3 100644 --- a/toyos-abi/src/inbox.rs +++ b/toyos-abi/src/inbox.rs @@ -11,13 +11,7 @@ use crate::RawHandle; pub const OP_NOP: u8 = 0; pub const OP_WATCH: u8 = 1; -// Op code 2 unused (formerly IORING_OP_POLL_REMOVE): a watch this kernel takes -// is one-shot, consumed by the completion it posts, so the interest a remove -// would withdraw is gone before there is anything to name. pub const OP_ACCEPT: u8 = 3; -// Op code 4 unused (formerly IORING_OP_CLOSE): it is the one handle path that -// cannot obey the bad-handle policy, because it runs under the ring's own lock -// where taking the process down is not available. /// Readiness flags for [`OP_WATCH`], stored in `Submission::op_flags`. /// diff --git a/toyos-abi/src/inventory.rs b/toyos-abi/src/inventory.rs index f4be551442d..36e959d9a3f 100644 --- a/toyos-abi/src/inventory.rs +++ b/toyos-abi/src/inventory.rs @@ -587,7 +587,7 @@ mod tests { Err(Undecodable::Variant { at: 7, value: u64::from(psiv) }) ); } - // A class number no `DeviceType` carries: 3 and 4 are retired. + // A class number no `DeviceType` carries. let mut raw = Record::Claim(Claim { on: Claimed::Class(DeviceType::Keyboard), holder: Holder { pid: 1, name: name("x") }, diff --git a/toyos-abi/src/syscall.rs b/toyos-abi/src/syscall.rs index 1ec384059a9..df929578428 100644 --- a/toyos-abi/src/syscall.rs +++ b/toyos-abi/src/syscall.rs @@ -1,13 +1,8 @@ // Syscall numbers (must match kernel dispatch table) pub const SYS_WRITE: u64 = 0; pub const SYS_READ: u64 = 1; -// Syscall numbers 2-4 are unused (formerly SYS_ALLOC/FREE/REALLOC). pub const SYS_THREAD_EXIT: u64 = 5; pub const SYS_RANDOM: u64 = 6; -// Syscall number 7 unused (formerly SYS_SCREEN_SIZE). -// Syscall number 8 is retired and unused: it was `SYS_CLOCK`, the monotonic -// clock as a kernel transition. A process reads it off its clock page -// (`crate::clock`) with no transition at all. pub const SYS_OPEN: u64 = 9; pub const SYS_CLOSE: u64 = 10; pub const SYS_SEEK: u64 = 13; @@ -18,28 +13,12 @@ pub const SYS_DELETE: u64 = 18; pub const SYS_SHUTDOWN: u64 = 19; pub const SYS_CHDIR: u64 = 20; pub const SYS_GETCWD: u64 = 21; -// Syscall number 23 unused (formerly SYS_SET_KEYBOARD_LAYOUT: the kernel has -// no layout to set — it delivers key transitions and userland translates). pub const SYS_PIPE: u64 = 24; /// Start a program, endowing it exactly what the caller names. Answers a /// `Process` handle — see [`spawn`]. pub const SYS_SPAWN: u64 = 25; -// Syscall number 26 unused (formerly SYS_WAITPID: a pid is not authority over a -// process, and pids are reissued. Waiting is SYS_PROCESS_WAIT on the handle the -// spawn answered with). pub const SYS_MARK_TTY: u64 = 28; -// Syscall numbers 29-31 unused (formerly SYS_SEND_MSG/SYS_RECV_MSG and -// SYS_OPEN_DEVICE: first-come claiming, where whoever asked first got the -// device. Arbitration is the manifest — init mints every claim from a `SysCap` -// and endows it). -// Syscall numbers 32-33 unused (formerly SYS_REGISTER_NAME/SYS_FIND_PID). -// Syscall number 34 unused (formerly SYS_SET_SCREEN_SIZE). pub const SYS_GPU_PRESENT: u64 = 35; -// Syscall numbers 36-39 unused (formerly SYS_ALLOC_SHARED, SYS_GRANT_SHARED, -// SYS_MAP_SHARED and SYS_RELEASE_SHARED: a shared-memory token was an id -// treated as a capability and the grant list was a pid ACL. A region is a -// handle — SYS_SHM_CREATE and SYS_SHM_MAP — and giving one away is -// SYS_HANDLE_SEND). pub const SYS_THREAD_SPAWN: u64 = 40; pub const SYS_THREAD_JOIN: u64 = 41; pub const SYS_CLOCK_REALTIME: u64 = 42; @@ -101,9 +80,6 @@ impl SysinfoHeader { /// it is a secondary thread, resident memory, CPU nanoseconds, and a 28-byte /// name. pub const SYSINFO_ENTRY_SIZE: usize = 64; -// Syscall numbers 46-48 unused (formerly SYS_NET_INFO/SYS_NET_SEND/SYS_NET_RECV: -// an ungated frame-copy path that no program ever used — netd drives the NIC -// through its DMA descriptor instead). pub const SYS_NANOSLEEP: u64 = 49; /// A second handle to the same object, carrying no more than the first. See /// [`dup`] and [`dup_narrowed`]. @@ -122,18 +98,8 @@ pub const SYS_STACK_INFO: u64 = 61; pub const SYS_CPU_COUNT: u64 = 62; pub const SYS_MMAP: u64 = 63; pub const SYS_MUNMAP: u64 = 64; -// Syscall number 65 unused (formerly SYS_KILL: pid-addressed, and gated on -// being the target's parent — a relationship the kernel happened to remember, -// not a capability anyone was given. SYS_PROCESS_KILL takes a handle carrying -// `Rights::MANAGE`). pub const SYS_READ_NONBLOCK: u64 = 66; pub const SYS_WRITE_NONBLOCK: u64 = 67; -// Syscall numbers 68 and 70 unused (formerly SYS_PIPE_OPEN and SYS_PIPE_ID: a -// pipe id was guessable, and openable by anyone its creator had ever spoken to. -// A pipe end travels as itself, over SYS_HANDLE_SEND). -// Syscall numbers 71 and 84 unused (formerly SYS_AUDIO_SUBMIT and -// SYS_AUDIO_POLL: the kernel drives no sound card, so a period is published -// into a ring the kernel built and there is nothing to submit). pub const SYS_EXIT: u64 = 72; pub const SYS_GET_ENV: u64 = 73; /// A second handle to the same object, at a slot the caller picks. See @@ -142,43 +108,23 @@ pub const SYS_HANDLE_DUP_AT: u64 = 74; pub const SYS_CLOCK_EPOCH: u64 = 75; /// Join a pipe read end and a pipe write end into one duplex `Connection`. /// See [`connection_join`]. -/// -/// It keeps the number of `SYS_SOCKET_CREATE`, which was the same operation -/// over two pipe *ids*: what is retired is addressing a pipe by a number anyone -/// could guess, not making a duplex object out of two simplex ends. pub const SYS_CONNECTION_JOIN: u64 = 76; pub const SYS_PIPE_MAP: u64 = 77; -// Syscall numbers 78-80 unused (formerly SYS_NIC_RX_POLL, SYS_NIC_RX_DONE and -// SYS_NIC_TX: a NIC driver inside the kernel answering for its claimant. A -// claimed PCI function's driver is the claimant, and reaches its device -// through SYS_DEVICE_BAR_MAP, SYS_DEVICE_DMA_ALLOC and its claim handle). pub const SYS_SYMLINK: u64 = 81; pub const SYS_READLINK: u64 = 82; pub const SYS_GPU_SET_RESOLUTION: u64 = 83; -// Syscall number 85 is retired and unused: it was `SYS_LISTEN`, which took a -// service name first-come from a flat global registry. There is no registry; -// a server is endowed an acceptor. /// Accept a queued connection from an [`Acceptor`] handle. /// /// [`Acceptor`]: crate::handle::RawHandle pub const SYS_ACCEPT: u64 = 86; -// Syscall number 87 is retired and unused: it was `SYS_CONNECT`, which -// resolved a name through that registry. A name resolves in a namespace a -// process was given, through `SYS_NAMESPACE_OPEN`, or nowhere. /// Allocate a TLS block for a dlopen'd module on the current thread. /// Arg0: module_id (1-based DTV index). Returns the block's virtual address, /// or a `SyscallError` word — see [`tls_alloc_block`]. pub const SYS_TLS_ALLOC_BLOCK: u64 = 88; /// Create an [`inbox`](crate::inbox) and map its rings. See [`inbox_setup`]. -/// -/// **A rename is not a retirement, which is why this is still 89.** The rule -/// that a deleted syscall's number is retired and never reused is about a -/// *deleted* call: 89 and 90 kept their arguments and their struct layouts when -/// `SYS_IO_URING_SETUP`/`SYS_IO_URING_ENTER` became these, so no number was -/// taken. pub const SYS_INBOX_SETUP: u64 = 89; /// Hand queued submissions to the kernel and/or wait for completions. See -/// [`inbox_submit`]; on the number, see [`SYS_INBOX_SETUP`]. +/// [`inbox_submit`]. pub const SYS_INBOX_SUBMIT: u64 = 90; pub const SYS_QUERY_MODULES: u64 = 91; /// Debug syscall. Arg0 selects the action: @@ -188,9 +134,6 @@ pub const SYS_DEBUG: u64 = 92; pub const SYS_SCHED_INFO: u64 = 93; pub const SYS_PROCESS_STATS: u64 = 94; pub const SYS_SET_THREAD_NAME: u64 = 95; -// Syscall number 96 unused (formerly SYS_SET_RT_PRIORITY: gated on holding a -// sound-device claim, and a claim is not a privilege. [`SYS_RT_ENTER`] is the -// privilege that gate was standing in for). /// Read one register of a claimed device. See [`device_reg_read`]. pub const SYS_DEVICE_REG_READ: u64 = 97; /// Write one register of a claimed device. See [`device_reg_write`]. @@ -241,11 +184,6 @@ pub const SYS_SHM_CREATE: u64 = 105; /// Map a region into the caller. Idempotent: a second call answers the first /// call's address. See [`shm_map`]. pub const SYS_SHM_MAP: u64 = 106; -// Syscall number 107 is retired and unused: it took a process's mapping away -// while it kept the handle. A region's mappings go with its last handle -// (`ZeroHandles for SharedMemObject`), so letting the handle go is the whole of -// letting the mapping go, and unmapping behind a handle its holder still has is -// a second spelling of the same event that the two can disagree about. /// Wait for the process a handle names and take its exit code, gated by /// [`Rights::WAIT`]. See [`process_wait`]. @@ -290,7 +228,7 @@ pub const SYS_RT_ENTER: u64 = 112; // the one thing that would make any `serves` daemon restartable. Nothing // needs it yet, so nothing is built. // -// 115 is likewise held, for `SYS_SLEEP_UNTIL`, which would replace the retired +// 115 is likewise held, for `SYS_SLEEP_UNTIL`, which would replace // `SYS_NANOSLEEP`. // // **Both are recorded here and nowhere else**, because this file is where an @@ -833,10 +771,6 @@ pub mod debug_action { /// whichever comes first. pub const TLB_ACK_DELAY_ARM: u64 = 12; pub const TLB_ACK_DELAY_DISARM: u64 = 13; - // Actions 14 and 15 are retired and unused: they were CENSUS_TOTAL and - // CENSUS_BREAKDOWN. A total hides a leak of one kind behind churn in - // another, and a breakdown written into the kernel log is a reading no - // guest test can see. /// How many kernel objects of one kind are alive right now. The argument is /// an [`OBJECT_KINDS`](super::OBJECT_KINDS) index. /// @@ -1253,13 +1187,6 @@ device_classes! { Keyboard = 0 => "keyboard", Mouse = 1 => "mouse", Framebuffer = 2 => "framebuffer", - // 3 was `Nic`: a network card the kernel drove, whose holder got rx and tx - // tokens. Retired rather than reused for `PciFunction`, since a caller - // that still names 3 wants a capability of a different shape. - // 4 was `Audio`, a sound card the kernel drove on the claimant's behalf. - // Retired rather than reused for the stubs below: a claim here authorizes - // register writes and answers no submit, so a caller that still names 4 is - // refused rather than handed a capability of a different shape. /// An Intel HDA controller the kernel has brought up but drives no policy /// on. HdaAudio = 5 => "hda-audio", @@ -2459,7 +2386,6 @@ mod tests { "pci", // the bare class name names no function at all "pci:", // nor does the prefix on its own "pci:1af4", - "nic", // the retired class the NIC used to be claimed as "gpu", // a class name this table has never had "part", // a bare partition class names no partition "part:", diff --git a/toyos/src/census.rs b/toyos/src/census.rs index faf7c05ab97..1a57603d00d 100644 --- a/toyos/src/census.rs +++ b/toyos/src/census.rs @@ -5,10 +5,7 @@ //! released is invisible behind ordinary churn in another — and six of the //! thirteen kinds (`File`, `Device`, `Acceptor`, `Connection`, `IoUring`, //! `Console`) were exercised by no census assertion at all, which is where -//! three of this branch's defects lived. The kernel has always counted per -//! kind; the readers that answered a total or wrote the breakdown into the -//! kernel log, where no guest test can see one, are retired, and this is what -//! is left. +//! three of this branch's defects lived. //! //! Two readings and a comparison, never one reading against a constant: an //! object released by another process is dropped from the deferred queue on From 1879a5b1be42e910f3e8e5f41fdc7123f1bbafef Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 1 Oct 2026 02:25:13 +0200 Subject: [PATCH 2/7] Answer review round 1 of #651: an unknown number is refused silently, and pinned B1: the default syscall arm goes back to `_ => InvalidArgument`, with no record. Round 1 gave it a `crate::log!` per call so two tests had a cheap record to make. Any process can drive that site at syscall rate with no authority, which floods every CPU's shard and rotates `/log`; the exit-time `syscalls:` record already names every number a process called. B2: the two tests make their records with `SYS_DEBUG` `LOG_PATTERNED`, the log path's own generator. `panic_halts_first`'s siblings record `logstorm t=0 i=0` and its judge reads that. `log_hold` records `i=0..191`, boots the test kernel, and its judge counts each index exactly once. A staged image picks its kernel from its parameters and `log_hold` arms nothing, so `qemu::build_test_kernel_image` and `logstream::stage_on_test_kernel` stage one on the test kernel, and `origin::staged_job` is the boot `one_job` and `one_job_armed` now share. `log_hold`'s inline `asm!` goes. The test crate's `arch` module stays, because the B3 test calls it. B3: `syscall_unassigned` runs on the shipping kernel's shared boot. It asserts that syscall 26 and syscall `u64::MAX` answer `InvalidArgument`. It goes red if the default arm answers `NotSupported`, and on e754dc888, where 26 answered `NotSupported` as retired. NOTEs: - CLAUDE.md now says the SDK breaks as freely as the ABI. - inventory.rs tests one past the highest declared device class instead of the once-retired 3 and 4. - The window protocol's retired clipboard type is filed as issues/design-debt/the-window-protocol-keeps-a-retired-message-type.md. The staged test-kernel boot is counted as a shipping boot by the suite's census line. That, and the opposite miscount that already exists, are filed as issues/build/the-boot-census-guesses-a-staged-images-kernel.md. REMOVEs: the 113/115 reservation paragraph in toyos-abi/src/syscall.rs, and census.rs's history and count. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- CLAUDE.md | 2 +- ...t-census-guesses-a-staged-images-kernel.md | 25 ++++++++++ ...w-protocol-keeps-a-retired-message-type.md | 25 ++++++++++ kernel/src/syscall/dispatch.rs | 5 +- tests/common/logstream.rs | 21 +++++++-- tests/common/origin.rs | 46 ++++++++++++++----- tests/common/qemu.rs | 11 +++++ tests/toyos-rust-tests/src/bin/log_hold.rs | 25 +++------- .../src/bin/panic_halts_first.rs | 21 ++++----- .../src/bin/syscall_unassigned.rs | 25 ++++++++++ tests/toyos.rs | 4 +- toyos-abi/src/inventory.rs | 3 +- toyos-abi/src/syscall.rs | 12 ----- toyos/src/census.rs | 7 --- 14 files changed, 159 insertions(+), 73 deletions(-) create mode 100644 issues/build/the-boot-census-guesses-a-staged-images-kernel.md create mode 100644 issues/design-debt/the-window-protocol-keeps-a-retired-message-type.md create mode 100644 tests/toyos-rust-tests/src/bin/syscall_unassigned.rs diff --git a/CLAUDE.md b/CLAUDE.md index acffc08bb80..19c841619dd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -44,7 +44,7 @@ A subdirectory `CLAUDE.md` loads when a file in that subtree is `Read`, and not **The log is a userland file.** `/system/bin/logd` reads records on a cursor and owns `/log`; the kernel keeps the record ring, the console and the panel, and writes no file. `SYS_FSYNC` reaches the device's cache flush because logd's durability claim rests on it. -**Syscall ABI** — `toyos-abi/`: struct layouts, syscall numbers, typed wrappers; read the code. Never add or change a syscall without discussion; the ABI is unstable until ToyOS is adopted and breaks freely. `toyos/` builds on it with typed handles, IPC framing, ports, namespaces and `surface` — userland uses `toyos`, the kernel uses `toyos-abi` only. +**Syscall ABI** — `toyos-abi/`: struct layouts, syscall numbers, typed wrappers; read the code. Never add or change a syscall without discussion; the ABI and the SDK (`toyos-abi` and `toyos/`) are unstable and break freely until ToyOS is adopted. `toyos/` builds on it with typed handles, IPC framing, ports, namespaces and `surface` — userland uses `toyos`, the kernel uses `toyos-abi` only. **Capabilities** — a process holds exactly what its parent moved into it, and among kernel objects there is nothing it can name to get more. No registry, no connect-by-name, no pid-as-authority: `/system/bin/init` builds every program's namespace and device claims from `system.toml` before spawning it, and a handle a process does not hold is a bug in that process — the kernel ends it rather than answering a word it can ignore. **Isolation is non-negotiable, and the filesystem is inside it**: a process names only the paths in the view its parent built for it, the unit of isolation is the program, and a user is the part of the tree a session was handed. Not yet true of files: the kernel still resolves every path against one machine-wide tree until the storage track's per-program views land. diff --git a/issues/build/the-boot-census-guesses-a-staged-images-kernel.md b/issues/build/the-boot-census-guesses-a-staged-images-kernel.md new file mode 100644 index 00000000000..3992a07a4ab --- /dev/null +++ b/issues/build/the-boot-census-guesses-a-staged-images-kernel.md @@ -0,0 +1,25 @@ +--- +status: open +kind: tooling +opened: 2026-10-01 +--- + +# The boot census guesses a staged image's kernel + +The suite's closing line, ` guests, of them not the shipping kernel`, +counts a boot as not the shipping kernel when `qemu::kernel_of` says so +(`tests/common/qemu.rs`). A boot handed a staged `BootOptions::boot_image` +builds nothing, and `kernel_of` answers from the boot's own options instead of +from the image, so two staged boots are counted as the other kernel: + +- an image staged with valued parameters only is the shipping kernel + (`build_boot_image_carrying`), and is counted as the test kernel, because + `kernel_params` is not empty; +- an image staged by `build_test_kernel_image` is the test kernel with nothing + armed (`origin::after_records`), and is counted as the shipping kernel. + +The image records its parameters and not its kernel build, so the boot cannot +ask it. + +**Exit**: a staged image carries the kernel build it was made with, and +`kernel_of` reads that for a staged boot. diff --git a/issues/design-debt/the-window-protocol-keeps-a-retired-message-type.md b/issues/design-debt/the-window-protocol-keeps-a-retired-message-type.md new file mode 100644 index 00000000000..c8999e99af4 --- /dev/null +++ b/issues/design-debt/the-window-protocol-keeps-a-retired-message-type.md @@ -0,0 +1,25 @@ +--- +status: open +kind: defect +opened: 2026-10-01 +--- + +# The window protocol keeps a retired message type + +The SDK breaks freely until ToyOS is adopted, and a deleted syscall, `SYS_DEBUG` +action or inbox op is simply deleted. The window protocol still carries one +retired type and refuses it by name: + +- `userland/toyos-window/src/lib.rs`: `MSG_RETIRED_CLIPBOARD_SET_SHM = 10`; +- `userland/compositor/src/client.rs`: `DropReason::Retired` and its `why`; +- `userland/compositor/src/session.rs`: `Session::dispatch`'s arm for it; +- `tests/toyos-rust-tests/src/bin/compositor_hostile_clipboard.rs`: the case + that sends it, and `tests/toyos.rs`'s judge of the named drop. + +Deleting the type makes a frame of 10 an unknown type, which `Session::dispatch` +reads and drops without a word +(`issues/isolation/the-compositor-ignores-a-message-it-does-not-know.md`). That +issue lands first or with this one. + +**Exit**: `git grep -i retired userland/toyos-window userland/compositor` finds +nothing, and the hostile client's type 10 is refused as `DropReason::OutOfProtocol`. diff --git a/kernel/src/syscall/dispatch.rs b/kernel/src/syscall/dispatch.rs index d5f3d3542f4..72a3c7c066d 100644 --- a/kernel/src/syscall/dispatch.rs +++ b/kernel/src/syscall/dispatch.rs @@ -621,10 +621,7 @@ pub(crate) fn syscall_dispatch(num: u64, a1: u64, a2: u64, a3: u64, a4: u64) -> }, SYS_DEVICE_REG_READ => sys_device_reg(RawHandle(a1 as u32), a2, a3, None), SYS_DEVICE_REG_WRITE => sys_device_reg(RawHandle(a1 as u32), a2, a3, Some(a4)), - _ => { - crate::log!("syscall {num} is unknown"); - SyscallError::InvalidArgument.to_u64() - } + _ => SyscallError::InvalidArgument.to_u64(), } })(); // The first of the object layer's three drain sites. Here, not at the drop that diff --git a/tests/common/logstream.rs b/tests/common/logstream.rs index e991aab264d..752bdd8359c 100644 --- a/tests/common/logstream.rs +++ b/tests/common/logstream.rs @@ -70,10 +70,25 @@ pub fn stage_armed( rust_bins: &[(String, Vec)], ) -> Result { let config = compile::repo_root().join(config); - let bytes = qemu::build_boot_image(&config, c_bins, rust_bins, params); + write_staged(name, &qemu::build_boot_image(&config, c_bins, rust_bins, params)) +} + +/// [`stage`] on the test kernel with nothing armed +/// ([`qemu::build_test_kernel_image`]). +pub fn stage_on_test_kernel( + config: &str, + name: &str, + c_bins: &[(String, Vec)], + rust_bins: &[(String, Vec)], +) -> Result { + let config = compile::repo_root().join(config); + write_staged(name, &qemu::build_test_kernel_image(&config, c_bins, rust_bins)) +} + +fn write_staged(name: &str, bytes: &[u8]) -> Result { let image = super::lane::dir().join(format!("{name}.img")); - std::fs::write(&image, &bytes).map_err(|e| format!("write {}: {e}", image.display()))?; - let (start, len) = volumes::log_extent(&bytes, &image)?; + std::fs::write(&image, bytes).map_err(|e| format!("write {}: {e}", image.display()))?; + let (start, len) = volumes::log_extent(bytes, &image)?; Ok(Staged { image, start, len }) } diff --git a/tests/common/origin.rs b/tests/common/origin.rs index a1eae3af50a..1971447d291 100644 --- a/tests/common/origin.rs +++ b/tests/common/origin.rs @@ -117,7 +117,8 @@ fn one_job( c_bins: &[(String, Vec)], rust_bins: &[(String, Vec)], ) -> Result<(qemu::TestResult, String), String> { - one_job_armed(config, name, job, &[], timeout, c_bins, rust_bins) + let staged = logstream::stage(config, name, c_bins, rust_bins)?; + staged_job(config, &staged, &[], job, timeout, c_bins, rust_bins) } /// [`one_job`], its kernel armed with `params`. @@ -131,6 +132,19 @@ fn one_job_armed( rust_bins: &[(String, Vec)], ) -> Result<(qemu::TestResult, String), String> { let staged = logstream::stage_armed(config, name, params, c_bins, rust_bins)?; + staged_job(config, &staged, params, job, timeout, c_bins, rust_bins) +} + +/// [`one_job`] on `staged`, armed with the `params` it was built with. +fn staged_job( + config: &str, + staged: &logstream::Staged, + params: &'static [&'static str], + job: &str, + timeout: Duration, + c_bins: &[(String, Vec)], + rust_bins: &[(String, Vec)], +) -> Result<(qemu::TestResult, String), String> { let options = BootOptions { boot_image: Some(qemu::Staged::Written(staged.image.clone())), kernel_params: params, @@ -140,7 +154,7 @@ fn one_job_armed( let mut guest = QemuInstance::boot_with_options(&config, c_bins, rust_bins, options); let mut console = guest.boot_log().to_string(); let ran = guest.run_test(job, timeout); - let file = logstream::shut_down(guest, &mut console, &staged)?; + let file = logstream::shut_down(guest, &mut console, staged)?; let _ = std::fs::remove_file(&staged.image); Ok((ran, file.concat())) } @@ -495,8 +509,9 @@ pub fn keeps_the_owners_slots(rust_bins: &[(String, Vec)]) -> Result<(), Str const HOLD_JOB: &str = "test_rs_log_hold"; const HOLD_LINE: &str = "log hold: said after 192 records"; const HOLD_RECORDS: usize = 192; -/// The kernel's record of each of those. -const UNKNOWN: &str = "syscall 18446744073709551615 is unknown"; +/// The kernel's record of each of those, `logstorm t=0 i= …` for index +/// 0 up to [`HOLD_RECORDS`]. +const PATTERNED: &str = "logstorm t=0 i="; /// **A program's line lands between the records written before and after /// it.** `test_rs_log_hold` has the kernel write three batches of records, @@ -505,8 +520,10 @@ const UNKNOWN: &str = "syscall 18446744073709551615 is unknown"; /// last of them are, and only the stamp each was written with puts it after /// them all in `/log` — and before the kernel's record of its exit. pub fn after_records(c_bins: &[(String, Vec)], rust_bins: &[(String, Vec)]) -> Result<(), String> { - let (ran, log) = - one_job("tests/testcases", "log-hold", HOLD_JOB, Duration::from_secs(60), c_bins, rust_bins)?; + const CONFIG: &str = "tests/testcases"; + // The test kernel: the job's records are `SYS_DEBUG`'s. + let staged = logstream::stage_on_test_kernel(CONFIG, "log-hold", c_bins, rust_bins)?; + let (ran, log) = staged_job(CONFIG, &staged, &[], HOLD_JOB, Duration::from_secs(60), c_bins, rust_bins)?; if ran.exit_code != Some(0) { return Err(format!("{HOLD_JOB} exited {:?}\n{}", ran.exit_code, ran.stdout)); } @@ -515,16 +532,23 @@ pub fn after_records(c_bins: &[(String, Vec)], rust_bins: &[(String, Vec .iter() .position(|l| toyos_logstream::program_line(l).is_some_and(|s| s.tag == RUNNER && s.text == HOLD_LINE)) .ok_or_else(|| format!("/log carries no {HOLD_LINE:?} under {RUNNER:?}"))?; - let records: Vec = lines + // Each record's line in /log, and the index it carries. + let records: Vec<(usize, &str)> = lines .iter() .enumerate() - .filter(|(_, l)| !toyos_logstream::is_program_line(l) && l.contains(UNKNOWN)) - .map(|(i, _)| i) + .filter(|(_, l)| !toyos_logstream::is_program_line(l)) + .filter_map(|(at, l)| Some((at, l.split_once(PATTERNED)?.1.split(' ').next()?))) .collect(); if records.len() != HOLD_RECORDS { - return Err(format!("/log carries {} of the job's {HOLD_RECORDS} records", records.len())); + return Err(format!("/log carries {} records for the job's {HOLD_RECORDS}", records.len())); + } + for index in (0..HOLD_RECORDS).map(|i| i.to_string()) { + let times = records.iter().filter(|(_, i)| *i == index).count(); + if times != 1 { + return Err(format!("/log carries the record {PATTERNED}{index} {times} times")); + } } - let after = records.iter().filter(|&&i| i > said).count(); + let after = records.iter().filter(|&&(at, _)| at > said).count(); if after > 0 { return Err(format!( "{after} of the {HOLD_RECORDS} records written before {HOLD_LINE:?} are after it in \ diff --git a/tests/common/qemu.rs b/tests/common/qemu.rs index 7feb27c1729..03972f42561 100644 --- a/tests/common/qemu.rs +++ b/tests/common/qemu.rs @@ -2559,6 +2559,17 @@ pub fn build_boot_image_carrying( build_boot_image_with(SUITE_ARCH, test_crate, c_tests, rust_tests, staged, kernel, kernel_params, false) } +/// [`build_boot_image`] on [`toyos_build::build::TEST_KERNEL`] with nothing +/// armed: for a staged boot whose program calls `SYS_DEBUG`, which no +/// parameter asks for. +pub fn build_test_kernel_image( + test_crate: &Path, + c_tests: &[(String, Vec)], + rust_tests: &[(String, Vec)], +) -> Vec { + build_boot_image_with(SUITE_ARCH, test_crate, c_tests, rust_tests, &[], toyos_build::build::TEST_KERNEL, &[], false) +} + /// Refuse a staged [`BootOptions::boot_image`] that is not the image this /// boot's other options describe. /// diff --git a/tests/toyos-rust-tests/src/bin/log_hold.rs b/tests/toyos-rust-tests/src/bin/log_hold.rs index 5eea708be44..0f7c3d48953 100644 --- a/tests/toyos-rust-tests/src/bin/log_hold.rs +++ b/tests/toyos-rust-tests/src/bin/log_hold.rs @@ -3,28 +3,15 @@ //! program's ring before the kernel's records, so only the stamps order them. //! `log_program_line_after_its_records` runs it. -/// Three times what `logd` asks of the ring at once (`BATCH`, 64). -const RECORDS: usize = 192; +use toyos_abi::syscall::debug_action::LOG_PATTERNED; -/// No syscall's number: each call is refused and is one kernel record naming -/// it (`kernel/src/syscall/dispatch.rs`'s unknown-number arm). -const UNKNOWN: u64 = u64::MAX; +/// Three times what `logd` asks of the ring at once (`BATCH`, 64). +const RECORDS: u64 = 192; fn main() { - for _ in 0..RECORDS { - let ret: u64; - // SAFETY: a register-only `syscall` whose number the kernel refuses - // without reading any argument; nothing in this process is touched. - unsafe { - core::arch::asm!( - "syscall", - in("rdi") UNKNOWN, - lateout("rax") ret, - out("rcx") _, - out("r11") _, - ); - } - assert_ne!(ret, 0, "syscall {UNKNOWN} answered as if it were live"); + for index in 0..RECORDS { + let answer = toyos_abi::syscall::debug_with(LOG_PATTERNED, index); + assert_eq!(answer, 0, "SYS_DEBUG LOG_PATTERNED answered {answer:#x} at index {index}"); } println!("log hold: said after {RECORDS} records"); } diff --git a/tests/toyos-rust-tests/src/bin/panic_halts_first.rs b/tests/toyos-rust-tests/src/bin/panic_halts_first.rs index 48d598691bf..8dc2181fc2a 100644 --- a/tests/toyos-rust-tests/src/bin/panic_halts_first.rs +++ b/tests/toyos-rust-tests/src/bin/panic_halts_first.rs @@ -7,20 +7,15 @@ use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; -#[path = "../arch/mod.rs"] -mod arch; +use toyos_abi::syscall::debug_action::{FATAL_HALT, LOG_PATTERNED}; -/// No syscall's number: each call is refused and is one kernel record naming -/// it. -const UNKNOWN: u64 = u64::MAX; /// One per other CPU of the boot that runs this. const SIBLINGS: usize = 3; -fn unknown() { - // SAFETY: no syscall's number, which the kernel refuses without reading any - // argument; nothing in this process is touched. - let ret = unsafe { arch::bare_syscall(UNKNOWN) }; - assert_ne!(ret, 0, "syscall {UNKNOWN} answered as if it were live"); +/// One kernel record, `logstorm t=0 i=0 …`. +fn record() { + let answer = toyos_abi::syscall::debug_with(LOG_PATTERNED, 0); + assert_eq!(answer, 0, "SYS_DEBUG LOG_PATTERNED answered {answer:#x}"); } fn main() { @@ -28,10 +23,10 @@ fn main() { for _ in 0..SIBLINGS { let started = Arc::clone(&started); std::thread::spawn(move || { - unknown(); + record(); started.fetch_add(1, Ordering::Release); loop { - unknown(); + record(); } }); } @@ -40,7 +35,7 @@ fn main() { while started.load(Ordering::Acquire) < SIBLINGS { std::hint::spin_loop(); } - let rc = toyos_abi::syscall::debug(toyos_abi::syscall::debug_action::FATAL_HALT); + let rc = toyos_abi::syscall::debug(FATAL_HALT); eprintln!("ERROR: SYS_DEBUG FATAL_HALT returned {rc:#x}"); std::process::exit(1); } diff --git a/tests/toyos-rust-tests/src/bin/syscall_unassigned.rs b/tests/toyos-rust-tests/src/bin/syscall_unassigned.rs new file mode 100644 index 00000000000..5bf096f3a3c --- /dev/null +++ b/tests/toyos-rust-tests/src/bin/syscall_unassigned.rs @@ -0,0 +1,25 @@ +//! A number no syscall has is refused as `InvalidArgument`, on the kernel an +//! image ships. + +use toyos_abi::syscall::SyscallError; + +#[path = "../arch/mod.rs"] +mod arch; + +/// Numbers no syscall has: one inside the table's range and the last a +/// register holds. +const UNASSIGNED: [u64; 2] = [26, u64::MAX]; + +fn main() { + for number in UNASSIGNED { + // SAFETY: a number the kernel refuses without reading any argument; + // nothing in this process is touched. + let answer = unsafe { arch::bare_syscall(number) }; + assert_eq!( + answer, + SyscallError::InvalidArgument.to_u64(), + "syscall {number} answered {:?}", + SyscallError::from_u64(answer), + ); + } +} diff --git a/tests/toyos.rs b/tests/toyos.rs index bdc433d6065..9abafe8ce5e 100644 --- a/tests/toyos.rs +++ b/tests/toyos.rs @@ -16069,7 +16069,7 @@ fn the_others_halt_first(mut qemu: QemuInstance, arch: toyos_build::arch::Arch) let fatal = fatal_past_the_stop(&console).expect("awaited above"); let before = &console[..console.find(FATAL_HALT_NONCE).expect("awaited above")]; // Non-vacuity: another CPU was making records up to the fatal one. - if !before.lines().any(|l| l.contains(UNKNOWN_RECORD) && record_cpu(l).is_some_and(|cpu| cpu != fatal)) { + if !before.lines().any(|l| l.contains(SIBLING_RECORD) && record_cpu(l).is_some_and(|cpu| cpu != fatal)) { return Err(format!( "no other CPU made a record before the fatal one on cpu{fatal}, so nothing was running \ to be stopped\n{console}" @@ -16099,7 +16099,7 @@ fn the_others_halt_first(mut qemu: QemuInstance, arch: toyos_build::arch::Arch) } /// The record each sibling of `test_rs_panic_halts_first` makes, over and over. -const UNKNOWN_RECORD: &str = "syscall 18446744073709551615 is unknown"; +const SIBLING_RECORD: &str = "logstorm t=0 i=0 "; /// The CPU a kernel record is stamped with: `[kernel cpu]`. fn record_cpu(line: &str) -> Option { diff --git a/toyos-abi/src/inventory.rs b/toyos-abi/src/inventory.rs index 36e959d9a3f..7bf94dde811 100644 --- a/toyos-abi/src/inventory.rs +++ b/toyos-abi/src/inventory.rs @@ -593,7 +593,8 @@ mod tests { holder: Holder { pid: 1, name: name("x") }, }) .encode(); - for class in [3u64, 4, 1 << 40] { + let past_the_last = DeviceType::ALL.iter().map(|&class| class as u64).max().expect("a class") + 1; + for class in [past_the_last, 1 << 40] { raw.0[4..12].copy_from_slice(&class.to_le_bytes()); assert_eq!(Record::decode(&raw), Err(Undecodable::Variant { at: 4, value: class })); } diff --git a/toyos-abi/src/syscall.rs b/toyos-abi/src/syscall.rs index df929578428..f5f5468b5b3 100644 --- a/toyos-abi/src/syscall.rs +++ b/toyos-abi/src/syscall.rs @@ -223,18 +223,6 @@ pub const SYS_DEVICE_CLAIM: u64 = 111; /// [`Rights::RT`]: crate::handle::Rights::RT pub const SYS_RT_ENTER: u64 = 112; -// Number 113 is **reserved, not free**: it is held for `SYS_PORT_REARM`, -// which would mint a fresh `Acceptor` for a port whose server died and is -// the one thing that would make any `serves` daemon restartable. Nothing -// needs it yet, so nothing is built. -// -// 115 is likewise held, for `SYS_SLEEP_UNTIL`, which would replace -// `SYS_NANOSLEEP`. -// -// **Both are recorded here and nowhere else**, because this file is where an -// agent allocating a number looks and a reservation nobody reads is not a -// reservation. - /// Copy kernel log records into a caller's buffer, advancing a cursor the /// caller owns. Gated by [`Rights::LOG`] on a `SysCap`. See [`log_read`] and /// [`crate::log`]. diff --git a/toyos/src/census.rs b/toyos/src/census.rs index 1a57603d00d..ccf42c877a3 100644 --- a/toyos/src/census.rs +++ b/toyos/src/census.rs @@ -1,12 +1,5 @@ //! The kernel's live-object count, per kind. //! -//! **A total hides a leak.** Every leak assertion in the test estate used to be -//! against one machine-wide number, where an object of one kind that is never -//! released is invisible behind ordinary churn in another — and six of the -//! thirteen kinds (`File`, `Device`, `Acceptor`, `Connection`, `IoUring`, -//! `Console`) were exercised by no census assertion at all, which is where -//! three of this branch's defects lived. -//! //! Two readings and a comparison, never one reading against a constant: an //! object released by another process is dropped from the deferred queue on //! whichever CPU drains next, so a single sample can be high by whatever has From c0dfaf0839fb81f767b68655c2c336f8799d3654 Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 1 Oct 2026 02:43:34 +0200 Subject: [PATCH 3/7] syscall_unassigned names every number's answer in its one verdict The assertion now checks syscall 26 and syscall u64::MAX together and prints both answers when it fails. On e754dc888 the red therefore shows both sides: u64::MAX answers InvalidArgument, the answer main already gave a number nobody assigned, and 26 answers NotSupported, the retired answer this branch removes. The negative control run becomes a differential against the base's own refusal. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- .../src/bin/syscall_unassigned.rs | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/tests/toyos-rust-tests/src/bin/syscall_unassigned.rs b/tests/toyos-rust-tests/src/bin/syscall_unassigned.rs index 5bf096f3a3c..b7adc7b82f8 100644 --- a/tests/toyos-rust-tests/src/bin/syscall_unassigned.rs +++ b/tests/toyos-rust-tests/src/bin/syscall_unassigned.rs @@ -11,15 +11,13 @@ mod arch; const UNASSIGNED: [u64; 2] = [26, u64::MAX]; fn main() { - for number in UNASSIGNED { - // SAFETY: a number the kernel refuses without reading any argument; - // nothing in this process is touched. - let answer = unsafe { arch::bare_syscall(number) }; - assert_eq!( - answer, - SyscallError::InvalidArgument.to_u64(), - "syscall {number} answered {:?}", - SyscallError::from_u64(answer), - ); - } + // SAFETY: a number the kernel refuses without reading any argument; + // nothing in this process is touched. + let answers = UNASSIGNED.map(|number| (number, unsafe { arch::bare_syscall(number) })); + // Every number's answer in the one verdict, so a red names each of them. + assert!( + answers.iter().all(|&(_, answer)| answer == SyscallError::InvalidArgument.to_u64()), + "answered {:?}", + answers.map(|(number, answer)| (number, SyscallError::from_u64(answer))), + ); } From 38bf8e855ceb666bfcd96a92441ab12b50ed680a Mon Sep 17 00:00:00 2001 From: japabu Date: Thu, 1 Oct 2026 03:15:20 +0200 Subject: [PATCH 4/7] syscall_unassigned probes one past the ABI's highest number, not a free one MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Under the ruling that numbers are free, a probe of 26 is the retirement legacy this branch removes: the next syscall to take 26 would turn it red with no defect behind it. The probe now calls `HIGHEST_SYSCALL + 1` and `u64::MAX`. `toyos_abi::syscall::HIGHEST_SYSCALL` names the highest number the ABI issues. It replaces the profile bound's hand-named `SYS_DEVICE_DMA_UNMAP`. A host test reads every `pub const SYS_…: u64 = ;` declaration in syscall.rs and asserts that their maximum is HIGHEST_SYSCALL, so a syscall numbered past it cannot land without moving it. The whole-change revert onto e754dc888 no longer goes red. There, 124 and u64::MAX were not in the retired table, so both already answered InvalidArgument. A deletion of legacy has no red arm beyond the review's `NotSupported` mutation. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016t9wjdQkB8SH7bmfUoiy6L --- .../src/bin/syscall_unassigned.rs | 6 +++--- toyos-abi/src/syscall.rs | 16 +++++++++++++++- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/tests/toyos-rust-tests/src/bin/syscall_unassigned.rs b/tests/toyos-rust-tests/src/bin/syscall_unassigned.rs index b7adc7b82f8..1ee505bbae6 100644 --- a/tests/toyos-rust-tests/src/bin/syscall_unassigned.rs +++ b/tests/toyos-rust-tests/src/bin/syscall_unassigned.rs @@ -1,14 +1,14 @@ //! A number no syscall has is refused as `InvalidArgument`, on the kernel an //! image ships. -use toyos_abi::syscall::SyscallError; +use toyos_abi::syscall::{SyscallError, HIGHEST_SYSCALL}; #[path = "../arch/mod.rs"] mod arch; -/// Numbers no syscall has: one inside the table's range and the last a +/// Numbers no syscall has: the first past the ABI's own and the last a /// register holds. -const UNASSIGNED: [u64; 2] = [26, u64::MAX]; +const UNASSIGNED: [u64; 2] = [HIGHEST_SYSCALL + 1, u64::MAX]; fn main() { // SAFETY: a number the kernel refuses without reading any argument; diff --git a/toyos-abi/src/syscall.rs b/toyos-abi/src/syscall.rs index f5f5468b5b3..37039adc48d 100644 --- a/toyos-abi/src/syscall.rs +++ b/toyos-abi/src/syscall.rs @@ -280,7 +280,10 @@ pub const SYSCALL_PROFILE_BINS: usize = 128; /// a reader can see in the line; dropping is one nobody can. pub const SYSCALL_PROFILE_OTHER: usize = SYSCALL_PROFILE_BINS - 1; -const _: () = assert!(SYS_DEVICE_DMA_UNMAP < SYSCALL_PROFILE_OTHER as u64); +/// The highest number this ABI issues; every number past it is refused. +pub const HIGHEST_SYSCALL: u64 = SYS_DEVICE_DMA_UNMAP; + +const _: () = assert!(HIGHEST_SYSCALL < SYSCALL_PROFILE_OTHER as u64); pub const WNOHANG: u64 = 1; @@ -2260,6 +2263,17 @@ pub fn process_stats(proc: RawHandle, stats: &mut ProcessStats) -> Result<(), Sy mod tests { use super::*; + /// Read off this file's `pub const SYS_…: u64 = ;` declarations, so a + /// syscall numbered past [`HIGHEST_SYSCALL`] cannot land without moving it. + #[test] + fn highest_syscall_is_the_highest_number_declared() { + let highest = include_str!("syscall.rs") + .lines() + .filter_map(|l| l.strip_prefix("pub const SYS_")?.split_once(": u64 = ")?.1.strip_suffix(';')?.parse().ok()) + .max(); + assert_eq!(highest, Some(HIGHEST_SYSCALL)); + } + /// **The encoder is the wire, so the test decodes the wire.** /// /// Not `as_bytes().len() == size_of::()`, which a padded From 9b6dea58a1cf49438a8cc1eedd23d7fdf99204c6 Mon Sep 17 00:00:00 2001 From: japabu Date: Fri, 2 Oct 2026 21:38:36 +0200 Subject: [PATCH 5/7] The last retirement entries go, their issue closes, and syscall_unassigned probes one number What main grew under this branch: - toyos-abi/src/syscall.rs: the "retired and unused" notes for `SYS_DEBUG` actions 8, 17 and 18 go, as 14 and 15's did. - issues/design-debt/the-abi-still-keeps-retired-syscall-numbers.md is this branch's exit and is deleted. The plans it named that still followed the rule lose it: - sys-debug-actions-and-two-loader-words-that-nothing-calls: its exit asked for a name to be retired and its number never reused. `LOG_PATTERNED` leaves its list, because `panic_halts_first` calls it again. - the-kernel-is-small-interrupts-post-and-threads-wait: steps 4 and 9 planned by retirement. - the-supervisor-is-host-tested-and-owns-the-stop: "briefed as an ABI brief" named a gate no prompt has. - issues/design-debt/the-window-protocol-keeps-a-retired-message-type.md no longer cites `compositor_hostile_clipboard` and its judge, which main deleted, and names its owner. `syscall_unassigned` probes `u64::MAX` alone. `HIGHEST_SYSCALL + 1` reached the same default arm and no other code on the way: the one use of the number before the match is the profile's `(num as usize).min(SYSCALL_PROFILE_OTHER)`, and only `u64::MAX` reaches its clamp. `HIGHEST_SYSCALL` existed for that probe, and `highest_syscall_is_the_highest_number_declared` parsed this file's source to guard it, so both go and the profile bound's assert is main's again. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm --- ...abi-still-keeps-retired-syscall-numbers.md | 42 ------------------- ...w-protocol-keeps-a-retired-message-type.md | 14 +++---- ...rvisor-is-host-tested-and-owns-the-stop.md | 4 +- ...and-two-loader-words-that-nothing-calls.md | 10 ++--- ...-small-interrupts-post-and-threads-wait.md | 6 +-- .../src/bin/syscall_unassigned.rs | 15 ++----- toyos-abi/src/syscall.rs | 19 +-------- 7 files changed, 20 insertions(+), 90 deletions(-) delete mode 100644 issues/design-debt/the-abi-still-keeps-retired-syscall-numbers.md diff --git a/issues/design-debt/the-abi-still-keeps-retired-syscall-numbers.md b/issues/design-debt/the-abi-still-keeps-retired-syscall-numbers.md deleted file mode 100644 index cb5d6473474..00000000000 --- a/issues/design-debt/the-abi-still-keeps-retired-syscall-numbers.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-01 ---- - -# The ABI still keeps retired syscall numbers - -The ABI is completely unstable and a removed syscall's number is free (owner, -2026-10-01): there are no retired numbers and no compatibility shims. The tree -still keeps them: - -- `kernel/src/syscall/dispatch.rs`'s `retired_syscalls!` names deleted calls so - that "an old binary is told which call it was", logging each call of one; - every other unassigned number answers `InvalidArgument` silently. -- `toyos-abi/src/syscall.rs` and `toyos-abi/src/inbox.rs` carry a "formerly …" - or "retired and unused" entry per deleted syscall, `SYS_DEBUG` action and - inbox op, `SYS_INBOX_SETUP`'s doc states the retirement rule, and - `kernel/src/inbox/mod.rs` names op 2 retired. -- `toyos-abi/src/syscall.rs`'s `device_classes!` keeps device classes 3 (`Nic`) - and 4 (`Audio`) "retired rather than reused", and the decode test in - `toyos-abi/src/inventory.rs` refuses them as retired. -- `tests/toyos-rust-tests/src/bin/panic_halts_first.rs` and `tests/toyos.rs` - take syscall 26 as their logged refusal and read `syscall 26 is retired`. -- Plans still follow the old rule: - `issues/kernel/sys-clock-realtime-is-now-a-format-of-sys-clock-epoch.md`, - `issues/kernel/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md`, - `issues/kernel/the-kernel-still-parses-what-userland-writes.md`, - `issues/kernel/the-capability-end-state-is-twelve-answers.md`, - `issues/kernel/sys-debug-actions-and-two-loader-words-that-nothing-calls.md`, - `issues/kernel/the-kernel-is-small-interrupts-post-and-threads-wait.md`, - `issues/diagnostics/the-kernel-keeps-nothing-it-enumerates.md`, - `issues/diagnostics/no-cyclictest.md`, and - `issues/isolation/the-supervisor-is-host-tested-and-owns-the-stop.md`, whose - "ABI brief" names a gate `.claude/agents/implementer.md` no longer has. - -**Exit**: `retired_syscalls!` and every retirement entry are gone, a deleted -number answers as an unassigned one does, both test sites take their logged -refusal from something live, and no issue plans by retirement. - -Owner: `toyos-abi` and `kernel/src/syscall/dispatch.rs`, whoever next changes -the ABI. diff --git a/issues/design-debt/the-window-protocol-keeps-a-retired-message-type.md b/issues/design-debt/the-window-protocol-keeps-a-retired-message-type.md index c8999e99af4..1e014e49440 100644 --- a/issues/design-debt/the-window-protocol-keeps-a-retired-message-type.md +++ b/issues/design-debt/the-window-protocol-keeps-a-retired-message-type.md @@ -6,20 +6,20 @@ opened: 2026-10-01 # The window protocol keeps a retired message type -The SDK breaks freely until ToyOS is adopted, and a deleted syscall, `SYS_DEBUG` -action or inbox op is simply deleted. The window protocol still carries one -retired type and refuses it by name: +A deleted syscall, `SYS_DEBUG` action or inbox op is simply deleted, and its +number is free. The window protocol still carries one retired type and refuses +it by name: - `userland/toyos-window/src/lib.rs`: `MSG_RETIRED_CLIPBOARD_SET_SHM = 10`; - `userland/compositor/src/client.rs`: `DropReason::Retired` and its `why`; -- `userland/compositor/src/session.rs`: `Session::dispatch`'s arm for it; -- `tests/toyos-rust-tests/src/bin/compositor_hostile_clipboard.rs`: the case - that sends it, and `tests/toyos.rs`'s judge of the named drop. +- `userland/compositor/src/session.rs`: `Session::dispatch`'s arm for it. Deleting the type makes a frame of 10 an unknown type, which `Session::dispatch` reads and drops without a word (`issues/isolation/the-compositor-ignores-a-message-it-does-not-know.md`). That issue lands first or with this one. +Owner: `Session::dispatch` in `userland/compositor/src/session.rs`. + **Exit**: `git grep -i retired userland/toyos-window userland/compositor` finds -nothing, and the hostile client's type 10 is refused as `DropReason::OutOfProtocol`. +nothing, and a frame of type 10 drops its client with `DropReason::OutOfProtocol`. diff --git a/issues/isolation/the-supervisor-is-host-tested-and-owns-the-stop.md b/issues/isolation/the-supervisor-is-host-tested-and-owns-the-stop.md index dbd4f3f6901..55f1128816c 100644 --- a/issues/isolation/the-supervisor-is-host-tested-and-owns-the-stop.md +++ b/issues/isolation/the-supervisor-is-host-tested-and-owns-the-stop.md @@ -16,8 +16,8 @@ nothing. 1. **The rename**, one mechanical PR, first after #536. Before stage 1 is briefed, the exit's search below runs once over the `rust/` fork's delta as well as the superproject, so its hits are known going in. It touches `toyos/src`, - `toyos-abi/src`, `userland/libc/src` and the `rust/` fork's delta, so it is - briefed as an ABI brief, and its `CLAUDE.md` edits are placed in the same PR + `toyos-abi/src`, `userland/libc/src` and the `rust/` fork's delta, and its + `CLAUDE.md` edits are placed in the same PR by an agent briefed for them. Issue slugs carrying an old name are renamed with every citation. diff --git a/issues/kernel/sys-debug-actions-and-two-loader-words-that-nothing-calls.md b/issues/kernel/sys-debug-actions-and-two-loader-words-that-nothing-calls.md index a5c68b063cd..ef61c9306b2 100644 --- a/issues/kernel/sys-debug-actions-and-two-loader-words-that-nothing-calls.md +++ b/issues/kernel/sys-debug-actions-and-two-loader-words-that-nothing-calls.md @@ -8,7 +8,7 @@ opened: 2026-10-01 The guest suite's first cut (`issues/build/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md`) deleted the only -callers of nine `SYS_DEBUG` actions and the loader's reading of two boot words, and left their +callers of eight `SYS_DEBUG` actions and the loader's reading of two boot words, and left their names in `toyos-abi/src`, because a deletion there is an ABI change and that pull request was not one. Each kernel arm still answers its action. `git grep -w -- tests userland toyos src` finds no caller of: @@ -18,14 +18,12 @@ finds no caller of: `syscall_fault_halts` and `heap_over_ceiling_halts` and stage C's `idle_stack_guard`; - `HEAP_AT_CEILING` (5), `HEAP_AT_CEILING_PAGE_ALIGNED` (7) and `LOWER_SYSINFO_BOUND` (19), `heap_ceiling`'s, for stage C's `heap_ceiling_bounds`; -- `LOCK_ACROSS_SWITCH` (2), `test_panic_child`'s, and `LOG_PATTERNED` (21), test-runner's - `log-gate` and `log-storm`. Their stage C items are a type (`lock_across_switch_halts`) and - a host test (`log_conservation_smp2`), and neither calls `SYS_DEBUG`, so no stage brings a - caller back; +- `LOCK_ACROSS_SWITCH` (2), `test_panic_child`'s. Its stage C item is a type + (`lock_across_switch_halts`), which calls no `SYS_DEBUG`, so no stage brings a caller back; - `boot::WRITE_NO_LAYOUT_PARAM` and `boot::WITHHOLD_ROOT_PARAM`, for stage A's `kernel_args_layout_refused` and `root_withheld_refused`. Owner: the orchestrator. **Exit**: for each name, `git grep -w` finds a caller again, its stage's row, or an ABI change -retires the name with its kernel arm and never reuses its number. +deletes the name with its kernel arm. diff --git a/issues/kernel/the-kernel-is-small-interrupts-post-and-threads-wait.md b/issues/kernel/the-kernel-is-small-interrupts-post-and-threads-wait.md index 065269837c1..37828986d08 100644 --- a/issues/kernel/the-kernel-is-small-interrupts-post-and-threads-wait.md +++ b/issues/kernel/the-kernel-is-small-interrupts-post-and-threads-wait.md @@ -94,7 +94,7 @@ times: restart is a supervisor's in the test. 4. **Partitions in blockd**. **Exit**: a partition held by one session at a time, the idle slot claimed by GUID and written through blockd, and - `SYS_PARTITION_READ/WRITE` retired once no disk the kernel drives serves + `SYS_PARTITION_READ/WRITE` deleted once no disk the kernel drives serves a claim. **Built** (#525) but for the last: the kernel's disks, the stick among them, still serve claims until step 10, and a `part:` row still mints a kernel claim. @@ -120,8 +120,8 @@ times: stage's escape suite, run against fsd. 9. **Delete the kernel storage stack**: the VFS down to ROOT's resolver, both writable adapters, both caches, write-back, durability, tmpfs, the - block layer, GPT, the NVMe driver, the refusal chain and the retired file - syscalls' numbers. **Exit**: `BudgetExpired`, `DEADMAN` and + block layer, GPT, the NVMe driver, the refusal chain and the file + syscalls. **Exit**: `BudgetExpired`, `DEADMAN` and `between_attempts` appear nowhere, and the kernel's lines and longest interrupts-off and preemption-off windows are measured. 10. **usbd**, stage 5's second half: the whole xHCI moves, HID to the diff --git a/tests/toyos-rust-tests/src/bin/syscall_unassigned.rs b/tests/toyos-rust-tests/src/bin/syscall_unassigned.rs index 1ee505bbae6..f02f010728d 100644 --- a/tests/toyos-rust-tests/src/bin/syscall_unassigned.rs +++ b/tests/toyos-rust-tests/src/bin/syscall_unassigned.rs @@ -1,23 +1,14 @@ //! A number no syscall has is refused as `InvalidArgument`, on the kernel an //! image ships. -use toyos_abi::syscall::{SyscallError, HIGHEST_SYSCALL}; +use toyos_abi::syscall::SyscallError; #[path = "../arch/mod.rs"] mod arch; -/// Numbers no syscall has: the first past the ABI's own and the last a -/// register holds. -const UNASSIGNED: [u64; 2] = [HIGHEST_SYSCALL + 1, u64::MAX]; - fn main() { // SAFETY: a number the kernel refuses without reading any argument; // nothing in this process is touched. - let answers = UNASSIGNED.map(|number| (number, unsafe { arch::bare_syscall(number) })); - // Every number's answer in the one verdict, so a red names each of them. - assert!( - answers.iter().all(|&(_, answer)| answer == SyscallError::InvalidArgument.to_u64()), - "answered {:?}", - answers.map(|(number, answer)| (number, SyscallError::from_u64(answer))), - ); + let answer = unsafe { arch::bare_syscall(u64::MAX) }; + assert_eq!(answer, SyscallError::InvalidArgument.to_u64(), "syscall {} answered {answer:#x}", u64::MAX); } diff --git a/toyos-abi/src/syscall.rs b/toyos-abi/src/syscall.rs index d474d5eed49..a11bbad0d5b 100644 --- a/toyos-abi/src/syscall.rs +++ b/toyos-abi/src/syscall.rs @@ -271,10 +271,7 @@ pub const SYSCALL_PROFILE_BINS: usize = 128; /// a reader can see in the line; dropping is one nobody can. pub const SYSCALL_PROFILE_OTHER: usize = SYSCALL_PROFILE_BINS - 1; -/// The highest number this ABI issues; every number past it is refused. -pub const HIGHEST_SYSCALL: u64 = SYS_DEVICE_DMA_UNMAP; - -const _: () = assert!(HIGHEST_SYSCALL < SYSCALL_PROFILE_OTHER as u64); +const _: () = assert!(SYS_DEVICE_DMA_UNMAP < SYSCALL_PROFILE_OTHER as u64); pub const WNOHANG: u64 = 1; @@ -755,7 +752,6 @@ pub mod debug_action { pub const HEAP_AT_CEILING: u64 = 5; pub const HEAP_OVER_CEILING: u64 = 6; pub const HEAP_AT_CEILING_PAGE_ALIGNED: u64 = 7; - // Action 8 is retired and unused: it was SCREEN_GRAFFITI, and no test reads it. /// Read the guard page below this CPU's idle stack. pub const IDLE_GUARD_READ: u64 = 9; /// The kernel canary's address, and whether it still holds what the kernel @@ -776,8 +772,6 @@ pub mod debug_action { /// **Per kind and not a total**: an object of one kind that is never /// released is invisible behind ordinary churn in another. pub const CENSUS_KIND: u64 = 16; - // Actions 17 and 18 are retired and unused: they were IDLE_STACK_HIGH_WATER - // and IDLE_STACK_SIZE, and no test reads them. /// Put a count this guest can reach in `MAX_SYSINFO_THREADS`'s place, for /// the rest of the boot. `SYS_SYSINFO`'s real bound is a thread count no /// guest can make, so only the number can move and moving it runs the @@ -2297,17 +2291,6 @@ pub fn process_stats(proc: RawHandle, stats: &mut ProcessStats) -> Result<(), Sy mod tests { use super::*; - /// Read off this file's `pub const SYS_…: u64 = ;` declarations, so a - /// syscall numbered past [`HIGHEST_SYSCALL`] cannot land without moving it. - #[test] - fn highest_syscall_is_the_highest_number_declared() { - let highest = include_str!("syscall.rs") - .lines() - .filter_map(|l| l.strip_prefix("pub const SYS_")?.split_once(": u64 = ")?.1.strip_suffix(';')?.parse().ok()) - .max(); - assert_eq!(highest, Some(HIGHEST_SYSCALL)); - } - /// **The encoder is the wire, so the test decodes the wire.** /// /// Not `as_bytes().len() == size_of::()`, which a padded From ea0ee48e2ffe59ce46d7f4f8f72bdf3863bf46a8 Mon Sep 17 00:00:00 2001 From: japabu Date: Fri, 2 Oct 2026 21:53:07 +0200 Subject: [PATCH 6/7] sys-debug-actions issue: LOG_PATTERNED has a caller, and its pattern has no reader `panic_halts_first` calls `LOG_PATTERNED` for a record, so the action leaves the issue's list of uncalled names. What `kernel/src/log/storm.rs` writes into the record, a checksum and a payload, lost its readers in the guest suite's first cut, and the issue says so and asks for a reader or the deletion. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm --- ...debug-actions-and-two-loader-words-that-nothing-calls.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/issues/kernel/sys-debug-actions-and-two-loader-words-that-nothing-calls.md b/issues/kernel/sys-debug-actions-and-two-loader-words-that-nothing-calls.md index ef61c9306b2..42d0b9ce542 100644 --- a/issues/kernel/sys-debug-actions-and-two-loader-words-that-nothing-calls.md +++ b/issues/kernel/sys-debug-actions-and-two-loader-words-that-nothing-calls.md @@ -23,7 +23,11 @@ finds no caller of: - `boot::WRITE_NO_LAYOUT_PARAM` and `boot::WITHHOLD_ROOT_PARAM`, for stage A's `kernel_args_layout_refused` and `root_withheld_refused`. +`LOG_PATTERNED` (21) has one caller, `panic_halts_first`, which wants a record and reads none of +the pattern `kernel/src/log/storm.rs` writes into it: the checksum and payload had test-runner's +`log-gate` and `log-storm` as their readers, and stage C's `log_conservation_smp2` is a host test. + Owner: the orchestrator. **Exit**: for each name, `git grep -w` finds a caller again, its stage's row, or an ABI change -deletes the name with its kernel arm. +deletes the name with its kernel arm; and `storm.rs`'s pattern has a reader or is deleted. From b198b3ab9b09874ff9fdc2191d4bb01f1e09f355 Mon Sep 17 00:00:00 2001 From: japabu Date: Fri, 2 Oct 2026 22:29:04 +0200 Subject: [PATCH 7/7] Review of ea0ee48e2: the dead AArch64 half of the test assembly goes, and the history comments get an issue `tests/toyos-rust-tests/src/arch/aarch64.rs` and the selector's arm compiled nowhere: `arch` has one includer, `syscall_unassigned.rs`, and for AArch64 the harness builds only `panic_halts_first` and `abuse_readonly_copyout`, neither of which includes it. On main `panic_halts_first` ran it on `virt`; this branch moved that binary to `LOG_PATTERNED`. `issues/design-debt/comments-still-name-syscalls-the-abi-deleted.md` records the six comments that tell the history of a deleted syscall, which this branch found and does not edit: they hold no number and refuse nothing. `toyos-abi/src/inventory.rs`: the comment over the refused-class case goes; `past_the_last` and the assertion say it. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013UDZQ6fSKw14e4w2TKTRfm --- ...nts-still-name-syscalls-the-abi-deleted.md | 32 +++++++++++++++++++ tests/toyos-rust-tests/src/arch/aarch64.rs | 16 ---------- tests/toyos-rust-tests/src/arch/mod.rs | 5 --- toyos-abi/src/inventory.rs | 1 - 4 files changed, 32 insertions(+), 22 deletions(-) create mode 100644 issues/design-debt/comments-still-name-syscalls-the-abi-deleted.md delete mode 100644 tests/toyos-rust-tests/src/arch/aarch64.rs diff --git a/issues/design-debt/comments-still-name-syscalls-the-abi-deleted.md b/issues/design-debt/comments-still-name-syscalls-the-abi-deleted.md new file mode 100644 index 00000000000..2b1ea8cc468 --- /dev/null +++ b/issues/design-debt/comments-still-name-syscalls-the-abi-deleted.md @@ -0,0 +1,32 @@ +--- +status: open +kind: defect +opened: 2026-10-02 +--- + +# Comments still name syscalls the ABI deleted + +A deleted syscall is simply deleted, and a past implementation lives in a commit +message, never in source. Six comments still tell the history of a syscall +`toyos-abi` no longer declares: + +- `userland/libc/src/misc.rs`, `waitpid`'s doc: "`SYS_WAITPID` is retired"; +- `kernel/src/pipe.rs`, `Pipe::backing`'s doc: a pending `SYS_CONNECT`; +- `userland/soundd/src/virtio.rs`, `submit`'s doc: "the deleted + `SYS_AUDIO_SUBMIT`"; +- `tests/toyos-rust-tests/src/bin/abuse_pipe_owner.rs`, the module doc: + `SYS_PIPE_OPEN` and `SYS_SOCKET_CREATE`, "that family is retired"; +- `tests/toyos-rust-tests/src/bin/abuse_connect_flood.rs`, the module doc: + "it used to `SYS_LISTEN`"; +- `tests/toyos-rust-tests/src/bin/shm_release_reclaims.rs`, the module doc: + what `SYS_RELEASE_SHARED` did. + +None holds a number or refuses anything, so none is a retirement entry. Each is +deleted down to what is true of its item now. + +Owner: the item each comment documents — `waitpid`, `Pipe::backing`, +`Virtio::submit`, and the three test binaries' module docs. + +**Exit**: `git grep -w -E +'SYS_WAITPID|SYS_CONNECT|SYS_AUDIO_SUBMIT|SYS_PIPE_OPEN|SYS_SOCKET_CREATE|SYS_LISTEN|SYS_RELEASE_SHARED' +-- kernel userland tests` finds nothing. diff --git a/tests/toyos-rust-tests/src/arch/aarch64.rs b/tests/toyos-rust-tests/src/arch/aarch64.rs deleted file mode 100644 index 89bf330a22a..00000000000 --- a/tests/toyos-rust-tests/src/arch/aarch64.rs +++ /dev/null @@ -1,16 +0,0 @@ -//! AArch64's half of the test binaries' assembly. - -/// Syscall `number` with no argument set, past every `toyos_abi` wrapper; the -/// kernel's answer. -/// -/// # Safety -/// The kernel refuses `number` without reading any argument register. -pub unsafe fn bare_syscall(number: u64) -> u64 { - let answer: u64; - // SAFETY: the caller's; the `svc` is the ABI's (`toyos_abi::syscall`), - // which answers in x0 and keeps every other register. - unsafe { - core::arch::asm!("svc #0", inlateout("x0") number => answer); - } - answer -} diff --git a/tests/toyos-rust-tests/src/arch/mod.rs b/tests/toyos-rust-tests/src/arch/mod.rs index 665e1444620..51218abfbac 100644 --- a/tests/toyos-rust-tests/src/arch/mod.rs +++ b/tests/toyos-rust-tests/src/arch/mod.rs @@ -1,10 +1,5 @@ //! What the test binaries 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")] diff --git a/toyos-abi/src/inventory.rs b/toyos-abi/src/inventory.rs index 7bf94dde811..66cec75eff5 100644 --- a/toyos-abi/src/inventory.rs +++ b/toyos-abi/src/inventory.rs @@ -587,7 +587,6 @@ mod tests { Err(Undecodable::Variant { at: 7, value: u64::from(psiv) }) ); } - // A class number no `DeviceType` carries. let mut raw = Record::Claim(Claim { on: Claimed::Class(DeviceType::Keyboard), holder: Holder { pid: 1, name: name("x") },