diff --git a/issues/a-120000-ms-boot-deadline-fired-132859-ms-late-on-the-t14.md b/issues/a-120000-ms-boot-deadline-fired-132859-ms-late-on-the-t14.md index a16752a584..c271d903b1 100644 --- a/issues/a-120000-ms-boot-deadline-fired-132859-ms-late-on-the-t14.md +++ b/issues/a-120000-ms-boot-deadline-fired-132859-ms-late-on-the-t14.md @@ -6,36 +6,33 @@ opened: 2026-09-16 # A 120000 ms boot deadline fired 132859 ms late on the T14, and the hard-lockup bound armed beside it was silent too -`kernel/src/deadline.rs` reads the bound off the parameter line (`claim`, -`:150-162`), turns it into a TSC deadline once there is a clock (`start`, -`:168-183`), and `poll` (`:194-200`) compares one relaxed load of `AT_TSC` -against `rdtsc` from the timer interrupt entry — `kernel/src/arch/x86_64/idt/timer.rs:71` -(`:79`) in Ring 0 and `:96` in Ring 3 — and calls `expire` (`:208-225`), which -seals the record and writes the reset register through `acpi::reset_now`. -`start` arms the other half of the same parameter at `:182`, +`kernel/src/deadline.rs` reads the bound off the parameter line (`claim`), +turns it into a counter deadline once there is a clock (`start`), and `poll` +compares one relaxed load of `AT_COUNTER` against `arch::cpu::counter` from the +timer interrupt entry — `kernel/src/arch/x86_64/idt/timer.rs`, through +`poll_in_kernel` in Ring 0 and `poll` in Ring 3 — and calls `expire`, which +seals the record and writes the reset register through `power::reset_now`. +`start` arms the other half of the same parameter, `crate::hardlockup::start(ms)`: a bound of `toyos_tco::hard_lockup_bound_ms`, -`deadline_ms / 2` (`lib/tco/src/lib.rs:154-156`), sampled by an NMI every -second on every CPU (`kernel/src/hardlockup/mod.rs:1-31`). The two share one -seal (`claim_the_reset`, `deadline.rs:69-71`). +`deadline_ms / 2` (`lib/tco/src/lib.rs`), sampled by an NMI every +second on every CPU (`kernel/src/hardlockup/mod.rs`'s header). The two share +one seal (`deadline::claim_the_reset`). A boot armed with `boot-deadline=120000` prints both arms at 0.060 s — quoted -from bench run 42's kernel log, the text `deadline.rs:174-177` and -`hardlockup/mod.rs:193-198` write: +from bench run 42's kernel log, the text `deadline::start` and +`hardlockup`'s `Armed` write: boot deadline: 120000 ms, after which this kernel seals a WEDGED record and writes the reset register itself hard lockup: 60000 ms, sampled every 1000 ms by each cpu's own performance counter, after which a cpu that has taken no interrupt seals a WEDGED record and resets the machine -Run 55's boot — armed at 2026-09-14 20:54:57Z, the `lancase` image of the -branch `i219-delivery` (#461) at `4d604c86`, whose `kernel/src/deadline.rs`, -`kernel/src/hardlockup/mod.rs` and `kernel/src/drivers/acpi.rs` are -byte-identical to this tree's (`git diff --stat 4d604c86 HEAD -- ` is -empty) — did not come back inside the loop's 420 s, and its own kernel log -never reached the stick, so its print of the two lines is inferred from -`start` and not read. Its `WEDGED` record was read by run 56's loader pass: -a pass that printed the record and ended the chain without booting a kernel -(`loader/src/blackbox.rs:149-154`, `loader/src/main.rs:857-860`), so -that the machine came back to Ubuntu after 100 s and the loop read the -stick's `loader.log`, which reads, whole above the ring tail: +Run 55's boot — armed at 2026-09-14 20:54:57Z, the `lancase` image of the branch +`i219-delivery` (#461) at `4d604c86` — did not come back inside the loop's 420 +s, and its own kernel log never reached the stick, so its print of the two lines +is inferred from `start` and not read. Its `WEDGED` record was read by run 56's +loader pass: a pass that printed the record and ended the chain without booting +a kernel (`loader/src/blackbox.rs`'s `harvest`), so that the machine came back +to Ubuntu after 100 s and the loop read the stick's `loader.log`, which reads, +whole above the ring tail: --- the pass after the reset, reading what the boot above left ToyOS Bootloader 1.0 @@ -49,7 +46,7 @@ stick's `loader.log`, which reads, whole above the ring tail: it. In the same boot the 60000 ms hard-lockup bound sealed nothing — the seal is the deadline's — and that detector seals only for a CPU whose interrupt count is stale for its bound *and* whose sampled frame has `IF` clear -(`hardlockup/mod.rs:21-31`). So for 133 s past the bound no CPU's timer entry +(`hardlockup/mod.rs`'s header). So for 133 s past the bound no CPU's timer entry ran `poll`, and no CPU met both of the detector's conditions for 60 s, or none was sampled; which of those is unmeasured. @@ -108,12 +105,13 @@ runner asks for no reboot; the deadline was the only bound left, and it fired now.** The next occurrence carries one census, the deadline's own: `expire` seals the machine's census into the `WEDGED` record above the ring's tail (`kernel/src/census.rs`), read at the moment the bound fired rather than at -whichever process last died before it. +whichever process last died before it, and names where each CPU's timer last +found the kernel (`deadline::KernelPcs`). ## The instrument that measures this already exists `tests/metal/src/lib.rs`'s `deadline_lateness_ms` computes exactly -`reached − bound` out of the `DEADLINE_EXPIRED` line (`build/bootlog.rs:28`). +`reached − bound` out of the `DEADLINE_EXPIRED` line (`build/bootlog.rs`). The arithmetic above is the instrument's own, done by hand because the run was `toyos-metal` invoked directly and not the harness. No second instrument is owed; a cause is. @@ -129,10 +127,9 @@ is owed; a cause is. Run 55's census shows cpu0 in that state at 3.47 s and other CPUs not; it does not show what held every CPU's timer entry off `poll` for 133 s. - The earlier deadline-ended T14 boot — run 25's `ccorpus`, back after 187 s - with no record because the reset left its stick unenumerable — was tracked - as a defect whose exit condition was a `loader.log` from a boot that ran to - its deadline with the ring tail naming what the machine was doing; this - record is that, and the commit that added this file closed it. Its + with no record because the reset left its stick unenumerable — is + `issues/a-job-list-hangs-with-interrupts-on-and-the-deadline-ends-it.md`, + restored in `14186a833` because run 55 ran no job list. Its inference — that a deadline which fires is a machine on which some CPU was still taking a timer interrupt — holds for the instant it fired; this record shows the 133 s before it in which none did. diff --git a/issues/a-bcachefs-hole-costs-a-block-write-per-page.md b/issues/a-bcachefs-hole-costs-a-block-write-per-page.md index b4ac62bb7c..1a321121c6 100644 --- a/issues/a-bcachefs-hole-costs-a-block-write-per-page.md +++ b/issues/a-bcachefs-hole-costs-a-block-write-per-page.md @@ -12,12 +12,10 @@ shrink most recently freed and a hole that reads the previous tenant's bytes is a data leak. That is correct and it is what FAT32 already does (`Fat32::set_len` calls `zero_range` on every grow). The cost is linear in the hole: reaching page N over a gap of M pages writes M zero blocks, one device write each, inside the -VFS lock a flush holds. +file server's write that reaches the page (`system/fileserver/src/data.rs`). -Every writer this kernel has appends -sequentially, so `covered` reaches `target` in one block and the zeroing loop -does not run — `fs_large_file` writes 1024 pages and allocates one block per -page with no gap. The paths that pay are a `lseek` past the end followed by a +A sequential append reaches `target` in one block, so the zeroing loop +does not run. The paths that pay are a `lseek` past the end followed by a write, and a shrink-then-write-above-the-mark, whose gap is bounded by the file. diff --git a/issues/a-boot-start-refused-a-present-device-runs-without-it.md b/issues/a-boot-start-refused-a-present-device-runs-without-it.md index 0702b2c38d..2f0b73caff 100644 --- a/issues/a-boot-start-refused-a-present-device-runs-without-it.md +++ b/issues/a-boot-start-refused-a-present-device-runs-without-it.md @@ -7,15 +7,17 @@ opened: 2026-09-24 # A boot start refused a device the machine has runs without it A boot start's device refusal is fatal only for a device its `[programs]` row -marks as required; for any other, init logs the refusal loudly and starts the -program without that device (owner, 2026-09-30). No row can mark a device +marks as required; for any other, the supervisor logs the refusal loudly and +starts the program without that device (owner, 2026-09-30). No row can mark a device required, so every boot start refused a device the machine has — `NotSupported`, `AlreadyExists`, `ResourceExhausted` — runs without it: -`/system/bin/init`'s `start` says the refusal in the kernel's own word -(`init: netd: pci:8086:10c9 is on this machine and could not be handed over`) -and starts the program. +`system/supervisor/src/main.rs` says the refusal in the kernel's own word +(`supervisor: netstack: pci:… is on this machine and could not be handed +over`) and starts the program, since `claims::goes_on_without` +(`system/supervisor/lifecycle/src/claims.rs`) goes on for every refusal but an +ambiguous matcher or a claim the replaced process held. **Exit**: a `[programs]` row can mark a device required, a host test holds the -mark and init's choice between a fatal refusal and a logged one, and a metal -row or, where none can, a guest test shows a boot start refused a required +mark and the supervisor's choice between a fatal refusal and a logged one, and a +metal row or, where none can, a guest test shows a boot start refused a required device is fatal. diff --git a/issues/a-boot-with-no-kernel-log-is-not-no-boot-complete.md b/issues/a-boot-with-no-kernel-log-is-not-no-boot-complete.md index c5f42a0210..896ec63b18 100644 --- a/issues/a-boot-with-no-kernel-log-is-not-no-boot-complete.md +++ b/issues/a-boot-with-no-kernel-log-is-not-no-boot-complete.md @@ -11,7 +11,7 @@ Run 22's hung mkdir image left the stick holding `attempts` and `loader.log` and left a 325-line file ending at a spawn. `loader.log` shows an ordinary handoff, so that kernel started; `logd` either never created its file or its first write never completed. The stick after the power cut is -`/Users/jan/.claude/jobs/2280e09e/tmp/t14-run22/stick-after-cycle.log`. +`t14-run22/stick-after-cycle.log`, outside the tree. `tests/metal/src/lib.rs` reads that back as the absence of `Boot: complete`, which is the same verdict it gives a boot that ran and stopped late. They are not the same diff --git a/issues/a-claim-spends-device-addresses-its-slot-never-gets-back.md b/issues/a-claim-spends-device-addresses-its-slot-never-gets-back.md index ddeed4e7fd..93ac9fc4f2 100644 --- a/issues/a-claim-spends-device-addresses-its-slot-never-gets-back.md +++ b/issues/a-claim-spends-device-addresses-its-slot-never-gets-back.md @@ -8,17 +8,17 @@ opened: 2026-09-26 Every `SYS_DEVICE_DMA_ALLOC` grant of a claim is placed at a fresh address of its slot's domain (`kernel/src/pcidev/mod.rs`, `dma_alloc`), and the domain -never hands an address out twice (`kernel/src/arch/x86_64/vtd/table.rs`, -`Domain::reserve`). A holder bounded to `MAX_GRANT_TOTAL` per claim can still -claim, allocate and die over and over — a service a supervisor restarts does -exactly this — and each claim spends up to that much of the slot's addresses -and the remapping tables under them, which are never freed. The domain running -dry is a refusal (`ResourceExhausted`) and not a crash, but the slot is then -dead for the rest of the boot, and the tables are memory nothing returns. -Nor is running dry always a refusal: every table under a fresh address comes -from `Tables::alloc` (`kernel/src/arch/x86_64/vtd/table.rs`), whose -`expect("no physical memory for a remapping table")` panics the kernel when -physical memory runs out first, and no second-level table a claim made is +never hands an address out twice (`kernel/pure/iova.rs`, `Window::reserve`, +through `kernel/src/iommu/domains.rs`). A holder bounded to `MAX_GRANT_TOTAL` +per claim can still claim, allocate and die over and over — a service a +supervisor restarts does exactly this — and each claim spends up to that much of +the slot's addresses and the remapping tables under them, which are never freed. +The domain running dry is a refusal (`ResourceExhausted`) and not a crash, but +the slot is then dead for the rest of the boot, and the tables are memory +nothing returns. Nor is running dry always a refusal: every table under a fresh +address comes from `Tables::alloc` (`kernel/src/arch/x86_64/vtd/table.rs`), +whose `expect("no physical memory for a remapping table")` panics the kernel +when physical memory runs out first, and no second-level table a claim made is ever freed. What a holder *lends* (`SYS_DEVICE_DMA_MAP`) does not spend: it is placed in a @@ -28,9 +28,13 @@ residue mechanism answers that only for a function no reset quiets. **Exit condition.** A slot's grant addresses are bounded as its lent ones are — reused once the function that held them is reset and quiet, or placed in a -window reserved once — and a guest test claims, allocates and releases a -function more times than a narrowed domain (`iommu-domain-narrow`) has room -for, and the last claim is served; the tables under a slot's addresses are +window reserved once — and a test claims, allocates and releases a +function more times than its domain has room for, and the last claim is +served. With no narrowed domain that is about 262,144 2 MiB grants on a 39-bit +unit, so a host test over `kernel/pure/iova.rs` and +`kernel/src/iommu/domains.rs` is the place to hold it; the +`iommu-domain-narrow` actuator, which narrowed a domain for this, was removed +in `29e100612`. And the tables under a slot's addresses are bounded or freed once nothing maps through them, and a remapping table the machine has no memory for is a refusal of the grant that needed it, not a panic. diff --git a/issues/a-close-of-one-handle-ends-every-rings-poll-on-its-object.md b/issues/a-close-of-one-handle-ends-every-rings-poll-on-its-object.md index 910446a18c..0e1bf68343 100644 --- a/issues/a-close-of-one-handle-ends-every-rings-poll-on-its-object.md +++ b/issues/a-close-of-one-handle-ends-every-rings-poll-on-its-object.md @@ -7,14 +7,15 @@ opened: 2026-10-02 # A close of one handle ends every ring's poll on its object `ops::close` answers every poll on a watch its object ends with `-NotFound` -(`Watch::cancel_polls`), in every ring, when any one handle to a pipe's read -end or an acceptor closes. A sibling handle from `dup` keeps the object open, -and its polls end all the same. `toyos::poller`'s `wait` hands the token of a +(`Watch::cancel_polls`), in every ring, when any one handle to a pipe's read end +or an acceptor closes. A sibling handle from `dup` keeps the object open, and +its polls end all the same. `toyos::poller`'s `wait` hands the token of a negative completion to its caller as it does a ready one's and drops the kernel's word for it, so a reader that takes that token for bytes and reads blocking parks on an object that is open and empty. `wait_answers` hands the -word beside the token. No caller in the tree reaches it: fsd's acceptors are endowed and -never duplicated. `inbox_cancel_wakes` stages the close. +word beside the token. No caller in the tree reaches it: the file server's +acceptors are endowed and never duplicated. `inbox_cancel_wakes` stages the +close. **Exit**: a poll ends only when the last handle to its source closes, or `wait`, the form that drops a completion's result, is gone and every caller of diff --git a/issues/a-connect-between-two-accepts-is-reset.md b/issues/a-connect-between-two-accepts-is-reset.md index 129b7793fa..e232547c07 100644 --- a/issues/a-connect-between-two-accepts-is-reset.md +++ b/issues/a-connect-between-two-accepts-is-reset.md @@ -36,9 +36,9 @@ netstack's listener is the node's now, whose queue is [tcp]'s: a connect that arrives while another waits to be accepted is queued (`a_connect_between_two_accepts_is_queued_not_reset`, `system/netstack/node/tests/listeners.rs`), and in a guest two host peers -that dial before any accept are both accepted (`netstack_streams`, which on -smoltcp ended `the listener was woken for 1 of two peers`). What is left of the -exit is `lan_swap`. +that dialled before any accept were both accepted (`netstack_streams`, which on +smoltcp ended `the listener was woken for 1 of two peers`, and which +`ff1a11dbd` cut). What is left of the exit is `lan_swap`. QEMU's own forward is a third place such a reset can come from, and it was not ruled out of the reading above: its listener queues one connection, and a diff --git a/issues/a-console-watch-waits-on-the-keyboard-not-the-serial-line.md b/issues/a-console-watch-waits-on-the-keyboard-not-the-serial-line.md index ad56c35f24..038224d419 100644 --- a/issues/a-console-watch-waits-on-the-keyboard-not-the-serial-line.md +++ b/issues/a-console-watch-waits-on-the-keyboard-not-the-serial-line.md @@ -6,8 +6,11 @@ opened: 2026-09-25 # A console's watch waits on the keyboard, and its readiness is the serial line -A blocking read of a console was moved onto the serial line's own wait; a -watch through an inbox (`Poller::watch` of a console handle) was not. Its one +A console's readiness is the serial line's (`drivers::serial::has_data`), +and nothing on the serial line posts a wake. A blocking read of a console +(`sys_read`, `kernel/src/syscall/io.rs`) parks on its own thread's watch and is +woken only by its own 10 ms repoll (`CONSOLE_REPOLL`). A watch through an +inbox (`Poller::watch` of a console handle) has no source at all. Its one source was the kernel's keyboard watch, which went with the kernel's input streams (the small-kernel track's step 10), so a console has no readable watch at all now: registering one is refused, and libc's `poll` reads every @@ -16,6 +19,6 @@ program that polls its console for input spins rather than waits. ## Exit condition -A console's poll source and its readiness are the same source, and a test that -watches a console handle through a poller and types on the serial line sees -the watch complete. +A console's poll source and its readiness are the same source, a blocking read +waits on it with no repoll, and a test that watches a console handle through a +poller and types on the serial line sees the watch complete. diff --git a/issues/a-corrupt-chain-met-by-a-queued-free-is-answered-by-the-next-call.md b/issues/a-corrupt-chain-met-by-a-queued-free-is-answered-by-the-next-call.md index 3d9ed900e3..821640daa6 100644 --- a/issues/a-corrupt-chain-met-by-a-queued-free-is-answered-by-the-next-call.md +++ b/issues/a-corrupt-chain-met-by-a-queued-free-is-answered-by-the-next-call.md @@ -7,13 +7,13 @@ opened: 2026-09-26 # A corrupt chain met by a queued free is answered by the next call `Fat32::atomic` re-drives an earlier call's queued repair before a mutating -call starts (`settle_first`, `lib/fat32/src/repair.rs`). A queued +call starts (`settle`, `lib/fat32/src/repair.rs`). A queued `Repair::Free` walks the rest of a chain whose name is already gone, and a walk that meets a cyclic or out-of-range link answers `Error::CorruptChain`. That error is returned by whichever call came next, for a chain it never -named, and the call did not start. fsd logs it as that call failing -(`logged` in `userland/fsd/src/fat.rs`: "`` of '``': corrupt cluster -chain") and its client reads `SyscallError::Io`. +named, and the call did not start. The file server logs it as that call failing +(`logged` in `system/fileserver/src/fat.rs`: "`` of '``': corrupt +cluster chain") and its client reads `fs::Error::Io`. Two things are lost: @@ -27,7 +27,7 @@ Two things are lost: ## Owner -`toyos-fat32`, which owns `settle_first` and `free_chain`; the adapter's log +`toyos-fat32`, which owns `settle` and `free_chain`; the adapter's log line follows from what the crate answers. ## Exit condition diff --git a/issues/a-counters-read-under-host-load-can-go-silent-for-15-s.md b/issues/a-counters-read-under-host-load-can-go-silent-for-15-s.md index 06cbae354f..e0885c0627 100644 --- a/issues/a-counters-read-under-host-load-can-go-silent-for-15-s.md +++ b/issues/a-counters-read-under-host-load-can-go-silent-for-15-s.md @@ -136,29 +136,29 @@ the step each stopped in is not read from it: 77 (38 s, with ceilings paid at 8.00x and 31 s between its image and its first guest line) and 75 to 67 (7 s). -Three in one run, `wt/toyos-nohotkey` at `22b7bb464`, the whole suite, 41 of -44: `virt_smp` (`waiting for the job test_rs_counters_read to end`), -`virt_mask_windows` and `virt_off_names_the_cpus_left_on` (each `waiting for -the boot's last word`), all three verdicts within 4 s of the host's clock. -Load 54.08 60.22 55.97 when the suite began and 71.66 62.30 57.69 when it -ended, 14 cores; fastest boot 716 ms against the reference 1424 ms, ceilings -at 1.00x; workers 1309 s building against 1181 s testing. `virt_smp`'s last -three lines are this file's profile: `===TEST_END unmap_touch exit=0===` and -`===TEST_START test_rs_counters_read===` at 11.606 and 11.607, the spawn of -`pid=13` at 11.616 on cpu4. The other two said nothing while they were waited -on, so the step each stopped in is not read; `virt_off_names_the_cpus_left_on` -is also one of `issues/the-boots-last-word-can-miss-the-console-when-klogd-holds-the-wire.md`'s -waits. The branch removes the blocked-task dump from `drain_irqs`, which every -pass runs. The three alone at that commit passed (load 56.98 to 54.99). Not -known: whether the guests ran; no register capture was taken. Against that -removal as the cause: at `2375834a6`, which already carries it and whose -guest kernel differs from `22b7bb464`'s only by three `pub` methods with no -caller, the whole suite passed 43 of 43, the same three among them, at load -28.66 rising to 61.52; what it took out of a pass is three unarmed checks -that each load an atomic and return; and the read is answered from the kick -vector (`counters::serve_here`), not from a pass. The three images were -built within 2 s of each other, after a 23 s wait on the artifact lock, and -the three guests failed together. +Three in one run, `wt/toyos-nohotkey` at `22b7bb464`, the whole suite, 41 of 44: +`virt_smp` (`waiting for the job test_rs_counters_read to end`), +`virt_mask_windows` and `virt_off_names_the_cpus_left_on` (each `waiting for the +boot's last word`), all three verdicts within 4 s of the host's clock. Load +54.08 60.22 55.97 when the suite began and 71.66 62.30 57.69 when it ended, 14 +cores; fastest boot 716 ms against the reference 1424 ms, ceilings at 1.00x; +workers 1309 s building against 1181 s testing. `virt_smp`'s last three lines +are this file's profile: `===TEST_END unmap_touch exit=0===` and `===TEST_START +test_rs_counters_read===` at 11.606 and 11.607, the spawn of `pid=13` at 11.616 +on cpu4. The other two said nothing while they were waited on, so the step each +stopped in is not read; `virt_off_names_the_cpus_left_on` was also one of the +waits of the record of the boot's last word missing the console while `klogd` +held the wire, which `241832e01` closed. The branch removes the blocked-task +dump from `drain_irqs`, which every pass runs. The three alone at that commit +passed (load 56.98 to 54.99). Not known: whether the guests ran; no register +capture was taken. Against that removal as the cause: at `2375834a6`, which +already carries it and whose guest kernel differs from `22b7bb464`'s only by +three `pub` methods with no caller, the whole suite passed 43 of 43, the same +three among them, at load 28.66 rising to 61.52; what it took out of a pass is +three unarmed checks that each load an atomic and return; and the read is +answered from the kick vector (`counters::serve_here`), not from a pass. The +three images were built within 2 s of each other, after a 23 s wait on the +artifact lock, and the three guests failed together. The fork compiler's ScalarEvolution fault (llvm/llvm-project#175729, which `build/miscompile.rs` now refuses a sysroot for) is not their cause. diff --git a/issues/a-deliberate-wedge-inside-a-driver-panics-every-other-cpu-that-wants-its-lock.md b/issues/a-deliberate-wedge-inside-a-driver-panics-every-other-cpu-that-wants-its-lock.md deleted file mode 100644 index 193e7eaebb..0000000000 --- a/issues/a-deliberate-wedge-inside-a-driver-panics-every-other-cpu-that-wants-its-lock.md +++ /dev/null @@ -1,48 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-13 ---- - -# A deliberate wedge inside a driver panics every other CPU that wants its lock - -`deadline::stage_a_wedge` stops every CPU at its next scheduler pass. The two -arms that use it from the shutdown syscall (`wedge-before-reset`, -`hard-lockup-probe`) hold no lock when they stop, so nothing else in the machine -is waiting on them. An arm that stops *inside a driver* does: the USB wedge arms -(`usb-wedge-data-owed` and its two siblings) stop with the xHCI controller lock -and the block layer's partition lock held for the rest of the boot. - -`sync::Lock`'s deadlock detector then panics whichever CPU next reaches the -disk. On the T14 that is `logd`'s `SYS_FSYNC`, which reaches -`Partition::write_blocks` seconds after the wedge and is panicked at 500M spins: - -``` -[39.941 cpu4] PANIC: DEADLOCK at src/block.rs:182:20: 500M spins, ticket=2676 now=2675 - ::write_blocks - >::alloc_cluster - kernel::object::ops::fsync - kernel::arch::syscall::gate::syscall_entry -``` - -Two things are true and neither is decided here: - -- The detector firing is right in general — a lock held for ever by a CPU that - will not run again is a deadlock, and the detector cannot know the wedge is - deliberate. -- A wedge arm that means to hold a driver's lock for the rest of the boot is - telling the detector something it has no way to hear. - -## Where it bites - -Any actuator that stops a CPU inside a driver: the `usb-wedge-*` arms, which -went with `usb_reset_records_the_phase_it_cut`'s QEMU registration and come back -as T14 rows in stage E of -`issues/the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md`. - -## Exit condition - -Either a wedge declares the locks it is about to hold so the detector can leave -them alone, or the detector's refusal names the staged wedge and halts the -spinner instead of panicking it. A run of any `usb-wedge-*` arm then carries no -`DEADLOCK` record. diff --git a/issues/a-device-without-a-domain-of-its-own-reaches-all-memory.md b/issues/a-device-without-a-domain-of-its-own-reaches-all-memory.md index 6d9a129f78..4be393006a 100644 --- a/issues/a-device-without-a-domain-of-its-own-reaches-all-memory.md +++ b/issues/a-device-without-a-domain-of-its-own-reaches-all-memory.md @@ -7,9 +7,10 @@ opened: 2026-09-29 # A device without a domain of its own reaches all memory Every enumerated PCI function is bound to one identity domain over -`[0, pmm::top())` (`kernel/src/arch/x86_64/vtd/mod.rs:148,456-490`), and only +`[0, pmm::top())` (`kernel/src/arch/x86_64/vtd/mod.rs`, `table::identity_domain` and +`identity_root`), and only a driver that asks gets an address space of its own -(`kernel/src/iommu/mod.rs:3-5`). A device the kernel drives, or one nothing +(`kernel/src/iommu/mod.rs`'s header). A device the kernel drives, or one nothing drives, can read and write every page by DMA. Linux at `Ubuntu-6.8.0-142.142`, under the T14's `CONFIG_INTEL_IOMMU_DEFAULT_ON=y` and `CONFIG_IOMMU_DEFAULT_DMA_LAZY=y`, turns DMA remapping on diff --git a/issues/a-domains-addresses-are-never-given-back.md b/issues/a-domains-addresses-are-never-given-back.md index 9bd2825461..d0cd120d4f 100644 --- a/issues/a-domains-addresses-are-never-given-back.md +++ b/issues/a-domains-addresses-are-never-given-back.md @@ -6,19 +6,22 @@ opened: 2026-09-03 # A domain's addresses are never given back -`Domain::reserve` (`kernel/src/arch/x86_64/vtd/table.rs:213-224`) is a monotonic -bump: an address is never handed out twice, unmapped or not, and `unmap` -returns no range. A driver that maps and unmaps repeatedly therefore consumes -its domain's device address space for good. The kernel's virtio-gpu driver -was the first such driver — every mode change mapped a new scanout and -unmapped the old one, and one boot with three mode changes walked -`device=0x400000200000`, `0x400000800000`, `0x400000a00000`, -`0x400000e00000` with nothing between them reusable; the compositor lends its -scanouts now, in a window whose addresses are lent again -(`kernel/src/pcidev/mod.rs`). From `first_address = 1 << (bits - 2)` and the `1 << bits` ceiling, a -39-bit unit (the harness's `narrow` shape) has 196,608 leaves to give before a -mode change is refused with `ResourceExhausted`; the refusal is the right -answer to exhaustion and the wrong thing to reach by changing modes. +A domain's device addresses are a hand-out-once window +(`kernel/pure/iova.rs`, `Window::reserve`, through +`kernel/src/iommu/domains.rs`'s `Domains::map`): an address is never handed +out twice, unmapped or not, and an unmap returns no range. A driver that maps +and unmaps repeatedly therefore consumes its domain's device address space for +good. The kernel's virtio-gpu driver was the first such driver — every mode +change mapped a new scanout and unmapped the old one, and one boot with three +mode changes walked `device=0x400000200000`, `0x400000800000`, +`0x400000a00000`, `0x400000e00000` with nothing between them reusable; the +compositor lends its scanouts now, in a window whose addresses are lent again +(`kernel/src/pcidev/mod.rs`). A window runs from the second leaf to +`1 << bits` less the reserved ranges, so a 39-bit unit has under 262,144 +2 MiB leaves to give before a map is refused with `AddressesExhausted`; the +refusal is the right answer to exhaustion and the wrong thing to reach by +remapping. A claim's grants are the case +`issues/a-claim-spends-device-addresses-its-slot-never-gets-back.md` holds. Exit: `unmap` returns its range to a free list, or the reservation is per-attachment and recycled when the attachment ends — either way a domain's diff --git a/issues/a-double-fault-on-cpu-1-under-a-wide-suite.md b/issues/a-double-fault-on-cpu-1-under-a-wide-suite.md index 6cd2b02844..8762df63e1 100644 --- a/issues/a-double-fault-on-cpu-1-under-a-wide-suite.md +++ b/issues/a-double-fault-on-cpu-1-under-a-wide-suite.md @@ -61,7 +61,8 @@ itself is recovered too. That hole's general form — a death with no report vocabulary in it at all, where the arm prints an empty `serial` — is closed at the same field: `WaitVerdict::for_test` carries the window when `started` is false. Gated in both directions under `serial_vocabulary`, and against the real -`#DF` `double_fault_stack` already puts on the wire. +`#DF` `double_fault_stack` put on the wire (that guest test was cut in +`520c0d129`). **So what the next sighting shows that this one did not** — every line of it, verbatim, in the failure message: @@ -119,13 +120,14 @@ are written on every context switch from the incoming task's id (`kernel/src/arch/x86_64/hw.rs`), and the idle context's id is `None` (`sched/driver::enter_idle_loop` sets both to `None` before it moves `rsp`). A live pid therefore means CPU 1 was running a task. The idle stack is the one -with an unmapped page under it (`IDLE_GUARD_SIZE`, `guard_kernel_page`), and an +with an unmapped page under it (`kernel/src/sched/idle_stack.rs`'s `GUARD`, +unmapped by `guard_4k`), and an unmapped page under a stack is what turns an overflow into a `#PF` the CPU cannot deliver — because the frame it must push goes to the same bad `rsp`. **And the stack it *was* on does not fault on overflow.** A thread's kernel stack is `KERNEL_STACK_SIZE` = 128 KiB of ordinary heap (`OwnedAlloc::new` in -`loader::alloc_kernel_stack`) with a canary word at the bottom +`alloc_kernel_stack`, `kernel/src/loader/start.rs`) with a canary word at the bottom (`write_stack_canary`) checked at every scheduler pass (`sched::driver::check_stack_canary`) and **no unmapped page beneath it**. An overflow off it writes into whatever the allocator put there and is reported later, from ordinary context, as @@ -139,7 +141,7 @@ reaching for it. IST1 cannot be the cause either: `ist 1` is on the `DoubleFault` row alone (`arch/idt/mod.rs`), so nothing but a `#DF` ever runs on it. It can only be overrun *while reporting* one, and the report says whether it was — -`[ist1] used N of M bytes, guard intact` against the `IST1_STACK_SIZE` of 16384 +`[ist1] used N of M bytes, guard intact` against the `IST_STACK_SIZE` of 16384 that `0b4f305` and `9148057` sized, whose declared margin is half the stack. Measured on this tree on 2026-08-18, two `double_fault_stack` runs minutes apart: **6688** and **5312** bytes used of 16384, guard intact both times. The diff --git a/issues/a-double-panic-at-boots-edge-says-nothing-but-its-name.md b/issues/a-double-panic-at-boots-edge-says-nothing-but-its-name.md index 0628f0b519..e64dfd4266 100644 --- a/issues/a-double-panic-at-boots-edge-says-nothing-but-its-name.md +++ b/issues/a-double-panic-at-boots-edge-says-nothing-but-its-name.md @@ -40,7 +40,9 @@ panel is the only channel a laptop has. The line also names *which* state the arriving panic found, which is the fact this sighting most wanted: `DOUBLE PANIC` with the depth guard at zero means the first event was a fault or a demand-paging fault and not a panic at all. -`double_panic_names_the_first` stages both dead ends and reads them. +The guest test that staged both dead ends and read them +(`double_panic_names_the_first`, later `double_panic_names_the_fault`) was +cut in `520c0d129`. **Finding 1 stays open**, and the next sighting of it will carry what this one could not: the first crash's identity and site, the second panic's site, and diff --git a/issues/a-driver-that-lends-its-device-a-handle-a-peer-sent-is-ended-when-it-is-no-region.md b/issues/a-driver-that-lends-its-device-a-handle-a-peer-sent-is-ended-when-it-is-no-region.md index 050b7fa7c5..8d25e43c59 100644 --- a/issues/a-driver-that-lends-its-device-a-handle-a-peer-sent-is-ended-when-it-is-no-region.md +++ b/issues/a-driver-that-lends-its-device-a-handle-a-peer-sent-is-ended-when-it-is-no-region.md @@ -13,16 +13,16 @@ kind is `HandleError::WrongType`, which ends the caller, exit 139 for it instead, and is what `SYS_SPAWN` and `SYS_NAMESPACE_BUILD` take a handle a peer sent with (`kernel/src/object/handle.rs`). -The region a holder lends is a peer's by contract: `toyos_pci_claim::lend` -maps "`region`, which a peer lent", and reads `InvalidArgument` as -`Unlendable` (`lib/pci-claim/src/lib.rs`), a refusal the kernel never answers -for a handle of another kind. Since #953 the hda driver lends its controller -the ring soundserver sent with its acceptance, unchecked -(`offered` and `toyos_pci_claim::lend` in `system/hda/src/main.rs`): a -soundserver that sends a pipe end or a connection there ends hda rather than -being refused. diskserver is not reached, because `SharedMemory::lent` refuses -what `SYS_FSTAT` does not answer as a region before it maps one -(`system/diskserver/src/region.rs`). A region's kind is one `SYS_FSTAT` settles, +The region a holder lends is a peer's by contract: `toyos_pci_claim::lend` maps +"`region`, which a peer lent", and reads `InvalidArgument` as `Unlendable` +(`lib/pci-claim/src/lib.rs`), a refusal the kernel never answers for a handle of +another kind. Since #953 the hda driver lends its controller the ring +soundserver sent with its acceptance, unchecked (`offered` and +`toyos_pci_claim::lend` in `system/hda/src/main.rs`): a soundserver that sends a +pipe end or a connection there ends hda rather than being refused. A disk driver +is not reached, because `Region::lent` refuses what `SYS_FSTAT` does not answer +as a region before the session lends it (`Sessions::open`, +`lib/blockserve/src/sessions.rs`). A region's kind is one `SYS_FSTAT` settles, so this is not `issues/a-received-handle-has-no-knowable-type.md`'s case. The kernel owns the fix rather than each lender: what a holder lends is a diff --git a/issues/a-fat-files-mtime-through-its-handle-is-its-last-level.md b/issues/a-fat-files-mtime-through-its-handle-is-its-last-level.md index a8f4b6fb34..de11f4bb7e 100644 --- a/issues/a-fat-files-mtime-through-its-handle-is-its-last-level.md +++ b/issues/a-fat-files-mtime-through-its-handle-is-its-last-level.md @@ -6,8 +6,8 @@ opened: 2026-09-29 # A FAT file's mtime through an open handle is its entry's last level, not its last write -fsd's FAT volume answers a file's mtime off its directory entry -(`userland/fsd/src/fat.rs`, `node_meta` and `lstat`), and an entry is stamped +The file server's FAT volume answers a file's mtime off its directory entry +(`system/fileserver/src/fat.rs`, `node_meta` and `lstat`), and an entry is stamped only when it is brought level: at the last close, an fsync or a sync. A write moves nothing an `fstat` reads, so a writer that writes and `fstat`s reads the stamp of the entry's last level, and the same file closed and opened again reads diff --git a/issues/a-fatal-event-stands-down-both-bounds-and-may-leave-nothing-to-end-the-machine.md b/issues/a-fatal-event-stands-down-both-bounds-and-may-leave-nothing-to-end-the-machine.md index f9fff3e413..d95270d85c 100644 --- a/issues/a-fatal-event-stands-down-both-bounds-and-may-leave-nothing-to-end-the-machine.md +++ b/issues/a-fatal-event-stands-down-both-bounds-and-may-leave-nothing-to-end-the-machine.md @@ -6,24 +6,22 @@ opened: 2026-09-14 # A fatal event stands down both bounds, and on a machine that cannot reset itself nothing is left to end the boot -`apic::halt_all_cpus` (`kernel/src/arch/x86_64/apic.rs:228`) calls +`panic::halt_all_cpus` (`kernel/src/panic.rs`) calls `crate::hardlockup::stand_down()` and `crate::deadline::stand_down()` before it -holds the panel (`kernel/src/arch/x86_64/apic.rs:235-236`). That is deliberate and the -deadline's own header says so — "a panic in progress, which is not a gap but a -stand-down: `apic::halt_all_cpus` calls `stand_down` before it holds the panel, -so a panic report is never replaced by an expiry" -(`kernel/src/deadline.rs:30-32`). Both of this machine's bounds are gone from -that instant: the 120 s boot deadline polled from the timer entry, and the 60 s -per-CPU hard-lockup NMI. +holds the panel. That is deliberate and the deadline's own header says so — "a +panic in progress, which is not a gap but a stand-down: `panic::halt_all_cpus` +calls `stand_down` before it holds the panel, so a panic report is never +replaced by an expiry" (`kernel/src/deadline.rs`). Both of this machine's +bounds are gone from that instant: the 120 s boot deadline polled from the +timer entry, and the 60 s per-CPU hard-lockup NMI. **What is left is one bound, and it is conditional.** `panic_reboot::arm` -returns `Bound::At(cycles)` only when `acpi::can_reboot()` holds, and -`can_reboot` is `RESET_PORT != 0` — the FADT's reset register and nothing else, -with no fallback by design (`kernel/src/drivers/acpi.rs:277-283`). Where the -table named none, `arm` returns `Bound::Held` -(`kernel/src/panic_reboot.rs:134-145`) and `hold_the_panel` loops -`while bound.is_armed()` (`kernel/src/drivers/panic_console/mod.rs:647-650`), -which never ends. +returns `Bound::At(cycles)` only when `power::can_reboot()` holds, which on +x86-64 is a reset register the FADT named (`arch::power::can_reset`, +`kernel/src/arch/x86_64/power.rs`) and nothing else. Where the table named +none, `arm` returns `Bound::Held` (`kernel/src/panic_reboot.rs`) and +`hold_the_panel` (`kernel/src/drivers/panic_console/mod.rs`) halts the CPU +for good. **On the T14 that last bound is the one thing already known not to work.** `issues/an-armed-tco-has-never-reset-the-t14.md` records that no claim @@ -37,11 +35,12 @@ one mechanism that has never been observed to fire. Metal run 49 (`lancase`, branch `i219-phy`) was away 303 s with no ping answered at any point and no readback at all — the boot left the USB device in a state its next host could not enumerate, so `/dev/sda3` never came back. **Nothing -from the machine side survived**, which is the shape this file is about rather than a gap in it: -the channels that would have named the fault are the record ring (reaches the -stick only through `logd`) and a sealed `WEDGED` record (`seal_wedge`, reached -from `deadline::expire` and `hardlockup::locked_up`) — and the stand-down is -what guarantees the second one is not written. +from the machine side survived**, which is the shape this file is about rather +than a gap in it: the channels that would have named the fault are the record +ring (reaches the stick only through `logd`, now `logkeeper`) and a sealed +`WEDGED` record (`seal_wedge`, reached from `deadline::expire` and +`hardlockup::locked_up`) — and the stand-down is what guarantees the second one +is not written. This is not `issues/no-console-between-boot-and-terminal.md`, which is about a diff --git a/issues/a-fault-before-the-kernels-idt-vectors-into-the-firmwares.md b/issues/a-fault-before-the-kernels-idt-vectors-into-the-firmwares.md index e0a65b1c65..549c1565ad 100644 --- a/issues/a-fault-before-the-kernels-idt-vectors-into-the-firmwares.md +++ b/issues/a-fault-before-the-kernels-idt-vectors-into-the-firmwares.md @@ -6,8 +6,11 @@ opened: 2026-09-25 # A fault before the kernel's IDT vectors into the firmware's, and says nothing -From `_start` until `percpu::init_bsp` loads the kernel's IDT (`kernel/src/main.rs`), -the table in `IDTR` is the one firmware left. A CPU exception in that window +On x86-64, from `_start` until `percpu::init_bsp` loads the kernel's IDT +(`arch::boot::interrupts`, `kernel/src/arch/x86_64/boot.rs`), the table in +`IDTR` is the one firmware left; `after_console` refuses `test-early-fault` by +name for that reason. AArch64 points `VBAR_EL1` at its own table before anything +can fault (`kernel/src/arch/aarch64/trap.rs`). A CPU exception in that window (`pat::init`, `panic_console::arm`'s walk of the boot map, `mm::init`, the ACPI parse) vectors into firmware code, which on EDK2 dead-loops; no panic runs, so neither the panel nor the black box nor serial carries anything. diff --git a/issues/a-file-on-data-is-refused-at-250-runs-while-blocks-are-free.md b/issues/a-file-on-data-is-refused-at-250-runs-while-blocks-are-free.md index 0d74ffd817..32df23d716 100644 --- a/issues/a-file-on-data-is-refused-at-250-runs-while-blocks-are-free.md +++ b/issues/a-file-on-data-is-refused-at-250-runs-while-blocks-are-free.md @@ -6,7 +6,7 @@ opened: 2026-09-27 # A file on DATA is refused at 250 runs while the volume has free blocks -The DATA volume (`userland/fsd/src/data.rs`, over the `bcachefs` crate) keeps a +The DATA volume (`system/fileserver/src/data.rs`, over the `bcachefs` crate) keeps a file's extents in its one btree entry, and one entry is at most `MAX_ENTRY_SIZE` bytes (`lib/bcachefs/src/btree.rs`): 250 runs for `home/a` (`file_entry_fits`, `lib/bcachefs/src/fs.rs`). On a volume whose free blocks are @@ -15,10 +15,10 @@ all apart every run is one block, so a write that takes a file past 250 blocks `SPREAD` lengthens the runs of files that grow in turn; it does nothing for free space that is already fragmented. -Owner: the fsd DATA volume. +Owner: the file server's DATA volume. Evidence: `a_write_its_entry_could_not_name_is_refused_and_every_accepted_one_kept` -(`userland/fsd/src/data.rs`) is refused at `pages == 250` on a volume with +(`system/fileserver/src/data.rs`) is refused at `pages == 250` on a volume with free blocks. **Exit**: a file's runs held outside its one entry, so that test writes past diff --git a/issues/a-file-server-killed-mid-sync-leaves-a-half-written-btree.md b/issues/a-file-server-killed-mid-sync-leaves-a-half-written-btree.md deleted file mode 100644 index 386dd6494e..0000000000 --- a/issues/a-file-server-killed-mid-sync-leaves-a-half-written-btree.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-27 ---- - -# A file server killed mid-sync leaves a half-written btree - -`userland/fsd`'s DATA volume is `bcachefs`'s interim format: the btree is -rewritten in place and there is no journal. A sync writes the dirty blocks -fsd's cache holds (`userland/fsd/src/cache.rs`) in block order and then asks -the device to flush; nothing orders a node's children before the node that -names them, and nothing records which of them reached the device. A server -that ends between two of those writes — a crash, a kill, the restart budget -`toyos_manifest::RESTARTS` exists for — or a power cut in the same window -leaves a volume whose next mount reads a node naming blocks that hold another -generation's bytes. - -`fsd_restart` does not see it: it ends the server under a write that no sync -has started, so what is on the device is the last sync's whole state. - -**Exit**: a sync that a cut at any block boundary leaves mountable, as the -last acknowledged sync or the one before it — copy-on-write with a superblock -that is written last, or a journal replayed at mount — with a host test that -cuts a sync after every block it writes and mounts what is left. diff --git a/issues/a-firmware-call-does-what-its-handler-chooses-and-the-kernel-bounds-only-the-call.md b/issues/a-firmware-call-does-what-its-handler-chooses-and-the-kernel-bounds-only-the-call.md index f976e60481..7af3fadd70 100644 --- a/issues/a-firmware-call-does-what-its-handler-chooses-and-the-kernel-bounds-only-the-call.md +++ b/issues/a-firmware-call-does-what-its-handler-chooses-and-the-kernel-bounds-only-the-call.md @@ -106,9 +106,11 @@ measurement: the model of the T14's AML that was read makes two calls in one evaluation at the most, and retries a call its handler has not answered once a millisecond, ten thousand times; eight holds that storm to eight calls a second and refuses the rest, and whether it refuses a call a healthy machine -needs is unread. No call but the kernel's own enable and disable has been -made on the T14: the server passes no write its AML asks for to the kernel -yet (`system/acpiserver/src/host.rs`), so nothing there asks for one. On QEMU's q35 the chipset model keeps the byte and its +needs is unread. Since `d2006cc65` the server +asks the kernel for a byte its AML writes to `SMI_CMD` +(`system/acpiserver/src/host.rs`), and since `15536e2e0` for every port write +it makes, so the T14's AML can make calls; no reading of what those calls +cost there is recorded. On QEMU's q35 the chipset model keeps the byte and its `SMI_EN` reads 0, so no guest's call interrupts a firmware, and what a guest reads of one is that it was written, where, and how often. @@ -122,9 +124,10 @@ beside `issues/the-acpi-servers-holder-drives-the-embedded-controller-unfiltered to the port, checked by something other than the holder, or the owner rules the bound above is the one ToyOS keeps; and the rate is held against the calls a T14 row reads the machine's own AML making, with the time each held -the boot processor; and the span is ruled. The slice that passes the first -write its AML asks for to the kernel reads on the T14 the time each call its -AML makes holds cpu0, and the owner rules, against those readings, whether a -handler that returns after `DEAF_CPU` ends the machine. Where the interrupt -stops every CPU the kernel such a handler returns to is whole, so the panic -there is this kernel's choice. That slice does not land without the ruling. +the boot processor; and the span is ruled. +The T14 reading owed is the time each call its AML makes holds cpu0 (the +slice that passes those writes landed in `d2006cc65`, and no such reading or +ruling is recorded here), and the owner rules, against those readings, +whether a handler that returns after `DEAF_CPU` ends the machine. Where the +interrupt stops every CPU the kernel such a handler returns to is whole, so +the panic there is this kernel's choice. diff --git a/issues/a-foreign-data-partition-is-answered-with-memory.md b/issues/a-foreign-data-partition-is-answered-with-memory.md index 327ddb4a9a..c176d6bf72 100644 --- a/issues/a-foreign-data-partition-is-answered-with-memory.md +++ b/issues/a-foreign-data-partition-is-answered-with-memory.md @@ -6,18 +6,18 @@ opened: 2026-09-28 # A foreign DATA partition is answered with memory -`userland/fsd/src/main.rs`'s `data_on` answers `Probed::Foreign` — a +`system/fileserver/src/main.rs`'s `data_on` answers `Probed::Foreign` — a TOYOS-DATA partition that holds no volume of ours and no designation stamp — with `ram()`: `/apps`, `/config`, `/home` and `/state` are served from memory, and everything written under them is lost at the next reboot. That is a disk that is there and cannot be used, answered as though the machine had none, -which is the harm `fsd::absent::Absent` exists to refuse; a refused NVMe +which is the harm `fileserver::absent::Absent` exists to refuse; a refused NVMe claim and two DATA partitions are refused by name for the same reason. -`foreign_disk_untouched` (`tests/common/storage.rs`) boots this arm and -asserts the partition is not written and the `no volume of ours` line, and -nothing about what stands in for it. +`foreign_disk_untouched`, the guest test that booted this arm, asserted the +partition is not written and the `no volume of ours` line, and nothing about +what stands in for it; it was cut in `520c0d129`. **Exit condition:** a foreign DATA partition serves DATA absent by name, and -`foreign_disk_untouched` asserts fsd's absent line and that the in-memory line -is never said. +a test of `data_on`'s `Probed::Foreign` arm reads the absent line and that the +in-memory line is never said. diff --git a/issues/a-function-the-smmuv3-does-not-route-panics-at-attach.md b/issues/a-function-the-smmuv3-does-not-route-panics-at-attach.md index f7a60b0fde..85a7b032b5 100644 --- a/issues/a-function-the-smmuv3-does-not-route-panics-at-attach.md +++ b/issues/a-function-the-smmuv3-does-not-route-panics-at-attach.md @@ -6,11 +6,12 @@ opened: 2026-10-10 # A function the SMMUv3 does not route panics at attach -`DeviceSpace::create` and `OwnSpace::create` take no function, so on the -SMMUv3 (`kernel/src/arch/aarch64/smmu/domain.rs`) a domain is made for any -caller. `attach` then looks the function up in the unit's routes, and -`Live::stream` panics where it finds none: an enumerated function whose IORT -route names another unit, names none, or was refused +`OwnSpace::create` takes no function, and `DeviceSpace::create` asks of its +functions only `domain::untranslated`, which on the SMMUv3 +(`kernel/src/arch/aarch64/smmu/domain.rs`) answers `None` for every one, so a +domain is made for any caller. `attach` then looks the function up in the unit's +routes, and `Live::stream` panics where it finds none: an enumerated function +whose IORT route names another unit, names none, or was refused (`kernel/src/arch/aarch64/smmu/mod.rs`, `init`'s route loop). A gap in a firmware table becomes a kernel panic at `attach`, where it is a refusal at `create`. Nothing on QEMU's `virt` reaches it: every function there is routed diff --git a/issues/a-guest-close-after-a-refused-handshake-can-leave-its-peer-waiting.md b/issues/a-guest-close-after-a-refused-handshake-can-leave-its-peer-waiting.md index ef1f54befe..9550f15f44 100644 --- a/issues/a-guest-close-after-a-refused-handshake-can-leave-its-peer-waiting.md +++ b/issues/a-guest-close-after-a-refused-handshake-can-leave-its-peer-waiting.md @@ -6,15 +6,15 @@ opened: 2026-10-09 # A guest's close after a refused handshake can leave its peer waiting -`https_fetch` (`tests/toyos.rs`) runs `https_get` three times on one boot of -`tests/images/netcase.toml`, each a new process and one TCP connection through -`/system/bin/netstack` to a `rustls` server on the host. The second and third -runs refuse the server's certificate during the handshake, print their -refusal, drop the connection and exit 2. On smoltcp, the stack `netstack` ran -before ToyOS's own replaced it, the server's read of the handshake's next -record was answered by nothing: in one boot its peer's end of stream arrived -and in the other it did not, 180 seconds after the process exited, no FIN and -no reset. netstack logged nothing about either connection. +`https_fetch` (`tests/toyos.rs`, cut in `ff1a11dbd`) ran `https_get` three times +on one boot of `tests/images/netcase.toml`, each a new process and one TCP +connection through `/system/bin/netstack` to a `rustls` server on the host. The +second and third runs refuse the server's certificate during the handshake, +print their refusal, drop the connection and exit 2. On smoltcp, the stack +`netstack` ran before ToyOS's own replaced it, the server's read of the +handshake's next record was answered by nothing: in one boot its peer's end of +stream arrived and in the other it did not, 180 seconds after the process +exited, no FIN and no reset. netstack logged nothing about either connection. The same client and server both on the host (macOS) see each refused connection end at once with an end of stream and no alert: the client sends @@ -32,7 +32,7 @@ by a probe patch on `https_fetch` that waits up to 150 seconds for each refused connection's end (patch and logs on that branch's pull request): in two boots, all four refused connections had already ended at the peer when the harness looked, after the third run exited. Two boots are no rate, and -`https_fetch` still asserts nothing of these ends. +`https_fetch` asserted nothing of these ends. **Exit condition**: a guest test whose process closes a connection mid- handshake, with the peer's flight unread, and exits, sees the host's peer diff --git a/issues/a-held-launcher-that-will-not-open-is-a-direct-spawn.md b/issues/a-held-launcher-that-will-not-open-is-a-direct-spawn.md index eecd2d6875..e4189bc42f 100644 --- a/issues/a-held-launcher-that-will-not-open-is-a-direct-spawn.md +++ b/issues/a-held-launcher-that-will-not-open-is-a-direct-spawn.md @@ -6,7 +6,9 @@ opened: 2026-10-04 # A held launcher that will not open is a direct spawn -std's ToyOS `launch` (`sdk/std/sys/process.rs`) reads `toyos::endow::launcher().and_then(|held| held.open(LAUNCHER).ok())`: +`toyos::spawn::Spawn`'s `ask_launcher` (`sdk/toyos/src/spawn.rs`), which std's +`Command` spawn takes, reads +`crate::endow::launcher().and_then(|held| held.open(launch::LAUNCHER).ok())`: a process that holds a launcher and whose open of it fails is treated as one that holds none, and its spawn of a declared program goes on as a direct spawn, without that program's row and with no error. The shape is the one `main` had diff --git a/issues/a-holders-queued-line-can-reach-the-console-after-the-last-word.md b/issues/a-holders-queued-line-can-reach-the-console-after-the-last-word.md index ff553e7ea0..512769f11a 100644 --- a/issues/a-holders-queued-line-can-reach-the-console-after-the-last-word.md +++ b/issues/a-holders-queued-line-can-reach-the-console-after-the-last-word.md @@ -18,7 +18,7 @@ FAIL quiesce_stops_the_machine: 3 line(s) reached the console after the boot's l ``` The lines are stamped before the stop and reached the wire after `Rebooting.`. -The mechanism read off the code: `logd` puts a program's line in +The mechanism read off the code: `logd` (now `logkeeper`) puts a program's line in `kernel/src/log/console.rs`'s queue, and `klogd` drains it a chunk of records and then a chunk of queued lines per hold of the wire, whatever their age. The stop stops every userland thread and writes its last word as a record, which diff --git a/issues/a-job-list-hangs-with-interrupts-on-and-the-deadline-ends-it.md b/issues/a-job-list-hangs-with-interrupts-on-and-the-deadline-ends-it.md index 357115ff5e..8374d6cc94 100644 --- a/issues/a-job-list-hangs-with-interrupts-on-and-the-deadline-ends-it.md +++ b/issues/a-job-list-hangs-with-interrupts-on-and-the-deadline-ends-it.md @@ -6,10 +6,10 @@ opened: 2026-09-08 # A job list hangs with interrupts on, and only the deadline ends it -T14 run 25 (`/Users/jan/.claude/jobs/2280e09e/tmp/t14-run25-metal.log`, lines -26-47). The `ccorpus` image — 118 C cases that had run in 49 s on the previous -tip — came back after **187 s**, so its boot spent the whole -`boot-deadline=120000` and the deadline is what ended it. +T14 run 25 (its metal log, outside the tree, lines 26-47). The `ccorpus` image — +118 C cases that had run in 49 s on the previous tip — came back after **187 +s**, so its boot spent the whole `boot-deadline=120000` and the deadline is what +ended it. **That narrows it past the hang this branch already fixed.** A deadline that fires is a machine where some CPU was still taking a timer interrupt, so this is @@ -27,9 +27,12 @@ hung in run 22, `metaldevicecase` in run 20 showed the same span of **The record exists and could not be read.** The deadline seals `EXPIRED` and the tail of the log ring into the black box, and the next loader pass prints it into `loader.log` on the stick — but this reset cut a bulk transfer in its data -phase and left the stick unenumerable, so there was no readback at all. The stop -now waits a transfer out first (`xhci/stop.rs`'s `settle_transfers`), so the -next occurrence should leave a stick the driver can read. +phase and left the stick unenumerable, so there was no readback at all. The kernel +drives no USB now (`af1c5af25`): the stick is usbd's, and a reset this kernel +performs stops every claimed function's mastering first, usbd's controller +among them (`pcidev::stop_before_reset`, from `kernel/src/power.rs`). What a +cut command costs the stick is +`issues/a-cut-bulk-only-command-does-not-brick-the-benchs-stick.md`'s. **Run 55 does not bear on this.** Its `lancase` boot ran no job list — the ring tail records `spawn: /system/bin/test-runner: ELF: fewer bytes than a diff --git a/issues/a-known-mask-is-copied-out-of-toyos-abi-by-hand.md b/issues/a-known-mask-is-copied-out-of-toyos-abi-by-hand.md index e8acbaf492..6befb5ed34 100644 --- a/issues/a-known-mask-is-copied-out-of-toyos-abi-by-hand.md +++ b/issues/a-known-mask-is-copied-out-of-toyos-abi-by-hand.md @@ -17,12 +17,15 @@ change that added them was not allowed to touch the shared sysroot. compiling clean, and `MMAP_PROT_KNOWN` — still `READ.0 | WRITE.0` — then refuses a bit the ABI defines. Fail-closed, so it is a maintenance hazard and not a hole: the caller is told `InvalidArgument` rather than served something else. -The same union is copied a third time into each guest arm's local `const KNOWN`, -so a drift would take the test with it and the arm would still pass. +The same union is copied a third time into a guest arm's local `const KNOWN` +(`tests/guest/rust-tests/src/bin/handle_lifetime.rs`, the one left after the +guest suite's cut), so a drift would take the test with it and the arm would +still pass. **The fix is one line per word and the precedent is in the same file.** -`NAMESPACE_FLAGS_KNOWN` (`sdk/abi/src/syscall.rs:1340`) is the mask that +`NAMESPACE_FLAGS_KNOWN` (`sdk/abi/src/syscall.rs`) is the mask that cannot drift, because it sits beside the constants it unions and every reader names it. Four more of those — for `MmapProt`, `MmapFlags`, `OpenFlags` and -`WNOHANG` — delete this entry, the three copies in the kernel and the three in -the tests. +`WNOHANG` — delete this entry, the copies in the kernel (`kernel/src/syscall/vm.rs`, +`kernel/src/object/ops.rs`, `kernel/src/inbox/mod.rs`, and the +`!WNOHANG` test in `kernel/src/syscall/proc.rs`) and the one in the tests. diff --git a/issues/a-launch-too-large-for-one-frame-becomes-a-direct-spawn.md b/issues/a-launch-too-large-for-one-frame-becomes-a-direct-spawn.md index 98e3fba559..70958ed9cb 100644 --- a/issues/a-launch-too-large-for-one-frame-becomes-a-direct-spawn.md +++ b/issues/a-launch-too-large-for-one-frame-becomes-a-direct-spawn.md @@ -6,11 +6,12 @@ opened: 2026-10-02 # A launch too large for one frame becomes a direct spawn -`toyos::launch::launch` (`sdk/toyos/src/launch.rs`) answers `LaunchError::NotSent` -when `Launch::encode` refuses the request, and std's `Command::launch` -(`sdk/std/sys/process.rs`) answers `NotSent` with the direct -spawn. `encode` refuses a request whose header, program, argv, environment, -working directory and connector names do not fit `MAX_FRAME_LEN` +`toyos::launch::launch` (`sdk/toyos/src/launch.rs`) answers +`LaunchError::NotSent` when `Launch::encode` refuses the request, and +`Spawn::launch` (`sdk/toyos/src/spawn.rs`, `unmade`), which std's `Command` +spawn takes (`sdk/std/sys/process.rs`), answers `NotSent` with the direct spawn. +`encode` refuses a request whose header, program, argv, environment, working +directory and connector names do not fit `MAX_FRAME_LEN` (`sdk/toyos/src/ipc.rs`). Its other refusal, too many connectors or slots, std never reaches: it refuses the connectors itself, and a launch carries stdio alone. @@ -19,7 +20,8 @@ So a declared program started with an argv and environment past one frame is not launched. Its caller spawns it: it holds its caller's namespace and not its manifest row, a connector the caller `provide`d does not reach it, and it carries no `HOME` unless its caller named one. Neither side is told. A child -asked of init (`under_init`) is refused `PermissionDenied` instead. +asked of the supervisor (`under_supervisor`) is refused `PermissionDenied` +instead. Evidence: by reading. No test sends a launch past one frame. diff --git a/issues/a-listeners-tcp-nodelay-has-no-setter-in-std-or-the-forks-and-a-c-program-cannot-order-the-waiting-case.md b/issues/a-listeners-tcp-nodelay-has-no-setter-in-std-or-the-forks-and-a-c-program-cannot-order-the-waiting-case.md index 2b0a598ba8..ede3987cd7 100644 --- a/issues/a-listeners-tcp-nodelay-has-no-setter-in-std-or-the-forks-and-a-c-program-cannot-order-the-waiting-case.md +++ b/issues/a-listeners-tcp-nodelay-has-no-setter-in-std-or-the-forks-and-a-c-program-cannot-order-the-waiting-case.md @@ -26,46 +26,38 @@ carries its listener's options and `MsgType::TcpListenerSetOption` sets one afterwards, and an accept's answer says what its connection holds (`TcpOptions`). netstack answers the rule for both by the node's calls (`system/netstack/src/serve.rs`, `system/netstack/node/src/listeners.rs`), -and the `libc_sockets` guest test reads it: `nodelay_accepted` by `toyos::net`, for a connection that -waited when the option was set, its wake read first, and for the next one -dialled; `tests/guest/c/nodelay_kept.c` by libc's `setsockopt` and -`getsockopt`, for a listener's set before `bind`, its set and its clear after, -and the connections dialled after each. What no program reads yet: - -- A C program cannot set the option while a connection waits and know that - one does: libc's `poll` watches kernel handles and a socket descriptor is - none (`sdk/libc/src/posix_io.rs`), so the only call that says a - connection waits is the `accept` that takes it. `nodelay_kept.c` therefore - holds the half of the rule a second dial orders, and not the first. +and the node takes a listener's option in its `listen` and in +`Node::set_listener_nodelay`, and answers an accept with what its stream's +connection took from its listener. The `libc_sockets` guest test that read it +(`nodelay_accepted` by `toyos::net`, `tests/guest/c/nodelay_kept.c` by libc's +`setsockopt` and `getsockopt`) was cut in `ff1a11dbd`. What no program reads: + +- Whether a C program can set the option while a connection waits and know + that one does. libc's `poll` now watches a listener by its wake pipe + (`sdk/libc/src/posix_io.rs`); no test reads that it answers a waiting + connection. - No Rust program can set the option on a listener. `std::net::TcpListener` has no call for it on any host, where a program reaches the socket through socket2 by its descriptor. On ToyOS, read from the forks at the commits the - lock names and not run: socket2 (`1d90250`, `src/sys/toyos.rs`) keeps a - table of its own sockets, so a std listener's descriptor names none of - them; on a listener of its own its `setsockopt` of `TCP_NODELAY` answers - `Ok` and hands the option to nobody, and its `accept` records the accepted - socket's as off whatever the answer says; its bind names no option - (`toyos::net::tcp_bind`, which `tcp_bind_with` stands beside only until - the fork's bind passes its own). mio (`d1edcaa`, - `src/net/tcp/toyos_stream.rs`) answers `nodelay()` true for every stream - and its `set_nodelay` sends nothing. std's `TcpListener::accept` - (`sdk/std/sys/net/connection.rs`) takes the accepted stream's option from - the answer, which no test can turn red while no std listener can hold one. - -The node (`system/netstack/node/src/listeners.rs`) takes a listener's -option in its `listen` and in `Node::set_listener_nodelay`, and answers an -accept with what its stream's connection took from its listener; nothing ships -it until the move. + lock names and not run: socket2 (`12abd8f`, `src/sys/toyos.rs`) holds std's + `TcpListener` for a listening socket, binds it through + `TcpListener::bind`, which carries no option, answers a listener's + `setsockopt` of `TCP_NODELAY` `Ok` and sets nothing, and its `getsockopt` + 0; an accepted socket is std's `TcpStream`, whose option it reads. mio + (`836d14f`) delegates a stream's option to std's. std's + `TcpListener::accept` (`sdk/std/sys/net/connection.rs`) takes the accepted + stream's option from the answer, which no test can turn red while no std + listener can hold one; `toyos::net` still has two bind calls, `tcp_bind` + and `tcp_bind_with`. **Exit condition**: on ToyOS a program that sets `TCP_NODELAY` on a listening socket and accepts one connection begun before and one begun after reads the option on each as that test has a host answer, by libc's -`getsockopt`, which needs libc's `poll` to answer for a listening socket, and -by std's `nodelay()`, which needs a listener a Rust program can set the option +`getsockopt`, with libc's `poll` answering for the waiting listener, and by +std's `nodelay()`, which needs a listener a Rust program can set the option on: the socket2 fork's, with the lock naming the commit that hands its -listener's option to netstack in its bind and reads its accepted socket's from -the answer, and mio's that reads a stream's; and `toyos::net` has one bind +listener's option to netstack in its bind; and `toyos::net` has one bind call, which takes its options. **Owner**: whoever holds `issues/toyos-has-its-own-network-stack.md`; the -fork commits are on socket2's and mio's `toyos` branches. +fork commit is on socket2's `toyos` branch. diff --git a/issues/a-listing-still-scans-the-whole-mount.md b/issues/a-listing-still-scans-the-whole-mount.md index 44c936c61c..d039f76c64 100644 --- a/issues/a-listing-still-scans-the-whole-mount.md +++ b/issues/a-listing-still-scans-the-whole-mount.md @@ -8,35 +8,38 @@ opened: 2026-09-01 The `FileSystem::list` bound is per directory now (`kernel/src/vfs.rs`, `under_directory`), and FAT32 pays only the named subtree because it descends -to it (`Fat32::walk`'s `under`). The other two mounts still read every name in -the mount and drop the ones that do not match: +to it (`Fat32::walk`'s `under`). The other two formats still read every name +in the volume and drop the ones that do not match: - `TmpFs::list` (`kernel/src/tmpfs.rs`) iterates the whole `entries` map. - `Mounted::list` (`lib/bcachefs/src/fs.rs`) walks the whole B+tree through - `btree::for_each_live`, decoding every leaf value to learn its name. + `btree::for_each_live`, decoding every leaf value to learn its name: ROOT + in the kernel, and DATA in the file server. -So a `readdir` of one directory is O(mount) on `/tmp` and `/home`, and it -decodes a leaf value per entry on `/home`. Only the *allocation* is bounded by -what the directory holds; the work is not. Nothing measures it, and no test -would notice the difference. +So a `readdir` of one directory is O(volume) on `/tmp`, ROOT and `/home`, and it +decodes a leaf value per entry on the two bcachefs volumes. Only the +*allocation* is bounded by what the directory holds; the work is not. Nothing +measures it, and no test would notice the difference. -**This is a bound that was there and is not any more.** The old -`list(16_384)` stopped at the 16,384th entry it materialised and refused, so a -mount with a million names cost 16,384 entries of work and then an error. The -new one visits every name in the mount and filters, so that same mount costs a -million — the refusal now counts only what the listed directory holds, and -nothing counts the walk. It runs under the VFS lock a `SYS_READDIR` holds, -which every other filesystem syscall on the machine waits behind. +**This is a bound that was there and is not any more.** The old `list(16_384)` +stopped at the 16,384th entry it materialised and refused, so a mount with a +million names cost 16,384 entries of work and then an error. The new one visits +every name in the mount and filters, so that same mount costs a million — the +refusal now counts only what the listed directory holds, and nothing counts the +walk. On a kernel mount it runs under the VFS lock a `SYS_READDIR` holds, which +every other kernel filesystem syscall waits behind; on DATA it holds the file +server. The end of it is a real directory index — a store keyed by (directory, name) rather than a flat namespace of paths that happen to contain `/`. On bcachefs that is an on-disk format change, so it is not a small one. -**`SYS_CHDIR` and `SYS_SPAWN` pay the same worst case on bcachefs.** Both judge -their directory with `FileSystem::is_dir` under the VFS lock, and +**`SYS_CHDIR` and `SYS_SPAWN` pay the same worst case on ROOT.** Both judge +a directory on a kernel mount with `FileSystem::is_dir` under the VFS lock +(`Vfs::is_mounted_dir`; a served directory is the file server's to answer), and `Mounted::is_dir` (`lib/bcachefs/src/fs.rs`) is a `for_each_live` walk that stops at the first name beneath the directory — so a directory that is there costs the walk up to its first entry, and one that is not costs every node on the mount. Every spawn states a cwd, so this is on the path of every process start -whose cwd is under `/system`, `/apps` or `/home`. Nothing is kept, and a node the walk +whose cwd is on ROOT. Nothing is kept, and a node the walk cannot read refuses that one call. The same (directory, name) index ends it. diff --git a/issues/a-long-name-entry-whose-write-was-refused-is-on-the-volume-and-the-create-erases-nothing.md b/issues/a-long-name-entry-whose-write-was-refused-is-on-the-volume-and-the-create-erases-nothing.md deleted file mode 100644 index d4c3682baa..0000000000 --- a/issues/a-long-name-entry-whose-write-was-refused-is-on-the-volume-and-the-create-erases-nothing.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-21 ---- - -# A long-name entry whose write was refused is on the volume, and the create that wrote it erases nothing - -Five T14 boots under `usb-transport-break` left the log partition failing -`toyos-fat32-check` the same way: `/: entry 3 is a long-name entry carrying -checksum 0xAC of a short name whose checksum is 0xB0` and `entry 3 is long-name -ordinal 2 where the run requires 1`. Runs 65, 72 and 73 are the old USB driver, -run 74 and the volume saved before run 76's flash (run 75's) are PR #466's. - -## What the volumes hold - -Sector 1080 of each saved partition is the root directory's first (8 reserved -sectors and two 536-sector FATs, one sector per cluster). Bytes 0x60 to 0x7F of -it, as `xxd` prints them from `t14-run76/sda3-before-flash.img`: - -``` -00000060: 4233 0035 0033 0039 002e 000f 00ac 6c00 B3.5.3.9......l. -00000070: 6f00 6700 0000 ffff ffff 0000 ffff ffff o.g............. -``` - -Ordinal `0x42`: the last long-name entry of a two-entry run, checksum `0xAC`, -for a name ending `3539.log`. The ordinal-1 entry and the short entry that -belong after it are not there: on run 75's volume the next 32 bytes are zero, -and on the other four they hold the `ATTEMPTS` entry the loader's next pass -wrote into the first free slot it found. - -## How it gets there - -`toyos_fat32::dir::insert_entry` writes a long-name run one 32-byte entry at a -time, each through `write_entry_at`, and so each as its own block write. The -first WRITE(10) a boot issues is `logd` creating its file, which is the write -the arm abandons: `usb-storage: write of 1 blocks at 11399 failed on disk 0`, -and 11399 host blocks of 4096 B is sector 91192, the log partition's 90112 plus -1080. The driver answered that write with an error and the stick had taken it. -A write a transport broke in has an outcome nobody knows; this one reached the -flash in five boots of five. - -`insert_entry` then calls `erase_inserted(dir_start, start, written)` with -`written == 0`, because `written` counts the writes that returned `Ok`. It -erases nothing, and the entry whose write failed is the one entry that may be on -the medium. - -## What is not known - -Whether the entry reached the flash from the abandoned command or from the one -the driver re-issued after its recovery; both carried the same bytes. No record -says, and it does not change the defect: either way the volume holds an entry -the filesystem believes it never wrote. - -## Exit condition - -A create whose first long-name write is refused by the device, with the device -still answering afterwards, leaves a directory `toyos-fat32-check` passes: the -erase covers the entry whose write failed as well as the ones before it. Judged -under QEMU by a block device that commits one write and answers it with an -error, and on the T14 by the fat check of a `usb-transport-break` boot. diff --git a/issues/a-megabyte-written-to-the-stick-starves-a-tone-beside-it.md b/issues/a-megabyte-written-to-the-stick-starves-a-tone-beside-it.md index ae29d3d7c0..3c0250a8f7 100644 --- a/issues/a-megabyte-written-to-the-stick-starves-a-tone-beside-it.md +++ b/issues/a-megabyte-written-to-the-stick-starves-a-tone-beside-it.md @@ -7,21 +7,21 @@ opened: 2026-09-26 # A megabyte written to the boot stick starves a tone playing beside it `audio_tone_load` at eight CPUs drops out when a program writes a megabyte to -`/log` while the tone plays. Seen on `wt/toyos-logtrack`, whose `logd` then -created each part at its whole length (one mebibyte of zeros, written back -while the test's tone played): four runs, 181, 183, 185 and 189 underruns of -32 allowed, at host load averages from 19 to 35; the same tree with the part -created empty, 0 of 32 at load 25. The failing windows are the client -starving — soundd's `deferred` in the thousands and `starve_max` a whole -window — beside 15 564 xHCI interrupts on cpu0, the CPU that also takes the -sound device's, and a `usb-storage … transport broke on SCSI 0x2a: no answer -in the status phase in 2000 ms` in the middle of the tone. +`/log` while the tone plays. Seen on `wt/toyos-logtrack`, whose `logd` (now +`logkeeper`) then created each part at its whole length (one mebibyte of zeros, +written back while the test's tone played): four runs, 181, 183, 185 and 189 +underruns of 32 allowed, at host load averages from 19 to 35; the same tree with +the part created empty, 0 of 32 at load 25. The failing windows are the client +starving — soundd's (now soundserver's) `deferred` in the thousands and +`starve_max` a whole window — beside 15 564 xHCI interrupts on cpu0, the CPU +that also takes the sound device's, and a `usb-storage … transport broke on SCSI +0x2a: no answer in the status phase in 2000 ms` in the middle of the tone. Not investigated: which of the interrupt load on cpu0, the two-second transport break, or the write-back holding what the client waits on is what starves it; and whether real hardware shows it, where the stick is not an emulated one. -Nothing in `logd` is special about the write: any program writing that much to -the stick while a tone plays is the same stimulus. +Nothing in the log writer is special about the write: any program writing that +much to the stick while a tone plays is the same stimulus. Since the small-kernel track's step 10 the stick is usbd's: the interrupts are its claim's and the waits its thread's, and the kernel's two-second transport @@ -30,5 +30,5 @@ break is gone. Whether the starvation remains is unmeasured. ## Exit condition On the T14, a `METAL` row plays `hda_tone`'s tone while a program writes a -mebibyte to `/log`, and soundd reports `underruns=0` over the tone's window +mebibyte to `/log`, and soundserver reports `underruns=0` over the tone's window across repeated boots. Owner: the metal suite (`tests/toyos.rs`'s `METAL`). diff --git a/issues/a-metal-job-that-fails-on-the-amd-bench-is-reported-as-failing-on-the-t14.md b/issues/a-metal-job-that-fails-on-the-amd-bench-is-reported-as-failing-on-the-t14.md index 405f8afc68..6f9bf2a613 100644 --- a/issues/a-metal-job-that-fails-on-the-amd-bench-is-reported-as-failing-on-the-t14.md +++ b/issues/a-metal-job-that-fails-on-the-amd-bench-is-reported-as-failing-on-the-t14.md @@ -6,7 +6,7 @@ opened: 2026-10-11 # A metal job that fails on the AMD bench is reported as failing on the T14 -`Boot::job_passed` (`tests/common/metal.rs:490-498`) words every non-zero +`Boot::job_passed` (`tests/common/metal.rs`) words every non-zero exit as `"{binary} exited {code} on the T14"`, whichever bench ran the boot. A `--metal-host` run on the AMD bench printed `FAIL test_rs_vector_state: test_rs_vector_state exited 101 on the T14` diff --git a/issues/a-moved-handle-is-always-re-movable.md b/issues/a-moved-handle-is-always-re-movable.md index 4a24361ff7..2ea8cdb7ea 100644 --- a/issues/a-moved-handle-is-always-re-movable.md +++ b/issues/a-moved-handle-is-always-re-movable.md @@ -12,8 +12,9 @@ into the destination unchanged. So every handle that arrives by a move arrives carrying `TRANSFER`, and its new holder can move it on. There is no way to express "you may map this and nobody else may". -soundd's per-client audio ring would ideally travel as a `shm_h_with_MAP_only`, -so that a client cannot re-send it on. The rights model rules that out: sending +soundserver's per-client audio ring (`system/soundserver/src/client.rs`) would +ideally travel as a `shm_h_with_MAP_only`, so that a client cannot re-send it +on. The rights model rules that out: sending needs `TRANSFER`, so that handle cannot be sent. The same hole is in spawn endowment. @@ -23,7 +24,7 @@ The pid ACL this replaced was explicitly non-transitive — `shared_memory::gran was owner-only, and its doc said why: *"a grantee that could re-grant makes the owner's ACL transitive, so soundd's per-client audio ring reaches anyone that client names and the owner is never told."* Under handles that is no longer -true. A client can pass soundd's ring to anybody it can reach. +true. A client can pass soundserver's ring to anybody it can reach. The practical exposure today is small — a client can memcpy the ring's contents to a peer anyway, so what leaks is the ability to *write* it — but the property @@ -68,7 +69,7 @@ merely the cheaper fix, it is the only one on the table. finding recording an unproven-but-plausible boundary weakness is promoted, not folded). The rights model cannot spell "you may map this and nobody else may", so the property the capability design claims is not the property the code has — -soundd's per-client ring is re-sendable by its client. Owed by whoever next +soundserver's per-client ring is re-sendable by its client. Owed by whoever next opens the handle ABI: a rights word on `EndowEntry` **and** on `SYS_MESSAGE_SEND`, on one ABI-only PR, since doing either alone makes the two move verbs disagree. diff --git a/issues/a-netcase-boot-under-host-load-said-nothing-past-the-firmwares-screen-clears.md b/issues/a-netcase-boot-under-host-load-said-nothing-past-the-firmwares-screen-clears.md index 36922f6f91..66e1394782 100644 --- a/issues/a-netcase-boot-under-host-load-said-nothing-past-the-firmwares-screen-clears.md +++ b/issues/a-netcase-boot-under-host-load-said-nothing-past-the-firmwares-screen-clears.md @@ -33,6 +33,9 @@ netcase image among them. Load averages 69.67, 65.99 and 62.40 as the suite began and 44.91, 57.07 and 59.57 as it ended, with other worktrees' work beside it; the 16550 file was not kept. +The tests named here were cut: `netstack_socket_churn` in `c6a2861e8`, +`https_fetch`, `libc_sockets` and `netstack_streams` in `ff1a11dbd`. + Not reduced: whether the firmware is stuck or starved, which a QMP read of a silent guest's registers would say (`tests/CLAUDE.md`'s method for a death during boot), and at what rate. diff --git a/issues/a-page-in-its-backing-refused-is-reported-as-a-segfault.md b/issues/a-page-in-its-backing-refused-is-reported-as-a-segfault.md index 5638542408..8b007e801b 100644 --- a/issues/a-page-in-its-backing-refused-is-reported-as-a-segfault.md +++ b/issues/a-page-in-its-backing-refused-is-reported-as-a-segfault.md @@ -38,7 +38,7 @@ The demand-fault path in `kernel/src/process.rs` and the exception report in ## Exit condition A fault left unhandled because the backing refused the read ends the process -with a report and an exit that name the device failure and the file, not a -protection or mapping fault, and a guest test that fails a DATA-backed -binary's page-in (the `blkdebug` pattern `root_chunk_refused` uses) asserts +with a report and an exit that name the backing's refusal and the file, not a +protection or mapping fault, and a guest test that maps a `/tmp` file and +has it deleted under the mapping, the one refusal still reachable, asserts that report and that it contains no `SEGFAULT` line. diff --git a/issues/a-panic-inside-mm-init-may-not-reach-the-panel.md b/issues/a-panic-inside-mm-init-may-not-reach-the-panel.md index ce8a376e52..e7bebc4d92 100644 --- a/issues/a-panic-inside-mm-init-may-not-reach-the-panel.md +++ b/issues/a-panic-inside-mm-init-may-not-reach-the-panel.md @@ -6,14 +6,18 @@ opened: 2026-09-28 # A panic inside `mm::init` may not reach the panel -From `paging::init`'s CR3 load to `drivers::panic_console::remap` -(`kernel/src/main.rs`), the panel is written through the kernel's own direct -map, which reaches `toyos_bootmap::x86_64::direct_map_end`: the boot map's -4 GiB, or the end of the highest memory range. A scanout past that end has no -mapping until `remap` maps it, so a panic in that span — `alloc::init`, and +On x86-64, from `paging::init`'s CR3 load (`mm::init`, `kernel/src/mm/mod.rs`) +to `drivers::panic_console::remap`, the panel is written through the kernel's +own direct map, which reaches `toyos_bootmap::x86_64::direct_map_end`: the boot +map's 4 GiB, or the end of the highest memory range. A scanout past that end has +no mapping until `remap` maps it, so a panic in that span — `alloc::init`, and `paging::seal_kernel_half`'s up to 255 table allocations — faults on the first -pixel instead of painting. The extent before it reached every descriptor in -the map, so a scanout the map describes was covered. +pixel instead of painting. The extent before it reached every descriptor in the +map, so a scanout the map describes was covered. + +AArch64's `paging::init` maps the scanout it is handed before its tables go +live (`kernel/src/arch/aarch64/paging.rs`); x86-64's takes the same argument +and ignores it. Nothing has been observed to reach it: every firmware this tree has booted puts its scanout below 4 GiB. diff --git a/issues/a-panic-on-a-wedged-virtio-console-panics-again.md b/issues/a-panic-on-a-wedged-virtio-console-panics-again.md index 4b3a997229..abfab42dd5 100644 --- a/issues/a-panic-on-a-wedged-virtio-console-panics-again.md +++ b/issues/a-panic-on-a-wedged-virtio-console-panics-again.md @@ -7,27 +7,27 @@ opened: 2026-07-30 # A panic while the virtio-console TX queue is wedged *and* unlocked panics again, and the report never reaches the 16550 **The wait is no longer unbounded, and that is the whole of what changed.** -`Virtqueue::submit_and_wait` (`kernel/src/drivers/virtio.rs:611`) carries -`ANSWERS`, a `Tripwire::absurd(5 s, "far above any completion a live device -delivers")` (`:621`), and on expiry it panics by name (`:642`). A `Tripwire` is -the kind whose expiry *is* a panic (`kernel/src/time.rs:192`), so on the panic -path the bound converts a hang into a second panic — which is not the answer, -only a different wrong one. The heading this file carried until now said +`Virtqueue::submit_and_wait` (`kernel/src/drivers/virtio.rs`) waits through +`wait_used`, bounded by `ANSWERS`, a `Tripwire::absurd(5 s, "far above any +completion a live device delivers")`, and on expiry it panics by name. A +`Tripwire` is the kind whose expiry *is* a panic (`kernel/src/time.rs`), so on +the panic path the bound converts a hang into a second panic — which is not the +answer, only a different wrong one. The heading this file carried until now said "spins"; the tree refutes it. The unlocked case is the one that reaches it. `panic_flush` -(`kernel/src/drivers/serial.rs:219`) spins for a clean handoff, and the -virtio-console `disable()` that would fall the report back to the 16550 sits at -`:237` — on the *other* branch, the one taken only when a holder never -releases. With the queue wedged but the guard free, the first `try_lock` -succeeds and `drain_locked` runs at `:227`; that drain is deliberately +(`kernel/src/drivers/serial.rs`) takes the registers through +`panic_registers`, and the virtio-console `disable()` that would fall the +report back to the 16550 is on the *other* branch, the one taken only when a +holder never releases. With the queue wedged but the guard free, the hold is taken +(`Hold::Held`) and `drain_locked` runs; that drain is deliberately unbounded in records ("the report should be whole", -`kernel/src/log/console.rs:111-113`), and each record goes out through -`BackendGuard::write_raw`'s `Backend::Virtio` arm (`serial.rs:147`) into -`virtio_console::write_bytes_locked` (`kernel/src/drivers/virtio_console.rs:95`) -and `submit_and_wait` (`:105`). Five seconds later `:642` panics inside the -panic handler, which re-enters `kernel/src/main.rs:114`, takes the reentry -guard at `:121`, and halts on `PANIC REENTRY: CPU halted` (`:124`). +`kernel/src/log/console.rs`), and each record goes out through +`BackendGuard::write_raw`'s `Backend::Virtio` arm into +`virtio_console::write_bytes_locked` (`kernel/src/drivers/virtio_console.rs`) +and `submit_and_wait`. Five seconds later `wait_used` panics inside the +panic handler, which re-enters `kernel/src/main.rs`'s panic handler, takes the +reentry guard, and halts on `PANIC REENTRY: CPU halted`. So the machine still halts — the terminal action was never in question — but it halts five seconds late, under a message that names the console rather than the @@ -37,19 +37,18 @@ never delivered to the two channels that cannot wedge. ## The ruling, 2026-09-03: a panic never waits on a device The panic path's wait is a `Budget`, not a `Tripwire` — the kind whose expiry is -a degraded answer and never a panic (`kernel/src/time.rs:222`); both kinds are +a degraded answer and never a panic (`kernel/src/time.rs`); both kinds are already in that file and nothing new is needed there. On expiry the console output is **dropped**: `virtio_console::disable()`, then the panic proceeds to the channels that cannot wedge — the 16550 (`serial::panic_raw`) and the panel (`panic_console::render`) — which is exactly what `panic_flush`'s wedged-holder -branch at `serial.rs:234-240` already does for the other case. Terminal action +branch already does for the other case. Terminal action is unchanged: the machine still halts on the panic; it just no longer depends on virtio answering to get there. The 5 s `Tripwire` stays what it is for every non-panic caller of `submit_and_wait`; the panic path is the caller that may not pay it. -**Not built here.** `kernel/src/drivers/virtio.rs` is inside the IOMMU pull -request's collision fence, so this file stays open and records what a fix owes. +**Not built.** This file records what a fix owes. ## The two checks a fix owes diff --git a/issues/a-process-name-is-written-into-the-kernels-own-records-unsanitised.md b/issues/a-process-name-is-written-into-the-kernels-own-records-unsanitised.md index 3311e74cab..9a53ad2ef9 100644 --- a/issues/a-process-name-is-written-into-the-kernels-own-records-unsanitised.md +++ b/issues/a-process-name-is-written-into-the-kernels-own-records-unsanitised.md @@ -9,18 +9,19 @@ opened: 2026-09-23 `make_name` (`kernel/src/loader/start.rs`) keeps a spawned file's name as the process-table name, and the kernel writes that name into records a judge parses by field: `exit: {name} pid=N code=N cpu=Nms` (`kernel/src/process.rs`) is the -verdict channel `bootlog::EXIT` and `devices::exit_of` read. A program +verdict channel `bootlog::EXIT` and the metal judge's `Boot::exit_code` +(`tests/common/metal.rs`) read. A program that can write and spawn a file whose name holds a space and the record's own words — `test_rs_job pid=1 code=0`, inside `THREAD_NAME_LEN` — gets a kernel record that opens as the job's own: `exit: test_rs_job pid=1 code=0 pid=7 -code=3 cpu=0ms`. `exit_of(log, "test_rs_job")` matches its head, takes the -first `code=` after it, and the forger's record is the last one. Not run: the -reading of the two functions. +code=3 cpu=0ms`. `exit_code("test_rs_job")` matches its head, takes the +first `pid=` and `code=` after it, and keeps the record with the lowest pid, +which is the one the forger named. Not run: the reading of the two functions. -A program's own lines are not this: they reach `/log` under the head `logd` -writes for them, whose name is init's and refused outside `[A-Za-z0-9._+-]` -(`toyos_logstream::Tag`). This is the kernel's record quoting a name it does -not clean. +A program's own lines are not this: they reach `/log` under the head `logkeeper` +writes for them, whose name is the supervisor's and refused outside +`[A-Za-z0-9._+-]` (`toyos_logstream::Tag`). This is the kernel's record quoting +a name it does not clean. ## Exit condition diff --git a/issues/a-process-that-starts-four-billion-threads-panics-the-kernel.md b/issues/a-process-that-starts-four-billion-threads-panics-the-kernel.md index 7a0faeaf2b..fcb0f7484a 100644 --- a/issues/a-process-that-starts-four-billion-threads-panics-the-kernel.md +++ b/issues/a-process-that-starts-four-billion-threads-panics-the-kernel.md @@ -6,12 +6,12 @@ opened: 2026-10-01 # A process that starts four billion threads panics the kernel -`process::spawn_thread` takes a thread's id from its process's -`IdMap` (`kernel/src/id_map.rs`), whose `insert` steps its -counter with `Tid + Tid` (`sdk/abi/src/lib.rs`). The kernel builds with -`overflow-checks = true` (`kernel/Cargo.toml`), so the 2^32nd thread a single -process starts panics the kernel, and it does so under `PROCESS_TABLE`, which -hangs the machine instead of reporting. The thread before it is issued +`process::spawn_thread` takes a thread's id from its process's `IdMap` (`kernel/src/id_map.rs`), whose `insert` steps its counter with +`Tid + Tid` (`sdk/abi/src/lib.rs`). The kernel builds with `overflow-checks = +true` (`[profile.toyos]` in the root `Cargo.toml`), so the 2^32nd thread a +single process starts panics the kernel, and it does so under `PROCESS_TABLE`, +which hangs the machine instead of reporting. The thread before it is issued `Tid(u32::MAX)`, which is the per-CPU word for no thread (`arch::x86_64::percpu::current_tid`), so `handle_page_fault` refuses every fault that thread takes. diff --git a/issues/a-programs-two-streams-map-its-one-ring-twice.md b/issues/a-programs-two-streams-map-its-one-ring-twice.md index 45fce2da1b..6298e14b1c 100644 --- a/issues/a-programs-two-streams-map-its-one-ring-twice.md +++ b/issues/a-programs-two-streams-map-its-one-ring-twice.md @@ -6,19 +6,18 @@ opened: 2026-09-26 # A program's two streams map its one ring twice -Found in the review of PR #527. init puts one log ring in a program's slots 1 -and 2, and `sdk/toyos/src/log/stdio.rs`'s `ask` takes each stream on its own: it -duplicates the slot's handle and maps the region, once for stdout and once for -stderr. So every program holds two handles and two 2 MiB mappings of the same -ring, within one instance of the SDK — before the second instance -`issues/a-rust-program-holds-two-copies-of-its-stream-state.md` -counts. +Found in the review of PR #527. The supervisor puts one log ring in a program's +slots 1 and 2, and `sdk/toyos/src/log/stdio.rs`'s `ask` takes each stream on its +own: it duplicates the slot's handle and maps the region, once for stdout and +once for stderr. So every program holds two handles and two 2 MiB mappings of +the same ring, within one instance of the SDK — before the second instance +`issues/a-rust-program-holds-two-copies-of-its-stream-state.md` counts. Nothing a program can ask today tells it that two slots name one object, which is why `ask` does not share the mapping: assuming it would be wrong for a child whose parent put two different rings there. -**Exit condition**: one mapping per ring a process holds — the kernel -answering whether two handles name one object, or init handing the ring once -and the SDK binding both streams to it — shown by a guest test that counts the +**Exit condition**: one mapping per ring a process holds — the kernel answering +whether two handles name one object, or the supervisor handing the ring once and +the SDK binding both streams to it — shown by a guest test that counts the process's mappings of its ring. diff --git a/issues/a-provided-name-cannot-reach-an-undeclared-child.md b/issues/a-provided-name-cannot-reach-an-undeclared-child.md index 628e7be400..a485e4b47b 100644 --- a/issues/a-provided-name-cannot-reach-an-undeclared-child.md +++ b/issues/a-provided-name-cannot-reach-an-undeclared-child.md @@ -7,9 +7,10 @@ opened: 2026-08-10 # A transferred connector cannot be merged into an inherited namespace `Command::provide(name, connector)` says "give the child this connector under -this name, on top of whatever else it holds". The launcher can do that: init -builds the child's namespace itself, from the manifest row plus the extras the -caller transferred (`userland/init/src/main.rs`, `build_namespace`). +this name, on top of whatever else it holds". The launcher can do that: the +supervisor builds the child's namespace itself, from the manifest row plus the +extras the caller transferred (`system/supervisor/src/main.rs`, +`build_namespace`). The **direct** path still cannot, and the reason is no longer the ABI. @@ -20,36 +21,33 @@ base's whole entry set when the bit is set and refuses a bit it does not define (`kernel/src/syscall/ipc.rs`), and `endowment_denied`'s `the_base_plus_one_more_name` asserts both halves in a guest. -**What is left is std's ToyOS backend, and only the std lane can do it.** -`sdk/std/sys/process.rs`'s `spawn` endows the parent's -namespace handle unchanged — `inherited_namespace` duplicates it and pushes it -under `SVC_LABEL` — so a caller that transferred a connector to a program the +**What is left is the SDK's direct spawn.** std's `Command` spawn +(`sdk/std/sys/process.rs`) goes through `toyos::spawn::Spawn`, whose `direct` +(`sdk/toyos/src/spawn.rs`) endows the parent's namespace handle unchanged — it +duplicates it and pushes it under `SVC_LABEL` — and carries none of +`provided`, so a caller that transferred a connector to a program the manifest does **not** declare, the one case where the launcher answers `MSG_NOT_DECLARED` and the SDK falls back to the direct path, still hands the child a name it cannot resolve. Neither side is told. It is unreached today only by coincidence: every extras-carrying caller in the tree (the terminal and the console, both transferring `surface` to -`/system/bin/shell`) happens to go through the launcher, and the shell's own children -inherit `surface` because init merged it in. That is the kind of safety that -ends the moment somebody adds a caller. - -**And one caller is already past it, masked rather than absent.** -`build_command` (`system/shell/src/main.rs:709`) calls `Command::provide` for -*every* child the shell starts, and `test_rs_window_child` is a harness binary -no `[programs]` row declares — so that spawn takes the direct path and the -provided `surface` connector is dropped, exactly as described above. Nothing -reds: the inherited namespace already carries `surface`, so the child resolves -the name by the other route and the loss is invisible. The coincidence is not -that no caller reaches this; it is that the one which does asks for a name its -inheritance already answers. +`/system/bin/shell`) happens to go through the launcher, and the shell's own +children inherit `surface` because the supervisor merged it in. That is the kind +of safety that ends the moment somebody adds a caller. + +**And every shell child is past it, masked rather than absent.** +`build_command` (`system/shell/src/main.rs`) calls `Command::provide` for +*every* child the shell starts, so a child no `[programs]` row declares takes +the direct path and its provided `surface` connector is dropped. Nothing reds: +the inherited namespace already carries `surface`, so the child resolves the +name by the other route and the loss is invisible. ## What closing it takes -`spawn`'s direct path building `keep_all(parent) + add(extras)` and endowing -the result as `svc`, in place of the bare duplicate. That is one function in the -rust submodule, which needs an exclusive machine window, so it is owed to the -std lane and to nothing else. +`Spawn::direct` building `keep_all(parent) + add(extras)` and endowing the +result as `svc`, in place of the bare duplicate: one function in +`sdk/toyos/src/spawn.rs`. The instrument is a guest arm: a binary the manifest does not declare, spawned with a `provide`d connector by a caller that holds a `launcher` connector, must diff --git a/issues/a-reallocated-extent-on-data-keeps-the-deleted-files-bytes.md b/issues/a-reallocated-extent-on-data-keeps-the-deleted-files-bytes.md index 13d6b931ed..50f745a97f 100644 --- a/issues/a-reallocated-extent-on-data-keeps-the-deleted-files-bytes.md +++ b/issues/a-reallocated-extent-on-data-keeps-the-deleted-files-bytes.md @@ -50,7 +50,9 @@ capture either way. ## Exit condition A test deletes a file, writes another that reuses its blocks, and finds the new -file's bytes on the device. `pkg_install_gbae` runs its symlink and removal -probes *after* the install it reads back, so that judge is about packages and -not about this; the ordering is stated at that site and comes out when this -closes. +file's bytes on the device. + +`pkg_install_gbae`, whose boot measured this, was cut in `520c0d129`. DATA was +then the kernel's; it is the file server's now (`system/fileserver/src/data.rs`), +and its commits have been shadow-paged since `3adcf424e`. Nothing has looked +again, so whether either change removed the cause is not known. diff --git a/issues/a-record-cannot-name-thread-zero.md b/issues/a-record-cannot-name-thread-zero.md index ad4a7012bb..7961c841d3 100644 --- a/issues/a-record-cannot-name-thread-zero.md +++ b/issues/a-record-cannot-name-thread-zero.md @@ -23,7 +23,7 @@ every `tid=` in the T14 boot logs then committed: **738 `tid=0` against 49 `tid=1`** — the value the formatter drops is the one almost every line carries. The kernel's own sentinel is a third value again: `PerCpu::current_tid` is -`u32::MAX` when no thread is running (`kernel/src/arch/x86_64/percpu.rs:85`), which the +`u32::MAX` when no thread is running (`kernel/src/arch/x86_64/percpu.rs`), which the formatter would render as `tid=4294967295` on every line a kernel thread logs. ## What is in the tree now @@ -38,23 +38,23 @@ render identically, where the byte ring's prefix distinguished them (`[kernel the panel alone.** The byte ring is gone (`kernel/src/log/mod.rs`: "the byte ring is gone, and the line the console carries is rendered from the record by `log::console`, through the one formatter in `toyos-abi`"), and -`console::write_line` is public precisely so that `/log`'s sink renders the -same line — `logd` puts a wall-clock prefix in front of the same `Display` and -changes nothing else. Serial, panel and file are one rendering, and it is the -one that drops `tid=0`. +`console::write_line` is public precisely so that `/log`'s sink renders the same +line — `logd` (now `logkeeper`) puts a wall-clock prefix in front of the same +`Display` and changes nothing else. Serial, panel and file are one rendering, +and it is the one that drops `tid=0`. ## Why it was not fixed there -`sdk/abi/src/log.rs` is a sysroot source (`build/toolchain.rs`'s +`sdk/abi/src/log.rs` is a sysroot source (`build/sysroot.rs`'s `SYSROOT_SOURCES`), so an ABI change lands on its own pull request and the kernel-side commit could not carry one. ## The options -1. **A `flags` bit.** `LogRecord::flags` has one bit used (`FLAG_EARLY`); a +1. **A `flags` bit.** `LogRecord::flags` has one bit used (`FLAG_UNTIMED`); a `FLAG_NO_THREAD` says "this record has no thread" out loud and leaves `tid` meaning only what it says. Costs nothing on the wire and matches how the - early-boot label is already carried. + untimed label is already carried. 2. **Render `pid`/`tid` together.** The record already holds `pid` and no consumer prints it, which is its own smell: a per-process tid is only an identity beside the pid it belongs to. `[0.123 cpu0 3/0]` names a thread; diff --git a/issues/a-refused-framebuffer-reads-as-no-display.md b/issues/a-refused-framebuffer-reads-as-no-display.md index 80a5838a0b..0b1394b601 100644 --- a/issues/a-refused-framebuffer-reads-as-no-display.md +++ b/issues/a-refused-framebuffer-reads-as-no-display.md @@ -6,10 +6,10 @@ opened: 2026-09-03 # The kernel cannot tell "no display" from "a display that published no framebuffer" -Both arrive in `KernelArgs` as zeros, so `kernel/src/main.rs:284-288` records the -same line for a machine with no display device at all and for one whose firmware -published a Graphics Output Protocol that has no linear framebuffer to hand over. -Three shapes, one image, the guest's own 16550: +Both arrive in `KernelArgs` as zeros, so `kernel/src/main.rs`'s `boot: gop` line +records the same line for a machine with no display device at all and for one +whose firmware published a Graphics Output Protocol that has no linear +framebuffer to hand over. Three shapes, one image, the guest's own 16550: ``` == no-display (-vga none) diff --git a/issues/a-relative-program-is-refused-a-launch.md b/issues/a-relative-program-is-refused-a-launch.md index bd84d0b6e7..6029b374d3 100644 --- a/issues/a-relative-program-is-refused-a-launch.md +++ b/issues/a-relative-program-is-refused-a-launch.md @@ -11,11 +11,13 @@ it, and the supervisor refuses any path that is not canonical and absolute (`lib/manifest/src/package.rs`'s `is_canonical`), answering `MSG_REFUSED`. So under a parent that holds a launcher, `Command::new("./tool")` and `posix_spawn` of `"./tool"` fail, where the same path spawned directly would -start. The shell works around it for what its user types -(`package::launch_path`); std and libc do not. Read from the code, not +start. The shell makes what its user types absolute itself +(`system/shell/src/main.rs`, with `package::launch_path`, which sits beside +the gate's `is_canonical` in `lib/manifest/src/package.rs`); std and libc do +not. Read from the code, not reproduced: no guest test spawns a relative path under a launcher. *Exit*: a relative program spawned under a parent that holds a launcher starts, by std and by libc, made absolute against the caller's working -directory by one rule the supervisor's gate shares, and the shell's copy of -that rule goes. +directory by one rule beside the supervisor's gate (`is_canonical`), and the +shell's own resolution goes. diff --git a/issues/a-replacement-netstack-reuses-its-predecessors-ports.md b/issues/a-replacement-netstack-reuses-its-predecessors-ports.md deleted file mode 100644 index f3d1f763e1..0000000000 --- a/issues/a-replacement-netstack-reuses-its-predecessors-ports.md +++ /dev/null @@ -1,29 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-24 ---- - -# A replacement netd opens its first connection on its predecessor's port - -netd's ephemeral ports start at 49152 in every process (`NetDaemon::new`, -`next_local_port`). A swapped netd is a second process on the same address, and -the connections the first one held end with it: no FIN, no reset, and the peer -still has them established. So the new netd's first connect, which is `logd` -asking for its record stream again, goes out on the same address and port pair -the peer's stale connection has. - -Measured on `wt/toyos-swap` (QEMU, e1000e, slirp): `logd`'s reopening -connect answered `Ok(49152)` 714 ms after it was asked. The SYN hits slirp's -still-established socket, and only a reset from the new stack clears it before -a retransmitted SYN gets through. A peer that treats a SYN on an established -connection differently decides whether and when the stream comes back, not -netd. - -Before `metaltalk::Stream` ended an old connection when a new one arrived, the -collision was also what let `lan_swap` pass. A reopened stream on a fresh port -(forced by starting the second netd's ports elsewhere) left the host reading the -dead connection for ever: 3 of 3 red with the CI failure's exact words. - -RFC 6056's randomised ephemeral port selection is the usual remedy. The -alternative is a restarted stack that keeps quiet before it opens anything. diff --git a/issues/a-rust-program-holds-two-copies-of-its-stream-state.md b/issues/a-rust-program-holds-two-copies-of-its-stream-state.md index ad59b057b4..2b8d45aa4e 100644 --- a/issues/a-rust-program-holds-two-copies-of-its-stream-state.md +++ b/issues/a-rust-program-holds-two-copies-of-its-stream-state.md @@ -19,9 +19,7 @@ What that costs, today: through `std` as well: every daemon. - A partial line is joined only within one instance. `print!("a")` followed by `toyos::log::stdio::write(Stream::Out, b"b\n")` is two records on two lines, - and a process's exit ends only what `std`'s instance holds — - `tests/guest/rust-tests/src/bin/console_line_atomicity.rs` writes its - unended line through `std` for exactly that reason. + and a process's exit ends only what `std`'s instance holds. Both records reach the one ring, so nothing is lost and no line is spliced; the weakness is the doubled state and the unjoined piece. diff --git a/issues/mutual-kill-panicked-on-the-t14-with-stdio-slot-1-not-a-log-ring.md b/issues/mutual-kill-panicked-on-the-t14-with-stdio-slot-1-not-a-log-ring.md index 55dcbb0b04..d99f65cb1a 100644 --- a/issues/mutual-kill-panicked-on-the-t14-with-stdio-slot-1-not-a-log-ring.md +++ b/issues/mutual-kill-panicked-on-the-t14-with-stdio-slot-1-not-a-log-ring.md @@ -20,9 +20,10 @@ before the panic: [2026-09-29 18:49:34 10.010 cpu4] exit: test_rs_mutual_kill pid=155 code=0 cpu=10ms [2026-09-29 18:49:34 10.010 cpu5] exit: test_rs_mutual_kill pid=156 code=137 cpu=10ms -Then pid 28 itself, under the runner's name: +Then pid 28 itself, under the runner's name (the checkout's parent directory +masked as ``): - {2026-09-29 18:49:34 10.011 error pid=28 test-runner} thread 'main' (1) panicked at /Users/jan/Dev/jan/toyos-phdr/toyos/src/log/stdio.rs:209:33: + {2026-09-29 18:49:34 10.011 error pid=28 test-runner} thread 'main' (1) panicked at /toyos-phdr/toyos/src/log/stdio.rs:209:33: {2026-09-29 18:49:34 10.011 error pid=28 test-runner} stdio: slot 1 holds a region that is not a log ring ... 11: 0x1000006e6be - toyos[db68fa2b5c494fe7]::log::stdio::target::sink diff --git a/issues/netstack-ends-for-the-boot-on-a-nic-it-cannot-drive-on.md b/issues/netstack-ends-for-the-boot-on-a-nic-it-cannot-drive-on.md index 651a18b1ec..93dbcfc531 100644 --- a/issues/netstack-ends-for-the-boot-on-a-nic-it-cannot-drive-on.md +++ b/issues/netstack-ends-for-the-boot-on-a-nic-it-cannot-drive-on.md @@ -22,9 +22,8 @@ Ending is the right answer to the device: none of the four is something a conforming device writes, and a ring read on past one loses frames silently. What is missing is what comes after the end. -A restart is not one line in `system.toml`. A second netstack on the same -address meets `issues/a-replacement-netstack-reuses-its-predecessors-ports.md`, -every claim it mints spends device addresses +A restart is not one line in `system.toml`. Every claim a second netstack +mints spends device addresses (`issues/a-claim-spends-device-addresses-its-slot-never-gets-back.md`), and section 3 of `issues/the-lan-is-not-yet-production-grade.md` splits the driver from the stack so that a driver's end does not take the stack's connections. diff --git a/issues/the-shipping-kernel-actuator-gate-reads-the-checkouts-own-path.md b/issues/the-shipping-kernel-actuator-gate-reads-the-checkouts-own-path.md index 9b8af5e928..ec19d8bb21 100644 --- a/issues/the-shipping-kernel-actuator-gate-reads-the-checkouts-own-path.md +++ b/issues/the-shipping-kernel-actuator-gate-reads-the-checkouts-own-path.md @@ -11,7 +11,8 @@ carries an actuator by searching the kernel image for each declared name as a byte string. The kernel image embeds absolute source paths, so the search also matches the directory the checkout sits in. -Measured on the worktree `/Users/jan/Dev/jan/toyos-heartbeat`, at `dc38a054`: +Measured on the worktree `/toyos-heartbeat`, at `dc38a054` (`` +masks the directory the worktrees sit in): ``` $ cargo run -- --build-only @@ -20,13 +21,13 @@ the shipping kernel names 1 of the 111 actuators `kernel/src/actuator.rs` declares: ["heartbeat"]. $ strings -a target/kernel-42942dede1d2807a | grep -c heartbeat 48 -$ strings -a target/kernel-42942dede1d2807a | grep -c 'Dev/jan/toyos-heartbeat' +$ strings -a target/kernel-42942dede1d2807a | grep -c '/toyos-heartbeat' 48 ``` All forty-eight hits are the checkout path. No symbol, string or record in the shipping kernel names the actuator; the gate is reading `toyos-heartbeat` out of -`/Users/jan/Dev/jan/toyos-heartbeat/kernel/src/...`. The test kernel built in +`/toyos-heartbeat/kernel/src/...`. The test kernel built in the same tree passes, because its direction of the same assertion wants every name present.