⬢ DragonFlyBSD Kernel Audit
DF-3229 / verdict.json
← back to finding ↓ download raw
{
 "finding_id": "DF-3229",
 "status": "reproduced",
 "reproduced": 1,
 "impact": "panic",
 "confidence": "certain",
 "verdict": "Deterministically reproduced (2/2 stock runs, 20/20 victims each): fuse_ipc_tx()s error exits at fuse_ipc.c:163-167 (dead-at-entry) and :198-201 (dead-after-blind-reply) return without fuse_ipc_remove(), so the error-path fuse_ipc_put() at tx:270 frees a fuse_ipc still linked on fmp->request_head (both queues for the :163 path). Staging: drained-queue daemon + 20 one-shot fstatfs victims + blind-replies of only the three predicted umount-internal uniques (FSYNCDIR/RELEASEDIR/DESTROY) bring fuse_mount_kill() down with the victims unread and hung; post-kill storm writes then complete every victim through the :198 error exit (write(2) EPROTO/0 = matched; fuse_device_write completes the IPC regardless of the audit failure). The freed-while-linked state is latent on a dead mount (stock shows no panic from this path alone), but its reality was kernel-confirmed during fix validation: with only this findings fix applied (DF-3227 still open), the fixs own fuse_ipc_remove() walk panicked on Bad link elm ... next->prev != elm over the corrupted list. With DF-3229+DF-3227 fixes composed, a disclosed witness kprintf showed the fix dequeuing each victim (done=1 sent=1) before the free and the guest survived the identical run.",
 "exploit_chain": "Attacker = fuse daemon / mount owner (uid0-gated on stock config, operator+vfs.usermount expands). (1) Queue unread FUSE requests via any fs op; (2) umount the fs (kill) with the requests hung, or race the kill into the tx enqueue window for the both-lists :163 variant; (3) blind-reply the victims uniques post-kill (sequential uniques, fuse_device_write has no dead check) - each reply completes the waiter via :198 and the callers put() frees the chunk while linked on request_head; (4) the freed type-stable objcache chunk is the same corruption primitive DF-3227 verified as destructive (Bad tailq panics, 0-byte stale-head pops, recycled-chunk corruption, 16-byte fip->reply=fb UAF write for the reply_head-linked :163 variant); (5) unkillable 35s-per-request no-PCATCH tsleep wedge applies to the hung waiters. No uid0 chain on this guest: the bug is root/operator-gated at the daemon boundary (same as DF-3227s verified assessment).",
 "evidence": [
  "run.log / run.2.log: storm done: matched=20 of 20 victims (+4 probes) with exact unique predictions (U0=13: victims 14-33, FSYNCDIR 34, RELEASEDIR 35, DESTROY 36; U0=5 likewise) and all-beyond ENOMSG",
  "VERDICT.md Kernel-level proof: my-fix-only run panicked in fuse_ipc_remove() on Bad link elm ... next->prev != elm - the list really contained freed-linked chunks",
  "fix_run_serial.txt: witness DF3229FIX: removed fip=... done=1 sent=1 firing for victims on the both-fixes kernel, guest survived",
  "fuse_ipc.c:163-167, :198-201 vs dequeueing siblings :186/:193 and wait_sent :213/:240 - the asymmetry is the defect"
 ],
 "kernel_refs": [
  "sys/vfs/fuse/fuse_ipc.c:258",
  "sys/vfs/fuse/fuse_ipc.c:163",
  "sys/vfs/fuse/fuse_ipc.c:198",
  "sys/vfs/fuse/fuse_ipc.c:268",
  "sys/vfs/fuse/fuse_ipc.c:110",
  "sys/vfs/fuse/fuse_device.c:190",
  "sys/vfs/fuse/fuse_vfsops.c:272"
 ],
 "poc_changes": "Rewrote the seed concept entirely: v4 choreography (quiesced daemon, barrier-released one-shot fstatfs victims so the victim count and unique range are exact, ENOMSG-safe retry-polls of the three predicted umount-internal uniques, post-kill storm). Earlier versions (statfs hammer, getfsstat, fstatfs loop) failed because kill-detection required an empty request_head, vflush serialized namei-based traffic, and post-kill fstatfs loops hit the separate DF-3230 panic; the deterministic barrier staging eliminates all races.",
 "attempts": 9,
 "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": 4800,
 "guest_dirty": 0,
 "build_cmd": "mkdir -p /tmp/df3229d && cc -O2 -Wall -o /tmp/df3229d/df3229 /tmp/df3229.c -lpthread",
 "run_cmd": "kldload fuse && /tmp/df3229d/df3229 20  (expects: KILL confirmed; storm done: matched=20 of 20 victims)",
 "code_hash": "1171c579211e9d837445a0e43ce321d8dad076c0b1afc27e877c8e51a55b26e8",
 "notes": "Guest reset to clean with-src state after validation (stock fuse_ipc.c confirmed restored, fuse not loaded). The three umount-internal blind replies in the staging exercise DF-3227s success-path defect by design (unavoidable: DESTROY sits behind the victims in the FIFO) - they are excluded from this findings markers by unique. Cross-file note for the fuse_vnops pass: fuse_vop_strategy BUF_CMD_WRITE evaluates fwo = fuse_out_data(fip) unconditionally after tx even on the error path where tx already put/freed the fip (fuse_vnops.c:2088) - value unused, benign UAF read, belongs to the vnops audit.",
 "recommended_fix": "Call fuse_ipc_remove(fip) on both un-dequeued wait exits (ipc.c:165 and ipc.c:200) before returning ENOTCONN - see fix.diff; must be combined with DF-3227s success-path remove for a consistent queue.",
 "fix_status": "fixed",
 "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #0 + in-guest rebuilt fuse.ko (fix.diff + DF-3227 fix.diff + disclosed witness kprintf)",
 "fix_baseline_reproduced": 1,
 "fix_patched_reproduced": 0,
 "fix_verdict": "Baseline stock: 20/20 victims completed via the :198 error exit and freed while linked (code-certain; the freed-linked list state kernel-confirmed by the my-fix-only Bad link elm panic). Patched (both fixes): identical daemon-visible protocol (storm still matches 20/20 - the fix changes lifetime, not semantics) but the witness proves each victim is dequeued by fuse_ipc_remove BEFORE the put; lists left consistent; no panic; guest survived. My-fix-only run additionally panicked over the still-unfixed DF-3227 chunks - the two fixes compose and must land together.",
 "fix_evidence": "fix_run.log + fix_run_serial.txt (witness lines DF3229FIX: removed fip=... done=1 sent=1, RESULT matched=20, guest up); VERDICT.md quotes the my-fix-only panic: panic: Bad link elm 0xfffff80117ccf420 next->prev != elm / fuse_ipc_remove+0x239 <- fuse_ipc_tx <- fuse_statfs <- kern_fstatfs"
}