⬢ DragonFlyBSD Kernel Audit
DF-3227 / verdict.json
← back to finding ↓ download raw
{
 "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"
 ]
}