Repository navigation
fileserver: an open file is a description the server keeps, and an export shares it with another process - #975
Conversation
…port shares it with another process A served file's offset lived in the client (std's Served kept it beside the fid), so two processes could not share one open file as POSIX does, and nothing could hand an open file to a child. A description (fileserver::descriptions) is the node, the access an open fixed and the offset; a connection names descriptions by id. READ and WRITE act at the description's offset and move it (an append writes at the end and leaves the offset there), SEEK moves it, and DUP makes a second id sharing it. A stream holds its description and writes at its offset, and before a request that reads or moves the offset the server drains what that description's streams hold, at most a ring's worth, so a child writing continuously cannot hold the loop. A description's node closes with its last id or stream, so Volume::hold has no caller and goes. EXPORT makes a connection for another process holding the descriptions of the ids named, as ids 1.. in that order. The server connects to itself through a private port only it holds a connector to: connect, then accept, which answers exactly that connection on its single-threaded loop. The export takes its served slot when it is made, of the asking connection's share, and is refused TooManyConnections there, so a full share is the parent's refusal and never a dead descriptor in a child; the exported connection counts as served until its hello, which is not counted again, and no handshake bound or timeout applies to it. An export from an exported connection is charged to the same share. The connection names no path: a path request is refused NoDirectory (32; 29 to 31 stay retired). toyos::fs::Dir reads, writes and seeks at the server's offset, gains dup, seek, export and adopt (serving an exported connection, which never reconnects), and std's served File keeps no offset of its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Metal, T14 (ThinkPad T14 Gen 2), head |
Review of #975 r1: head
|
…a stream's bytes come before the write after them An EXPORT's id count is the client's word, and multiplying it by eight overflowed and panicked the server for every client of its role. bounds::export_ids refuses a count outside 1..=MAX_IDS before anything is read. The end an export hands out is narrowed to a connection's rights less DUP, so whoever holds it is its one holder, as the design has it: it may still pass it on. FSTAT and FSYNC settle the description's streams first, as READ, WRITE and SEEK do, so a size or a sync after a child streamed counts what it wrote. Whether a client was exported is its having no directory, one field rather than two that could disagree. an_export_shares_its_descriptions gains the ordering the stream rule promises: a megabyte given to a stream and then a write through the id lands after all of it, and the file ends there. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…5c85ebb7dd5a: host controls, --ci host, both builds, 7 QEMU rows, metal staging incl. the drain control; masked
|
Review r1, round 2, at
Checks, all at the committed head:
Metal is restaged, 2 fix images and 1 drain control, not yet run. Evidence: https://github.com/ToyOSOrg/evidence/tree/ce61cd599ae0536817f27b43363e2341996d2922/r2 |
|
Metal, T14 (ThinkPad T14 Gen 2), head
|
r2: head
|
Kept this branch's deletions of the kernel xHCI, irq_ring, log recovery and the scheduler's Operation framework, and with them #976's per-CPU fields for irq_ring and the no-task operation slot. fileserver takes #975's descriptions without the partition claims this branch removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Implements the owner's decision (Jan, 2026-10-21, claude-questions item 10): small pipe writes are indivisible as ordinary ToyOS pipe behaviour. Closes `issues/a-pipe-write-of-a-few-bytes-can-tear-between-two-writers.md`. ## Problem `pipe::try_write` wrote `min(len, free space)`. Whenever the reader lagged, a 40-byte line could split, and another writer's bytes could land between its two parts. Pipe writers are separate processes that share no lock, so only the pipe can keep a write whole. ## Resulting behaviour - **The bound.** A pipe write of at most `MAX_WHOLE_PIPE_WRITE` = 4096 bytes (`toyos-abi`) lands whole or waits until there is room for all of it. The non-blocking form lands whole or answers `WouldBlock`. Longer writes still land as far as they fit, as before. libc declares the bound as `PIPE_BUF` 4096 in `limits.h`, and a libc host test (`pipe_buf_is_the_kernels_whole_pipe_write`) holds the two equal. A compile-time assertion in `kernel/src/pipe.rs` keeps the bound within the ring (2 MiB less its header). - **One write function.** `kernel::pipe::Landing` (`kernel/pure/pipe.rs`) says how a write lands: `Whole` (a message), `Pipe` (whole up to the bound) or `Partial` (a connection's plain write). Its `room(len)` is the room the write waits for, and `takes(space, len)` is what it writes; both come from the same comparison. `crate::pipe::write(id, buf, landing)` replaces both `try_write` and `write_whole`. #973's message send is now the same write with `Landing::Whole`, so the whole-write mechanism is reused rather than duplicated. - **Size-aware waiting.** `sys_write` waits on `has_space(id, landing.room(len))` instead of one byte. A waiting writer wakes only when its write will land. Every read and the reader's departure still post the write watch, which wakes every waiter to check its own condition again. No new wake path exists, and none is missing. - **Readiness.** A pipe's write end polls writable only when `MAX_WHOLE_PIPE_WRITE` bytes are free. A non-blocking write that was refused and is retried on readiness therefore lands instead of spinning. - **Empty writes wait for nothing.** `Holders::writable` no longer raises the room it wants to one byte. A zero-byte write to a full pipe or connection answers 0 (or `Gone` with no reader) instead of parking. - **Unchanged.** Byte counts, `Gone` / `ResourceExhausted` errors, files, consoles and devices, and the connection's plain write (`Landing::Partial`) behave as before. ## Decisions - **Connections are excluded.** The approval covers pipes, not sockets, so a connection's plain write stays partial and its writability stays one byte. Messages on a connection were already whole. Extending `Landing::Pipe` to connections is a one-line change if the owner wants it. - **4096 bytes.** That is the common `PIPE_BUF`, above POSIX's 512 minimum. It is far below the 2 MiB ring, so a whole write always fits eventually, and readiness costs little. - **No guest test** (review r1 agrees). The behaviour lives in the pure `Landing`/`Holders` functions, which the host tests exercise. The kernel glue around them is three call sites, held by inspection and not by any test or control: `sys_write` passes `room`, `ops::has_space` passes the bound, and `sys_write_nonblock` already maps a refused write to `WouldBlock`. The QEMU smoke run shows the write path is not regressed. It does not establish that the size-aware wait or readiness behave as designed; those rest on inspection plus the host tests. - **Declined review suggestions:** S1 (`Landing::ready_room()`) would need a meaningless arm for `Whole`, which no object polls; S2 (renaming `Landing::Pipe`) is kept as is, since its doc states the behaviour. ## Checks Round 2 (head `1fcbfd9d2`, rebased onto `e74dbf183`, after #975 and #976). Review r1's fixes: stale `pipe::try_write` names fixed in `kernel/loom/tests/inbox_answer.rs` and `issues/the-global-pipe-lock-spans-a-user-copy.md`; the `PIPE_BUF` check; and one clause in `kernel/pure/pipe.rs`'s header. | check | exit | |---|---| | `cargo run -- --ci host` | 0 (88 steps) | | `cargo run -- --build-only` (x86_64), `--arch aarch64` | 0, 0 | | control c7: `PIPE_BUF` set to 512, `cargo test -p toyos-libc-copies pipe_buf` | 101; restored 0 | Round 1 at `584d4f246` (the pipe code is unchanged since, apart from a module comment, so its results carry over): | check | exit | |---|---| | `cargo run -- --ci host` | 0 (88 steps) | | `cargo run -- --build-only` (x86_64), `--arch aarch64` | 0, 0 | | `cargo test -p kernel --lib --features sched-check pipe::` | 0 (12 tests, 7 new) | | QEMU x86_64 `console_image_boots libc_children` (stdio pipes, `spawn_pipe_eof`, `spawn_pipe_reader_gone`, `popen`) | 0 (2/2) | | negative controls c1–c6, each applied, built, run and reverted | each builds (0) and reds (101); restored 0 | New host tests in `kernel/pure/pipe.rs` model the pipe's write, wait condition and poll readiness over the real `Landing` and `Holders`: - whole-or-wait at every amount of room, with a long write still partial; - a refused non-blocking write is answered by readiness, and the retry lands; - empty writes; - the reader leaving answers a waiting writer, and the write is refused; - wake-iff-lands, checked exhaustively over room, length and landing; - three writers with a lagging reader: 1042 writes are refused for room, every record arrives intact and every writer finishes. The same competing run under `Partial` tears, which is the test's own control. Controls: - c1 is the old partial landing. - c2 is the old `want.max(1)`. - c3 is a `writable` that ignores the reader. - c4 mutates `Holders::writable` to want at most one byte. Its effect equals the old one-byte wait (a busy wake), but it does not mutate the `sys_write` glue. - c5 wants one byte more than needed (a missed wake). - c6 mutates only the test model's poll, to one byte. It shows the test's sensitivity to a busy retry, not a product property. No control can catch a revert of the `io.rs` `room` argument or the `ops.rs` bound; those are held by inspection. Red test names are in `controls/summary.txt`. Evidence (masked): round 2 https://github.com/ToyOSOrg/evidence/tree/afbfa442ca959f2a73f339b8dfb6c62189410a3c/r2, round 1 https://github.com/ToyOSOrg/evidence/tree/afbfa442ca959f2a73f339b8dfb6c62189410a3c/r1 ## Risks - **No order among waiting writers.** A whole write of up to 4096 bytes can keep losing freed room to smaller writes that fit first. Starving it would require the ring to stay within 4 KiB of full under continuous competing writes. This is not measured, and POSIX promises no fairness here either. - **Writability now needs 4 KiB free rather than one byte.** A poller that would have written a few bytes into a nearly full pipe now waits until 4 KiB is free. No in-tree program relies on writing to a pipe that is almost full. - **The glue is held by inspection only.** The three kernel call sites have no test or control; the QEMU smoke run shows only that the write path is not regressed. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
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>
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>
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>
Draft, PR 1 of 4 in the descriptor work (
fd-design.mdrevision 8). This one makes a served open file a description that the server keeps and that another process can share. The SDK fd table (PR 2), libc on it (PR 3) and inheritance (PR 4) stack on it.Problem
A served file's offset lived in the client: std's
Servedkept it beside the fid. Two processes could not share one open file as POSIX does, and nothing could hand an open file to a child.Resulting behaviour
system/fileserver/src/descriptions.rs): a description is the node, the access an open fixed, and the offset. A connection names descriptions by id.READandWRITEact at the description's offset and move it; an append writes at the end and leaves the offset there.SEEK(21) moves the offset.DUP(20) makes a second id sharing it.Volume::holdhas no caller and is deleted.EXPORT(22): makes a connection for another process holding the descriptions of the named ids, as ids 1.. in order.TooManyConnectionsthere. A full share is therefore the parent's refusal, never a dead descriptor in a child. The connection counts as served until its HELLO, which is not counted again, and no handshake bound or 2 s timeout applies to it. A grandchild's export is charged to the same share.NoDirectory(32; 29 to 31 stay retired).toyos::fs::Dirreads, writes and seeks at the server's offset. It gainsdup,seek,exportandadopt, which serves an exported connection; an adoptedDirnever reconnects. std's servedFilekeeps no offset of its own, and itstry_clonestill shares oneServedwithin the process.Checks
Round 2 (
8dde655b3, review r1's fixes): the export count is bounded before anything is read (bounds::export_ids); the exported end is narrowed to dropDUP; FSTAT and FSYNC settle streams too; and the stream-ordering rule is tested. All checks reran at the committed head: host base 65 pass; 4 host controls each red on their own test;--ci host0 (88 steps); both builds 0; 7 QEMU rows 7/7. Evidence: https://github.com/ToyOSOrg/evidence/tree/ce61cd599ae0536817f27b43363e2341996d2922/r2Round 1 (tree of
337e74c0a, baseb48aab819); its T14 run was 6/6 PASS:cargo run -- --ci hostcargo run -- --build-only,--arch aarch64cargo test -p fileserver --libdupmakes a fresh descriptionexportnever refusesconsole_image_boots nvme_disk_boots desktop_without_x2apic libc_children a_launch_gives_each_unsent_stdio_slot_a_null machine_shutdown_wire_held a_drives_row_holds_one_process_per_functionEvidence (masked): https://github.com/ToyOSOrg/evidence/tree/053d57458587f7225368bd0e4b46edf0d3beabf4/r1
Metal (staged, not run)
The new guest binary
an_export_shares_its_descriptionsis a discovered Rust test, so it runs only in the T14's shared boots.NoDirectory; the description outlives the exporter's id; and 1 MiB given to a stream comes before a write through the id, which lands at pattern length + 4, where the file ends.settleddrains nothing. Expected: red at "the write did not land after what the stream was given".file_mtime app_view std_fs std_fs_write fs_truncate_persist, in 2 images.What it establishes that host tests cannot is the server's composition of the self-connect, the exported HELLO and the shared offset across two connections.
Risks
pread/pwrite(PR 3) adds them with their caller.ROOMrequest (20 on that branch) renumbers when pkg streams a package into /apps within its volume's room, and packages the toolchain #970 rebases onto PR 3.🤖 Generated with Claude Code