From 0a6275f27596842f868456685d126a6ff6797148 Mon Sep 17 00:00:00 2001 From: japabu Date: Wed, 21 Oct 2026 20:47:57 +0200 Subject: [PATCH] issues: audit batch 2 against a8ea09d20 A one-off accuracy audit of 117 open defect records. Each was read against the code it cites at origin/main db0cf636d16d265ac400ab5f32658ed81f5cd632, with the owner's uncommitted requirements/ and claude-questions.md beside it. After rebasing onto a8ea09d209c5 (#978, #981), each record whose cited code #978 changed was read again. Deleted, fixed: - netstack-watches-both-pipes-of-every-connection-on-every-pass: c91e72b83 gave a pipe end's leaving its own watch (kernel/src/pipe.rs) and made netstack submit only a changed or answered watch (watches.rs `Armed`). The commit named the record and left it. - shell-c-discards-the-cwd-its-launch-carried: `shell -c` runs in its launch's directory since b3770cf6d. - std-answers-every-spawn-refusal-but-notfound-as-other: std's spawn maps each refusal by name, and every SyscallError through to_io_error, since 61a283a40. Deleted, superseded: - kernel_stack_addr-names-an-offset-not-an-address: the field is still `kernel_stack_addr` (sdk/abi/src/boot.rs:9), and #583, its named owner, merged without the rename. The rename is a stated step of the-loader-does-only-what-must-precede-the-handover.md, which carries it. - os-toyos-io-traits-keep-a-posix-name: the traits still exist, and now name a real descriptor. The owner's 2026-10-21 answers supersede the 2026-08-19 "fds belong only in libc jargon" ruling (64f806daf) the record rested on: keep C numbering and dup2 conventions in userland, including Rust/C descriptor interoperability; Rust std's C-style descriptor interoperation uses the same userland adaptation; one descriptor-inheritance rule honored by both libc spawning and Rust Command. #978 implements that, and the kernel handle has its own word, std::os::toyos::io::AsRawHandle. - logkeeper-flushes-the-records-its-own-refused-flush-made: the measured loop was the kernel's retry of a budget-refused fsync, whose four records made logd's next round flush again. That retry and its records went with the durability ledger (80a1f1ceb) and the kernel's USB storage (13ed7b4f4). logkeeper ends its volume on a refused append or flush (system/logkeeper/src/main.rs:625-632) and warns of a slow round once, behind its `degraded` latch (:633-643); the file server logs only an operation that failed (system/fileserver/src/data.rs:136, 461), and a failed flush ends the volume. No current route is known. The rest have code or a test that no longer exists: - log-flush-retry-deadman-arm: the kernel's fsync deadman went with the durability ledger (80a1f1ceb), its kernel USB with usbd (13ed7b4f4), and the test with 9ebf080e8. - metal-device-probe-usbread-answered-iofailed-once-beside-other-guests: one sighting through the kernel's usb-storage, gone with 13ed7b4f4; test deleted 24aa31815. - netstack-refused-accept-hung-waiting-for-a-wake-that-never-came: netd and its listen.rs are gone, the test was deleted (7967fba91), and so is netcase_against_host. - process-stats-exits-101-beside-other-guests: both recorded reds were the test's own premise; the test is deleted (4a5b228d2). - screen-diag-boot-leaves-no-i8042-line-on-the-panel: the kernel's i8042 and its health line left for ps2server (febfd21e8); test deleted. Corrected and kept, each with what remains: - io-uring-enter-trips-the-one-queue-invariant becomes a-wait-was-registered-while-its-task-still-read-waiting: the assertion is now Watch::register's; every recorded route is gone; the cause never was found. - libc-loses-the-kernels-word-on-three-write-paths becomes ...-two-...: send's stream arm answers through stream_errno; sendto's datagram arm and stdio's sys_write and sys_read still drop the kernel's word. - std-calls-undated-what-it-did-not-stat becomes std-set-times-answers-ok-and-sets-nothing: the mtime half is fixed. - remove-dir-all-empties-a-directory-and-leaves-it: std's is fixed; pkg's remove_tree still gives that as its reason. - netstack-holds-a-pending-request-for-a-client-that-left: the connect half is gone; the waiting receive is still unwatched. - netstack-keeps-receiving-for-a-client-that-closed-its-receive-end, netstack-socket-ids-are-ambient, netstack-lookup-slots-have-no-per-client-share, netstacks-places-are-one-number-for-every-client, most-t14-leases-land-one-dhcp-retry-late: restated for regions, badges and netstack. - smp-ap-hole-and-log-reserve-window-red-under-a-loaded-host: the ROOT read half is gone; the first spawn's WouldBlock is kept. - soundserver-past-due-wake-max-1: only the null sink's wait still does it. - std-says-a-launch-moves-its-handles-...: the std comment moved into toyos::spawn, where it is right; provide's doc is still wrong. - std-maps-a-device-error-to-other-not-uncategorized: the consumers that kept Other load-bearing are gone. - sharedmemory-slices-rest-on-a-size-nobody-checks: a peer's region maps through `lent`, which checks it; `adopt` does not. - sshserver-authorized-keys-unprotected: installed packages no longer see /state; every shipped program still does. - libc-munmap-refuses-part-of-a-mapping: the owner ruled on 2026-10-21 that whole-mapping-only unmap is not the final contract and the refusal is to be revised, so the "rule it final" exit goes. - libc-refuses-what-toyos-cannot-yet-answer: after #978 F_DUPFD_CLOEXEC, F_DUPFD's lowest free and F_GETFD's EBADF hold; the fcntl bullet keeps record locks and F_GETFL, F_SETFL, F_GETOWN, F_SETOWN. - Sites, names and paths moved for: kernel-text-is-writable..., libc-answers-einval..., libc-answers-epipe..., libc-host-tests..., listing-home..., logkeeper-ends..., map-mmio..., metalprobes-usb-read..., mio-deregister..., no-console..., one-diskserver-session..., one-write-far-past..., probe-mounts-on-a-checksum, sshserver-holds-one-channel..., sshserver-still-speaks-rsa, std-lookup-host..., null-sink-applies-one-connect (annotated, not re-seen). parallel-tests-red-under-other-suites.md loses its pointer to a deleted record. no-physical-memory-fairness is owner-held and untouched; 58 records are still valid and 9 uncertain (stdio-buffering's deleted-file citation now names its deleting commit), both unchanged; four were already deleted on main (#971, #980, #978). The per-record table with file and line evidence is in the pull request. Co-Authored-By: Claude Opus 5.5 --- ...tered-while-its-task-still-read-waiting.md | 43 +++++++ ...ing-enter-trips-the-one-queue-invariant.md | 107 ------------------ ...itable-and-every-kernel-page-executable.md | 3 +- ...ack_addr-names-an-offset-not-an-address.md | 19 ---- ...einval-for-every-error-it-does-not-name.md | 12 +- ...ibc-answers-epipe-and-raises-no-sigpipe.md | 8 +- ...take-the-host-c-library-as-their-oracle.md | 2 +- ...s-the-kernels-word-on-three-write-paths.md | 38 ------- ...ses-the-kernels-word-on-two-write-paths.md | 27 +++++ .../libc-munmap-refuses-part-of-a-mapping.md | 14 +-- ...bc-refuses-what-toyos-cannot-yet-answer.md | 11 +- ...t-found-and-its-bound-has-no-guest-test.md | 10 +- issues/log-flush-retry-deadman-arm.md | 80 ------------- ...used-create-and-nothing-durable-says-so.md | 23 ++-- ...-the-records-its-own-refused-flush-made.md | 33 ------ ...s-an-address-past-the-direct-map-window.md | 9 +- ...wered-iofailed-once-beside-other-guests.md | 36 ------ ...read-is-answered-from-fileservers-cache.md | 14 +-- ...eregister-fd-leaves-a-pending-poll-live.md | 78 +++++-------- ...ost-t14-leases-land-one-dhcp-retry-late.md | 21 ++-- ...-pending-request-for-a-client-that-left.md | 27 ++--- ...or-a-client-that-closed-its-receive-end.md | 28 ++--- ...k-lookup-slots-have-no-per-client-share.md | 17 +-- ...hung-waiting-for-a-wake-that-never-came.md | 36 ------ issues/netstack-socket-ids-are-ambient.md | 54 ++++----- ...pipes-of-every-connection-on-every-pass.md | 32 ------ ...-places-are-one-number-for-every-client.md | 3 +- .../no-console-between-boot-and-terminal.md | 19 ++-- issues/null-sink-applies-one-connect.md | 6 + ...rver-session-can-hold-every-device-slot.md | 14 +-- ...ar-past-a-files-end-holds-data-s-server.md | 4 +- .../os-toyos-io-traits-keep-a-posix-name.md | 18 --- .../parallel-tests-red-under-other-suites.md | 6 +- issues/probe-mounts-on-a-checksum.md | 15 +-- ...ess-stats-exits-101-beside-other-guests.md | 40 ------- ...r-all-empties-a-directory-and-leaves-it.md | 46 ++------ ...-boot-leaves-no-i8042-line-on-the-panel.md | 28 ----- ...ory-slices-rest-on-a-size-nobody-checks.md | 7 +- ...l-c-discards-the-cwd-its-launch-carried.md | 37 ------ ...-reserve-window-red-under-a-loaded-host.md | 53 ++++----- issues/soundserver-past-due-wake-max-1.md | 14 ++- .../sshserver-authorized-keys-unprotected.md | 12 +- ...r-holds-one-channel-and-does-not-say-so.md | 2 +- issues/sshserver-still-speaks-rsa.md | 2 +- ...ery-spawn-refusal-but-notfound-as-other.md | 25 ---- .../std-calls-undated-what-it-did-not-stat.md | 23 ---- ...lookup-host-answers-no-address-as-other.md | 17 ++- ...device-error-to-other-not-uncategorized.md | 40 ++----- ...-launcher-even-when-the-move-is-refused.md | 40 +++---- ...d-set-times-answers-ok-and-sets-nothing.md | 14 +++ ...ffering-saw-one-long-line-arrive-as-two.md | 5 +- 51 files changed, 374 insertions(+), 898 deletions(-) create mode 100644 issues/a-wait-was-registered-while-its-task-still-read-waiting.md delete mode 100644 issues/io-uring-enter-trips-the-one-queue-invariant.md delete mode 100644 issues/kernel_stack_addr-names-an-offset-not-an-address.md delete mode 100644 issues/libc-loses-the-kernels-word-on-three-write-paths.md create mode 100644 issues/libc-loses-the-kernels-word-on-two-write-paths.md delete mode 100644 issues/log-flush-retry-deadman-arm.md delete mode 100644 issues/logkeeper-flushes-the-records-its-own-refused-flush-made.md delete mode 100644 issues/metal-device-probe-usbread-answered-iofailed-once-beside-other-guests.md delete mode 100644 issues/netstack-refused-accept-hung-waiting-for-a-wake-that-never-came.md delete mode 100644 issues/netstack-watches-both-pipes-of-every-connection-on-every-pass.md delete mode 100644 issues/os-toyos-io-traits-keep-a-posix-name.md delete mode 100644 issues/process-stats-exits-101-beside-other-guests.md delete mode 100644 issues/screen-diag-boot-leaves-no-i8042-line-on-the-panel.md delete mode 100644 issues/shell-c-discards-the-cwd-its-launch-carried.md delete mode 100644 issues/std-answers-every-spawn-refusal-but-notfound-as-other.md delete mode 100644 issues/std-calls-undated-what-it-did-not-stat.md create mode 100644 issues/std-set-times-answers-ok-and-sets-nothing.md diff --git a/issues/a-wait-was-registered-while-its-task-still-read-waiting.md b/issues/a-wait-was-registered-while-its-task-still-read-waiting.md new file mode 100644 index 00000000000..7227a228859 --- /dev/null +++ b/issues/a-wait-was-registered-while-its-task-still-read-waiting.md @@ -0,0 +1,43 @@ +--- +status: open +kind: defect +opened: 2026-08-15 +--- + +# A wait was registered while its task still read *waiting* + +The kernel panicked three times on the assertion that a task waits on at most +one queue, now `Watch::register`'s `assert!(task.set_waiting(), "a task waits +on at most one watch")` (`kernel/pure/sched/watch.rs`). What the assertion +reports is a `waiting` flag (`TaskShared::set_waiting`, +`kernel/pure/sched/task.rs`) left set by an earlier wait of the same thread. +Which wait left it set was never established. + +## The sightings, all on paths since removed + +- Once in logd's `io_uring_enter` on a KVM shard (nightly dispatch + `31900050723`, job `95049280131`, 2026-08-15), 4.7 s after the boot stick + was pulled and the kernel's usb-storage gave the disk up. The same tree was + green on the same name minutes earlier; one guest per machine, so host + contention does not explain it. +- Twice from a host-side keyboard flood of a few thousand key events a second + into the in-guest runner blocked on stdin at `===READY===`, through + `scheduler::wait_until::`, once with the i8042 + present and once with `q35,i8042=off`. Not at ordinary typing rates. + +`io_uring` and the shared wait queues went with the one-watch rewrite +(`aaddf38a7`, `232f69bb6`), and the kernel's keyboard with the i8042 and USB +drivers leaving it (`febfd21e8`, `13ed7b4f4`), so none of these routes exists. +`usb_boot_stick_pulled` is deleted (`02366d741`). + +One way in was found and closed in the primitive: a wake popped a waiter and +cleared its flag in two steps, so a waiter withdrawing between them found its +own flag still set (`aaddf38a^:toyos-sched/loom/tests/loom_ticket.rs`, +`cancel_and_wake_agree_on_who_won`). That was not shown to be the sightings' +path, and nothing has reproduced the panic since. + +**Exit**: either a reproduction on the current watch, accounted for by the +wait that left the flag set and fixed there, or a loom model of +`Watch::register` and `unregister` over every exit of a wait (commit, cancel, +revoke) that shows each clears the flag, with a mutation that leaves it set +going red. diff --git a/issues/io-uring-enter-trips-the-one-queue-invariant.md b/issues/io-uring-enter-trips-the-one-queue-invariant.md deleted file mode 100644 index 52535e19b18..00000000000 --- a/issues/io-uring-enter-trips-the-one-queue-invariant.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-08-15 ---- - -# `io_uring_enter` trips "a task waits on at most one queue" in logd - -On a KVM shard, in the boot `usb_boot_stick_pulled` stages, the kernel panicked -in logd's syscall: - -``` -[kernel 8.982 cpu2] PANIC: panicked at /__w/toyos/toyos/toyos-sched/src/waitq.rs:124:9: -a task waits on at most one queue -[kernel 8.982 cpu2] Backtrace: -[kernel 8.982 cpu2] >::prepare_wait+0x1bc -[kernel 8.982 cpu2] ::register+0x9c -[kernel 8.982 cpu2] kernel::io_uring::enter+0xe8f -[kernel 8.982 cpu2] kernel::arch::syscall::sys_io_uring_enter+0x8e -[kernel 8.982 cpu2] Running: pid=2 tid=Some(Tid(0)) -[kernel 8.982 cpu2] Process: logd pid=2 state=Live -[kernel 8.982 cpu2] User backtrace: -[kernel 8.983 cpu2] ::submit+0x32 -[kernel 8.983 cpu2] ::wait::+0x11 -[kernel 8.983 cpu2] logd::main+0x9c3 -``` - -**The stimulus is the device going away, not input.** The boot stick was pulled -at 4.297 s — `usb-storage: reset recovery failed; disk is offline`, then -`usb-storage: write of 1 blocks at 17409 failed on disk 0` — and logd had -already said -`/log has not answered (the sync: other error) - this boot's log is on the -console only from /log/2026-08-15-181448_0010.log`. The panic is 4.7 s after -that, in logd's next `Poller::submit`. - -**The stimulus is not the site.** The same assertion was recorded twice more -from a keyboard flood into a thread blocked in `sys_read` on stdin, reaching it -through `scheduler::wait_until::`: `Profile::MetalUsb` -under a few thousand injected key events a second, once with the i8042 present -and once with `q35,i8042=off`, so both the PS/2 and the USB delivery paths reach -it, and the victim both times was the in-guest runner blocked on stdin at -`===READY===`. It does not reproduce at ordinary typing rates. Two ways in, one -subject: a `waiting` flag left set by a previous wait of that thread, over -`set_waiting()` in `kernel/pure/sched/task.rs`. - -What the assertion says happened: this thread's task word still carried -*waiting* when `enter` prepared a new wait. `enter`'s loop consumes its ticket -on every exit it can see — `cancel()` on the error path, `cancel()` on the -satisfied re-check, `block_on` otherwise — so what is unaccounted for is a -previous wait of this thread that ended without clearing, and the pulled device -is what makes the ring's completions abnormal. Which wait that was is not -established here and a capture of one panic cannot establish it. - -**The machine did not stop**, which is why this is a red and not a wedge: the -capture continues past the report to `pull-probe-91` at 13.795 s, with the -stick's re-insertion enumerating at 9.202 s in between. `usb_boot_stick_pulled` -refuses any post-pull capture carrying `PANIC:`, so the test names it. - -Evidence, once: nightly dispatch `31900050723`, job `95049280131` -(`guest (3)`), `wt/toyos-ciwall`, 2026-08-15, in the serial phase. -`ALONE usb_boot_stick_pulled: GREEN, and it was alone both times — nothing the -harness controls differed, so it failed once and passed once. That is a rate -and not a classification.` The sibling dispatch `31900045901` (`main` at -`e064a96`) minutes earlier was green on this name, and the two trees differ -only in `tests/metal/src/testargs.rs`, `tests/toyos.rs` and one deleted issue file — the -kernel is the same kernel in the green run and the red one. A KVM shard runs -one guest per machine at `--jobs 1`, so host contention is not available as an -explanation either. - -## 2026-08-16: the capture's path no longer exists, and the invariant is narrower - -The completion work's one-park-site change took every blocking site off the -shared queues. `io_uring::enter` no longer registers on the ring's queue — it -arms on the ring as a *completion subject* and parks on the calling thread's -own `TaskHandle::park_queue`, one waiter per queue, machine-wide. The -`Ticket::register ← io_uring::enter` frame in the capture above is gone with -`enter`'s ticket, and so is the shape the assertion is about: with one queue -per task, "a task waits on at most one queue" can no longer be violated by two -*different* queues holding one task's node. - -**The entry stays open, because that is not the same as the root cause.** What -the assertion actually reports is a `waiting` flag left set by a previous wait -of that thread, and the capture does not establish which wait that was. With -one park site the search is much smaller — the only queue a task can be on is -its own, and -every exit from `completion::wait` either commits (and `pass_block` finishes -the registration) or cancels (which dequeues) — but "smaller" is not "proved -absent", and no reproduction has been run against the new shape. Whoever sees -it again should re-read this: the backtrace will not look like the one above. - -## One way in, found and closed - -`WaitQueue::wake_one`/`wake_all` popped a waiter and cleared its flag as two -steps, so a waiter withdrawing in between found `dequeue` empty and its own -flag still set; `aaddf38a^:toyos-sched/loom/tests/loom_ticket.rs`'s -`cancel_and_wake_agree_on_who_won` reds on that schedule. Both clear under the -list lock now. It is a hole in the primitive rather than the capture above's -path — `scheduler::wake_sched` claims through `wake_direct`, and no kernel -caller of either reaches the list at all — so the entry stays open on every -path that has not been ruled out. - -**Exit condition.** Reproducing the keyboard route deliberately, which means a -guest-side key generator rather than a host-side flood, and then accounting for -whichever wait left the flag set. - -**`usb_boot_stick_pulled` is deleted**, as a red test is: `02366d741` took it -out. diff --git a/issues/kernel-text-is-writable-and-every-kernel-page-executable.md b/issues/kernel-text-is-writable-and-every-kernel-page-executable.md index fe1548068ef..ca320451646 100644 --- a/issues/kernel-text-is-writable-and-every-kernel-page-executable.md +++ b/issues/kernel-text-is-writable-and-every-kernel-page-executable.md @@ -7,7 +7,8 @@ opened: 2026-09-29 # Kernel text is writable, and every kernel page is executable The kernel maps the whole direct map `PRESENT | WRITE` with no `NX` -(`kernel/src/arch/x86_64/paging.rs:946-949`), and the kernel image runs from +(`kernel_leaf`, which `paging::init` maps every 2 MiB with, +`kernel/src/arch/x86_64/paging.rs`), and the kernel image runs from it, at `PHYS_OFFSET` plus its physical address. Its text can be overwritten, and its heap, its stacks and every user page's direct-map alias can be executed at CPL 0. Linux at `Ubuntu-6.8.0-142.142`, under the T14's diff --git a/issues/kernel_stack_addr-names-an-offset-not-an-address.md b/issues/kernel_stack_addr-names-an-offset-not-an-address.md deleted file mode 100644 index 8bf1eca1ede..00000000000 --- a/issues/kernel_stack_addr-names-an-offset-not-an-address.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-28 ---- - -# `kernel_stack_addr` names an offset, not an address - -`toyos_abi::boot::KernelArgs::kernel_stack_addr` holds an offset into the -kernel's own image — `StackedImage::place`'s `stack` — never a physical -address. Both architectures' entry code already treat it as an offset -(`kernel/src/arch/aarch64/boot.rs` reads it into a local named -`stack_offset`); `kernel/src/main.rs` reads it under its `KernelArgs` name and -must not repeat the confusion the name invites. - -Owner: the author of PR #583 (`wt/toyos-loader1`), which is already changing -`KernelArgs`'s layout. -Exit: the field renamed to `kernel_stack_offset` (or equivalent), landed as -part of that change so `KernelArgs` moves once. diff --git a/issues/libc-answers-einval-for-every-error-it-does-not-name.md b/issues/libc-answers-einval-for-every-error-it-does-not-name.md index 38188f2ed35..d4290ff0f26 100644 --- a/issues/libc-answers-einval-for-every-error-it-does-not-name.md +++ b/issues/libc-answers-einval-for-every-error-it-does-not-name.md @@ -6,16 +6,20 @@ opened: 2026-10-01 # libc answers EINVAL for every error it does not name -Read from the code, not run. `set_errno` (`sdk/libc/src/posix_io.rs`) +Read from the code, not run. `errcode::of` (`sdk/libc/src/errcode.rs`), which +`posix_io.rs`'s `set_errno` asks, sends `Unknown`, `BadAddress`, `ResourceExhausted` and `NotSupported` through `_ => EINVAL`, so each reaches a C caller as `EINVAL`, which POSIX spells -"invalid argument". A full handle table (`kernel/src/object/handle.rs`) -answers `dup`, `open` and `pipe` `EINVAL` where POSIX has `EMFILE`, a full +"invalid argument". libc's own descriptor table answers `EMFILE` when it is +full (`fd_errno`, `sdk/libc/src/posix_io.rs`), but a full kernel handle table +(`kernel/src/object/handle.rs`) refuses the `open` or `pipe` beneath it first +with `ResourceExhausted`, which reaches the caller as `EINVAL` where POSIX has +`EMFILE`; a full bcachefs volume (`kernel/src/bcachefs_adapter.rs`) `write` `EINVAL` where it has `ENOSPC`, and an operation a filesystem lacks `EINVAL` where it has `ENOTSUP`. A refusal that is truly `EINVAL` reads the same as each of them, so a case asserting `EINVAL` passes whichever happened. -**Exit**: `set_errno` matches every `SyscallError` by name, with no catch-all, +**Exit**: `errcode::of` matches every `SyscallError` by name, with no catch-all, each call that can meet `ResourceExhausted` answering the POSIX errno its own page names for it, and a host test holds each call's mapping. diff --git a/issues/libc-answers-epipe-and-raises-no-sigpipe.md b/issues/libc-answers-epipe-and-raises-no-sigpipe.md index ccf351c1f41..b76c51d7a2f 100644 --- a/issues/libc-answers-epipe-and-raises-no-sigpipe.md +++ b/issues/libc-answers-epipe-and-raises-no-sigpipe.md @@ -10,9 +10,9 @@ POSIX's `write` to a pipe or socket whose reader is gone, and its `send` on a stream that can send no more, fail `EPIPE` and also send the calling thread `SIGPIPE`, whose default action ends the process; `send` raises none only with `MSG_NOSIGNAL`. ToyOS's libc fails `EPIPE` and raises nothing -(`sdk/libc/src/posix_io.rs`, `SyscallError::Gone`): it has no signals -(`signal`, `sigaction` and `raise` in `sdk/libc/src/misc.rs` do nothing), -and none of those +(`sdk/libc/src/errcode.rs`, `SyscallError::Gone`): it raises no signal but +`SIGALRM` (`signal` and `sigaction` in `sdk/libc/src/misc.rs` act only on that +one, and `raise` does nothing), and none of those `issues/a-childs-end-is-an-event-and-a-parent-takes-its-children-down.md` has it imitate is `SIGPIPE`. So every write behaves as if `MSG_NOSIGNAL` were given, and a C program that relies on the default action to stop writing into a @@ -24,7 +24,7 @@ others, is a decision of libc's design that nobody has made. Read from the code, not measured. -**Exit condition**: the decision is made and recorded at `posix_io.rs`, and a +**Exit condition**: the decision is made and recorded at `errcode.rs`, and a guest C case asserts it on both paths, a `write` into a pipe whose reader is gone and a `send` after `shutdown(SHUT_WR)`: with `SIGPIPE` raised, the default action ends the process, an ignored one leaves `EPIPE` and `MSG_NOSIGNAL` diff --git a/issues/libc-host-tests-take-the-host-c-library-as-their-oracle.md b/issues/libc-host-tests-take-the-host-c-library-as-their-oracle.md index 3c58da67f9d..de0d03a2b74 100644 --- a/issues/libc-host-tests-take-the-host-c-library-as-their-oracle.md +++ b/issues/libc-host-tests-take-the-host-c-library-as-their-oracle.md @@ -16,7 +16,7 @@ the host has, and the verdict differs between a Linux host and a macOS host: `getsockname`, with `darwin_reads` for where macOS parts from Linux. - `socket_options.rs`: `setsockopt` and `getsockopt` on real host sockets, with `darwin` for where macOS parts from Linux. -- `fparts_differential.rs`: `modf` and `logb`. +- `fparts_differential.rs`: `modf`, `round`, `lround` and `logb`. - `text_differential.rs`: `strnlen` (its `strsignal` half is held to a committed glibc table and conforms). diff --git a/issues/libc-loses-the-kernels-word-on-three-write-paths.md b/issues/libc-loses-the-kernels-word-on-three-write-paths.md deleted file mode 100644 index ecdb2cfd00a..00000000000 --- a/issues/libc-loses-the-kernels-word-on-three-write-paths.md +++ /dev/null @@ -1,38 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-08-31 ---- - -# `send`, `sendto` and `fwrite` throw the kernel's error away - -`sdk/libc/src/posix_io.rs`'s `write(2)` passes the kernel's -`SyscallError` to `set_errno`, so a write whose reader is gone is `EPIPE` and a -full non-blocking pipe is `EAGAIN`. Three sibling paths do not: - -* `sdk/libc/src/socket.rs:354` — `send`'s TCP arm: - `Err(_) => { set_errno(EIO); -1 }`. Every refusal is `EIO`, including the - dead peer that `write(2)` reports as `EPIPE`. -* `sdk/libc/src/socket.rs:430-431` — `sendto`'s UDP arm, the same - `if let Err(_) = ... { set_errno(EIO); return -1; }`. -* `sdk/libc/src/stdio.rs:32` — `sys_write` answers `-1` and sets **no** - `errno` at all, so a failed `fwrite`/`fputs` leaves whatever the last failing - call left behind. - -The tx pipe on both socket paths is netd's, and netd exiting is exactly the -`Gone` the kernel now answers, so `send` on a dead netd says "I/O error" where -POSIX says "broken pipe" and where this same libc's `write` already says it. - -Found while landing the kernel's `Gone` for a broken pipe; not fixed there -because none of the three is the defect that change was closing. - -## What closing it takes - -`Err(e) => set_errno(e)` at all three sites, and `sys_write` returning through -`set_errno` rather than a bare `-1`. `EIO` stays the answer only where the -kernel actually said `SyscallError::Io`. - -The instrument is a guest arm through libc rather than through the SDK: a C -program whose `send` into a socket whose peer has gone must set `errno` to -`EPIPE`, measured against the same program's `write(2)` on a plain pipe, which -already does. diff --git a/issues/libc-loses-the-kernels-word-on-two-write-paths.md b/issues/libc-loses-the-kernels-word-on-two-write-paths.md new file mode 100644 index 00000000000..aa7e629c1e5 --- /dev/null +++ b/issues/libc-loses-the-kernels-word-on-two-write-paths.md @@ -0,0 +1,27 @@ +--- +status: open +kind: defect +opened: 2026-08-31 +--- + +# `sendto`, `fwrite` and `fread` throw the kernel's error away + +`sdk/libc/src/posix_io.rs`'s `write(2)` passes the kernel's `SyscallError` to +`set_errno`, so a write whose reader is gone is `EPIPE` and a full non-blocking +pipe is `EAGAIN`, and `send`'s stream arm answers through `stream_errno` +(`sdk/libc/src/socket.rs`). Two sibling paths do not: + +* `sdk/libc/src/socket.rs`, `Datagrams::send` for `SocketEntry`, under + `sendto`'s UDP arm: the write into netstack's pipe is + `.map_err(|_| EIO)`. Every refusal is `EIO`, including netstack gone, which + the kernel answers `Gone` and `write(2)` reports as `EPIPE`. +* `sdk/libc/src/stdio.rs`, `sys_write` and `sys_read`: a refused write or read + answers `-1` and sets **no** `errno`, so a failed `fwrite`, `fputs` or + `fread` leaves whatever the last failing call left behind. + +## What closing it takes + +The datagram arm answers the kernel's `SyscallError` through `errcode::of`, +whose mapping the host already tests, so `EIO` is the answer only where the +kernel said `SyscallError::Io`. `sys_write` and `sys_read` already hold an +`Errno` (`write_fd` and `on_fd`, `sdk/libc/src/posix_io.rs`) and set it. diff --git a/issues/libc-munmap-refuses-part-of-a-mapping.md b/issues/libc-munmap-refuses-part-of-a-mapping.md index 0b70bd93ea3..b050da4afa4 100644 --- a/issues/libc-munmap-refuses-part-of-a-mapping.md +++ b/issues/libc-munmap-refuses-part-of-a-mapping.md @@ -24,9 +24,9 @@ told `EINVAL` and keeps all of what it mapped. Nothing it kept is taken. The kernel is why: a mapping is one `vma::Region` over one `PageAlloc`, with no part to take away, and `SYS_MUNMAP` takes only the length the mapping's -`mmap` was asked for (`kernel/src/syscall/vm.rs`). That is the ruling in +`mmap` was asked for (`kernel/src/syscall/vm.rs`). That was the ruling in `issues/process-memory-is-2-mib-pages-and-that-caps-the-process-count.md`'s -stage 4. +stage 4, which the owner has since revised (below). `mmap` with `MAP_FIXED` has the same gap. POSIX replaces whatever pages `[addr, addr + len)` overlaps, of any mappings, and leaves the rest of each @@ -52,11 +52,11 @@ corpus, and not across what a user builds against this libc. `206_libc_refusals` reads 0 for `munmap` of the first of two pages, the second page still holds its byte, and a touch of the first faults; and its `mmap the first of two pages fixed` line reads 0, with the second page's byte -kept after it. Or the owner rules the refusals final and this file becomes -`kind: rejected`. +kept after it. ## Owner -The orchestrator, who holds the 2 MiB track and ruled the refusal in its -stage 4. The first exit needs a region that splits, and no stage of that -track builds one. +The orchestrator, who holds the 2 MiB track. The owner ruled on 2026-10-21 +that whole-mapping-only unmap and replacement is not the final application +contract, and that the track's refusal of partial unmaps is to be revised. The +exit needs a region that splits, and no stage of that track builds one yet. diff --git a/issues/libc-refuses-what-toyos-cannot-yet-answer.md b/issues/libc-refuses-what-toyos-cannot-yet-answer.md index 77f635d797a..934a963cdfd 100644 --- a/issues/libc-refuses-what-toyos-cannot-yet-answer.md +++ b/issues/libc-refuses-what-toyos-cannot-yet-answer.md @@ -27,14 +27,9 @@ These answer failure in their POSIX form and do nothing maps neither. - `madvise`'s `MADV_DONTNEED`, `ENOSYS`: no call discards a range's pages and keeps it mapped, so it cannot read back as zeros as Linux's does. -- `fcntl`: a record lock, `EINVAL`, POSIX's answer for a file that supports no - locking; `F_DUPFD_CLOEXEC`, `F_GETFL`, `F_SETFL`, `F_GETOWN` and `F_SETOWN`, - `ENOSYS`. `F_DUPFD` answers a duplicate at or above its argument, the first - the kernel hands `dup`, where POSIX has the lowest free number, and refuses - `EINVAL` an argument at or above the slots a handle table has - (`RawHandle::MAX_SLOTS`). `F_GETFD` and `F_SETFD` of a descriptor the - process does not hold answer 0, where POSIX has `EBADF`: the kernel ends a - process that names a handle it does not hold, so libc asks it nothing. +- `fcntl` (`sdk/libc/src/fdreq.rs`): a record lock, `EINVAL`, POSIX's answer + for a file that supports no locking; `F_GETFL`, `F_SETFL`, `F_GETOWN` and + `F_SETOWN`, `ENOSYS`. Ruled out, and owed nothing while the ruling stands: `execv` and `execve`, no call replacing a process's image; `setsid` and `getsid`, no POSIX session; diff --git a/issues/listing-home-answers-not-found-and-its-bound-has-no-guest-test.md b/issues/listing-home-answers-not-found-and-its-bound-has-no-guest-test.md index b9f870c0183..ab83b674641 100644 --- a/issues/listing-home-answers-not-found-and-its-bound-has-no-guest-test.md +++ b/issues/listing-home-answers-not-found-and-its-bound-has-no-guest-test.md @@ -20,8 +20,10 @@ count: So on `main` the arm created 32,769 files through the DATA server (364 s and 442 s of `readdir_bound` on #536's two whole runs, against 46-181 s before it) to reach an answer an empty `/home` gives too. The arm is deleted; what is left -untested is the one refusal the DATA server has for a listing too large, the -`WINDOW_BYTES` check in `userland/fsd/src/main.rs`'s `READDIR`. +untested is the one refusal the DATA server has for a listing too large, +`Error::LimitExceeded` past `WINDOW_BYTES` in `system/fileserver/src/main.rs`'s +`READDIR`. Whether listing `/home` itself still answers `NotFound` has not been +read since #536's branch. ## Owner @@ -30,5 +32,5 @@ The storage track, which moved `/home` behind the DATA server. ## What would close it `read_dir("/home")` on a machine with a DATA volume answers `/home`'s entries, -and a guest test lists a served directory past `WINDOW_BYTES` and is refused -with `ResourceExhausted` by name rather than by any error. +and a test lists a served directory past `WINDOW_BYTES` and is refused with +`LimitExceeded` by name rather than by any error. diff --git a/issues/log-flush-retry-deadman-arm.md b/issues/log-flush-retry-deadman-arm.md deleted file mode 100644 index 7be68fdbf59..00000000000 --- a/issues/log-flush-retry-deadman-arm.md +++ /dev/null @@ -1,80 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-08 ---- - -# `log_flush_retry`'s deadman arm reds on `metal-suite`, and the kernel's own refusal line is what is missing - -`cargo test --test toyos-build -- --nightly log_flush_retry` fails on -`origin/metal-suite` (measured at d15f8e86, and again at 30d89859 with the -durability work on top — the same failure and the same words on both, so it is -the branch's and not that work's): - -``` -FAIL log_flush_retry: the deadman never declared the volume failed: -``` - -The test's second boot (`tests/common/volumes.rs`, "the deadman expires, and -that is a declared death") arms `fsync-budget-spent` and `fsync-deadman-now` and -then looks on the console for the kernel record -`fsync: is not durable after N attempt(s) in …` -(`kernel/src/object/ops.rs`'s `fsync`). The boot produces `logd`'s own -give-up line — - -``` -logd: /log has not answered (the sync: other error) - this boot's log is on the -console only from /log/2026-09-07-235907.log -``` - -— which says `SYS_FSYNC` answered with something that was **not** -`WouldBlock` (`policy::fate`'s `GiveUp` arm takes every kind but that one). So -the volume died, but it died on an arm that never reaches the deadman's own -line: `fsync` returns a device error straight through, and only the -`Err(WouldBlock)` path with `deadman.reached` logs what the test reads. - -The first boot of the same test passes, so the retry half is sound; what is -unresolved is whether the second boot's refusal is the deadman it is supposed to -stage or a different error arriving first, and the console does not say which. - -## Two older ways it reddened, not re-seen since ROOT-in-memory - -`d5c2d9c9^:issues/boot-media/log-flush-retry-reds-two-ways-at-two-in-five.md` -recorded four ways `log_flush_retry` had been seen to red; `d5c2d9c9` deleted -it once ROOT-in-memory gave the `[hung]` arm's own mechanism — a stick going -offline while userland is still paged from it — a fix and six green runs. Two -of its other ways were not re-seen in those six runs, so neither is known fixed -by that work: - -- **The `[hung]` arm missing its "transport broke on SCSI" line.** One of that - file's two originally-recorded assertions: the wide run reds on - `the deadman never declared the volume failed` while the *alone* re-run of - the same boot reds instead on - `no "transport broke on SCSI" in the log, so the staged hung device never - met its recovery` — two different assertions from the same staged fault, - which was that file's reason for treating this as more than a scheduling - classification. -- **A boot timeout before `===READY===`.** Measured 2026-09-07 on three - separate points (`main` 71ed50cf, `metal-suite` 7f16914d and f63bce1d), each - run alone: `log_flush_retry: [qemu] Boot timed out waiting for - ===READY===`, with the boot never coming up at all — a third shape beside - the two assertion failures, seen before ROOT-in-memory and on a branch that - does not carry it. - -## Exit condition - -The deadman boot produces the record the test reads, or the test reads the -record that boot actually produces — decided by which error `SYS_FSYNC` -returned, which is a value nothing on that console prints today. - -And `log_flush_retry` is re-measured (wide and alone) enough times, on a tree -that carries ROOT-in-memory, to say whether either older mode still occurs; if -seen again, it is deleted with this file recording the commit that restores -it, or it is fixed at its owner (usbd for the transport line, the boot -harness for the timeout). If not seen in that many runs, that half closes with -the count that supports it. - -**Its test is deleted**, as a flaky test is: `9ebf080e8` took -`log_flush_retry` out with `fsync-budget-spent`, `fsync-deadman-now` and -`usb-reset-break`, which it was the last to arm, and -`git revert 9ebf080e8` brings them back. diff --git a/issues/logkeeper-ends-the-boots-log-on-one-refused-create-and-nothing-durable-says-so.md b/issues/logkeeper-ends-the-boots-log-on-one-refused-create-and-nothing-durable-says-so.md index 58f6ccafbf7..68bbe58fc7c 100644 --- a/issues/logkeeper-ends-the-boots-log-on-one-refused-create-and-nothing-durable-says-so.md +++ b/issues/logkeeper-ends-the-boots-log-on-one-refused-create-and-nothing-durable-says-so.md @@ -4,7 +4,10 @@ kind: defect opened: 2026-09-21 --- -# `logd` ends the boot's log on one refused create, and nothing durable says so +# logkeeper ends the boot's log on one refused create, and nothing durable says so + +logkeeper was `logd` when this was measured, and the kernel still drove the +stick; the sections below keep the names of then. T14 run 69 (`usb-transport-break` on the boot stick, this tree at `a9adf0d7`) ran its job, synced, said `Rebooting.` at 5.808 s and read back `DONE`. Its @@ -29,15 +32,15 @@ reached the device before the shutdown's own. such boot has `spawn: /system/bin/logd` at 0.301 s, the break at 0.323 s and `logd: this boot's kernel log is …` after it: the write is `Volume::open` creating the file. -- **One refused create is no log for the boot.** `userland/logd/src/store.rs`'s - `Volume::open` answers `None` on any error from `create`, with no retry, and - `main` then runs to the end of the boot with `volume = None`. `policy::fate` - does not see this path at all; it rules on appends and flushes, where every - answer but a budget-refused flush is `GiveUp` for the rest of the boot. -- **What it says reaches nothing on this machine.** `logd: cannot create …` and - `logd: no /log on this machine …` go to `logd`'s console, which on a laptop - with no serial port is no channel, and they are not kernel records, so the - black box's tail does not carry them either. +- **One refused create is no log for the boot.** `Volume::open` + (`system/logkeeper/src/store.rs`) answers `None` on any error from `create`, + with no retry, and `main` then runs to the end of the boot with no volume. A + refused append or flush later in the boot ends the volume the same way + (`answered`, `system/logkeeper/src/main.rs`). +- **What it says reaches nothing on this machine.** `logkeeper: cannot create …` + is logkeeper's own record, which with no volume reaches only its console; a + laptop with no serial port has no channel there, and it is not a kernel + record, so the black box's tail does not carry it either. - Under QEMU the same boot's recovery takes in about a millisecond, the create succeeds and `/log` is complete, so no QEMU boot shows any of this. diff --git a/issues/logkeeper-flushes-the-records-its-own-refused-flush-made.md b/issues/logkeeper-flushes-the-records-its-own-refused-flush-made.md deleted file mode 100644 index 5f614f1708e..00000000000 --- a/issues/logkeeper-flushes-the-records-its-own-refused-flush-made.md +++ /dev/null @@ -1,33 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-27 ---- - -# `logd` flushes the records its own refused flush made, for as long as flushes are refused - -`logd` makes the volume durable after every round it wrote a line in. A -flush whose first attempt is budget-refused and then retried commits four -kernel records: the storage driver's `not issued` and `ran out of its -operation budget`, the volume's `the device would not answer in the caller's -own budget`, and `fsync: durable on attempt 2`. Those records are the -next round's lines, so that round flushes again. While every flush's first -attempt is refused, the log never goes quiet. It grows by those four records -per round, rotates, and the boot's own early files are rotated away. - -Measured at dbf4ace5 with `fsync-budget-spent` refusing every flush's first -attempt, as it did before this branch. In the 2 s after -`home_budget_refusal_retried`'s guest finished, 220 and 131 `/log` flushes -were retried (two runs, `cargo test --test toyos-build -- --nightly -home_budget_refusal_retried`, TCG on the dev host). In CI (run 36285169430, -KVM) the same storm reached `_0003.log` by 28 s, and neither -`home_budget_refusal_retried` nor `log_flush_retry` saw `===READY===`. - -The actuator now refuses once per file, so no test stages this any more. On -hardware the loop needs a device whose every flush overruns its operation -budget. Each round then costs at least that budget, so the loop is slow, but -it never ends while the device stays that slow. - -**Exit**: a refused-then-retried flush's own records do not by themselves make -`logd` flush again without end, shown by a boot that refuses every flush's -first attempt and whose log goes quiet. diff --git a/issues/map-mmio-takes-an-address-past-the-direct-map-window.md b/issues/map-mmio-takes-an-address-past-the-direct-map-window.md index 99b8891817e..4296b81890a 100644 --- a/issues/map-mmio-takes-an-address-past-the-direct-map-window.md +++ b/issues/map-mmio-takes-an-address-past-the-direct-map-window.md @@ -10,9 +10,12 @@ opened: 2026-09-28 `PHYS_OFFSET + phys` and bounds nothing. The direct map's window is `toyos_bootmap::DIRECT_MAP_WINDOW`, `0x800000000000` (128 TiB): root slots 256 to 511. A firmware-named register base at or past it overflows that sum. -`vtd::window` (`kernel/src/arch/x86_64/vtd/mod.rs`) bounds a DMAR register base -by its own `MAX_PHYS`, `1 << 52`, so a base between the two reaches it; a BAR -in a firmware window past 128 TiB reaches it through `pcidev::probe_dword`. +`iommu_unit::register_window` (`kernel/src/arch/x86_64/iommu_unit.rs`) bounds a +DMAR or IVRS register base by its own `MAX_PHYS`, `1 << 52`, so a base between +the two reaches it, and nothing read here holds a BAR a kernel driver maps +(`drivers/virtio.rs`, the MSI-X table in `drivers/pci.rs`) below the window. ECAM windows are already +checked against `DIRECT_MAP_WINDOW` before they are mapped +(`kernel/src/drivers/acpi.rs`, `ecam_windows`). Nothing has been observed to reach it: every firmware this tree has booted puts its registers far below. diff --git a/issues/metal-device-probe-usbread-answered-iofailed-once-beside-other-guests.md b/issues/metal-device-probe-usbread-answered-iofailed-once-beside-other-guests.md deleted file mode 100644 index 53607fcc938..00000000000 --- a/issues/metal-device-probe-usbread-answered-iofailed-once-beside-other-guests.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-21 ---- - -# `metal_device_probe`'s `usbread` answered `IoFailed` once beside other guests - -One full fast tier on the dev host (`cargo test`, `wt/toyos-govcut` at -`2bd5076a`, a branch whose kernel diff is two comment lines): 343 passed, 1 -failed, 344 total in 225.8 s, with another worktree's suite -(`toyos-usbtransport: usb_transport_break`) holding guest slots on the same -host. - -``` - [devices] usbwrite: 737075 us -FAIL metal_device_probe: usbread: the job refused — IoFailed (the volume was there and an operation on the open file failed) - FAIL metal_device_probe (11s) -... - [devices] usbread: 4998 us - PASS metal_device_probe (3s) - ALONE metal_device_probe: GREEN — it fails only beside other guests, so its Sched::Parallel is wrong. The run stays red on the classification. -``` - -The `usbwrite` before it took 737 ms where the alone run's read took 5 ms, so -the guest was slow when the read failed; which operation on the open file -answered `IoFailed`, and whether the transport broke under it, is not in the -capture the summary kept. One sighting, no rate, no mechanism. - -Owed: the read's failing operation named from the full capture on the next -sighting, and whether `usb-storage` logged a transport break before it. - -**Its test is deleted**, as a flaky test is: `24aa31815` took -`metal_device_probe` out, its QEMU and T14 rows both, with the T14 judge and -the device inventory in `tests/metal/src/devices.rs` only that judge read — among -them the T14's assertion that blockd drives no NVMe there. diff --git a/issues/metalprobes-usb-read-is-answered-from-fileservers-cache.md b/issues/metalprobes-usb-read-is-answered-from-fileservers-cache.md index 0a16697f1d0..554dc2a3f09 100644 --- a/issues/metalprobes-usb-read-is-answered-from-fileservers-cache.md +++ b/issues/metalprobes-usb-read-is-answered-from-fileservers-cache.md @@ -4,16 +4,16 @@ kind: defect opened: 2026-09-27 --- -# metalprobe's USB read is answered from fsd's cache, not the stick +# metalprobe's USB read is answered from the file server's cache, not the stick `tests/guest/metalprobe/src/usb.rs`'s `read` writes its file to `/log`, closes it, and times reading it back as "a cache miss the stick has to answer" — true while the kernel's write-back dropped a closed file from its cache. `/log` is -served by `/system/bin/fsd` now, whose block cache (`userland/fsd/src/cache.rs`) -keeps clean blocks until it holds `CLEAN_LIMIT` of them and drops nothing at a -close. So the timed read is fsd's memory, and the `usbread` span is no longer -the stick's. +served by a file server now, whose block cache +(`system/fileserver/src/cache.rs`) keeps clean blocks until it holds +`CLEAN_LIMIT` of them and drops nothing at a close. So the timed read is the +file server's memory, and the `usbread` span is no longer the stick's. -**Exit**: the timed read is one the stick answers — a file fsd has not read -since it started, or a read through a block session that bypasses the file +**Exit**: the timed read is one the stick answers — a file the file server has +not read since it started, or a read through a block session that bypasses the file server. diff --git a/issues/mio-deregister-fd-leaves-a-pending-poll-live.md b/issues/mio-deregister-fd-leaves-a-pending-poll-live.md index dd89a0d7bb1..1fa33c7f0ea 100644 --- a/issues/mio-deregister-fd-leaves-a-pending-poll-live.md +++ b/issues/mio-deregister-fd-leaves-a-pending-poll-live.md @@ -4,69 +4,53 @@ kind: defect opened: 2026-08-16 --- -# mio's ToyOS selector deregisters a token but not the kernel's poll on it +# mio's ToyOS selector deregisters a token but not the kernel's watch on it -`mio/src/sys/toyos/selector.rs` in the mio fork (currently pinned at `e8068c2`, -`Cargo.lock`) keeps its own registration list rather than asking the -kernel to track interest: +`src/sys/toyos/selector.rs` in the mio fork (pinned at `9f34d36c`, +`Cargo.lock`) keeps its own registration list rather than asking the kernel to +track interest: ```rust -pub fn deregister_fd(&self, fd: RawHandle) -> io::Result<()> { +pub fn deregister_handle(&self, handle: RawHandle) -> io::Result<()> { let mut inner = self.inner.lock().unwrap(); - inner.registrations.retain(|r| r.0 != fd); + inner.registrations.retain(|r| r.0 != handle); Ok(()) } ``` -This removes the entry from `SelectorInner::registrations`, so `select()` -(lines 169–214) stops re-arming a `POLL_ADD` for it on future calls — the -re-arm loop at 172–185 only iterates what is still in the list. But a -`POLL_ADD` already submitted for that fd in an *earlier* `select()` is not -touched: nothing here cancels it, and nothing waits for it to land. If the -kernel completes that stale poll after `deregister_fd` returns, the CQE still -carries the old token, and the next `select()`'s drain loop (198–211) does not -check the token against the current registration list before turning it into -an `Event` — it matches purely on `cqe.user_data`. The caller (tokio's +This removes the entry from `SelectorInner::registrations`, so `select()` stops +queuing an `OP_WATCH` for it on later calls: its re-arm loop iterates only what +is still in the list. But a watch already submitted for that handle in an +*earlier* `select()` is not touched: nothing here cancels it, and nothing waits +for it to land. If the kernel completes that stale watch after +`deregister_handle` returns, the completion still carries the old token, and +the next `select()`'s drain loop does not check the token against the current +registration list before turning it into an `Event`. The caller (tokio's reactor, or anything else built on mio's `Registry`) gets one readiness notification for a resource it was promised was gone. ## Why there is nothing to cancel with -There used to be an ABI op for this. `sdk/abi/src/inbox.rs` op code 2 was -`IORING_OP_POLL_REMOVE`, retired in PR #89 (`c41b831`, -"abi: four names retired, and the number each one held") as caller-less: - -``` -// Op code 2 unused (formerly IORING_OP_POLL_REMOVE). It had no submitter -// anywhere either — and the selector that would have been its caller cancels -// nothing: mio's ToyOS selector keeps its own registration list, re-arms every -// registration on each `select`, and deregisters by dropping the entry. A poll -// this kernel takes is one-shot, consumed by the completion it posts, so the -// interest a remove would withdraw is gone before there is anything to name. -``` - -The last sentence is the part this entry disagrees with. "One-shot, consumed -by the completion it posts" is true of the poll itself, but it says nothing -about *when* that completion is consumed relative to `deregister_fd` — and the -selector's own code shows the two are not synchronized. The retirement -correctly observed that mio never calls a remove op; it did not establish that -nothing needs one. Those are different claims, and PR #89 only re-verified the -first (grepped the estate for a submitter — found none, which is true and -remains true) rather than asking whether `deregister_fd`'s behavior was itself -correct. +There used to be an ABI op for this. `sdk/abi/src/inbox.rs`'s op code 2 was +`IORING_OP_POLL_REMOVE`, retired in PR #89 (`c41b831`, "abi: four names +retired, and the number each one held") as caller-less, on the reasoning that +a poll the kernel takes is one-shot and consumed by the completion it posts. +That is true of the watch itself, but says nothing about *when* the completion +is consumed relative to `deregister_handle`, and the selector shows the two are +not synchronized. The retirement established that mio never calls a remove op, +not that nothing needs one. ## What a fix has to promise A kernel wait a kill ends is answered `Cancelled` (`kernel/src/watch.rs`). mio's selector is the userland mirror of that, and a `deregister` that cannot -promise "no event after this point" is what it lacks: no ring op withdraws a +promise "no event after this point" is what it lacks: no inbox op withdraws a watch (`sdk/abi/src/inbox.rs` has `OP_NOP`, `OP_WATCH` and `OP_ACCEPT`). -Re-adding a cancel op (or otherwise making `deregister_fd` synchronous with -the kernel's pending state — draining the SQ/CQ for that fd, or having the -kernel answer a targeted query) is in scope for whoever fixes this; retiring -`IORING_OP_POLL_REMOVE` a second time without addressing this would repeat the -same reasoning gap. - -Filed rather than fixed: this is a fork (mio, not this repository) and a -design question (what the cancel primitive should look like), not a local bug -fix. +Re-adding a cancel op, or otherwise making `deregister_handle` synchronous with +the kernel's pending state (draining the inbox for that handle, or the drain +loop dropping a token no registration holds), is in scope for whoever fixes +this; retiring a remove op a second time without addressing this would repeat +the same reasoning gap. + +Filed rather than fixed: this is a fork (mio, not this repository) and a design +question (what the cancel primitive should look like), not a local bug fix. diff --git a/issues/most-t14-leases-land-one-dhcp-retry-late.md b/issues/most-t14-leases-land-one-dhcp-retry-late.md index cba17715434..40af1544099 100644 --- a/issues/most-t14-leases-land-one-dhcp-retry-late.md +++ b/issues/most-t14-leases-land-one-dhcp-retry-late.md @@ -12,18 +12,15 @@ after netd came up and 11 at 13.1–13.4 s, so the first lease of a boot, and with it `sshd`, arrived at 9.1–9.3 s of boot on 4 of the 13 boots and at 19.0–19.1 s on the other 9. -The ten-second step is smoltcp 0.12's default `RetryConfig::discover_timeout`, -which netd keeps. netd restarts -discovery when the link comes up (`userland/netd/src/dhcp.rs:42-61`), and on -the late boots the lease came from the DISCOVER sent one timeout after that -one. What became of the first, whether it left the machine and whether an -OFFER came back, is unmeasured. RFC 2131 §4.1 puts the first retransmission -at 4 s, randomized by ±1 s. +The ten-second step was smoltcp 0.12's default +`RetryConfig::discover_timeout`, which netd, netstack's predecessor, kept. netd +restarted discovery when the link came up, and on the late boots the lease came +from the DISCOVER sent one timeout after that one. What became of the first, +whether it left the machine and whether an OFFER came back, is unmeasured. RFC +2131 §4.1 puts the first retransmission at 4 s, randomized by ±1 s. -The network track owns it: netd leaves smoltcp in stage 5 of -`issues/toyos-has-its-own-network-stack.md`, whose stage 3 is -`toyos-dhcp`. Stage 6 of -`issues/the-loader-does-only-what-must-precede-the-handover.md` +The network track owns it (`issues/toyos-has-its-own-network-stack.md`). +Stage 6 of `issues/the-loader-does-only-what-must-precede-the-handover.md` waits on it. netstack runs `toyos-dhcp` now (`system/netstack/node`): an unanswered @@ -36,7 +33,7 @@ measurement branches took their leases 3,419, 7,206, 8,172 and 8,397 ms after netstack came up; netstack logs no DISCOVER and no OFFER, so what became of the first DISCOVER of each is unread. -**Exit**: netd logs each DISCOVER it sends and each OFFER it receives, and a +**Exit**: netstack logs each DISCOVER it sends and each OFFER it receives, and a T14 boot's log shows what became of the DISCOVER sent as the link came up; on every boot of a T14 run an unanswered DISCOVER is sent again within RFC 2131 §4.1's 4 ± 1 s. diff --git a/issues/netstack-holds-a-pending-request-for-a-client-that-left.md b/issues/netstack-holds-a-pending-request-for-a-client-that-left.md index bce0b42367e..6b6a409c087 100644 --- a/issues/netstack-holds-a-pending-request-for-a-client-that-left.md +++ b/issues/netstack-holds-a-pending-request-for-a-client-that-left.md @@ -4,19 +4,20 @@ kind: defect opened: 2026-09-26 --- -# netd holds a pending request for a client that left +# netstack holds a pending receive for a client that left -A UDP receive with no datagram yet and a connect waiting for its handshake keep -their client's connection in `Sockets::receiving` and `Sockets::connecting` -(`system/netstack/src/serve.rs`) until what they wait for happens. Neither -watches the connection, so a client that hangs up is noticed only when netstack -writes its answer: a receive on a socket nothing sends to holds its entry for -the life of the socket, and a connect with no timeout holds its stream and its -place until [tcp] gives the handshake up. +A UDP receive with no datagram yet keeps its client's connection in +`Sockets::receiving` (`system/netstack/src/serve.rs`) until a datagram arrives. +Nothing watches that connection (`Sockets::watch` watches each stream's +connection, each listener's wake pipe, each datagram socket's pipe and each +waiting lookup's connection), so a client that hangs up is noticed only when +netstack writes its answer: a receive on a socket nothing sends to holds its +entry for the life of the socket. -A lookup's client is watched and let go at once (`Node::let_go`, reached from -the `TOKEN_LOOKUP` watches in `Sockets::watch`). +A lookup's client is watched and let go at once (`TOKEN_LOOKUP`), and so is a +stream's from its connect: a stream is the connection it was asked on, watched +for its hang-up. -Exit condition: both pending lists watch their clients' connections the same -way, and a guest test that hangs up a pending receive and a pending connect -sees the socket and the slot freed without any traffic. +Exit condition: a waiting receive's connection is watched the same way, and a +test of `serve.rs` that hangs up a pending receive sees its entry freed without +any traffic. diff --git a/issues/netstack-keeps-receiving-for-a-client-that-closed-its-receive-end.md b/issues/netstack-keeps-receiving-for-a-client-that-closed-its-receive-end.md index 0e7dcef9436..ff638fa8862 100644 --- a/issues/netstack-keeps-receiving-for-a-client-that-closed-its-receive-end.md +++ b/issues/netstack-keeps-receiving-for-a-client-that-closed-its-receive-end.md @@ -4,21 +4,21 @@ kind: defect opened: 2026-09-26 --- -# netd keeps receiving for a client that closed its receive end +# netstack keeps receiving for a client that shut its receive half -When a stream's client closes its receive pipe while the peer is still -sending, the node lets its end of that pipe go and nothing else -(`system/netstack/node/src/streams.rs`, `Stream::pass`): the connection -stays open, [tcp] keeps its unread bytes and advertises a closing window, so -the peer's further bytes sit in flight and its sender blocks until the client -is done writing too. Then the node closes the connection, and [tcp] resets one -closed with text unread (`a_close_with_text_unread_resets`, -`system/netstack/node/tests/streams.rs`). Until then nothing tells the peer -the bytes will never be read. +A stream's `shutdown(Read)` sets the client's own `read_shut` and nothing else +(`toyos::net::stream::shutdown_read`, `sdk/toyos/src/net/stream.rs`): every read +then answers 0, but netstack is not told. It keeps taking the peer's bytes into +the region's receive ring, [tcp] advertises a closing window as the ring fills, +and the peer's further bytes sit in flight while its sender blocks, until the +client hangs up. Then the node closes the connection, and [tcp] resets one +closed with text unread (`a_client_that_leaves_with_text_unread_resets`, +`system/netstack/node/tests/streams.rs`). Until then nothing tells the peer the +bytes will never be read. Read from the code, not measured. -Exit condition: a connection whose receive end is gone with bytes still owed -is reset (or its receive side shut down) so the peer learns at once, with a -test whose host sender sees the connection end while the guest still holds its -send pipe. +Exit condition: a stream whose client shut its receive half with bytes still +owed is reset (or its receive side shut down at the peer) so the peer learns at +once, with a node test in which the peer sees the connection end while the +client still holds its send half. diff --git a/issues/netstack-lookup-slots-have-no-per-client-share.md b/issues/netstack-lookup-slots-have-no-per-client-share.md index 161512a89b2..54d9ed658e2 100644 --- a/issues/netstack-lookup-slots-have-no-per-client-share.md +++ b/issues/netstack-lookup-slots-have-no-per-client-share.md @@ -4,18 +4,21 @@ kind: defect opened: 2026-09-26 --- -# netd's lookup slots have no per-client share +# netstack's lookup slots have no per-client share -`toyos_dns::MAX_LOOKUPS` (`system/netstack/node/src/resolve.rs`) bounds the lookups in -flight across every client at once. One holder of `netd` that asks for names +`toyos_dns::MAX_LOOKUPS` (`system/netstack/dns/src/lib.rs`, held by +`system/netstack/node/src/resolve.rs`) bounds the lookups in flight across every +client at once. One holder of `netstack` that asks for names its servers answer slowly, or not at all, holds all of them for up to a lookup's whole schedule each, and every other program's lookup is refused with `ERR_RESOURCE_EXHAUSTED` meanwhile. -netd cannot share the slots out itself: a client is a connection, a program -opens as many as it likes, and netd holds no word for the program behind one -(there is no pid-as-authority, by design). The same is true of the -node's places (`system/netstack/node/src/places.rs`), one number across all +A client is a connection, and a program opens as many as it likes. The one +word netstack holds for the program behind one is its connector's badge, the +row and session the supervisor minted it for (`system/netstack/src/serve.rs`), +and anyone can multiply sessions, since every ssh login opens one +(`issues/netstack-replaces-one-programs-cached-answers-to-keep-anothers.md`). +The same is true of the node's places (`system/netstack/node/src/places.rs`), one number across all clients. Exit condition: a lookup is charged to something a program cannot multiply by diff --git a/issues/netstack-refused-accept-hung-waiting-for-a-wake-that-never-came.md b/issues/netstack-refused-accept-hung-waiting-for-a-wake-that-never-came.md deleted file mode 100644 index 4228e3c51e6..00000000000 --- a/issues/netstack-refused-accept-hung-waiting-for-a-wake-that-never-came.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-28 ---- - -# `netd_refused_accept` hung waiting for a wake that never came - -Orchestrator Fast tier at PR #562's head `2a9c77ee`: `FAIL netd_refused_accept: -timed out after 2341s, with the guest still talking 8s ago`. The guest's last -line was `netd_refused_accept: waiting for a wake for the connection an accept -refused for room left, once room returned` — the `wake()` call at -`tests/guest/rust-tests/src/bin/netd_refused_accept.rs:63`, blocked reading -`listener.notify`. #562 changes nothing this test runs: the guest binary, -netd, the netcase config and the harness function that drives it are -identical to main. - -Unconfirmed reading, not established from a kept console: the same run's -`the host's connections ended [Ok(585728), Ok(0), Ok(0), ...]` shows the -host's dial ending `Ok(585728)` rather than with the harness's 120 s -write-stall error. A reset would return the listener's socket to `Listen` -(`settle` in `userland/netd/src/listen.rs:73-85`, the `tcp::State::Closed` -arm re-`listen`s without producing an accept), after which netd owes the -test's `end(held.pop()...)` connection no wake. The guest console for this -run was not kept. - -Exit condition: the mechanism established from a kept console, fixed, and -`netd_refused_accept` green — which needs `netcase_against_host` -(`tests/toyos.rs:10113-10121`) to keep `result.serial` on its error path -instead of dropping it, since nothing else in the tree keeps this test's -console. - -**Owner**: whoever holds `issues/toyos-has-its-own-network-stack.md`. - -**Its test is deleted**: `7967fba91` took `netd_refused_accept` out, and -`git revert 7967fba91` brings it back. diff --git a/issues/netstack-socket-ids-are-ambient.md b/issues/netstack-socket-ids-are-ambient.md index 911c19dc54f..2b4566b1187 100644 --- a/issues/netstack-socket-ids-are-ambient.md +++ b/issues/netstack-socket-ids-are-ambient.md @@ -4,34 +4,34 @@ kind: defect opened: 2026-09-23 --- -# netd's socket ids are numbers any client can name +# netstack's socket ids are numbers any client can name -A netd socket is named by a `u32` id netd hands out, and every request that -acts on a socket — close, shutdown, accept, send, receive — names it by that -id alone. netd checks that the id is in its table and nothing else: **it does -not ask whether the client naming it is the one it was issued to.** Clients -open a fresh connection to netd per request (`toyos::net::NetdConn::connect`), -so there is no client identity for the check to be made against. Any process -holding the `netd` connector can close, shut down or accept on any other -process's socket by guessing its number — and until 2026-09-23 the numbers -counted from 1 in every netd, so guessing was reading. +A stream is its connection and is named by no number, but a listener and a +datagram socket are each named by a `u32` id netstack hands out, and every +request that acts on one (close, accept, send, receive) names it by that id +alone (`Sockets::listener` and `Sockets::datagram`, +`system/netstack/src/serve.rs`). netstack checks that the id is in its table and +nothing else: **it does not ask whether the client naming it is the one it was +issued to.** Clients open a fresh connection per request +(`toyos::net::NetstackConn::connect`), so there is no client identity for the +check to be made against. Any process holding the `netstack` connector can +close or accept on any other process's listener, or receive on its datagram +socket, by guessing its number. -**How it was found.** A service swap (`toyos-swap`) replaces netd while its -clients still hold ids from the netd before. sshd, on noticing, dropped its -listener, which closed the listener's old id — a number that, in the new netd, -was `logd`'s stream connect still waiting for its SYN-ACK. netd removed that -socket from smoltcp's set and left the pending connect holding its handle, and -its next pass panicked: `smoltcp-0.12.0/src/iface/socket_set.rs:116:21: handle -does not refer to a valid socket` (`tests/swapcase`, `swap_crash_rolls_back`, -one run of four). A client's words crashed netd. +**How it was found.** A service swap (`toyos-swap`) replaced netd, netstack's +predecessor, while its clients still held ids from the netd before. sshd, on +noticing, dropped its listener, which closed the listener's old id, a number +that in the new netd was `logd`'s stream connect still waiting for its SYN-ACK, +and netd's next pass panicked in smoltcp (`tests/swapcase`, +`swap_crash_rolls_back`, one run of four). A client's words crashed netd. -**What changed and what did not.** A close now answers a pending connect's -client instead of leaving its handle behind, so no id a client names can panic -netd that way; and netd's ids start at a random point, so two netds' ranges -overlap only by chance. Both bound the harm. Neither is the fix: an id is still -ambient authority over somebody else's socket. +**What bounds it now.** A stream, a pending connect among them, is not an id +any more, and the table starts at a random id, so two netstacks' ranges overlap +only by chance. None of that is the fix: an id is +still ambient authority over somebody else's socket. -**Exit condition.** A socket is named by something only its owner holds — the -data or notify pipe that already travels with it, or a per-socket handle — and -a request naming a socket its connection does not hold is refused. That is an -ABI change to `toyos::net` and netd's wire protocol, and lands on its own. +**Exit condition.** A listener and a datagram socket are each named by +something only their owner holds (a per-socket handle, or the connection that +made it, as a stream already is), and a request naming a socket its connection +does not hold is refused. That is an ABI change to `toyos::net` and netstack's +wire protocol, and lands on its own. diff --git a/issues/netstack-watches-both-pipes-of-every-connection-on-every-pass.md b/issues/netstack-watches-both-pipes-of-every-connection-on-every-pass.md deleted file mode 100644 index 02a6c51c58a..00000000000 --- a/issues/netstack-watches-both-pipes-of-every-connection-on-every-pass.md +++ /dev/null @@ -1,32 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-10-08 ---- - -# netstack watches both pipes of every connection on every pass - -Every pass of netstack's loop submits a watch for each pipe of each live piped -connection, ready or not, changed or not (`system/netstack/src/serve.rs`, -`Sockets::watch`, called by the loop in `main`): an idle connection submits two. A watch replaces its handle's -earlier one, so each is a poll allocated and registered again, with the takes -of the global pipe lock that `arm` makes to read the pipe and find its watches -(`kernel/src/inbox/mod.rs`). - -A poll that asks `OTHER_END_GONE` sits on its own end's watch, which is the -watch that end's readers or writers park on. So every write a client makes -through the kernel posts the poll netstack keeps on that connection's send -pipe, and every read the poll on its receive pipe: netstack's submitter is woken in the kernel for a -look that finds the other end held and arms again. No completion is written and -no pass runs, and the wake is paid per client read and write. - -Read from the code, not measured: nothing in the tree reads netstack's -throughput or its passes per second with connections open, and a duration is -the T14's to say. - -**Exit condition**: a pipe's watch is submitted only when its interest changes -or the kernel has answered it, and a post that is no leaving wakes no -`OTHER_END_GONE` poll; or a throughput reading on the T14, the same transfer -with and without the watches, that shows the cost is nothing. - -**Owner**: whoever holds `issues/toyos-has-its-own-network-stack.md`. diff --git a/issues/netstacks-places-are-one-number-for-every-client.md b/issues/netstacks-places-are-one-number-for-every-client.md index 25729dcc68b..82085a0d16f 100644 --- a/issues/netstacks-places-are-one-number-for-every-client.md +++ b/issues/netstacks-places-are-one-number-for-every-client.md @@ -14,8 +14,7 @@ listen, an accept and a bind with none left are refused with nothing made: `a_connect_past_the_places_is_refused_and_sends_nothing`, `a_listener_holds_a_place_and_a_listen_without_one_makes_nothing` and `a_datagram_socket_holds_a_place_and_a_bind_without_one_makes_nothing` -(`system/netstack/node/tests/listeners.rs`), and in a guest the connect past -them, answered `ERR_RESOURCE_EXHAUSTED` (`netstack_socket_churn`). The sockets +(`system/netstack/node/tests/listeners.rs`). The sockets of the node's own, the responder's for the machine's name and each query's of a lookup, hold no place: a lookup is bounded by `toyos_dns::MAX_LOOKUPS` and its rounds. diff --git a/issues/no-console-between-boot-and-terminal.md b/issues/no-console-between-boot-and-terminal.md index c38aa35aca0..cdea5768341 100644 --- a/issues/no-console-between-boot-and-terminal.md +++ b/issues/no-console-between-boot-and-terminal.md @@ -9,7 +9,8 @@ opened: 2026-08-03 **Measured under metal-sim, and worse than "no scrollback".** With `--metal-sim --mute` and no virtio-console the guest has no output channel at all once the last boot checkpoint has painted: the failure screen ends at -`Boot: complete`, and soundd's null-sink line and netd's exit line — printed seconds later, +`Boot: complete`, and soundd's null-sink line and netd's exit line (soundserver's +and netstack's now), printed seconds later, and read directly off the console by `metal_sim_compositor` on the same machine shape with the 16550 on — reach no pixel and no file. A running ToyOS on the T14 is mute between `Boot: complete` and the moment the compositor's terminal @@ -33,15 +34,15 @@ it by saying where the log will be, and cannot put a line in it. `panic_console::boot_checkpoint` returns immediately once `SCREEN_OWNED_BY_USERLAND` is set -(`kernel/src/drivers/panic_console/mod.rs:639`), and a compositor claiming the +(`kernel/src/drivers/panic_console/mod.rs`), and a compositor claiming the framebuffer sets it. So on `bootable.img` the last kernel screenful ever painted is the one at `Boot: complete`, the desktop overwrites it a few tens of milliseconds later, and no key pauses it. **The durable answer landed and is not this.** "A log sink that survives -userland" is `/system/bin/logd`: the kernel keeps the record ring and the console and -writes no file at all, logd owns `/log` and puts one file per boot there named -for the wall clock, `build/build.rs`'s `every_boot_config_runs_logd` refuses a boot +userland" is `/system/bin/logkeeper`: the kernel keeps the record ring and the console and +writes no file at all, logkeeper owns `/log` and puts one file per boot there named +for the wall clock, `build/build.rs`'s `every_boot_config_runs_logkeeper` refuses a boot config that omits it, and `9ca7631a^:kernel/src/log_file.rs` is deleted. Both ways of reading that file need what this window denies: pulling the stick takes the machine out of the session, and `cat /log/` from the desktop needs input, @@ -52,7 +53,7 @@ reflash.** **Two diagnostic modes exist and neither reaches the shipping image.** `--diag-boot` builds `target/bootable-diag.img` with no compositor in the init -list, so `boot_checkpoint` keeps painting; `screen_diag_boot` gates it, and its +list, so `boot_checkpoint` keeps painting; `screen_diag_boot` gated it, and its `[page n/m]` footer branch has never executed — QEMU's grid fits the log on one page where the T14's 240 columns give two, so that branch is a guard and not a certification until the laptop runs it. `--console-boot` puts `/system/bin/console` @@ -61,8 +62,8 @@ what stops `boot_checkpoint` painting, so a machine that wedges *before* userlan shows nothing at all in that mode; its scrollback seed is a file read once at startup rather than a cursor, and it needs a `/log` that only a USB boot mounts. -**What the panel cannot show even when it does paint.** `glyph` -(`kernel/src/drivers/panic_console/mod.rs:1076`) maps every byte outside +**What the panel cannot show even when it does paint.** `glyph_char` +(`kernel/src/drivers/panic_console/mod.rs`) maps every byte outside `0x20..=0x7E` to `.`, so a three-byte UTF-8 `—` is three dots in three columns instead of one. Measured on this tree: `rg -o 'log!\(' kernel/src | wc -l` is 686 sites and `rg -n 'log!\(.*—' kernel/src | wc -l` is 57 of them. Cosmetic @@ -71,4 +72,4 @@ a line is one display row or two and therefore whether it is on the page the checkpoint paints. The cheap fix is to render the sequence as a single `-`; the honest one is to keep non-ASCII out of `log!`. -`screen_diag_boot` is deleted; `issues/screen-diag-boot-leaves-no-i8042-line-on-the-panel.md` records the commit that restores it. +`screen_diag_boot` is deleted (`8bf3fc24d`), so nothing gates the diag image. diff --git a/issues/null-sink-applies-one-connect.md b/issues/null-sink-applies-one-connect.md index d2d49c182c3..617a143ad92 100644 --- a/issues/null-sink-applies-one-connect.md +++ b/issues/null-sink-applies-one-connect.md @@ -7,6 +7,12 @@ task: 172 # The null sink's mix loop applies exactly one connect and then stops, on the T14 only +**Read against the tree of 2026-08.** soundd is soundserver now, with its own +null-sink thread (`system/soundserver/src/mix.rs`); the kernel's `io_uring` +and its `IO_URINGS` are gone (`232f69bb6`), and +`tests/desktopaudiocase` and `null_sink_shipped_client` with them +(`520c0d129`). Not seen or measured since; the cause was never found. + Read off two T14 boots (`boot7-usba-doom.log`, `boot5-doom-wedge.log`). The shape is the same in both and it is narrow: diff --git a/issues/one-diskserver-session-can-hold-every-device-slot.md b/issues/one-diskserver-session-can-hold-every-device-slot.md index c673ac9befb..bdab47f12f4 100644 --- a/issues/one-diskserver-session-can-hold-every-device-slot.md +++ b/issues/one-diskserver-session-can-hold-every-device-slot.md @@ -4,9 +4,10 @@ kind: defect opened: 2026-09-26 --- -# One blockd session can hold every device slot +# One block session can hold every device slot -blockd's `pull` (`userland/blockd/src/main.rs`) takes what a session has +A disk driver serves its sessions through `toyos_blockserve::Sessions` +(`lib/blockserve/src/sessions.rs`), whose `pull` takes what a session has published while the session's completion ring and the controller have room. The only per-session bound is the ring's: what is on the device and what is posted unread stays below `toyos_blockring::layout::DEPTH`, so one session @@ -19,8 +20,7 @@ On QEMU's controller, four I/O queues of 63 commands, the ring's cap is already below any share a rule would give fewer than five sessions, so a rule there changes nothing that a test can see. -**Exit condition.** blockd bounds what one session holds on the device by a -share of the controller's slots, and a guest test runs two busy sessions -against a controller with one I/O queue (QEMU's NVMe with `max_ioqpairs=1`), -reads each partition's peak outstanding commands off QEMU's trace by LBA, and -goes red with the rule deleted. +**Exit condition.** `Sessions` bounds what one session holds on the device by +a share of the device's slots, and a host test of `pull` over a device model +with fewer slots than `DEPTH` runs two busy sessions, reads each one's peak +outstanding commands, and goes red with the rule deleted. diff --git a/issues/one-write-far-past-a-files-end-holds-data-s-server.md b/issues/one-write-far-past-a-files-end-holds-data-s-server.md index 48183111d8b..bd77d13c3c6 100644 --- a/issues/one-write-far-past-a-files-end-holds-data-s-server.md +++ b/issues/one-write-far-past-a-files-end-holds-data-s-server.md @@ -6,11 +6,11 @@ opened: 2026-09-27 # One write far past a file's end holds DATA's server for as long as the gap -fsd takes a `WRITE` at any offset up to `toyos::fs::MAX_FILE_BYTES` (16 TiB), +DATA's file server (`system/fileserver`) takes a `WRITE` at any offset up to `toyos::fs::MAX_FILE_BYTES` (16 TiB), and `Mounted::resolve_or_alloc_block` bridges the gap from the file's end to it by allocating and zeroing every block between (`issues/a-bcachefs-hole-costs-a-block-write-per-page.md` is why). -fsd is one thread, so the one request runs until the volume is full or the gap +The server is one thread, so the one request runs until the volume is full or the gap is bridged, and every other client of `/apps`, `/config`, `/home` and `/state` waits the whole of it: any program holding `/home` can stop every program's files with one `seek` and one byte. diff --git a/issues/os-toyos-io-traits-keep-a-posix-name.md b/issues/os-toyos-io-traits-keep-a-posix-name.md deleted file mode 100644 index 07f3de0af9b..00000000000 --- a/issues/os-toyos-io-traits-keep-a-posix-name.md +++ /dev/null @@ -1,18 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-08-24 ---- - -# `std::os::toyos::io` names its raw-handle traits `AsRawFd` and `FromRawFd` - -`os::toyos::*` is ToyOS's own extension API and speaks ToyOS, and "fds belong -only in libc jargon" (owner, 2026-08-19). -`sdk/std/os/io.rs` re-exports `std::os::fd`, so -`std::os::toyos::io::{AsRawFd, FromRawFd}` still speak POSIX. -`tests/guest/rust-tests/src/bin/std_fs.rs` is their one caller in this -repository. - -**Exit**: the next time the trait is touched, `AsRawFd` and -`FromRawFd` in `std::os::toyos::io` are renamed to `os::toyos`'s own word for -a raw handle, and `std_fs.rs` uses the new names (owner, 2026-09-30). diff --git a/issues/parallel-tests-red-under-other-suites.md b/issues/parallel-tests-red-under-other-suites.md index 6ab5ae3e5e2..7b174d62eac 100644 --- a/issues/parallel-tests-red-under-other-suites.md +++ b/issues/parallel-tests-red-under-other-suites.md @@ -474,9 +474,9 @@ A flaky test is deleted at once. Each commit below takes one out: and `log-close-cancels-any-syscap`; - `metal_sim_pointer_churn` — `525e59ad1`; - `blocked_dump` — `0a7fc5f70`, red after its retirement here on the - sightings `issues/log-reserve-window-negative-times-out-beside-other-guests.md` - and `issues/process-stats-exits-101-beside-other-guests.md` record; nothing - restores it, since the kernel no longer has the Ctrl+Alt+D report it drove; + sighting `issues/log-reserve-window-negative-times-out-beside-other-guests.md` + records and one beside `process_stats`; nothing restores it, since the kernel + no longer has the Ctrl+Alt+D report it drove; - `metal_sim_input` — `415d9a102`; - `xhci_full_speed_device` — `5e4223525`, with `Profile::MetalFullSpeed`. diff --git a/issues/probe-mounts-on-a-checksum.md b/issues/probe-mounts-on-a-checksum.md index 2778dd35527..78bd535ee05 100644 --- a/issues/probe-mounts-on-a-checksum.md +++ b/issues/probe-mounts-on-a-checksum.md @@ -7,7 +7,7 @@ opened: 2026-08-01 # `probe()` mounts on a checksum, and a stamp over a used volume does not reformat Two things, from reading DATA's probe (`DataVolume::probe` in -`userland/fsd/src/data.rs`, the kernel's `bcachefs_adapter::probe` before it) +`system/fileserver/src/data.rs`, the kernel's `bcachefs_adapter::probe` before it) against the crate: **The threshold does not match the consequence.** `Probed::Mounted` is a @@ -26,16 +26,15 @@ Recommendation, for the owner to decide: - **Done:** `Superblock::check` takes `block_count != device_blocks`, so a volume image copied onto a different disk stops mounting — the same property the designation stamp's block count gives a format. It is not authentication: - an attacker who knows the disk size writes the right number. `volume_from_another_disk` - is the guest arm and `a_volume_copied_onto_a_larger_device_does_not_mount` the - host one. + an attacker who knows the disk size writes the right number. + `a_volume_copied_onto_a_larger_device_does_not_mount` holds it. - **Done, 2026-08-23:** a file's extents are range-checked against the volume in `decode_leaf_value`, so "mounting a hostile volume is merely rude" no longer has an unchecked extent reaching a block read — or a bitmap write — behind it. The residual bound this left — `read_link` sizing one kernel allocation from the volume rather than the smaller heap ceiling — was closed - by giving `Mounted::read_link` a `max_len` its caller passes (fsd's - `MAX_LINK`), refused as `FsError::TargetTooLong` before the allocation. + by giving `Mounted::read_link` a `max_len` its caller passes (the file + server's), refused as `FsError::TargetTooLong` before the allocation. - **The real fix, if the threat model wants one:** read-write requires something the attacker cannot compute — a keyed MAC, or a designation-like stamp — and everything else mounts read-only. ToyOS has no key store and no @@ -58,8 +57,6 @@ read-write means written on sight. already held a ToyOS volume does **not** cause a reformat. `designate_for_format` writes block 0 only, `Superblock::read` falls back to the backup superblock at the last block when block 0 does not parse, and a stamp does not parse — so -`mount()` succeeds from the backup and `probe()` returns `Ours`, mounting the old +`mount()` succeeds from the backup and `probe()` answers `Mounted`, mounting the old volume. Harmless for the harness, which stamps freshly created sparse files, but it means "re-stamp the disk to reformat `/home`" is not a workflow that works. -`probe`'s doc comment claims the decision comes "from one read of block 0"; it -comes from two, and the second one wins. diff --git a/issues/process-stats-exits-101-beside-other-guests.md b/issues/process-stats-exits-101-beside-other-guests.md deleted file mode 100644 index aec736b19ca..00000000000 --- a/issues/process-stats-exits-101-beside-other-guests.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-06 ---- - -# `process_stats` exits 101 beside other guests - -Red once in a full `cargo test` on this dev host while the scanout fold was -being gated: `FAIL process_stats: exit code Some(101)` after 4 s, with no -assertion text carried out of the guest, and the harness's re-run pass reported -`ALONE process_stats: GREEN — it fails only beside other guests, so its -Sched::Parallel is wrong.` It is the third name to do this in three foldings of -this branch — `log_reserve_window_negative` and `blocked_dump` were the other -two, each a different name on a different run — and nothing in the diff being -gated goes near any of them. -`issues/blocked-time-is-invisible-while-the-park-lasts.md` names -`process_stats` as where a defect was found, not as a name that flakes, so this -is its own entry. Nothing here investigates the mechanism: `ALONE: GREEN` is the -harness naming a hypothesis, and one red is not a rate. - -Two more, each with its assertion. `640r3-loaderlines-r3-whole.log` (`wt/toyos-loaderlines` -`6e0d7da82`, "fastest boot 480 ms … ceilings paid at 1.00x") at `process_stats.rs:280`: "a -child that parked writing a full connection charged 0 ns to ipc and 0 ns to pipe". -`648-648-whole.log` (`wt/toyos-proclife1` `60ec86df3`, load average 84) at -`process_stats.rs:263`: "a child that parked reading a connection charged 0 ns to ipc and 0 ns -to pipe". Neither branch touches the test, `WaitClass` or the charge. The premise both arms read -is `roster::await_true` seeing the child's main thread `BLOCKED`, and nothing ties that park to -the connection: a park on anything else before the child reaches its `read` or `write` -satisfies it, the parent releases, and the connection's wait never parks. The assertion prints -two of the five classes, so which park was charged is not on record. Owner: the orchestrator. - -Exit: each arm waits for a park it can name as the connection's, so a park on anything else -fails the arm by name, and `process_stats` passes on the T14's shared boot. - -**Its test is deleted**, as a flaky test is: `4a5b228d2` took -`process_stats` out, and moved the nightly `tcg` job's one test to -`empty_dir_stat`. `blocked_dump` is -deleted too: `issues/parallel-tests-red-under-other-suites.md` records -the commit. diff --git a/issues/remove-dir-all-empties-a-directory-and-leaves-it.md b/issues/remove-dir-all-empties-a-directory-and-leaves-it.md index fa66c76544e..2820398632b 100644 --- a/issues/remove-dir-all-empties-a-directory-and-leaves-it.md +++ b/issues/remove-dir-all-empties-a-directory-and-leaves-it.md @@ -4,40 +4,18 @@ kind: defect opened: 2026-09-05 --- -# `std::fs::remove_dir_all` empties a directory and never removes it +# pkg keeps a tree walk for a `remove_dir_all` that no longer leaves the directory -`sdk/std/sys/fs.rs`'s `remove_dir_all` walks the directory, -unlinks every file and recurses into every subdirectory, and returns `Ok(())` -without ever calling `rmdir` on anything — its own path included. Every -directory in the tree it walked survives, empty. +std's `remove_dir_all` (`sdk/std/sys/fs.rs`) once emptied a directory and +returned `Ok(())` without removing it or any directory under it, so +`pkg remove gbae` reported success and the next `pkg install` of the same +archive answered `/apps/gbae is already there`. It now ends with `rmdir(path)` +at every level, as it has since the backend moved to `sdk/std` (`3831f291b`). -`SYS_RMDIR` (54) exists and `rmdir` in the same file wraps it; nothing calls it -on this path. +What is left is the workaround. `system/pkg/src/main.rs`'s `remove_tree` gives +that defect as the reason it is not `fs::remove_dir_all`, in a doc comment that +is now false. Its other reason may stand: it walks with an explicit stack where +std's recurses once per level. -## Reproduction - -Measured in `pkg_install_gbae`'s guest on 2026-09-05, before `system/pkg` -stopped using it. `pkg remove gbae` reported success and `pkg list` was empty — -`/apps/gbae/manifest.toml` was gone, so init refused a launch out of it — and -the next `pkg install` of the same archive answered - - pkg: /apps/gbae is already there — remove it first, this installer does not replace - -The kernel's own syscall census for that process is the shorter proof: four -`SYS_DELETE` and no `SYS_RMDIR` at all. - - [kernel 1.004 cpu1] syscalls: pid=14 total=18 syscall_wall=11ms 0=1 1=2 6=1 - 9=1 10=1 14=1 17=1 18=4 63=2 72=1 73=2 91=1 - -`/system/bin/rm -r` is the other caller (`system/toybox/src/rm.rs:30`) and has -the same hole. - -## Exit condition - -`remove_dir_all` removes the directory it was given and every directory under -it. Closes when that lands and `system/pkg`'s own `remove_tree` -is deleted with it — that walk exists only because this one does not finish. - -The fix is one `rmdir(path)` after the loop, and it is in a sysroot -source (`sdk/std`), so it lands on its own branch with the machine's -sysroot claim. +**Exit**: `remove_tree` either goes for `fs::remove_dir_all`, or keeps a doc +comment that states only the reason that holds. diff --git a/issues/screen-diag-boot-leaves-no-i8042-line-on-the-panel.md b/issues/screen-diag-boot-leaves-no-i8042-line-on-the-panel.md deleted file mode 100644 index 903b9fae732..00000000000 --- a/issues/screen-diag-boot-leaves-no-i8042-line-on-the-panel.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-27 ---- - -# `screen_diag_boot` finds no `i8042:` line on the panel - -`screen_diag_boot` fails with `"i8042:" is not on screen five -seconds after the boot finished`. The decoded panel ends at `Boot: complete`, -and no i8042 line is painted in the five seconds after it. The i8042 health -line (`report_health`, `to_screen`) never reaches the panel on the diag image. - -- Green on main's nightly at 3f46a019 (run 36111884575). -- Red at e8d7c9c0 (run 36228604597). -- Red at fd62f567 (run 36278449733, guest (9)). -- Red alone at 9583d913 merged with 5e446e5c: `cargo test --test toyos-build - -- --nightly screen_diag_boot` EXIT=1. - -The range 3f46a019..e8d7c9c0 holds fourteen landings and is not bisected. -The candidates that touch the idle path, the console or the log are #502 -(a288537b), #492 (b0adc600) and #506 (265a0fce). - -**Exit**: the landing named by a build at it and at its parent, and -`screen_diag_boot` green on a nightly. - -**Its test is deleted**, as a red test nobody has a fix for is: `8bf3fc24d` -took `screen_diag_boot` out, and `git revert 8bf3fc24d` brings it back. diff --git a/issues/sharedmemory-slices-rest-on-a-size-nobody-checks.md b/issues/sharedmemory-slices-rest-on-a-size-nobody-checks.md index 0e1779ab49d..75743c95967 100644 --- a/issues/sharedmemory-slices-rest-on-a-size-nobody-checks.md +++ b/issues/sharedmemory-slices-rest-on-a-size-nobody-checks.md @@ -11,8 +11,11 @@ are safe functions that make a slice of `size` bytes over the mapping. The type enforces neither premise that makes this sound: - `adopt` is safe and takes `size` from its caller without checking it against - the region the kernel mapped. A server that adopts a peer's region at the - peer's declared length reads and writes past it in safe code. + the region the kernel mapped. A peer's region is mapped through `lent`, which + refuses a length the kernel's `fstat` does not answer; `adopt` is held to the + kernel's own regions (a device window, a DMA grant, the scanout) only by its + doc comment, so a caller that adopts a peer's region at the peer's declared + length reads and writes past it in safe code. - `shm_map` is idempotent, so `share()` followed by `adopt` gives two `SharedMemory` values over one mapping in one process. `as_mut_slice` on one then aliases `as_slice` or `as_atomic` on the other. diff --git a/issues/shell-c-discards-the-cwd-its-launch-carried.md b/issues/shell-c-discards-the-cwd-its-launch-carried.md deleted file mode 100644 index 19449f61c4d..00000000000 --- a/issues/shell-c-discards-the-cwd-its-launch-carried.md +++ /dev/null @@ -1,37 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-05 ---- - -# `shell -c` discards the working directory its launch carried - -`system/shell/src/main.rs:27` sets the working directory to `/` before it runs -the line: - -``` - if args.len() >= 3 && args[1] == "-c" { - let input = args[2..].join(" "); - let _ = env::set_current_dir("/"); - execute_line(&input); -``` - -A launch carries a working directory — `toyos::launch::Launch::cwd`, which -`/system/bin/init` applies with `Command::current_dir` before it spawns — so a -caller that asked for one is overruled, and every relative path in the line -resolves against `/` instead. The interactive path has the same shape one line -down, where `set_current_dir(&home)` is at least a stated policy. - -## Reproduction - -Spawn `/system/bin/shell` with `Command::current_dir("/home/toy")` and the -arguments `-c`, `pwd`. It answers `/`. - -Read from the source rather than measured: `pkg_install_gbae`'s -`relative-path` probe is written around this behaviour (its dotted paths are -rooted at `/` for that reason) and so does not isolate it. - -## Exit condition - -`shell -c` runs its line in the working directory its launch gave it, and a -guest probe spawns it with a `current_dir` and reads `pwd` back. diff --git a/issues/smp-ap-hole-and-log-reserve-window-red-under-a-loaded-host.md b/issues/smp-ap-hole-and-log-reserve-window-red-under-a-loaded-host.md index 59a21bd77f6..4d9997039aa 100644 --- a/issues/smp-ap-hole-and-log-reserve-window-red-under-a-loaded-host.md +++ b/issues/smp-ap-hole-and-log-reserve-window-red-under-a-loaded-host.md @@ -4,41 +4,30 @@ kind: defect opened: 2026-09-16 --- -# `smp_failed_ap_leaves_no_hole` and `log_reserve_window` red under a loaded host +# The first spawn answered `WouldBlock` under a loaded host -Both `Sched::Parallel`, both first sightings, both in one `cargo test` fast-tier -run on `wt/toyos-usbhang` sharing the host's twelve guest slots throughout with -two other worktrees' suites (`[host-slots]` names `pid 49668` and `pid 2726` -holding slots across the whole run). The branch's own diff at the time touches -neither SMP bring-up, `spawn_init`, nor `rootfs.rs`, so it is filed rather than -chased. Same shape as -`issues/parallel-tests-red-under-other-suites.md`, filed separately -because that file is not this agent's to append to. +`smp_failed_ap_leaves_no_hole` red once in a `cargo test` fast tier on +`wt/toyos-usbhang`, sharing the host's twelve guest slots throughout with two +other worktrees' suites (`[host-slots]` named `pid 49668` and `pid 2726` +holding slots across the run). The branch touched neither SMP bring-up nor the +first spawn: -`smp_failed_ap_leaves_no_hole` FAILED at `[kernel 2.605 cpu0] PANIC: panicked -at src/loader/mod.rs:928:50: spawn_init: failed to spawn: WouldBlock` — a -`WouldBlock` surfacing from a call the boot path expects to complete, this -sibling file's canonical shape for a starved host (`fs_transactional`'s -`cleanup: Kind(WouldBlock)` is its precedent). `ALONE: GREEN`, -`cargo test -- smp_failed_ap_leaves_no_hole` alone immediately after, PASS in 2s. +`[kernel 2.605 cpu0] PANIC: panicked at src/loader/mod.rs:928:50: spawn_init: +failed to spawn: WouldBlock`, a `WouldBlock` from a call the boot path expects +to complete (`fs_transactional`'s `cleanup: Kind(WouldBlock)` was its +precedent). `ALONE: GREEN`, alone immediately after, in 2 s. -`log_reserve_window` FAILED at `[kernel 2.483 cpu0] PANIC: panicked at -src/rootfs.rs:71:9: boot: root=5481a0b70d73284f6ab07dd84e083efa matches 0 of -the 0 TOYOS-ROOT partition(s) this machine carries`, on both this boot and its -retry. The cause is upstream and in the same capture: `usb-storage: 00:01.0 -slot 1 transport broke on SCSI 0x28: no answer in the data phase in 2000 ms` — -the root disk's own read missed its 2000 ms budget under the host's load, so -the GPT scan that follows legitimately found no partition to mount. `ALONE: -GREEN`, `cargo test -- log_reserve_window` alone immediately after, PASS in 3s. +The first spawn is `spawn_supervisor` now (`kernel/src/loader/mod.rs`), and it +still panics on any refusal. What answered `WouldBlock` was never found. -Not investigated further. +The same run red `log_reserve_window` on a ROOT read that missed its 2000 ms +budget in the kernel's usb-storage driver. That half is gone: the kernel reads +ROOT only from the image the loader read into memory (`kernel/src/rootfs.rs`) +and drives no USB (`13ed7b4f4`). -**Both tests are deleted**, as flaky tests are. `aedcf17dc` took -`smp_failed_ap_leaves_no_hole` out with `smp_hole_shootdown`, the binary only -it ran, and `git revert aedcf17dc` brings it back. `4db54ffa5` took -`log_reserve_window` out with `log-nested-reserve`; `3b8102cf5` then took the -nest vector and the log gate it rode. +**Both tests are deleted**, as flaky tests are: `aedcf17dc` took +`smp_failed_ap_leaves_no_hole` out with `smp_hole_shootdown`, and `4db54ffa5` +and `3b8102cf5` `log_reserve_window` with what it rode. -**Exit**: a cause for `spawn_init`'s `WouldBlock` and for a root read that -misses its budget under host load, and both tests restored and green beside -other guests. +**Exit**: a cause for the first spawn's `WouldBlock`, fixed there, or the +first spawn shown unable to answer it. diff --git a/issues/soundserver-past-due-wake-max-1.md b/issues/soundserver-past-due-wake-max-1.md index 7cf16c2191a..7005895d2d6 100644 --- a/issues/soundserver-past-due-wake-max-1.md +++ b/issues/soundserver-past-due-wake-max-1.md @@ -4,13 +4,15 @@ kind: defect opened: 2026-08-08 --- -# Soundd spells a past-due wake `.max(1)`, which is a one-nanosecond timer +# soundserver's null sink spells a past-due wake `.max(1)`, which is a one-nanosecond timer -`userland/soundd/src/main.rs:955` and `:1327` both compute a poll timeout as -`…saturating_sub(clock_nanos()).max(1)`, with a comment saying the `1` is there -because `0` is the kernel's non-blocking sentinel. But non-blocking is exactly -what a grid point already in the past wants, so `0` is the right answer and `1` -is a park on a deadline that has passed. It is the trigger on the fifth of the +The null sink's wait in soundserver's mix loop (`system/soundserver/src/mix.rs`) +computes its poll timeout as `next_period_ns.saturating_sub(now).max(1)`, the +`1` there because `0` is the kernel's non-blocking sentinel. But non-blocking +is exactly what a grid point already in the past wants, so `0` is the right +answer and `1` is a park on a deadline that has passed. A device's wait no +longer does this: `armed` arms the next future grid point when the predicted +one has passed. soundd had the pattern in both, and it was the trigger on the fifth of the eight T14 boots captured 2026-08-08 while chasing the cpu0 stall, where cpu0 and cpu1 both stopped within 100 ms of `soundd: resumed`. diff --git a/issues/sshserver-authorized-keys-unprotected.md b/issues/sshserver-authorized-keys-unprotected.md index cb42c069efc..964c40bdda5 100644 --- a/issues/sshserver-authorized-keys-unprotected.md +++ b/issues/sshserver-authorized-keys-unprotected.md @@ -4,12 +4,14 @@ kind: defect opened: 2026-08-06 --- -# Sshd's keys are as protected as any other file, which is not at all +# sshserver's keys are as protected as any other file, which is not at all -`/state/sshd/host_ed25519` is the machine's SSH private key and -`/state/sshd/authorized_keys` is the list of who may log in. There is no -user model and no file permissions, so **any process on the machine can read -the first and rewrite the second** — the second being the one that matters: +`/state/sshserver/host_ed25519` is the machine's SSH private key and +`/state/sshserver/authorized_keys` is the list of who may log in. There is no +user model and no file permissions, and every program the image ships sees the +whole tree (`Program::view`, `lib/manifest/src/lib.rs`; only an installed +package is held to its own directories), so **any shipped program can read the +first and rewrite the second** — the second being the one that matters: appending a line to it is a remote login, and nothing stops a process doing it. This is not an sshd defect and cannot be fixed inside sshd. It is the absence of diff --git a/issues/sshserver-holds-one-channel-and-does-not-say-so.md b/issues/sshserver-holds-one-channel-and-does-not-say-so.md index cfe2f68c6a6..7e30cf9534c 100644 --- a/issues/sshserver-holds-one-channel-and-does-not-say-so.md +++ b/issues/sshserver-holds-one-channel-and-does-not-say-so.md @@ -6,7 +6,7 @@ opened: 2026-09-08 # A second session channel on one sshd connection aliases the first one's input -`userland/sshd`'s session handler keeps one channel and one input sender: +`system/sshserver`'s session handler keeps one channel and one input sender: `SshSession::channel` holds the channel a request has not taken yet, and `SshSession::input` is where `Handler::data` puts everything a client sends. Neither is keyed on a `ChannelId`, and `channel_open_session` accepts every diff --git a/issues/sshserver-still-speaks-rsa.md b/issues/sshserver-still-speaks-rsa.md index ecc9f78face..663365c7375 100644 --- a/issues/sshserver-still-speaks-rsa.md +++ b/issues/sshserver-still-speaks-rsa.md @@ -6,7 +6,7 @@ opened: 2026-09-24 # sshd still carries RSA -`userland/sshd/Cargo.toml` enables russh's `rsa` feature, which locks the `rsa` +`system/sshserver/Cargo.toml` enables russh's `rsa` feature, which locks the `rsa` crate at 0.9.10. That crate has an unfixed timing side channel (the Marvin attack, RUSTSEC-2023-0071) in its private-key operations. sshd's host key is Ed25519, so those operations never run here, but RSA is code with no use: diff --git a/issues/std-answers-every-spawn-refusal-but-notfound-as-other.md b/issues/std-answers-every-spawn-refusal-but-notfound-as-other.md deleted file mode 100644 index fb38cc0f617..00000000000 --- a/issues/std-answers-every-spawn-refusal-but-notfound-as-other.md +++ /dev/null @@ -1,25 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-25 ---- - -# std answers every spawn refusal but `NotFound` as `Other` - -std's direct spawn (`sdk/std/sys/process.rs`, the -`spawned.map_err` in `Command::spawn`) maps -`SyscallError::NotFound` to `io::ErrorKind::NotFound` and every other -`SyscallError` to `io::ErrorKind::Other`, and keeps no raw code. So a -`Command::current_dir` the kernel refuses as not absolute — `InvalidArgument` -from `SYS_SPAWN` — reaches the caller as `Other`, the same word as a refused -endowment, an exhausted table or a bad ELF, and `{e}` prints `other error`. -`issues/a-spawn-of-echo-was-refused-with-an-error-nothing-names.md` is -a failure this mapping already made unreadable. - -`tests/guest/rust-tests/src/bin/spawn_cwd.rs` checks the relative and empty -refusals by name only on its raw `SYS_SPAWN` arm for this reason. - -**Exit**: every `SyscallError` `SYS_SPAWN` can answer has its own -`io::ErrorKind` (or a raw code `raw_os_error` returns), and a `Command` whose -relative or empty directory reaches the kernel is asserted refused as -`InvalidInput` by name. diff --git a/issues/std-calls-undated-what-it-did-not-stat.md b/issues/std-calls-undated-what-it-did-not-stat.md deleted file mode 100644 index dfc52ebb86d..00000000000 --- a/issues/std-calls-undated-what-it-did-not-stat.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -status: open -kind: defect -opened: 2026-09-28 ---- - -# std calls undated what it did not stat, and sets no mtime it is asked to - -`DirEntry::metadata` answers `mtime: 0` without asking, so -`Metadata::modified` reports a file the kernel has a stamp for as undated: a -program walking a tree through `read_dir` and comparing -`entry.metadata()?.modified()?` fails on every entry. `fs::metadata` of a -directory and `fs::symlink_metadata` of a symlink answer `mtime: 0` too -(`stat`'s `is_dir` arm and `lstat`'s `readlink` arm), so `modified()` on either -is `Err(Unsupported)`. - -And `File::set_times`, `fs::set_times` and `set_times_nofollow` return `Ok(())` -having set nothing, so a tool that stamps an output (`touch`, a build system's -restat) is told it did. - -**Exit condition.** `DirEntry::metadata`, a directory's `stat` and a link's -`lstat` answer the mtime the kernel keeps for that name, and a time-setting -call sets the stamp through the kernel or is refused as `Unsupported`. diff --git a/issues/std-lookup-host-answers-no-address-as-other.md b/issues/std-lookup-host-answers-no-address-as-other.md index 7cbebea3905..a3bf8ee296d 100644 --- a/issues/std-lookup-host-answers-no-address-as-other.md +++ b/issues/std-lookup-host-answers-no-address-as-other.md @@ -7,14 +7,13 @@ opened: 2026-09-26 # std's lookup_host answers a name with no address as `Other` std's `lookup_host` (`sdk/std/sys/net/connection.rs`) -answers netd's empty answer with `io::ErrorKind::Other` and the message -`DNS lookup failed: no results`, and every netd error that is not one of six -kinds with `Other` and `netd error`. A program asking for a name therefore -cannot tell a name that does not exist from a resolver that failed without -matching on the message, which is what `dns_resolve` in `tests/toyos.rs` does -(`DNS_NO_ADDRESS`). netd itself answers the two differently: an empty answer -for a name with none, and `ERR_OTHER` with a named line for a lookup that ended -without one (`userland/netd/src/main.rs`, `process_pending`). +answers netstack's empty answer with `io::ErrorKind::Other` and the message +`DNS lookup failed: no results`, and a netstack refusal that is +`ResourceExhausted`, `Protocol` or `Io` with `Other` and `netstack error` +(`net_err_to_io`). A program asking for a name therefore cannot tell a name +that does not exist from a resolver that failed without matching on the +message. netstack itself answers the two differently: an empty answer for a +name with none, and a refusal for a lookup that ended without one. Exit condition: a missing name and a failed lookup reach a std caller as two -different `ErrorKind`s, and `dns_resolve` reads the kind. +different `ErrorKind`s. diff --git a/issues/std-maps-a-device-error-to-other-not-uncategorized.md b/issues/std-maps-a-device-error-to-other-not-uncategorized.md index 653a40240d4..bf74f1d9f3a 100644 --- a/issues/std-maps-a-device-error-to-other-not-uncategorized.md +++ b/issues/std-maps-a-device-error-to-other-not-uncategorized.md @@ -6,36 +6,18 @@ opened: 2026-09-01 # std on ToyOS answers `Other` for a device error, where upstream says `Uncategorized` -`sdk/std/sys/pal/mod.rs`'s one -`SyscallError -> ErrorKind` map sends `SyscallError::Io` to -`io::ErrorKind::Other`. Every other platform in the fork spells that -`Uncategorized` — `sys/io/error/unix.rs:189`, `hermit.rs:29`, `uefi.rs:53`, -`motor.rs:55` — and `Other` is documented upstream as a *custom* error, not as -the one a platform reaches for when it has no better word. +`sdk/std/sys/pal/mod.rs`'s one `SyscallError -> ErrorKind` map, `to_io_error`, +sends `SyscallError::Io` to `io::ErrorKind::Other`. Every other platform in the +fork spells that `Uncategorized` (`sys/io/error/unix.rs`, `hermit.rs`, +`uefi.rs`, `motor.rs`), and `Other` is documented upstream as a *custom* error, +not as the one a platform reaches for when it has no better word. -It is deliberate, and this is the record rather than a rediscovery. The word is -load-bearing in userland: `userland/logd/src/policy.rs:12` states "a stick that -cannot flush is `io::ErrorKind::Other` — `SyscallError::Io`", and -`a_device_that_cannot_flush_still_ends_the_volume` at `:187` asserts against -that spelling. Moving the std arm alone would leave that header false while -nothing failed, because `fate`'s non-`WouldBlock` arms are catch-alls -(`_ => Fate::GiveUp` at `:166`) and the *behaviour* is identical either way. -That is the whole reason it was left: a silent divergence between a module -header and the code it describes is worse than a non-idiomatic arm. +It was left because userland leaned on the spelling: logd's flush policy read a +stick that cannot flush as `ErrorKind::Other`, and a guest gate required +`kind=Other` for a boot volume that refused every read. Both are gone (logd is +logkeeper, which reads no kind; `tests/common/volumes.rs` is deleted), and no +`ErrorKind::Other` under `system/` means "the device refused". ## Exit condition -One change that moves all four together: `Io => ErrorKind::Uncategorized` in -that map, `userland/logd/src/policy.rs`'s header reworded to name the new -spelling, its test's constructed kind moved with it, and -`boot_volume_metadata_error` in `tests/common/volumes.rs`, which requires the -guest to print `kind=Other` for a boot volume that refused every read — a fourth -site outside `userland/` and spelled `kind=Other`, so the grep below misses it -twice over and it is named here instead. It is closed when `rg -'ErrorKind::Other'` over `userland/` returns nothing that means "the device -refused" and that gate requires the new word. - -Owned by whoever next touches either side; neither half is worth a landing on -its own, and the fork's upstream-mergeability argument is the thing that will -eventually force it, since `Other` in a platform map is what an upstream -reviewer asks about first. +`Io => ErrorKind::Uncategorized` in `to_io_error`. diff --git a/issues/std-says-a-launch-moves-its-handles-to-the-launcher-even-when-the-move-is-refused.md b/issues/std-says-a-launch-moves-its-handles-to-the-launcher-even-when-the-move-is-refused.md index 0dda673637a..80263bdab91 100644 --- a/issues/std-says-a-launch-moves-its-handles-to-the-launcher-even-when-the-move-is-refused.md +++ b/issues/std-says-a-launch-moves-its-handles-to-the-launcher-even-when-the-move-is-refused.md @@ -4,31 +4,23 @@ kind: defect opened: 2026-10-05 --- -# std says a launch moves its handles to the launcher, even when the move is refused +# std says a provided connector leaves the parent only on a successful spawn -`toyos::launch::launch` consumes every handle a `Launch` names: the kernel -moves them to the launcher, or refuses the move and `Connection::send_with` -closes them in the caller (`sdk/toyos/src/ipc.rs`, `sdk/toyos/src/launch.rs`). The -prose of std's ToyOS backend about the same handles says something else: +A spawn that provides a connector goes through the launcher, and the send moves +every handle it names: the kernel moves them to the launcher, or refuses the +move and `Connection::send_with` closes them in the caller. Every answer but +`LaunchError::NotSent` is past that point (`Spawn::ask_launcher`, +`sdk/toyos/src/spawn.rs`, which says so). -- `sdk/std/sys/process.rs`, the comment over the match on - `launch`'s answer: "The launcher releases what it took." On a refused move - the launcher took nothing; this process closed them. -- `sdk/std/os/process.rs`, `CommandExt::provide` (and - `endow`, whose rule it cites): the handle leaves the parent "after a - successful spawn". A `provide`d connector also leaves the parent on a spawn - that failed after the send — a refused move, `Refused`, `Gone`, a lost - answer — and std's caller cannot keep it. `endow` alone, which goes through - `SYS_SPAWN` and not the launcher, is unchanged. +std's documentation of the same handles says otherwise. +`CommandExt::provide` (`sdk/std/os/process.rs`) says the connector is moved +"like `endow`", whose rule is that the handle leaves the parent "after a +successful spawn". A provided connector also leaves the parent on a spawn that +failed after the send (a refused move, `Refused`, `Gone`, a lost answer), and +std's caller cannot keep it. An endowed spawn goes through `SYS_SPAWN`, never +the launcher (`route`), so `endow`'s own rule holds. -std's code is right on every arm: it releases its duplicates only on -`LaunchError::NotSent`, which still means nothing was consumed. Only the -prose is false. +**Owner:** the next commit that touches `sdk/std/os/process.rs`. -**Owner:** the next commit that touches `sdk/std/sys/process.rs` or -`sdk/std/os/process.rs`. - -**Exit:** a commit in which `provide`'s -doc says the connector leaves the parent once the launch is sent, whatever the -spawn answers, and the comment over `launch`'s answer names the close on a -refused move. +**Exit:** `provide`'s doc says the connector leaves the parent once the launch +is sent, whatever the spawn answers. diff --git a/issues/std-set-times-answers-ok-and-sets-nothing.md b/issues/std-set-times-answers-ok-and-sets-nothing.md new file mode 100644 index 00000000000..fd03d2fd1ca --- /dev/null +++ b/issues/std-set-times-answers-ok-and-sets-nothing.md @@ -0,0 +1,14 @@ +--- +status: open +kind: defect +opened: 2026-09-28 +--- + +# std's `set_times` answers `Ok` and sets nothing + +`File::set_times`, `fs::set_times` and `set_times_nofollow` +(`sdk/std/sys/fs.rs`) return `Ok(())` having set nothing, so a tool that stamps +an output (`touch`, a build system's restat) is told it did. + +**Exit condition.** A time-setting call sets the stamp through the kernel or +the file server that holds the file, or is refused as `Unsupported`. diff --git a/issues/stdio-buffering-saw-one-long-line-arrive-as-two.md b/issues/stdio-buffering-saw-one-long-line-arrive-as-two.md index c7508767a2b..e5781b4b4cb 100644 --- a/issues/stdio-buffering-saw-one-long-line-arrive-as-two.md +++ b/issues/stdio-buffering-saw-one-long-line-arrive-as-two.md @@ -22,8 +22,9 @@ head minutes later was green (345 passed). Not measured: whether the base reds the same way, or at what rate. The split is either the guest writing the line in two console writes with something -between them, or the host's capture cutting it (`tests/common/console.rs` -unjoins a line without a trailing newline from the next writer's). Which one +between them, or the host's capture cutting it (`tests/common/console.rs`, +deleted since in `520c0d129`, unjoined a line without a trailing newline from +the next writer's). Which one is the question; `console_line_atomicity` is the gate that should hold the first.