FIONREAD on an O_RDWR fifo descriptor always returns 0 (write-socket pass clobbers the answer)
Summary
fifo_ioctl() forwards every ioctl to BOTH sockets of the fifo's internal pair when the fd is FREAD|FWRITE; for FIONREAD the FREAD pass stores the true queued byte count and the FWRITE pass then overwrites it with wso->so_rcv.ssb_cc which is always 0 (the write socket never receives). ioctl(fd, FIONREAD) on an O_RDWR fifo therefore always reports an empty queue - applications polling O_RDWR fifo protocols busy-spin/stall. VERIFIED unpriv: writer queues 5 bytes; FIONREAD on O_RDWR fd = 0 while the O_RDONLY control = 5. Fix validated (skip FIONREAD on the write side): both return 5.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3163 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fionread_rdwr.c | β | 1.8 KB | view raw | |
| run.log | β | 247 B | view raw | |
| fix_run.log | β | 437 B | view raw | |
| env.txt | β | 210 B | view raw | |
| fix.diff | β | 626 B | view raw | |
| VERDICT.md | β | 1.7 KB | β raw | |
| README.md | β | 1.1 KB | β raw |
DF-3163 β FIONREAD on an O_RDWR fifo descriptor always returns 0
Root cause
fifo_ioctl() (sys/vfs/fifofs/fifo_vnops.c:436-461) forwards every ioctl
to both sockets when the descriptor is FREAD|FWRITE. For FIONREAD,
soo_ioctl() executes *(int *)data = so->so_rcv.ssb_cc
(sys/kern/sys_socket.c:152-155). The FREAD pass stores the true queued
byte count from the read socket; the FWRITE pass then overwrites it
with the write socket's so_rcv.ssb_cc, which is always 0 (that end of
the internal socket pair never receives). Result: FIONREAD on an O_RDWR
fifo fd always reports 0.
PoC
fionread_rdwr.c β writer queues 5 bytes; FIONREAD on an O_RDWR fd and
on an O_RDONLY fd (control).
Build / run
cc -O -Wall -o fionread_rdwr fionread_rdwr.c ./fionread_rdwr
Expected output (bug present)
FIONREAD on O_RDWR fd = 0 (expected 5) FIONREAD on O_RDONLY fd = 5 (control, expected 5) BUG-DF-3163: O_RDWR FIONREAD clobbered by the write-socket pass VERDICT: reproduced
Expected output (fixed kernel)
FIONREAD on O_RDWR fd = 5 (expected 5)
DF-3163 VERDICT β reproduced (ioctl returns wrong data; functional bug)
Reproduced? Yes.
Stock INVARIANTS guest kernel #0 (2026-09-07), unprivileged user: 5 bytes
queued in the fifo; FIONREAD on the O_RDONLY fd correctly returns 5,
on the O_RDWR fd returns 0 (run.log).
Why (line-level)
- fifo_vnops.c:442-450 β FREAD pass:
soo_ioctl(readsock, FIONREAD, data)storesrso->so_rcv.ssb_cc(= 5). - fifo_vnops.c:451-459 β FWRITE pass runs next for an O_RDWR fd:
soo_ioctl(writesock, FIONREAD, data)overwrites the answer withwso->so_rcv.ssb_cc(= 0; the write socket never receives β data flows wso->so_snd <-> rso->so_rcv via unp_connect2, fifo_vnops.c:210). - sys/kern/sys_socket.c:152-155 β soo_ioctl's FIONREAD case.
Impact
Any application using FIONREAD on an O_RDWR fifo descriptor (a legal and common pattern for bidirectional fifo protocols) is told the queue is always empty β busy-poll loops spin, protocol state machines stall. Information-correctness bug only; no memory safety impact. Same quirk exists in FreeBSD's fifo code, but that does not make it correct.
Fix validation
fix.diff: skip the FWRITE pass for FIONREAD. On the rebuilt guest
kernel (same build cycle as DF-3161/3162 validation) the PoC reports
FIONREAD on O_RDWR fd = 5 (see fix_run.log); O_RDONLY control
unchanged.
Chain summary
Unpriv user -> wrong ioctl answer. No escalation path.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
fixedOn rebuilt kernel #1 with fix.diff, FIONREAD on the O_RDWR fifo fd returns the queued byte count (5); control unchanged.
fix_run.log
Confirmed kernel references
Detail
Evidence (decisive lines)
['run.log: O_RDWR=0 vs O_RDONLY control=5 (stock kernel)', 'fix_run.log: O_RDWR=5 (patched kernel #1)']
PoC changes
Authored from scratch; no seed. mkfifo + O_RDWR fd + FIONREAD vs O_RDONLY control.
Verified recommended fix
fifo_ioctl: do not forward FIONREAD to the write socket.
Verdict
Reproduced as unprivileged user on stock kernel #0: with 5 bytes queued, ioctl(FIONREAD) on an O_RDWR fifo fd returns 0 while the O_RDONLY control fd returns 5 - the FWRITE pass of fifo_ioctl overwrites the read-socket answer with the write socket's always-empty so_rcv count (fifo_vnops.c:442-459, sys_socket.c:152-155). Functional correctness bug (apps polling O_RDWR fifos believe the queue is always empty); no memory-safety impact. Fix validated: skipping the FWRITE pass for FIONREAD, the O_RDWR fd returns 5 on rebuilt kernel #1.
No comments yet.