Skip to content

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

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

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

Copy link
Copy Markdown
Member

Root cause

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.

Fix — real named kernel pipes

  • 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>
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