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.