DF-3227 / verdict.json
{ "finding_id": "DF-3227", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "REPRODUCED deterministically (no race) on the stock INVARIANTS guest, twice from fresh resets: a FUSE daemon that blind-replies to predictable uniques (fuse_device_write matches ohd->unique without checking fip->sent, fuse_device.c:192) completes waiters whose fip is still linked on fmp->request_head; fuse_ipc_txs success path never dequeues (fuse_ipc.c:266-286), so the waiters fuse_ipc_put frees a still-queued object. Observable: 41/41 blind replies accepted and 6-7 zero-byte read()s per run popping freed fips (SIG-1, impossible on a healthy /dev/fuse), then teardown panic panic: Bad tailq NEXT(0x...->tqh_last) != NULL in fuse_ipc_tx+0x7e5 <- fuse_unmount (INVARIANTS TAILQ_INSERT through the dangling tqh_last of the freed entry; sys/queue.h:516). On non-INVARIANTS kernels the same insert silently writes a kernel pointer into freed/recycled fuse_ipc chunks. Root cause is DISTINCT from DF-0917 (timing UAF from the missing refcount around the device paths lock drop): this is a missing-dequeue invariant violation reachable with zero timing luck; DF-0917s refcount fix does not close it and this fix does not close DF-0917s. Attacker is the fuse daemon \u2014 root/operator-gated on stock config (same trust boundary as DF-0915/0917/3191/3192), so no unpriv->uid0 chain exists on this guest; the primitive is deterministic kernel heap corruption plus an unkillable-D-state wedge (35s uninterruptible fuse_ipc_wait per un-replied op).", "exploit_chain": "daemon reads FUSE_INIT (learns unique=0 base) -> predicts unique N -> write(/dev/fuse, reply unique=N) without ever reading request N -> fuse_device_write dequeues fip from reply_head only, sets replied, wakes waiter -> waiter consumes reply, fuse_ipc_put frees fip STILL LINKED as request_head head -> stale chunk popped by subsequent read()s (0-byte reads SIG-1) or recycled+double-linked (duplicate request delivery) -> later TAILQ_INSERT_TAIL writes through dangling tqh_last into freed/recycled heap: INVARIANTS panic (observed), production: silent pointer store into recycled fuse_ipc chunk at fixed offset (tqe_next), corrupting a live IPC objects queue links/fields.", "evidence": [ "findings/poc/DF-3227/panic.txt \u2014 panic: Bad tailq NEXT(0xfffff80117700380->tqh_last) != NULL fuse_ipc_tx+0x7e5 <- fuse_unmount (run 1)", "findings/poc/DF-3227/daemon_mode0.log \u2014 41 blind replies accepted, SIG-1 zero-byte reads x6", "findings/poc/DF-3227/run2_summary.txt + serial_run2.txt \u2014 fresh-reset repro: blind_ok=41 SIG1=7", "findings/poc/DF-3227/fix_run2.log \u2014 patched module: blind_ok=41 SIG1=0, umount -f rc=0, no panic" ], "kernel_refs": [ "sys/vfs/fuse/fuse_device.c:191-197", "sys/vfs/fuse/fuse_device.c:205-220", "sys/vfs/fuse/fuse_ipc.c:246-287", "sys/vfs/fuse/fuse_ipc.c:258-261", "sys/vfs/fuse/fuse_ipc.c:110-117", "sys/vfs/fuse/fuse_ipc.c:100", "sys/vfs/fuse/fuse_device.c:148-161", "sys/sys/queue.h:511-531" ], "poc_changes": "Adapted the DF-0917 daemon harness: removed the 35s race-delay entirely (bug needs no race), added predicted-unique blind replies with ENOMSG retry, alarm-guarded interleaved read()s to pop the stale queue head (SIG-1 zero-byte detection + duplicate-unique detection SIG-2), and a teardown umount phase that triggers the insert-time panic.", "attempts": 4, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 420, "guest_dirty": 0, "build_cmd": "cc -O2 -Wall -o /tmp/df3227/df3227 /tmp/poc-src/DF-3227/df3227.c -lpthread", "run_cmd": "kldload fuse && /tmp/df3227/df3227 0 ; kill $(pgrep df3227); umount /mnt/df3227 # panic expected at teardown", "code_hash": "98a7eabe2dfd47153a20236a1e64150c9e16bbbaed046de0e95c075f92cabfcf", "notes": "Run 1 panicked at teardown after ~40s of fuse_ipc_wait timeout/retry (un-replied child statfs waiters splice around the stale head leaving tqh_last dangling). Run 2 (fresh reset) reproduced all deterministic signatures (41 blind, 7 SIG-1) and wedged in uninterruptible D-state until forced reset \u2014 the panic insertion point is teardown-shape dependent, the freed-while-linked state itself is 100% deterministic. Secondary DoS: waiters sleep 35s per op in tsleep WITHOUT PCATCH (unkillable kill -9 observed).", "recommended_fix": "fuse_ipc_tx success path must fuse_ipc_remove(fip) before returning to the caller (see fix.diff); belt-and-suspenders: fuse_device_write can additionally reject replies whose fip->sent != 1 (Linux moves the request pending->processing on read, achieving the same).", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (fuse.ko module rebuilt in-guest with fix.diff, module-level fix)", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "Patched fuse.ko (fuse_ipc_remove on tx success): identical PoC run shows blind_ok=41 (replies still accepted), SIG1_zero_byte_reads=0 (no freed fip ever popped), reads deliver live requests or time out on a genuinely empty queue, forced umount returns 0, no panic, guest stays up. Baseline vs patched logs in run.log/daemon_mode0.log vs fix_run.log/fix_run2.log.", "fix_evidence": [ "findings/poc/DF-3227/fix_run.log", "findings/poc/DF-3227/fix_run2.log", "findings/poc/DF-3227/fix.diff" ] } |