Repository navigation
pipe: a write of at most MAX_WHOLE_PIPE_WRITE bytes lands whole - #977
Conversation
Review r1: #977 at 584d4f2 (base b48aab8)I read the commit, the full diff, Architectural assessmentThe design matches the owner's decision and the existing structure. The pipe that owns the ring is the only place that can keep a write whole, and the change is made there. Message send is unchanged in behaviour. With Connection plain writes change only as documented. A zero-byte write to a full ring now answers 0 instead of parking. The missing-pipe word changes from NotFound to Gone, but no caller can reach it while it holds an end. Kernel glue, checked by inspection:
Wakeups. Every byte-consuming read ( Behaviour changes.
Fairness. The risk is real but bounded. It needs the reader to keep the 2 MiB ring within 4 KiB of full while small writers keep landing, and the small writers are making progress while it lasts. Common POSIX pipes share the property. The competing-writers model shows every writer finishing under seeded random scheduling, but that is not a measurement of fairness. I do not require an issue for it; see S3. FindingsBLOCKER
SUGGESTION S1. S2. S3. Fairness: if the orchestrator wants the risk recorded durably rather than only in the PR body, file a short S4. Tests and evidence
SEND BACK |
A pipe write took min(len, free space), so whenever the reader lagged a 40-byte line could split, and another writer's bytes could land between its parts. Its writers are separate processes sharing no lock, so only the pipe can keep a write whole. Owner decision (Jan, 2026-10-21): up to a documented bound a pipe write lands whole or waits for room for all of it; the non-blocking form lands whole or answers WouldBlock; longer writes may stay partial; libc exposes the bound as PIPE_BUF. Pipes only, not files, sockets or devices. - toyos-abi: MAX_WHOLE_PIPE_WRITE = 4096, libc's PIPE_BUF. The kernel asserts at compile time that it fits the ring. - kernel::pipe::Landing says how a write lands: Whole (a message), Pipe (whole up to the bound, as far as it fits past it) or Partial (a connection's plain write, unchanged). room(len) is what the write waits for and takes(space, len) what it writes, from one comparison. - crate::pipe::write(id, buf, landing) replaces both try_write and write_whole: the message send is the same write with Landing::Whole. - sys_write waits on has_space(id, landing.room(len)) instead of one byte, so a waiter wakes only when its write lands, and every read and the reader's leaving still post the write watch, which wakes every waiter to re-read its condition. - A pipe end polls writable only with MAX_WHOLE_PIPE_WRITE bytes free, so a non-blocking write refused and retried on readiness lands rather than spinning. - An empty write waits for nothing: Holders::writable no longer raises the wanted room to one byte, so a zero-byte write to a full pipe or connection answers 0 (or Gone without a reader) instead of parking. 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, non-blocking refusal answered by readiness, empty writes, the reader leaving, wake-iff-lands at every amount of room, and three writers with a lagging reader whose records never tear (with the same run under Partial as the test's own control). Closes issues/a-pipe-write-of-a-few-bytes-can-tear-between-two-writers.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…names - sdk/libc/arch: a host test asserts limits.h's PIPE_BUF equals MAX_WHOLE_PIPE_WRITE, as path_max_is_limits_h_s does for PATH_MAX. - kernel/loom/tests/inbox_answer.rs and issues/the-global-pipe-lock-spans-a-user-copy.md name pipe::write, which replaced pipe::try_write. - kernel/pure/pipe.rs's header says it also owns how a write lands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
584d4f2 to
1fcbfd9
Compare
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_writewrotemin(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
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 answersWouldBlock. Longer writes still land as far as they fit, as before. libc declares the bound asPIPE_BUF4096 inlimits.h, and a libc host test (pipe_buf_is_the_kernels_whole_pipe_write) holds the two equal. A compile-time assertion inkernel/src/pipe.rskeeps the bound within the ring (2 MiB less its header).kernel::pipe::Landing(kernel/pure/pipe.rs) says how a write lands:Whole(a message),Pipe(whole up to the bound) orPartial(a connection's plain write). Itsroom(len)is the room the write waits for, andtakes(space, len)is what it writes; both come from the same comparison.crate::pipe::write(id, buf, landing)replaces bothtry_writeandwrite_whole. ipc: a frame and its handles travel as one message; the standalone handle send is retired #973's message send is now the same write withLanding::Whole, so the whole-write mechanism is reused rather than duplicated.sys_writewaits onhas_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.MAX_WHOLE_PIPE_WRITEbytes are free. A non-blocking write that was refused and is retried on readiness therefore lands instead of spinning.Holders::writableno longer raises the room it wants to one byte. A zero-byte write to a full pipe or connection answers 0 (orGonewith no reader) instead of parking.Gone/ResourceExhaustederrors, files, consoles and devices, and the connection's plain write (Landing::Partial) behave as before.Decisions
Landing::Pipeto connections is a one-line change if the owner wants it.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.Landing/Holdersfunctions, 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_writepassesroom,ops::has_spacepasses the bound, andsys_write_nonblockalready maps a refused write toWouldBlock. 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.Landing::ready_room()) would need a meaningless arm forWhole, which no object polls; S2 (renamingLanding::Pipe) is kept as is, since its doc states the behaviour.Checks
Round 2 (head
1fcbfd9d2, rebased ontoe74dbf183, after #975 and #976). Review r1's fixes: stalepipe::try_writenames fixed inkernel/loom/tests/inbox_answer.rsandissues/the-global-pipe-lock-spans-a-user-copy.md; thePIPE_BUFcheck; and one clause inkernel/pure/pipe.rs's header.cargo run -- --ci hostcargo run -- --build-only(x86_64),--arch aarch64PIPE_BUFset to 512,cargo test -p toyos-libc-copies pipe_bufRound 1 at
584d4f246(the pipe code is unchanged since, apart from a module comment, so its results carry over):cargo run -- --ci hostcargo run -- --build-only(x86_64),--arch aarch64cargo test -p kernel --lib --features sched-check pipe::console_image_boots libc_children(stdio pipes,spawn_pipe_eof,spawn_pipe_reader_gone,popen)New host tests in
kernel/pure/pipe.rsmodel the pipe's write, wait condition and poll readiness over the realLandingandHolders:The same competing run under
Partialtears, which is the test's own control.Controls:
want.max(1).writablethat ignores the reader.Holders::writableto want at most one byte. Its effect equals the old one-byte wait (a busy wake), but it does not mutate thesys_writeglue.No control can catch a revert of the
io.rsroomargument or theops.rsbound; those are held by inspection. Red test names are incontrols/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
🤖 Generated with Claude Code