Skip to content

kernel: implement FIFO (named pipe) semantics — fixes process substitution / brew config - #864

Closed
brandonpayton wants to merge 1 commit into
mainfrom
fix/fifo-named-pipes
Closed

kernel: implement FIFO (named pipe) semantics — fixes process substitution / brew config#864
brandonpayton wants to merge 1 commit into
mainfrom
fix/fifo-named-pipes

Conversation

@brandonpayton

@brandonpayton brandonpayton commented Jul 10, 2026

Copy link
Copy Markdown
Member

Why

A FIFO (first in, first out), also called a named pipe, lets one process read a byte stream written by another process through a filesystem path. EOF means end of file; a reader must not see EOF while a writer can still connect and produce data.

mknod(S_IFIFO) / mkfifo created a regular file with no pipe semantics — the mknod dispatcher stripped the S_IFIFO type bits (mode & 0o7777) and made a plain file. A concurrent reader of that "FIFO" saw an empty regular file and got a premature EOF.

This breaks bash process substitution read < <(cmd): bash mknod()s a FIFO in $TMPDIR (/tmp/sh-np.XXXXXX) and reads it while the writer subshell is still producing output. Traced end-to-end in a Kandelo guest: the reader's read returned 0 because the temp file was an empty S_IFREG (fstat fifo=false file=true size=0).

It blocked Homebrew in Kandelo: brew config/doctor parse git --version via read < <(printf …) and discover ruby via while read < <(which -a ruby). Both got empty output → "Please update your system Git" and "No Homebrew ruby available for wasm32". Both collapse to this one bug.

What changed

  • crate::fifo: registry mapping a FIFO's canonical path → backing pipe index.
  • mknod/mknodat(S_IFIFO) register a kernel pipe (PipeBuffer::new_fifo, no endpoints open) instead of creating a regular file.
  • open() connects a read or write end to that shared pipe (add_reader/add_writer) → real block/EAGAIN/EOF semantics. Fork inheritance works through the existing negative-host_handle FileType::Pipe OFD path (no new fork logic).
  • A FIFO reader with no writer yet blocks (EAGAIN) rather than reporting EOF, until the first writer connects — fixes the reader-opens-before-writer race.
  • stat/lstat/newfstatat report S_IFIFO for registered FIFOs (a FIFO has no VFS inode).
  • unlink/unlinkat remove the name and free the pipe once idle; FIFO pipes are exempt from is_fully_closed so they persist until unlinked.

Validation

  • Process-substitution repro: read a < <(sleep 0.5; printf X); echo [$a][X] (was []).
  • brew config --no-install-from-api now exits 0 inside Kandelo (previously failed on git-version + portable-ruby). API-mode config now proceeds past both blockers to a separate downstream curl issue.
  • 964 kernel unit tests pass, incl. new test_fifo_named_pipe_semantics (EAGAIN-before-writer, data after write, EAGAIN-when-empty-open, EOF after writer close, stat S_IFIFO, unlink).
  • No ABI change — reuses FileType::Pipe; no new syscalls or repr(C) structs; abi/snapshot.json unchanged.

🤖 Generated with Claude Code

mknod(S_IFIFO)/mkfifo created a regular file with no pipe semantics — the
mknod dispatcher stripped the S_IFIFO type bits and made a plain file. A
concurrent reader of that "FIFO" therefore saw an empty regular file and
got a premature EOF. This breaks bash process substitution `read < <(cmd)`,
which mknod()s a FIFO in $TMPDIR and reads it while the writer subshell is
still producing output. It blocked Homebrew: `brew config`/`doctor` parse
`git --version` and discover ruby via `read < <(...)`, so both failed
("Please update your system Git", "No Homebrew ruby available").

A FIFO is now a real named kernel pipe:

- crate::fifo: registry mapping a FIFO's canonical path to its backing pipe.
- mknod/mknodat(S_IFIFO) registers a kernel pipe (PipeBuffer::new_fifo, no
  endpoints open) instead of creating a regular file.
- open() connects a read or write end to that shared pipe (add_reader/
  add_writer) → real block/EAGAIN/EOF semantics. Fork inheritance works via
  the existing negative-host_handle Pipe OFD path.
- A FIFO reader with no writer yet blocks (EAGAIN) instead of reporting EOF
  until the first writer connects, fixing the reader-opens-first race.
- stat/lstat/newfstatat report S_IFIFO for registered FIFOs (no VFS inode).
- unlink/unlinkat remove the name and free the pipe once idle; a FIFO pipe
  is exempt from is_fully_closed so it persists until unlinked.

Validated: process-substitution repro (`read < <(sleep 0.5; printf X)`
returns X, previously empty); `brew config --no-install-from-api` now exits
0 in-Kandelo (previously failed on git-version + portable-ruby); 964 kernel
unit tests pass (adds test_fifo_named_pipe_semantics). No ABI change (no new
syscalls or repr(C) structs; snapshot unchanged).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@brandonpayton

Copy link
Copy Markdown
Member Author

Absorbed by rebase-merged #967. Main is now 6efb411 with the sealed tree b133bfa018f7e2bc84c4845edf2f945b2f844954, and ABI 41 activation completed successfully in run 29550069085. The absorption audit confirmed this PR\x27s behavior and tests are present in that merged tree, so this source PR is closed and its branch is deleted.

@brandonpayton
brandonpayton deleted the fix/fifo-named-pipes branch July 17, 2026 02:38
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