Skip to content

fd: one descriptor table numbers every raw fd, and a child's descriptors are its fd:<n> endowments - #978

Merged
Japabu merged 2 commits into
mainfrom
wt/toyos-fd-table
Oct 21, 2026
Merged

Japabu merged 2 commits into
mainfrom
wt/toyos-fd-table

Conversation

@Japabu

@Japabu Japabu commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

Draft, PR 2 of 4 in the descriptor work (fd-design.md revision 8; PR 1 was #975). It makes one table number every raw fd in a process, and makes a child's descriptors its fd:<n> endowments. PR 3 puts served files on the table; PR 4 adds the owner's inheritance rule (Q9), and its seam is the per-descriptor inheritable flag this PR adds.

Problem

  • No home for POSIX descriptor semantics. A raw fd on ToyOS was a kernel handle. So C's small descriptor numbers, std's raw-fd interop, and POSIX's lowest-free numbering, dup sharing an object, dup2 answering its target and per-descriptor close-on-exec had nowhere to live.
  • libc worked around it:
    • it kept close-on-exec marks of its own;
    • it numbered sockets from 1024, where close ended the process;
    • its dup2 could not answer POSIX's number.
  • Children got their descriptors from a kernel slot map. Their numbers were kernel slot positions.

Resulting behaviour

  • toyos::fd: the process's one descriptor table.
    • Owner: one copy of toyos owns it, the one whose fd-table feature is on. That is std's copy (rustc-dep-of-std) in a Rust program, or libc's own in a C program (fd-owner, its C-only build). Every copy reaches the table only through toyos_fd_*.
    • Objects: a descriptor names a kernel handle, or an object a library keeps itself (Object::opaque). Descriptors are numbered lowest-free, each with its own inheritable flag.
    • Lifetime: a call holds its object, so a racing close makes the next lookup BadFd, never a call on a closed handle. One exception: libc's poll takes each handle out of its call and waits on it afterwards, so a close racing a poll is still a handle fault, as it was before this PR.
    • Endowments: an fd:<n> label is read as canonical decimal only. One whose number is already open is refused at the boundary and its handle closed.
    • No panic under the lock: the table's operations answer a broken table as a fault, and the owner panics only once the lock is released. A panic writes to stderr, which is a descriptor.
    • No initialised data: the empty table is all zeros, so it lands in .bss. So are net::stream's tables, which every program now links through the table's release of a handle.
  • std:
    • Descriptors: File, pipes, stdin and the network types are table descriptors, as on unix. OwnedFd closes and duplicates through the table (rust fork a56b4057c).
    • AsRawHandle: std::os::toyos::io::AsRawHandle answers the kernel handle behind a descriptor. It is the only new ToyOS-specific surface. Console and terminal use it, as does the mio fork's selector; no portable app needs it.
    • Closed stdio: a closed stdin reads its end, with no probe to race a close, and a closed stdout drops its bytes, as std on unix does.
    • Streams stay put: toyos::fd::copy refuses a stream's end by the handle the descriptor names, so a stream dup2'd onto stdio and inherited is refused, as every other spawn path refuses one. File::as_child_stdio goes through the same check.
  • libc: every descriptor call is on the table.
    • dup2 answers its target, and F_DUPFD_CLOEXEC is supported.
    • fcntl's close-on-exec is the table's per-descriptor inheritable flag.
    • A socket is an object of libc's own, so close releases it. issues/libc-close-of-a-socket-is-unproven-on-a-connected-socket.md keeps its last exit, a C case. Its tag is disjoint from socket2's.
    • posix_spawn refuses a descriptor it holds but cannot give a child (a stream's end, or a socket), rather than starting the child without it.
    • poll answers POLLNVAL for a descriptor that is not open, as POSIX has it.
  • socket2 fork (2aa6b0049e70): its sockets are table descriptors, created not inherited (unix's SOCK_CLOEXEC), and try_clone is a dup that is not inherited either (F_DUPFD_CLOEXEC). A socket's key is reused once it is gone, rather than counted into the tag after 2^24 sockets.
  • Children:
    • Every descriptor a child is given, stdio included, is an endowment labelled fd:<n>, which its table installs at that number on first use.
    • The kernel spawn slot map, SYS_HANDLE_DUP_SLOT (129) and SYS_HANDLE_DUP_AT (74) are deleted, and so are HandleTable::for_child, install_at, held_at and duplicate_entry.
    • An endowed console is minted for the child, as the slot map minted one.
    • The launch protocol's stdio slots are descriptors 0 to 2, which the supervisor endows.
    • The supervisor's own stdio is the kernel's three fd: endowments, with its log ring put at 1 and 2 by the table's dup2.
    • Relocation's refusal line finds fd:2 in the endowment table, using only the stack.
  • Behaviour changes, each deliberate:
    • A C child asking about a descriptor it lacks is answered EBADF, as POSIX has it, rather than ended by a handle fault. spawn_cloexec.c asserts that, and spawn_signaled.c loses its handle-fault arm; the status mapping stays host-tested.
    • A refused relocation in a child that has no fd:2 exits 127 silently, rather than faulting.

Fork commits (append-only branches)

fork commit what
ToyOSOrg/mio toyos 9f34d36c the selector watches as_raw_handle(); bell is a RawHandle
ToyOSOrg/socket2 toyos d3885019, 2aa6b0049e70 sockets are table descriptors (adds a toyos dependency); not inherited, keys reused
ToyOSOrg/rust main a56b4057c OwnedFd closes and duplicates through the table

Cargo.lock names the mio and socket2 revisions and nothing else. A plain cargo update also re-resolved windows-sys, and I didn't take that. The rust gitlink names a56b4057c. Hazard: these branch tips need this PR's SDK, so nobody should move a pin to a tip before this lands.

Checks (head cbe0caeef, base e74dbf183)

check exit
cargo test -p toyos --lib fd:: (9 table tests) 0
negative controls, each applied, tested and reverted: a call holds nothing; dup makes a new object; highest-free numbering; an fd: label past the table; a non-canonical label accepted; install_at overwrites; a broken table panics 101 each, on its own test
cargo test -p toyos-libc-copies (includes a refused posix_spawn plan) 0
cargo run -- --ci host (includes the new -p toyos --lib --features fd-table clippy shape) 0 (88 steps)
cargo run -- --build-only, --arch aarch64 0, 0
QEMU x86_64: console_image_boots nvme_disk_boots desktop_without_x2apic the_compositor_drives_the_virtio_gpu libc_children a_launch_gives_each_unsent_stdio_slot_a_null bar_map_again machine_shutdown_wire_held machine_shutdown_wire_kept a_drives_row_holds_one_process_per_function hda_plays_to_the_controller_that_reaches_an_output the_opening_exchange_with_qemus_agent_completes 0 (12/12)

libc_children runs the C spawn cases (spawn_cloexec with its fd 5, popen, /dev/null, pipes) and the new poll_closed.c (POLLNVAL).

Size of the empty table (measured, x86_64, readelf -SW): calc's .data was 0x16a8 bytes at base, 0x356c0 at round 1's 7c60bc1bd, and is 0x16a8 at head. compositor's goes from 0x4200 to 0x15f8. .bss grows by 0x2ac20 (175 KiB), which the loader demand-pages, so a program touches only the pages it uses.

Metal: round 1's T14 run at 7c60bc1bd passed 17 of 17. Round 2 is staged and not yet run: 3 images of the 16 shared-boot binaries, plus 1 control image. In the control, stream_handles is built with fd::copy's stream check removed, and its new "inherited stdio" arm is expected to fail.

Evidence (masked): round 2 https://github.com/ToyOSOrg/evidence/tree/fb0376c94caf953ac7991068fb581446c0fdc89a/r2; round 1 https://github.com/ToyOSOrg/evidence/tree/a5d8d4f9e95c6614a568842729ed2cd842f81448/r1; round 1 T14 https://github.com/ToyOSOrg/evidence/tree/09440cbd7fb0c8156aadff5022190ad14246d888/r1/metal-run

Not done here

  • Served files have no descriptor yet. That is PR 3.
  • The inheritance rule (Q9) is not applied yet. That is PR 4. Until then a C child gets 0 to 2 and what its file actions name, std's Command gets its configured stdio and what inherit_handle names, and O_CLOEXEC and F_SETFD already set the per-descriptor flag PR 4 will read.
  • A stdio sink cached in one copy of toyos outlives a redirect made through another. A C dup2 onto 1 or 2 in a mixed Rust and C program leaves std writing to the ring it mapped earlier. This predates the table and is recorded in issues/a-stdio-sink-cached-in-one-copy-outlives-a-redirect-in-another.md.
  • A C child cannot be given a socket. posix_spawn now refuses one with EINVAL, rather than silently starting the child without it. Giving served kinds to a child is PR 4.
  • The 31-endowment cap did not bite. Stdio now uses up to 3 endowments of a spawn's 31. Every QEMU row that boots the full shipped system passed, so no shipped row was refused. Endowment counts per row were not measured.

🤖 Generated with Claude Code

@Japabu

Japabu commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

Metal, T14 (ThinkPad T14 Gen 2), head 7c60bc1bd, run by the orchestrator: the proctreecase, shared-debug and testcases images all booted (rc 0). The judge reported 17 passed, 0 failed, covering the 16 converted shared-boot binaries named in the request.

Japabu added a commit to ToyOSOrg/evidence that referenced this pull request Oct 21, 2026
@Japabu

Japabu commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

Review of #978 r1 (head 7c60bc1bd, base e74dbf183)

Architectural assessment

The approved shape is here and it is the right one. One userland table in toyos::fd is owned by exactly one copy: std's (rustc-dep-of-std turns on fd-table) in a Rust program, or libc's C-only build (fd-owner) in a C program. Every copy reaches the table through toyos_fd_*. In a mixed Rust+C program, libc's std-runtime build leaves the feature off, and build/libc.rs builds the two archives in separate cargo invocations, so features cannot unify. I found no second table, and no remaining consumer of the deleted kernel slot map, SYS_HANDLE_DUP_SLOT (129), SYS_HANDLE_DUP_AT (74), for_child, install_at, held_at, duplicate_entry, stream_at or stream::duplicate. I searched the tree, the rust fork at a56b4057c and mio 9f34d36c. The kernel change only deletes code, plus one move: console minting now happens in PendingHandles::commit, keyed by the parent's object. That is smaller than before.

dup/dup2/fcntl semantics are correct:

  • lowest-free numbering;
  • dup and dup2 in C make inheritable descriptors, std's are not;
  • dup2(fd, fd) checks that fd is valid;
  • an F_DUPFD or F_DUPFD_CLOEXEC floor at or above OPEN_MAX is EINVAL;
  • the flag is per descriptor.

Pins are coherent:

  • Cargo.lock names mio 9f34d36c and socket2 d3885019 on their toyos branches;
  • the gitlink names rust a56b4057c, which origin/main contains.

Net change: +1613/−1315. 532 lines are fd.rs, about 100 of them tests. Most of the rest is the consumer conversion, and the kernel shrinks.

Hot path (point 2): each std File, pipe or stream I/O does with = two extern "C" calls. Each takes the process-wide futex lock uncontended, makes an O(1) change and releases it. Nothing allocates. That is small next to a syscall. Contention between many threads on one lock is unmeasured, which is acceptable for now.

The 31-endowment cap (point 4): stdio adds at most 3. The most-endowed shipped row is the compositor: 5 serves, 4 devices, its namespaces and stdio, about 15 in all. So shipped rows cannot reach the cap. A refusal reads "more endowments and descriptors than a spawn carries", which is clear.

Behaviour changes (point 6) are as stated and sound:

  • a missing descriptor is EBADF;
  • the "handle" arm leaves spawn_signaled.c, and the status mapping stays host-tested;
  • a refused relocation without fd:2 exits 127.

Required

B1. sdk/toyos/src/fd.rs:314-317, 338-343: a malformed fd: label hangs the child instead of being refused.

  • Problem: endowed_fd parses with i32::from_str, which accepts fd:+3 and fd:03 as well as fd:3. Two labels naming one number reach the assert_eq! at :342, which panics while TABLE is held. The panic message goes to stderr. That path is log::stdio::write → fd::with → toyos_fd_acquire → TABLE.lock(), a non-reentrant futex lock already held by the same thread. The process hangs instead of failing. The same applies to the expects inside Table methods: an invariant failure under the lock is a hang unless stderr is a ring that was already asked.
  • Fix: accept only canonical decimal. Refuse a conflicting endowment at the boundary by closing its handle, not with an assert. Do no panicking work while the lock is held: return the result and assert after the guard drops.

B2. sdk/std/sys/process.rs (child_stdio's inherit) and sdk/toyos/src/spawn.rs (refuse_streams over self.fds): the stream refusal for inherited stdio can no longer fire.

  • Problem: inherit calls is_stream() on the fresh syscall::dup that fd::copy just made. net::stream's table finds by exact handle (stream.rs:166), so a new handle is never a stream. refuse_streams likewise sees std's stdio copies, not the parent's handle.
  • Consequence: a stream dup2'd onto 1 and inherited (Stdio::inherit) now passes the connection to the child. That is exactly what issues/a-stream-cannot-move-between-processes.md says std refuses. Before this PR, stream_at(slot) checked the original handle. This is a regression hidden behind code that looks like a check.
  • Fix: check is_stream(o.handle) inside fd::with before duplicating.
  • Test: add an arm to stream_handles.rs: toyos::fd::dup2 the stream onto 1, then spawn with inherited stdout. That catches it.

B3. sdk/toyos/src/fd.rs:328: the empty table is probably about 176 KiB of .data in every Rust and C binary. This is an inference; the cheap check is below.

  • Size: Option<Entry> (u16 + bool) uses the bool niche, so None is byte 2, not zero. The static therefore has a non-zero initialiser. That is 4096 × 4 B of fds plus 4096 × about 40 B of Option<Held>, about 176 KiB.
  • Consequence: every program grows by that much on disk, and in memory too if the loader copies .data eagerly. The table usually holds fewer than ten entries.
  • Cheapest check: llvm-readelf -S (or llvm-size) on one built program at base and at head.
  • Fix: make the free state all-zero so the static lands in .bss. For example, store object + 1 with 0 meaning free and keep the flag in a spare bit.

B4. build/clippy.rs: the new fd-table configuration arm (mod owner, cfg(all(feature = "fd-table", not(test)))) is linted by no shape.

  • Gap: the toyos shape runs without the feature, and libc is linted without fd-owner. Listing both in NOT_A_CONTROL exempts them from the control rule, not from clippy coverage. The repository rule requires changed features and configuration arms to be covered.
  • Fix: add a shape -p toyos --lib --features fd-table, or run the existing one with that feature.

B5. socket2 fork d3885019, src/sys/toyos.rs install/key: socket identity breaks after 2^24 sockets.

  • Problem: key = TAG | NEXT_KEY with TAG = 0x736f_636b_3200_0000, and key() masks only the low 24 bits. After 16,777,216 sockets the counter carries into the tag byte. Every later socket is then "invalid socket fd", while release still removes it.
  • Reachability: a long-lived client or server can make that many sockets through socket2.
  • Fix: keep the key inside the bits the mask excludes and reuse keys (index plus free list), or stop encoding identity in a counter.

B6. socket2 fork d3885019 install and try_clone: sockets are installed inheritable (true), and the comment says "inherited as a socket's is".

  • Why it is wrong: on unix, socket2 creates sockets with SOCK_CLOEXEC and clones with F_DUPFD_CLOEXEC. Q9 says Rust- and std-created descriptors default to non-inheritable. This PR is where the flag gets set, and PR 4 will spawn from it.
  • Consequence: under PR 4 every socket2 socket would be offered to every child. fd::copy refuses an opaque object, so the spawn would fail.
  • Fix: install with false and correct the comment.

Optional

S1. sdk/std/sys/stdio.rs (Stdin::read), sdk/libc/src/posix_io.rs (write_fd, dup2): toyos::fd::inheritable(fd).is_err() is used as an "is it open" probe.

  • In stdin it is also racy: a close between the probe and on_fd returns an Uncategorized error instead of EOF.
  • Making ToyOS's is_ebadf recognise the table's BadFd error would let std's own handle_ebadf give unix behaviour for stdin, stdout and stderr in one race-free place. The probe would then go.
  • Elsewhere, an fd::is_open would name the intent.

S2. sdk/toyos/src/relocate/mod.rs stderr() re-parses the kernel's endowment table and hardcodes ENTRY = 16. size_of::<EndowEntry>() is a constant and needs no relocation, and a stack-only parser shared with endow.rs would avoid a second copy of the layout.

S3. sdk/libc/src/fdreq.rs:OPEN_MAX and spawnreq.rs:CHILD_FDS are both 4096 and both mean toyos::fd::MAX_FDS. If the host-copied modules cannot import it, one libc constant is enough.

S4. sdk/libc/src/socket.rs SOCKET_TAG (0x736f636b << 32) has the same upper 32 bits as socket2's tag. Telling them apart only works because libc's range filter rejects socket2's words. Choose disjoint tags.

S5. In the rust fork, AsRawFd/AsFd for io::Stdin, Stdout and Stderr are still cfg'd out on ToyOS. They are now just 0, 1 and 2, and crates such as is-terminal and crossterm use them. This gap predates the PR.

S6. toyos::log::stdio's per-copy sink cache: libc's dup2 forgets the cached sink only in libc's toyos copy, so std's cache stays stale after a C-side redirect of 1 or 2. This predates the PR but now contradicts "one table". Record it if not fixed.

S7. posix_io.rs:699 (poll): the handle is taken out of with and used after it returns. A racing close during poll is therefore still a handle fault, not EBADF. This is not a regression, but the PR body's "a call holds its object" claim should exclude it. The orchestrator owns the body. Separately, a closed descriptor now maps to -1 and is ignored, where POSIX answers POLLNVAL.

Evidence

  • Host table tests: 7 tests, each with a red control. The controls cover numbering, sharing, label range and holding across a call. That is enough for the Table logic.
  • QEMU: 12 rows green, including libc_children for the C spawn cases.
  • Gaps:
    • nothing exercises conflicting or non-canonical fd: labels (B1);
    • nothing exercises stream refusal through inherited stdio (B2; a stream_handles.rs arm is the cheapest check);
    • binary size is not measured (B3).
  • Metal: T14 for the 16 converted shared-boot binaries is still pending.

SEND BACK

Japabu added a commit that referenced this pull request Oct 21, 2026
… and costs no data

Review r1 of #978.

An fd:<n> label is read as canonical decimal only, so "fd:+3" and
"fd:03" name nothing. An endowment whose number is already open is
refused at the boundary and its handle closed, where an assert used to
panic under the table's lock: the panic's message goes to stderr, a
descriptor, and the same thread waited on the lock forever. The table's
own operations no longer panic either. They answer a broken table as a
fault, which the owner panics on once the lock is released.

The table's empty state is all zeros, so it lands in .bss: a
descriptor's word is its object's index plus one with a spare bit for
the flag, and an object with no references is free. net::stream's
tables, which every program now links through the table's release of a
handle, are made all-zero the same way. calc's .data is back to the
base's 0x16a8 bytes from 0x356c0.

toyos::fd::copy refuses a stream's end by the handle the descriptor
names. The check ran on the fresh duplicate, which is never a stream,
so a stream dup2'd onto stdio was passed to a child. std's File child
copy goes through the same check. libc's posix_spawn refuses a
descriptor it holds but cannot give, where it started the child
without it.

Also:
- a closed stdin reads its end without a probe that raced a close;
- libc's poll answers POLLNVAL for a descriptor that is not open;
- libc sockets carry a tag disjoint from socket2's;
- libc's table-size constants are toyos::fd::MAX_FDS;
- clippy lints the fd-table owner.

socket2 now makes sockets not inherited and reuses keys
(2aa6b0049e70).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit to ToyOSOrg/evidence that referenced this pull request Oct 21, 2026
…25dfdf586a29: 7 fd table controls, libc copies, --ci host, both builds, 12 QEMU rows, base and head section sizes, metal staging with a stream control; masked
@Japabu

Japabu commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

Round 2 (cbe0caeef) answers review r1. Evidence: https://github.com/ToyOSOrg/evidence/tree/fb0376c94caf953ac7991068fb581446c0fdc89a/r2

B1 (fixed).

  • endowed_fd reads canonical decimal only. fd:+3, fd:03, fd:00 and fd: 3 now name nothing; a host test covers them, and a control that accepts them is red.
  • An endowment whose number is already open is refused through Table::install_at and its handle closed; there is no assert. A host test covers it, with a control.
  • The table moved to fd/table.rs. It has no expect, indexing that can fail, or unchecked arithmetic, and answers an inconsistency as Fault::Broken. owner::table panics on that only after the guard drops.
  • Endowments::get() is called before the lock is taken. Under the lock, all that runs is the endowment's take and a handle close.
  • A host test builds a broken table and checks that it is answered rather than panicked on. Its control (expect back in held) is red.

B2 (fixed). fd::copy refuses a stream inside with, by the handle the descriptor names, so std's inherit is now just fd::copy(..).map_err(refused).

  • File::as_child_stdio goes through it too.
  • libc's posix_spawn previously dropped a descriptor it could not copy and started the child anyway. It now refuses with that descriptor's errno, and a host test covers the refused plan.
  • stream_handles gains the arm you asked for: it dup2s the stream onto stdin and spawns with stdin inherited. It is metal-only (a discovered rust-tests binary), so the control is a staged image built with the check removed, which is expected to fail. Both are in the T14 request.

B3 (confirmed, then fixed). Measured with readelf -SW on x86_64:

  • calc's .data: 0x16a8 at base e74dbf183, 0x356c0 at 7c60bc1bd, 0x16a8 at head.
  • What made it all-zero: a descriptor's word is now object + 1 with an INHERITABLE bit, and an object with no references is free.
  • Also found: net::stream's tables (32 KiB of .data) are now linked into every program, because releasing a handle reaches let_go. Its Slot is now repr(u8) and its halves are a repr(u8) Kept, so they are zero too. compositor's .data drops from 0x4200 to 0x15f8.
  • .bss grows by 175 KiB, which the loader demand-pages.

B4 (fixed). New shape cargo clippy -p toyos --lib --features fd-table. Its first run was red on a mem::forget the repository's disallowed-methods list refuses (now into_raw), which is the lint this shape exists for. libc's fd-owner only forwards to that feature.

B5 and B6 (fixed in socket2, 2aa6b0049e70, pinned in Cargo.lock).

  • Keys are searched within the 24-bit mask, skipping keys in use, so a key is reused once its socket is gone. The search ends because a table holds far fewer descriptors than there are keys.
  • install and try_clone are not inherited (unix's SOCK_CLOEXEC and F_DUPFD_CLOEXEC), and the comment says so.

Optionals:

  • S1, taken. Stdin reads through on_open_fd, a closed descriptor reads as end of file, and there is no probe. libc's dup2(fd, fd) arm is gone, since the table already checks old before answering.
  • S2, not taken.
  • S3, taken. OPEN_MAX is toyos::fd::MAX_FDS, and CHILD_FDS is gone.
  • S4, taken. libc's tag is libc << 32, disjoint from socket2's.
  • S5, refuted. AsRawFd and AsFd for Stdin/Stdout/Stderr and their locks already exist on ToyOS at rust a56b4057c: library/std/src/os/fd/raw.rs:250-295 (0, 1, 2) and the uncfg'd impls in owned.rs:523-570.
  • S6, recorded in issues/a-stdio-sink-cached-in-one-copy-outlives-a-redirect-in-another.md.
  • S7.
    • poll now answers POLLNVAL for a descriptor that is not open, with a C test (poll_closed.c, in libc_children).
    • The handle-fault exclusion for a close racing a poll is in the PR body.

Also:

  • The dup-makes-a-new-object control passed on the first run. That mutation leaks the original object's reference, and no test counted live objects. The tests now assert that no object outlives its descriptors or its call, and the control is red.
  • --ci host 0, both builds 0, QEMU 12/12.
  • The round 2 T14 run is requested from the coordinator.

@Japabu

Japabu commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

r2 (head cbe0caeef, one commit on 7c60bc1bd)

I checked only the fixes and the code they touched.

r1 blockers

  • B1, resolved.
    • endowed_fd now accepts only canonical decimal. The new cases (fd:+3, fd:03, fd:00, fd: 3) are host-tested.
    • The table moved to sdk/toyos/src/fd/table.rs, which has no expect and no unchecked arithmetic. refs -= 1 runs only after held() has rejected refs == 0.
    • An inconsistency comes back as Fault::Broken. owner::table panics on it only after the guard's block ends.
    • Endowments::get() runs before the lock is taken.
    • A conflicting endowment goes through install_at, is refused, and its handle is closed. Under the lock only a close syscall runs, which cannot panic.
    • toyos_fd_unuse's expects now run outside the lock.
  • B2, resolved.
    • fd::copy checks is_stream(o.handle) inside with, on the descriptor's own handle, before duplicating.
    • std's inherit and File::as_child_stdio both go through fd::copy, and refused moved to sys::pal so both share it.
    • The stream_handles arm dup2s the stream onto 0 and spawns with stdin inherited. It is last because every later spawn would inherit that stdin. That placement is right.
    • The metal control image has the check removed. Its result is pending at T14.
  • B3, resolved and measured.
    • calc's .data was 0x16a8 at base, 0x356c0 at r1 and is 0x16a8 at head.
    • A descriptor's word is now object + 1 with an INHERITABLE bit, and a free object is FREE, which is all zeros (handle 0, word 0, no release, refs 0).
    • net::stream's Slot and the new Kept<T> are repr(u8) with the empty variant at 0. That removes another 32 KiB of .data that the release path had pulled into every program. Taking that too was right.
    • .bss grows by 175 KiB, which the loader demand-pages.
  • B4, resolved. The new shape -p toyos --lib --features fd-table covers the owner arm. libc's fd-owner only forwards to that feature.
  • B5 and B6, resolved in socket2 2aa6b0049e70, which is on origin/toyos and named by Cargo.lock.
    • The key search stays within KEYS, starts where the last search ended and skips keys still in the map. It always ends, because the map holds no more sockets than the 4096-descriptor table allows plus in-flight installs, far fewer than 2^24 keys.
    • install and try_clone make non-inheritable descriptors, matching unix's SOCK_CLOEXEC and F_DUPFD_CLOEXEC.

The unrequested change: libc posix_spawn now refuses instead of dropping

spawnreq::plan's held now answers Result<Option<_>, errno>. A descriptor the child is meant to get, either one of 0 to 2 or the source of an adddup2, that cannot be passed (a stream, a libc socket or another opaque object) now refuses the whole spawn with that descriptor's errno. Before, the child started without it.

I accept the change. It replaces a silent loss of the child's stdio with a clear refusal, which is the repository's rule, and std's Command already behaved this way. The refused plan is host-tested.

A consequence for PR 4 (for the orchestrator). Under Q9, a C socket() without SOCK_CLOEXEC is inheritable, and libc sockets cannot be passed. If PR 4 passes every inheritable entry, every posix_spawn from a C program that holds such a socket would be refused. PR 4 needs an answer for objects that cannot be passed; the owner's rule does not settle it. This is not a defect in this PR.

Optionals

  • S1, taken. read_stdin uses on_open_fd and turns BadFd into EOF without a separate probe, so the race is gone. Removing libc's dup2(fd, fd) arm is correct, since Table::dup2 checks old before handling new == old, and the doc says so.
  • S3, taken. OPEN_MAX and both libc bounds use toyos::fd::MAX_FDS.
  • S4, taken. libc's tag (libc << 32) and socket2's are now disjoint.
  • S5, refuted, and the refutation is correct. library/std/src/os/fd/raw.rs at a56b4057c has ToyOS AsRawFd impls for Stdin, Stdout, Stderr and their locks. My r1 S5 was wrong; I had read the cfg(not(toyos)) arms only.
  • S6, filed as issues/a-stdio-sink-cached-in-one-copy-outlives-a-redirect-in-another.md.
  • S7, taken. poll answers POLLNVAL for a descriptor that is not open, and the loop's ready_now returns at once. poll_closed.c is found by libc_children's directory scan (tests/common/libcchildren.rs). The race with close during poll is now stated in the PR body.

The tests now also count live objects, after the dup-makes-a-new-object control turned out not to go red. That makes the controls mean something.

New findings

None required.

Optional: endow closes a refused endowment's handle silently. A line after the lock is released would make a malformed parent visible. Not required, since the child correctly starts without that descriptor.

Evidence

  • --ci host 0, both builds 0, QEMU 12/12 at cbe0caeef, per the implementer's evidence fb0376c9…/r2, which I did not re-run.
  • T14 at cbe0caeef (3 fix images plus the control with the stream check removed) is still running. The landing decision should wait for its result, with the control expected red on inherited stdio.

LAND

@Japabu

Japabu commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

Metal, T14 (ThinkPad T14 Gen 2), head cbe0caeef, run by the orchestrator:

  • Fix: the proctreecase, shared-debug and testcases images all booted (rc 0). The judge reported 17 passed, 0 failed, and stream_handles reports "inherited stdio refused".
  • Control (the stream check removed): stream_handles FAIL (101), on exactly the new arm: "inherited stdio took the stream's handle".

@Japabu
Japabu marked this pull request as ready for review October 21, 2026 17:19
@Japabu
Japabu added this pull request to the merge queue Oct 21, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Oct 21, 2026
Japabu and others added 2 commits October 21, 2026 20:04
…ors are its fd:<n> endowments

A raw fd on ToyOS was a kernel handle, so C's small descriptor numbers, std's
raw-fd interop and POSIX semantics (lowest-free numbering, dup sharing an
object, dup2 answering its target, close-on-exec per descriptor) had no home,
and libc kept close-on-exec marks, a socket table numbered from 1024 and a
dup2 that could not answer POSIX's number.

toyos::fd is the process's one descriptor table, owned by the copy of
`toyos` whose `fd-table` feature is on: std's (rustc-dep-of-std) in a Rust
program, libc's own (`fd-owner`, its C-only build) in a C program, every
copy reaching it through `toyos_fd_*`. A descriptor names an object, a
kernel handle or an object a library keeps itself, numbered lowest-free with
an inheritable flag of its own; a call holds its object, so a racing close
makes the next lookup BadFd and never a call on a closed handle.

std's File, pipes, stdin and network types are table descriptors, as on
unix: as_raw_fd, into_raw_fd and from_raw_fd move descriptor numbers, and
OwnedFd closes and duplicates through the table (the rust fork's arms).
`std::os::toyos::io::AsRawHandle` answers the kernel handle behind a
descriptor, for ToyOS-only code and the mio fork's selector; a portable
program needs none. A closed stdin reads its end and a closed stdout drops
its bytes, as std on unix does. libc's every descriptor call is on the
table: open, pipe, dup, dup2 (answering its target), fcntl's close-on-exec
and F_DUPFD_CLOEXEC, and sockets, which are objects of its own (a close now
releases one; the close-socket issue keeps its last exit, a C case). The
socket2 fork's sockets converge the same way, and its try_clone is a dup.

A child's descriptors, stdio included, travel as endowments labelled
`fd:<n>`, which its table installs at their numbers at its first use. The
kernel's spawn slot map, SYS_HANDLE_DUP_SLOT and SYS_HANDLE_DUP_AT go, and
with them HandleTable's slot-placing paths; an endowed console is minted for
the child as the slot map's was. The launch protocol's stdio slots are
descriptors 0 to 2, which the supervisor endows. The supervisor's own stdio
is the kernel's three fd: endowments, its log ring put at 1 and 2 by the
table's dup2; relocation's refusal line finds fd:2 in the endowment table.

Guest tests moved off slot numbers and RawHandle(0..2); a C child asking
about a descriptor it lacks is answered EBADF, as POSIX has it, rather than
ended by a handle fault, so spawn_signaled loses its handle arm (the status
mapping stays host-tested). The capability record, the children's issue and
the stream issue say what the spawn now carries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… and costs no data

Review r1 of #978.

An fd:<n> label is read as canonical decimal only, so "fd:+3" and
"fd:03" name nothing. An endowment whose number is already open is
refused at the boundary and its handle closed, where an assert used to
panic under the table's lock: the panic's message goes to stderr, a
descriptor, and the same thread waited on the lock forever. The table's
own operations no longer panic either. They answer a broken table as a
fault, which the owner panics on once the lock is released.

The table's empty state is all zeros, so it lands in .bss: a
descriptor's word is its object's index plus one with a spare bit for
the flag, and an object with no references is free. net::stream's
tables, which every program now links through the table's release of a
handle, are made all-zero the same way. calc's .data is back to the
base's 0x16a8 bytes from 0x356c0.

toyos::fd::copy refuses a stream's end by the handle the descriptor
names. The check ran on the fresh duplicate, which is never a stream,
so a stream dup2'd onto stdio was passed to a child. std's File child
copy goes through the same check. libc's posix_spawn refuses a
descriptor it holds but cannot give, where it started the child
without it.

Also:
- a closed stdin reads its end without a probe that raced a close;
- libc's poll answers POLLNVAL for a descriptor that is not open;
- libc sockets carry a tag disjoint from socket2's;
- libc's table-size constants are toyos::fd::MAX_FDS;
- clippy lints the fd-table owner.

socket2 now makes sockets not inherited and reuses keys
(2aa6b0049e70).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Japabu
Japabu force-pushed the wt/toyos-fd-table branch from cbe0cae to 249293c Compare October 21, 2026 18:20
Japabu added a commit to ToyOSOrg/evidence that referenced this pull request Oct 21, 2026
…d840d917d85cd2c10587: --ci host, both builds, QEMU rows; masked
@Japabu

Japabu commented Oct 21, 2026

Copy link
Copy Markdown
Collaborator Author

Rebased onto db0cf636d (after #971 and #980). Head: 249293c2093355400f69d840d917d85cd2c10587.

The #971 conflicts were mechanical. Each takes #971's side and keeps #978's intent:

#980 rebased cleanly.

Checks:

Evidence: https://github.com/ToyOSOrg/evidence/tree/2a0e7277e8881439f6b8be3306e997037c6ccc33/r2-rebase

The T14 17/17 at cbe0caeef carries over, because the resolution changes no boot-time code of #978's beyond deleting references to what #971 removed.

@Japabu
Japabu added this pull request to the merge queue Oct 21, 2026
Japabu added a commit that referenced this pull request Oct 21, 2026
A one-off accuracy audit of 117 open defect records (batch 0 of 4). Each
claim was checked against origin/main at
6f4d8db and the owner's uncommitted
planning in the primary checkout, then brought onto
db0cf63 (#971, #978, #980) and the records
whose cited code #971 removed checked again there. The per-record
classification, with file:line evidence, is in the PR.

Deleted as fixed. For each, which halves of the exit were met, and which
were judged unnecessary and why:

- a-file-server-killed-mid-sync-leaves-a-half-written-btree: met whole.
  3adcf42 made DATA's commit shadow paging, the superblock written last
  after a flush, and its host test, lib/bcachefs/tests/crash.rs, stops a
  commit at every block write, torn sectors and a lost block 0 included,
  and mounts what is left: the exit's own test. The residual defects are
  owned by a-write-over-a-files-committed-page-is-not-shadowed and
  a-crash-leaks-the-blocks-of-its-last-commit.
- a-long-name-entry-whose-write-was-refused-is-on-the-volume-and-the-create-erases-nothing:
  the QEMU half is met by a cheaper test, the T14 half judged unnecessary.
  0d73b99 removed erase_inserted; write_entry_at queues the old bytes as
  a repair before every write, so an entry write refused after it landed
  is written back. lib/fat32/tests/refused_writes.rs models the device the
  exit asked QEMU to stage, a write that lands and is answered with an
  error, as a host test: `cargo test -p toyos-fat32 --test refused_writes
  every_refused_write_of_a_long_name_create_is_undone` exit 0 at
  6f4d8db, 24 refused and landed cases on the record's own file name,
  each checked by toyos-fat32-check. The T14 fat check of a
  usb-transport-break boot was not run: the mechanism is the crate's and
  the host model reaches every write, and since #971 that actuator no
  longer exists, so the arm cannot be staged.
- a-replacement-netstack-reuses-its-predecessors-ports: met. Since
  4b96de0 netstack runs ToyOS's own stack, whose TCP ephemeral ports are
  RFC 6056 keyed draws from keys each process takes from the system's
  randomness, the remedy the record named; NetDaemon's fixed 49152 is
  gone. Its citation in netstack-ends-for-the-boot-on-a-nic-it-cannot-drive-on.md
  goes with it.
- a-disk-whose-port-went-away-panics-the-boot-at-roots-hold was fixed by
  602cff0: its exit's code half is met, and its "green on a nightly"
  half by that commit's three local `--nightly` runs of
  usb_transport_break with a reverted negative control, not by a CI
  nightly. #971 deleted the record on main first, so this change no longer
  carries the deletion.

Deleted as superseded:

- a-deliberate-wedge-inside-a-driver-panics-every-other-cpu-that-wants-its-lock:
  the usb-wedge-* actuators it was about went in 29e1006, and 13ed7b4
  (#971) took the kernel's USB driver out and its restoration out of stage
  E of the-guest-suite-runs-only-what-no-cheaper-tier-reaches.md. The one
  staged wedge left, wedge-before-reset, holds no lock when it stops.

Corrected where the text no longer matched the code, the defect kept:
renamed programs (init, netd, logd, fsd, soundd), moved functions and
files, line numbers that drifted, guest tests cut since, citations of
deleted files, and statements the tree refutes (the console's blocking
read, the DMA address window's arithmetic, the mio half of the
TCP_NODELAY record, the firmware-call record's account of when AML
writes first reached the kernel, d2006cc, and its widening, 15536e2).
On the new base the console-watch and disk-offline records are main's
text, the console's carrying the residual correction to its blocking
read; held-disk, the NVMe boot wait and reset-stops-xHCI are main's
deletions, and the job-list record's citation of the kernel's xHCI stop
now names pcidev::stop_before_reset.

Left untouched because the owner's uncommitted planning edits them:
a-driver-that-reflashes-its-device-escapes-its-isolation,
a-file-held-across-a-file-servers-restart-answers-gone,
a-machine-without-an-iommu-refuses-every-claim, and
a-memory-overwrite-request-ubuntu-left-set-stays-set-under-toyos, which
the owner deleted locally as superseded (claude-questions.md, item 12)
together with its citation in the loader-wipe issue.

Also removes a home-directory path from
a-job-list-hangs-with-interrupts-on-and-the-deadline-ends-it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Oct 21, 2026
A one-off accuracy audit of batch 1 (117 open `kind: defect` records).
Each claim was checked against origin/main at 6f4d8db. Every record
that #971, #978 and #980 touched was then re-audited against db0cf63,
the base this lands on. The primary checkout's uncommitted planning
(requirements/, claude-questions.md) was read and not edited.

Counts at db0cf63: 2 fixed, 43 partly stale, 60 still valid,
8 uncertain, and 4 already deleted by #971 (hotplug-blocks-a-scheduler-
pass, a-shutdown-on-a-held-usb-disk-left-a-cpu-deaf-to-a-tlb-shootdown,
an-acknowledge-after-an-enumeration-clears-a-replug-no-look-has-seen,
an-xhci-storm-starves-the-cpu-that-takes-it). Still-valid and uncertain
records are unchanged, except for the two extended below.

Deleted, fixed:

- a-second-launch-of-a-serves-program-panics-the-supervisor: no launcher
  client can reach the panic. `may_start` refuses a target the caller's
  `starts` does not list (4e20480). The build refuses a `starts` entry
  naming a row that serves a port or a role
  (`starts_name_what_a_launch_can_start`, host-tested), and a package's
  synthesized row serves nothing. The panic is now an internal
  invariant; a-comment-says-a-second-launch-is-refused-where-the-
  supervisor-panics records the comment above it, which still says
  "refused".
- bus-mastering-rides-memory-decode: #971 removed the kernel xHCI
  driver, the last kernel site that armed bus mastering before its
  refusals. virtio arms in `accept` after `offer` has parsed; amdvi arms
  after programming, with no refusal after it; a claimed function starts
  mastering through its own driver's `dma_start`, after its parse
  (usbd, nvme). build/sourcegate.rs's `BUS_MASTER_SITES` comment no
  longer cites the record.

Renamed, the slug's claim refuted, a residual kept, every citation
moved:

- a-safe-port-read-reaches-any-io-port -> a-safe-port-read-reaches-any-
  declared-port: a `pio::Port` is made only from a declared run, but the
  fixed runs are pub consts, so a safe read still bypasses
  `serial::Registers`.
- a-wake-to-a-busy-cpu-waits-for-its-next-pass-and-no-idle-cpu-can-take-
  it -> no-row-holds-a-wake-beside-seven-spinners-to-100-us. #874's
  `wake_onto_busy` read p99 5 us (T14) and 25 us (AMD bench), with 200 of
  200 wakes run off the busy CPU. What is left is the 7-spinner arm's
  100 us bound, which no row holds. The 8-spinner arm moves to
  a-woken-thread-waits-out-a-sibling-spinners-quantum-when-every-cpu-is-
  busy, its owner. The speed track's two citations moved, and its stale
  "whole 10 ms quantum" line was corrected.
- diskserver-resets-a-drive-that-is-slow-and-healthy -> the-nvme-driver-
  resets-a-drive-that-is-slow-and-healthy: `COMMAND_SILENCE` is the nvme
  driver's (system/nvme/src/main.rs), a row apart from diskserver.

Extended: a-spawn-drops-an-empty-argument now covers the supervisor's
`serve_launch`, which drops empty and non-UTF-8 arguments from the same
blob; its exit asks for a host test of that decode.

Corrected to the current code, residual defect kept: paths and names
after the userland moves (fsd -> fileserver, init -> supervisor, soundd
-> soundserver, netd -> netstack, blockd -> nvme); line numbers replaced
by names; tests cut since filing recorded as cut (consent_prompt in
ff1a11d, screen_gop_firmware_mode in 520c0d1). Some mechanisms
changed under a record: netring stage 3's stream refusals; the DMA
ordering behind `DmaBuffers::publish`/`observe`; blocked-time
accounting's one caller; `SpawnArgs::image`; the blackbox tail keeping
the newest records; the T14's HDA now driven; and `close_ends_polls`'s
arms at db0cf63. a-usb3-device-attached-but-untrained-reads-as-gone
takes #971's rewrite as it stands.

The per-record table (class, evidence, action) is in the PR's evidence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merged via the queue into main with commit a6ab280 Oct 21, 2026
4 checks passed
@Japabu
Japabu deleted the wt/toyos-fd-table branch October 21, 2026 19:02
Japabu added a commit that referenced this pull request Oct 21, 2026
A one-off accuracy audit of batch 1 (117 open `kind: defect` records).
Each claim was checked against origin/main at 6f4d8db. Every record
that #971, #978 and #980 touched was then re-audited against db0cf63,
the base this lands on. The primary checkout's uncommitted planning
(requirements/, claude-questions.md) was read and not edited.

Counts at db0cf63: 2 fixed, 43 partly stale, 60 still valid,
8 uncertain, and 4 already deleted by #971 (hotplug-blocks-a-scheduler-
pass, a-shutdown-on-a-held-usb-disk-left-a-cpu-deaf-to-a-tlb-shootdown,
an-acknowledge-after-an-enumeration-clears-a-replug-no-look-has-seen,
an-xhci-storm-starves-the-cpu-that-takes-it). Still-valid and uncertain
records are unchanged, except for the two extended below.

Deleted, fixed:

- a-second-launch-of-a-serves-program-panics-the-supervisor: no launcher
  client can reach the panic. `may_start` refuses a target the caller's
  `starts` does not list (4e20480). The build refuses a `starts` entry
  naming a row that serves a port or a role
  (`starts_name_what_a_launch_can_start`, host-tested), and a package's
  synthesized row serves nothing. The panic is now an internal
  invariant; a-comment-says-a-second-launch-is-refused-where-the-
  supervisor-panics records the comment above it, which still says
  "refused".
- bus-mastering-rides-memory-decode: #971 removed the kernel xHCI
  driver, the last kernel site that armed bus mastering before its
  refusals. virtio arms in `accept` after `offer` has parsed; amdvi arms
  after programming, with no refusal after it; a claimed function starts
  mastering through its own driver's `dma_start`, after its parse
  (usbd, nvme). build/sourcegate.rs's `BUS_MASTER_SITES` comment no
  longer cites the record.

Renamed, the slug's claim refuted, a residual kept, every citation
moved:

- a-safe-port-read-reaches-any-io-port -> a-safe-port-read-reaches-any-
  declared-port: a `pio::Port` is made only from a declared run, but the
  fixed runs are pub consts, so a safe read still bypasses
  `serial::Registers`.
- a-wake-to-a-busy-cpu-waits-for-its-next-pass-and-no-idle-cpu-can-take-
  it -> no-row-holds-a-wake-beside-seven-spinners-to-100-us. #874's
  `wake_onto_busy` read p99 5 us (T14) and 25 us (AMD bench), with 200 of
  200 wakes run off the busy CPU. What is left is the 7-spinner arm's
  100 us bound, which no row holds. The 8-spinner arm moves to
  a-woken-thread-waits-out-a-sibling-spinners-quantum-when-every-cpu-is-
  busy, its owner. The speed track's two citations moved, and its stale
  "whole 10 ms quantum" line was corrected.
- diskserver-resets-a-drive-that-is-slow-and-healthy -> the-nvme-driver-
  resets-a-drive-that-is-slow-and-healthy: `COMMAND_SILENCE` is the nvme
  driver's (system/nvme/src/main.rs), a row apart from diskserver.

Extended: a-spawn-drops-an-empty-argument now covers the supervisor's
`serve_launch`, which drops empty and non-UTF-8 arguments from the same
blob; its exit asks for a host test of that decode.

Corrected to the current code, residual defect kept: paths and names
after the userland moves (fsd -> fileserver, init -> supervisor, soundd
-> soundserver, netd -> netstack, blockd -> nvme); line numbers replaced
by names; tests cut since filing recorded as cut (consent_prompt in
ff1a11d, screen_gop_firmware_mode in 520c0d1). Some mechanisms
changed under a record: netring stage 3's stream refusals; the DMA
ordering behind `DmaBuffers::publish`/`observe`; blocked-time
accounting's one caller; `SpawnArgs::image`; the blackbox tail keeping
the newest records; the T14's HDA now driven; and `close_ends_polls`'s
arms at db0cf63. a-usb3-device-attached-but-untrained-reads-as-gone
takes #971's rewrite as it stands.

The per-record table (class, evidence, action) is in the PR's evidence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Oct 21, 2026
#978 (the descriptor table) and this PR both edited
a-stream-cannot-move-between-processes. The merged text is this PR's
account of the refusals stage 3 landed, written in #978's descriptor
terms: the spawn refusal now runs through `toyos::fd::copy` and
`sdk/std/sys/pal/mod.rs`. #978 also changed std's panic for a served
file's raw fd to "has no descriptor", so
a-served-file-panics-when-asked-its-raw-fd quotes it and asks for a
descriptor. Batch 0 (#981) edited no record this PR touches.

The remaining #978-adjacent citations hold at a8ea09d: the kernel's
`SYS_SPAWN` argv filter, the supervisor's `serve_launch` filter,
test-runner's `SysCap::duplicate` endowment, libc's `open` through
`syscall::open`, and fds 1 and 2 through `toyos::log::stdio`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Oct 21, 2026
A one-off accuracy audit of 117 open defect records. Each was read
against the code it cites at origin/main db0cf63,
with the owner's uncommitted requirements/ and claude-questions.md beside it.
After rebasing onto a8ea09d (#978, #981), each record whose cited code
#978 changed was read again.

Deleted, fixed:
- netstack-watches-both-pipes-of-every-connection-on-every-pass: c91e72b
  gave a pipe end's leaving its own watch (kernel/src/pipe.rs) and made
  netstack submit only a changed or answered watch (watches.rs `Armed`).
  The commit named the record and left it.
- shell-c-discards-the-cwd-its-launch-carried: `shell -c` runs in its
  launch's directory since b3770cf.
- std-answers-every-spawn-refusal-but-notfound-as-other: std's spawn maps
  each refusal by name, and every SyscallError through to_io_error, since
  61a283a.

Deleted, superseded (the subject's code or test no longer exists):
- os-toyos-io-traits-keep-a-posix-name: #978 makes a RawFd a number in the
  process's one descriptor table, as on unix, and gives the kernel handle
  its own word, std::os::toyos::io::AsRawHandle.
- kernel_stack_addr-names-an-offset-not-an-address: #583 merged without
  the rename; the rename is a step of
  the-loader-does-only-what-must-precede-the-handover.md.
- log-flush-retry-deadman-arm: the kernel's fsync deadman went with the
  durability ledger (80a1f1c), its kernel USB with usbd (13ed7b4), and
  the test with 9ebf080.
- metal-device-probe-usbread-answered-iofailed-once-beside-other-guests: one
  sighting through the kernel's usb-storage, gone with 13ed7b4; test
  deleted 24aa318.
- netstack-refused-accept-hung-waiting-for-a-wake-that-never-came: netd and
  its listen.rs are gone, the test was deleted (7967fba), and so is
  netcase_against_host.
- process-stats-exits-101-beside-other-guests: both recorded reds were the
  test's own premise; the test is deleted (4a5b228).
- screen-diag-boot-leaves-no-i8042-line-on-the-panel: the kernel's i8042
  and its health line left for ps2server (febfd21); test deleted.

Corrected and kept, each with what remains:
- io-uring-enter-trips-the-one-queue-invariant becomes
  a-wait-was-registered-while-its-task-still-read-waiting: the assertion is
  now Watch::register's; every recorded route is gone; the cause never was
  found.
- libc-loses-the-kernels-word-on-three-write-paths becomes ...-two-...:
  send's stream arm answers through stream_errno; sendto's datagram arm and
  stdio's sys_write still drop the kernel's word.
- logkeeper-flushes-the-records-its-own-refused-flush-made becomes
  logkeeper-flushes-the-records-its-own-flush-made: the measured kernel
  retry is gone; whether any flush now makes records is unmeasured.
- std-calls-undated-what-it-did-not-stat becomes
  std-set-times-answers-ok-and-sets-nothing: the mtime half is fixed.
- remove-dir-all-empties-a-directory-and-leaves-it: std's is fixed; pkg's
  remove_tree still gives that as its reason.
- netstack-holds-a-pending-request-for-a-client-that-left: the connect half
  is gone; the waiting receive is still unwatched.
- netstack-keeps-receiving-for-a-client-that-closed-its-receive-end,
  netstack-socket-ids-are-ambient, netstack-lookup-slots-have-no-per-client-share,
  netstacks-places-are-one-number-for-every-client, most-t14-leases-land-one-dhcp-retry-late:
  restated for regions, badges and netstack.
- smp-ap-hole-and-log-reserve-window-red-under-a-loaded-host: the ROOT read
  half is gone; the first spawn's WouldBlock is kept.
- soundserver-past-due-wake-max-1: only the null sink's wait still does it.
- std-says-a-launch-moves-its-handles-...: the std comment moved into
  toyos::spawn, where it is right; provide's doc is still wrong.
- std-maps-a-device-error-to-other-not-uncategorized: the consumers that
  kept Other load-bearing are gone.
- sharedmemory-slices-rest-on-a-size-nobody-checks: a peer's region maps
  through `lent`, which checks it; `adopt` does not.
- sshserver-authorized-keys-unprotected: installed packages no longer see
  /state; every shipped program still does.
- libc-munmap-refuses-part-of-a-mapping: the owner's 2026-10-21 memory
  checkpoint revises the refusal, so the "rule it final" exit goes.
- libc-refuses-what-toyos-cannot-yet-answer: after #978 F_DUPFD_CLOEXEC,
  F_DUPFD's lowest free and F_GETFD's EBADF hold; the fcntl bullet keeps
  record locks and F_GETFL, F_SETFL, F_GETOWN, F_SETOWN.
- Sites, names and paths moved for: kernel-text-is-writable...,
  libc-answers-einval..., libc-answers-epipe..., libc-host-tests...,
  listing-home..., logkeeper-ends..., map-mmio..., metalprobes-usb-read...,
  mio-deregister..., no-console..., one-diskserver-session...,
  one-write-far-past..., probe-mounts-on-a-checksum, sshserver-holds-one-channel...,
  sshserver-still-speaks-rsa, std-lookup-host..., null-sink-applies-one-connect
  (annotated, not re-seen).

parallel-tests-red-under-other-suites.md loses its pointer to a deleted
record. no-physical-memory-fairness is owner-held and untouched; 58 records
are still valid and 9 uncertain, both unchanged; four were already deleted
on main (#971, #980, #978). The per-record table with
file and line evidence is in the pull request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Oct 21, 2026
A one-off accuracy audit of 117 open defect records. Each was read
against the code it cites at origin/main db0cf63,
with the owner's uncommitted requirements/ and claude-questions.md beside it.
After rebasing onto a8ea09d (#978, #981), each record whose cited code
#978 changed was read again.

Deleted, fixed:
- netstack-watches-both-pipes-of-every-connection-on-every-pass: c91e72b
  gave a pipe end's leaving its own watch (kernel/src/pipe.rs) and made
  netstack submit only a changed or answered watch (watches.rs `Armed`).
  The commit named the record and left it.
- shell-c-discards-the-cwd-its-launch-carried: `shell -c` runs in its
  launch's directory since b3770cf.
- std-answers-every-spawn-refusal-but-notfound-as-other: std's spawn maps
  each refusal by name, and every SyscallError through to_io_error, since
  61a283a.

Deleted, superseded:
- kernel_stack_addr-names-an-offset-not-an-address: the field is still
  `kernel_stack_addr` (sdk/abi/src/boot.rs:9), and #583, its named owner,
  merged without the rename. The rename is a stated step of
  the-loader-does-only-what-must-precede-the-handover.md, which carries it.
- os-toyos-io-traits-keep-a-posix-name: the traits still exist, and now name
  a real descriptor. The owner's 2026-10-21 answers supersede the 2026-08-19
  "fds belong only in libc jargon" ruling (64f806d) the record rested on:
  keep C numbering and dup2 conventions in userland, including Rust/C
  descriptor interoperability; Rust std's C-style descriptor interoperation
  uses the same userland adaptation; one descriptor-inheritance rule honored
  by both libc spawning and Rust Command. #978 implements that, and the
  kernel handle has its own word, std::os::toyos::io::AsRawHandle.
- logkeeper-flushes-the-records-its-own-refused-flush-made: the measured
  loop was the kernel's retry of a budget-refused fsync, whose four records
  made logd's next round flush again. That retry and its records went with
  the durability ledger (80a1f1c) and the kernel's USB storage
  (13ed7b4). logkeeper ends its volume on a refused append or flush
  (system/logkeeper/src/main.rs:625-632) and warns of a slow round once,
  behind its `degraded` latch (:633-643); the file server logs only an
  operation that failed (system/fileserver/src/data.rs:136, 461), and a
  failed flush ends the volume. No current route is known.
The rest have code or a test that no longer exists:
- log-flush-retry-deadman-arm: the kernel's fsync deadman went with the
  durability ledger (80a1f1c), its kernel USB with usbd (13ed7b4), and
  the test with 9ebf080.
- metal-device-probe-usbread-answered-iofailed-once-beside-other-guests: one
  sighting through the kernel's usb-storage, gone with 13ed7b4; test
  deleted 24aa318.
- netstack-refused-accept-hung-waiting-for-a-wake-that-never-came: netd and
  its listen.rs are gone, the test was deleted (7967fba), and so is
  netcase_against_host.
- process-stats-exits-101-beside-other-guests: both recorded reds were the
  test's own premise; the test is deleted (4a5b228).
- screen-diag-boot-leaves-no-i8042-line-on-the-panel: the kernel's i8042
  and its health line left for ps2server (febfd21); test deleted.

Corrected and kept, each with what remains:
- io-uring-enter-trips-the-one-queue-invariant becomes
  a-wait-was-registered-while-its-task-still-read-waiting: the assertion is
  now Watch::register's; every recorded route is gone; the cause never was
  found.
- libc-loses-the-kernels-word-on-three-write-paths becomes ...-two-...:
  send's stream arm answers through stream_errno; sendto's datagram arm and
  stdio's sys_write and sys_read still drop the kernel's word.
- std-calls-undated-what-it-did-not-stat becomes
  std-set-times-answers-ok-and-sets-nothing: the mtime half is fixed.
- remove-dir-all-empties-a-directory-and-leaves-it: std's is fixed; pkg's
  remove_tree still gives that as its reason.
- netstack-holds-a-pending-request-for-a-client-that-left: the connect half
  is gone; the waiting receive is still unwatched.
- netstack-keeps-receiving-for-a-client-that-closed-its-receive-end,
  netstack-socket-ids-are-ambient, netstack-lookup-slots-have-no-per-client-share,
  netstacks-places-are-one-number-for-every-client, most-t14-leases-land-one-dhcp-retry-late:
  restated for regions, badges and netstack.
- smp-ap-hole-and-log-reserve-window-red-under-a-loaded-host: the ROOT read
  half is gone; the first spawn's WouldBlock is kept.
- soundserver-past-due-wake-max-1: only the null sink's wait still does it.
- std-says-a-launch-moves-its-handles-...: the std comment moved into
  toyos::spawn, where it is right; provide's doc is still wrong.
- std-maps-a-device-error-to-other-not-uncategorized: the consumers that
  kept Other load-bearing are gone.
- sharedmemory-slices-rest-on-a-size-nobody-checks: a peer's region maps
  through `lent`, which checks it; `adopt` does not.
- sshserver-authorized-keys-unprotected: installed packages no longer see
  /state; every shipped program still does.
- libc-munmap-refuses-part-of-a-mapping: the owner ruled on 2026-10-21 that
  whole-mapping-only unmap is not the final contract and the refusal is to
  be revised, so the "rule it final" exit goes.
- libc-refuses-what-toyos-cannot-yet-answer: after #978 F_DUPFD_CLOEXEC,
  F_DUPFD's lowest free and F_GETFD's EBADF hold; the fcntl bullet keeps
  record locks and F_GETFL, F_SETFL, F_GETOWN, F_SETOWN.
- Sites, names and paths moved for: kernel-text-is-writable...,
  libc-answers-einval..., libc-answers-epipe..., libc-host-tests...,
  listing-home..., logkeeper-ends..., map-mmio..., metalprobes-usb-read...,
  mio-deregister..., no-console..., one-diskserver-session...,
  one-write-far-past..., probe-mounts-on-a-checksum, sshserver-holds-one-channel...,
  sshserver-still-speaks-rsa, std-lookup-host..., null-sink-applies-one-connect
  (annotated, not re-seen).

parallel-tests-red-under-other-suites.md loses its pointer to a deleted
record. no-physical-memory-fairness is owner-held and untouched; 58 records
are still valid and 9 uncertain (stdio-buffering's deleted-file citation
now names its deleting commit), both unchanged; four were already deleted
on main (#971, #980, #978). The per-record table with
file and line evidence is in the pull request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Oct 21, 2026
Each open defect record of batch 3 was checked against origin/main at
db0cf63, and every record whose cited
code changed up to a8ea09d (#978's
descriptor table, #981) was read again there. Seven of the batch were already
deleted by 13ed7b4, and tearing-is-what-gop-cannot-give-back carries
uncommitted owner edits, so it is untouched.

Deleted as fixed, each exit met:
- the-ack-delay-abi-doc-names-one-cpu: debug_action::TLB_ACK_DELAY_ARM's
  doc names each other CPU in turn, the smallest wait answered and the
  two-second window, as kernel/src/arch/x86_64/tlb.rs does.
- the-running-slots-volume-is-claimable: 13ed7b4 deleted the kernel's
  USB storage path; the kernel serves no partition claim (Claimed holds a
  class, a PCI function, an ISA row or the ACPI row) and hold_source is
  gone. What remains, the block service not refusing the running ROOT, is
  issues/the-block-service-does-not-refuse-the-running-root.md, rejected.
- the-sysroots-core-and-alloc-carry-the-compiler-profiles-debug-assertions:
  5f73215 set debug-assertions-std = false on its own line in
  std_config and re-ran the link-time no-panic proof against the built
  sysroot.
- thread-exits-completion-post-is-the-second-one: fa1e325 deleted
  publish_released, so process::thread_exit's post is the only one.
- the-socket2-forks-ownedfd-holds-a-table-key-not-a-handle: #978 gave
  the process one descriptor table, and socket2 at 2aa6b00, which the
  lock now pins, installs each socket as a descriptor in it and releases
  the socket when the last descriptor goes; std's OwnedFd closes through
  toyos::fd::close. The number OwnedFd holds is the socket's descriptor.

Deleted as superseded:
- t14-keyboard-will-not-report-its-scancode-set: ps2server (#899)
  replaced the kernel driver; its probe resets the keyboard with
  translation on, asks no scancode set and has no firmware-bit fallback,
  and names undecoded runs. The T14 passed ps2server_arms at a42489c.
- the-memory-map-buffer-is-the-unreported-pointer: the framebuffer lies
  in the boot map's scanout regions now, and the margin is MAP_MARGIN, 64.
  The memory map's report and the T14 reading it needs are folded into
  the-boot-map-reaches-4-gib-and-firmware-decides-what-lands-in-it,
  whose exit already asked for the report.
- the-supervisors-claim-of-a-pci-function-the-t14-lacks-holds-preemption-
  off-for-3-8-ms: #926 removed the measured claim path, as the record
  itself said. The preemption-off windows it read are
  syscall-preemption-is-incidental's, whose 131 us mask_windows bound on
  the T14 covers the inventory read that replaced the claim.
- watch-window-spins-out-a-hold-its-poster-is-queued-behind: dc62efc
  deleted the hold with its actuator. Its one durable fact, that the hold
  returns with git revert dc62efc, moves into the reference in
  a-wake-to-a-busy-cpu-waits-for-its-next-pass-and-no-idle-cpu-can-take-it.

Renamed, the old slug refuted, no inbound reference:
- the-panel-seeds-the-loaders-own-file-so-the-gcd-map-is-logged-nowhere
  -> the-gcd-maps-held-mmio-descriptors-are-logged-nowhere: the console
  seeds from logkeeper's stream, not /log's files.
- the-kernel-depends-on-toyos-fat32-and-uses-none-of-it
  -> three-comments-say-the-kernel-uses-toyos-fat32: 13ed7b4 removed
  the dependency; three comments remain.
- two-std-maps-still-answer-other-for-a-dead-peer
  -> std-net-maps-every-other-syscall-error-to-other: TcpStream moved to
  toyos::net::stream; one map remains, on UdpSocket and the listener.

Corrected and kept, each residual defect stated: suite-rows (a guest
binary deleted), t14-desktop-froze-at-64s (the drop line's wording),
t14-lost-every-integrated-input (ps2server's counters and decoder test),
the-t14-mouse-may-be-another-defect (usbd's HID path, no T14 reading),
t14-rows (Bench::blocks), test-runner-holds-logread (metalcase alone),
the EC record (mediated access, WR_EC for AML), the compiler workspace's
toyos-abi lock, the compositor's unknown message, the deadline header's
call sites, the pipe lock (user_ptr pins runs; names and lines), the
boot map's report_reach list, the VFS's function name, the black box's
foreign-record test, the power-off seal (kernel xHCI gone), the copy-out
test, the reset word, both shell records, the shared-object cache's
exit, the SSH key exchange
(toyos-crypto has ML-KEM-768 and its NIST answers), the roster readers,
toyos-dns, toybox's row (power is a connector), the click fold, the
I219 hang, the lendable region, x2APIC's declaration, the volatile
composite site and the file-size ledger. The SO_BROADCAST record's pin
is 2aa6b00, where the setter is still a no-op.

Inbound references fixed in the-t14-touchpad-is-i2c-hid-and-unbuilt,
a-shared-boot-stopped-answering-and-no-capture-says-why,
the-diary-computes-no-lateness-and-records-no-slow-system-call and
a-wake-to-a-busy-cpu-waits-for-its-next-pass-and-no-idle-cpu-can-take-it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Oct 21, 2026
Each open defect record of batch 3 was checked against origin/main at
db0cf63, and every record whose cited
code changed up to a8ea09d (#978's
descriptor table, #981) was read again there. Seven of the batch were already
deleted by 13ed7b4, and tearing-is-what-gop-cannot-give-back carries
uncommitted owner edits, so it is untouched.

Deleted as fixed, each exit met:
- the-ack-delay-abi-doc-names-one-cpu: debug_action::TLB_ACK_DELAY_ARM's
  doc names each other CPU in turn, the smallest wait answered and the
  two-second window, as kernel/src/arch/x86_64/tlb.rs does.
- the-running-slots-volume-is-claimable: 13ed7b4 deleted the kernel's
  USB storage path; the kernel serves no partition claim (Claimed holds a
  class, a PCI function, an ISA row or the ACPI row) and hold_source is
  gone. What remains, the block service not refusing the running ROOT, is
  issues/the-block-service-does-not-refuse-the-running-root.md, rejected.
- the-sysroots-core-and-alloc-carry-the-compiler-profiles-debug-assertions:
  5f73215 set debug-assertions-std = false on its own line in
  std_config and re-ran the link-time no-panic proof against the built
  sysroot.
- thread-exits-completion-post-is-the-second-one: fa1e325 deleted
  publish_released, so process::thread_exit's post is the only one.
- the-socket2-forks-ownedfd-holds-a-table-key-not-a-handle: #978 gave
  the process one descriptor table, and socket2 at 2aa6b00, which the
  lock now pins, installs each socket as a descriptor in it and releases
  the socket when the last descriptor goes; std's OwnedFd closes through
  toyos::fd::close. The number OwnedFd holds is the socket's descriptor.

Deleted as superseded:
- t14-keyboard-will-not-report-its-scancode-set: ps2server (#899)
  replaced the kernel driver; its probe resets the keyboard with
  translation on, asks no scancode set and has no firmware-bit fallback,
  and names undecoded runs. The T14 passed ps2server_arms at a42489c.
- the-memory-map-buffer-is-the-unreported-pointer: the framebuffer lies
  in the boot map's scanout regions now, and the margin is MAP_MARGIN, 64.
  The memory map's report and the T14 reading it needs are folded into
  the-boot-map-reaches-4-gib-and-firmware-decides-what-lands-in-it,
  whose exit already asked for the report.
- the-supervisors-claim-of-a-pci-function-the-t14-lacks-holds-preemption-
  off-for-3-8-ms: #926 removed the measured claim path, as the record
  itself said. The preemption-off windows it read are
  syscall-preemption-is-incidental's, whose 131 us mask_windows bound on
  the T14 covers the inventory read that replaced the claim.
- watch-window-spins-out-a-hold-its-poster-is-queued-behind: dc62efc
  deleted the hold with its actuator. The record's one inbound reference
  was in a-wake-to-a-busy-cpu-waits-for-its-next-pass-and-no-idle-cpu-can-
  take-it, which audit batch 1 (#982) deleted, so no reference remains.
  blocking-read-window-reds-beside-other-guests keeps the pointer that
  git revert dc62efc restores the hold. The hold's own defect, a 50 ms
  spin with the task that would post queued behind it on the same CPU,
  is recorded only here.

Renamed, the old slug refuted, no inbound reference:
- the-panel-seeds-the-loaders-own-file-so-the-gcd-map-is-logged-nowhere
  -> the-gcd-maps-held-mmio-descriptors-are-logged-nowhere: the console
  seeds from logkeeper's stream, not /log's files.
- the-kernel-depends-on-toyos-fat32-and-uses-none-of-it
  -> three-comments-say-the-kernel-uses-toyos-fat32: 13ed7b4 removed
  the dependency; three comments remain.
- two-std-maps-still-answer-other-for-a-dead-peer
  -> std-net-maps-every-other-syscall-error-to-other: TcpStream moved to
  toyos::net::stream; one map remains, on UdpSocket and the listener.

Corrected and kept, each residual defect stated: suite-rows (a guest
binary deleted), t14-desktop-froze-at-64s (the drop line's wording),
t14-lost-every-integrated-input (ps2server's counters and decoder test),
the-t14-mouse-may-be-another-defect (usbd's HID path, no T14 reading),
t14-rows (Bench::blocks), test-runner-holds-logread (metalcase alone),
the EC record (mediated access, WR_EC for AML), the compiler workspace's
toyos-abi lock, the compositor's unknown message, the deadline header's
call sites, the pipe lock (user_ptr pins runs; names and lines), the
boot map's report_reach list, the VFS's function name, the black box's
foreign-record test, the power-off seal (kernel xHCI gone), the copy-out
test, the reset word, both shell records, the shared-object cache's
exit, the SSH key exchange
(toyos-crypto has ML-KEM-768 and its NIST answers), the roster readers,
toyos-dns, toybox's row (power is a connector), the click fold, the
I219 hang, the lendable region, x2APIC's declaration, the volatile
composite site and the file-size ledger. The SO_BROADCAST record's pin
is 2aa6b00, where the setter is still a no-op.

Inbound references fixed in the-t14-touchpad-is-i2c-hid-and-unbuilt,
a-shared-boot-stopped-answering-and-no-capture-says-why and
the-diary-computes-no-lateness-and-records-no-slow-system-call.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Oct 21, 2026
#978 implemented fcntl's F_DUPFD_CLOEXEC on the descriptor table: the
lowest free descriptor at or above the argument, marked close-on-exec, as
POSIX has it. 206_libc_refusals still expected the old refusal (-1,
ENOSYS), and as a metal-only corpus case nothing ran it before the T14 boot
of #988, which answered 10. The record of what libc refuses loses the
same stale lines, and F_DUPFD and F_GETFD's departures with them: both are
POSIX's now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Oct 21, 2026
lowest free descriptor at or above the argument, marked close-on-exec, as
POSIX has it. 206_libc_refusals still expected the old refusal (-1,
ENOSYS), and as a metal-only corpus case nothing ran it before the T14 boot
of #988, which answered 10. The record of what libc refuses loses the
same stale lines, and F_DUPFD and F_GETFD's departures with them: both are
POSIX's now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Oct 21, 2026
The owner asked whether everything self-hosting needs is written down.
toyos-builds-itself.md held it in five lists in an order it called open:
what to build, what other tracks block, what stops M2, what M3 adds and
what else is owed. It is now one list in dependency order: each item has
its status, the issue that owns it and what blocks it, and the evidence it
rests on is below it, with libc's revision for the compiles.

The status words say only what is true: "direction set" is the owner's
ruling of what an item must do, with its design still engineering's. The
memory and socket designs, which are outside the tree and under review, are
named as such, and so is the sequencing the socket design proposes for the
descriptor work's spawn.

Folded in:
- C reaches served files (#975, #978, #988);
- #970's toolchain package;
- the memory direction;
- the process track's stages 4 to 7;
- socket sharing;
- the boot handoff, beside the path;
- the database feasibility study's libc findings;
- the issues that name this track as their owner, among them pread, libc++'s
  std::filesystem, and the two that keep a build from reproducing.

Each owner ruling of 2026-10-21 is recorded once, in its owning issue:
- the signal scope in the process track;
- sharing rather than moving, in the socket issue, whose exit described a
  move;
- the database rulings in a new track, split from the planning
  dispositions made under delegation.

Four findings get a home:
- unlink-while-open;
- libc's uid and gid;
- sigaction's false success;
- st_nlink 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Oct 21, 2026
The owner asked whether everything self-hosting needs is written down.
toyos-builds-itself.md held it in five lists in an order it called open:
what to build, what other tracks block, what stops M2, what M3 adds and
what else is owed. It is now one list in dependency order: each item has
its status, the issue that owns it and what blocks it, and the evidence it
rests on is below it, with libc's revision for the compiles.

The status words say only what is true: "direction set" is the owner's
ruling of what an item must do, with its design still engineering's. The
memory and socket designs, which are outside the tree and under review, are
named as such, and so is the sequencing the socket design proposes for the
descriptor work's spawn.

Folded in:
- C reaches served files (#975, #978, #988);
- #970's toolchain package;
- the memory direction;
- the process track's stages 4 to 7;
- socket sharing;
- the boot handoff, beside the path;
- the database feasibility study's libc findings;
- the issues that name this track as their owner, among them pread, libc++'s
  std::filesystem, and the two that keep a build from reproducing.

Each owner ruling of 2026-10-21 is recorded once, in its owning issue:
- the signal scope in the process track;
- sharing rather than moving, in the socket issue, whose exit described a
  move;
- the database rulings in a new track, split from the planning
  dispositions made under delegation.

Four findings get a home:
- unlink-while-open;
- libc's uid and gid;
- sigaction's false success;
- st_nlink 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Japabu added a commit that referenced this pull request Oct 21, 2026
The owner asked whether everything self-hosting needs is written down.
toyos-builds-itself.md held it in five lists in an order it called open:
what to build, what other tracks block, what stops M2, what M3 adds and
what else is owed. It is now one list in dependency order: each item has
its status, the issue that owns it and what blocks it, and the evidence it
rests on is below it, with libc's revision for the compiles.

The status words say only what is true: "direction set" is the owner's
ruling of what an item must do, with its design still engineering's. The
memory and socket designs, which are outside the tree and under review, are
named as such, and so is the sequencing the socket design proposes for the
descriptor work's spawn.

Folded in:
- C reaches served files (#975, #978, #988);
- #970's toolchain package;
- the memory direction;
- the process track's stages 4 to 7;
- socket sharing;
- the boot handoff, beside the path;
- the database feasibility study's libc findings;
- the issues that name this track as their owner, among them pread, libc++'s
  std::filesystem, and the two that keep a build from reproducing.

Each owner ruling of 2026-10-21 is recorded once, in its owning issue:
- the signal scope in the process track;
- sharing rather than moving, in the socket issue, whose exit described a
  move;
- the database rulings in a new track, split from the planning
  dispositions made under delegation.

Four findings get a home:
- unlink-while-open;
- libc's uid and gid;
- sigaction's false success;
- st_nlink 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant