β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3163

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3163 Β· 7 files
FileTypeDescriptionSize
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)
VERDICT.md
↓ download raw

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) stores rso->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 with wso->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

fixed
baseline reproduced→ patch + rebuild →patched clean

On rebuilt kernel #1 with fix.diff, FIONREAD on the O_RDWR fifo fd returns the queued byte count (5); control unchanged.

fix_run.log
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #1: Mon Sep 7 10:27:31 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

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.