# DF-3227 — VERDICT

**Status: REPRODUCED (deterministic, no race) — impact: panic (kernel heap
corruption class) — fix VALIDATED in-guest (module rebuild).**

## Root cause (line-by-line)

1. `fuse_ipc_get()` assigns sequential uniques from `fmp->unique`
   (starts at 0, `atomic_fetchadd_long`, fuse_ipc.c:100) — after seeing
   ONE request the daemon can predict every future unique.
2. `fuse_ipc_tx()` inserts the fip on **both** queues
   (fuse_ipc.c:258-261) and sleeps (fuse_ipc.c:266).
3. `fuse_device_write()` matches the daemon's reply by
   `fip->unique == ohd->unique` while walking `reply_head`
   (fuse_device.c:191-196) — **it never checks `fip->sent`**, i.e. it
   never verifies the daemon actually *read* the request
   (`fuse_device_read` is what dequeues from `request_head`,
   fuse_device.c:148). On match it removes the fip from `reply_head`
   only, assigns `fip->reply`, sets `replied`, wakes the waiter
   (fuse_device.c:205-220).
4. The woken waiter's success path (fuse_ipc.c:266-286) consumes the
   reply and returns; the caller (e.g. `fuse_statfs`, fuse_vfsops.c:395,
   411) calls `fuse_ipc_put()` → refcnt 1→0 → `objcache_put`
   (fuse_ipc.c:110-117). **Nothing ever removed the fip from
   `request_head`** — the kernel just freed an object that is still the
   head of a live TAILQ.
5. Every subsequent queue operation on `request_head` now touches freed
   memory: `fuse_device_read` pops the freed chunk
   (`fuse_in_size()` reads the `request.len` that `fuse_buf_free`
   zeroed → `uiomove(NULL, 0)` → **read() returns 0** — impossible on a
   healthy /dev/fuse); objcache recycling double-links chunks; and the
   next `TAILQ_INSERT_TAIL` (any new `fuse_ipc_tx`) stores a kernel
   pointer through the dangling `tqh_last` of the freed entry —
   `sys/queue.h:516` INVARIANTS check fires:
   `panic: Bad tailq NEXT(0x...->tqh_last) != NULL, last 0x...`.

## What ran on the guest (stock `#0` INVARIANTS kernel, fresh `with-src` resets)

- Run 1: 41/41 blind replies accepted (incl. unique 1 — the FUSE_STATFS
  issued *inside* fuse_mount, fuse_vfsops.c:238, completing the bug
  before mount(2) even returns); 6 zero-byte reads (SIG-1); teardown
  `umount` → after the un-replied waiters' 35 s timeouts spliced around
  the stale head → **panic: Bad tailq NEXT(0xfffff80117700380->tqh_last)
  != NULL, last 0xfffff8011718bd60 / fuse_ipc_tx+0x7e5 ← fuse_unmount**
  → guest down in DDB (panic.txt, serial_full.txt).
- Run 2 (fresh reset): blind_ok=41, SIG1=7 (daemon_mode0.log,
  run2_summary.txt, serial_run2.txt); teardown wedged uninterruptibly
  (kill -9 ineffective — `fuse_ipc_wait` tsleeps without PCATCH) until
  forced reset. The freed-while-linked state is 100% deterministic; the
  exact panic site depends on teardown shape (insert-time when the stale
  tail is reached).

## Why this is NOT a duplicate of DF-0917

DF-0917 = the device read/write paths drop `ipc_lock` then dereference
`fip` with no refcount, racing the 35 s timeout free — a *timing* UAF;
its fix (refcount_acquire around the device paths) does nothing for this
finding, because here the free is **fully synchronized**: the waiter
wakes, consumes, and puts *after* the write path released the fip
normally. The defect is the **missing dequeue** of a blind-reply-completed
fip — a protocol/lifetime invariant violation with zero timing
requirements. (DF-0918 covers a related but different NULL-reply panic
via `fuse_device_clear`.)

## Exploitability

Attacker = the fuse daemon (root on stock config; operator group opens
the fd, uid 0 mounts — identical trust boundary to DF-0915/0917/3191/
3192; no unpriv→uid0 chain exists on this guest for this family). The
primitive itself is deterministic kernel heap corruption: on
non-INVARIANTS kernels the poisoned insert writes a kernel pointer at a
fixed offset (`tqe_next`) into a freed, immediately-recyclable
`fuse_ipc` chunk (type-stable objcache) — corrupting a live IPC object's
queue links; plus unbounded freed-object deliveries to userspace and an
unkillable 35 s/op D-state wedge. INVARIANTS kernels (this guest) panic
at the first poisoned insert.

## Fix validation

`fix.diff` adds `fuse_ipc_remove(fip)` to the tx success path
(membership-checked under `ipc_lock` → no-op for well-behaved daemons).
Rebuilt fuse.ko in-guest, rebooted, reran the identical PoC:
blind_ok=41 (replies still accepted — protocol unchanged for honest
daemons), **SIG1=0**, reads deliver live requests or time out on a
genuinely empty queue, `umount -f` rc=0, guest up, no panic
(fix_run.log, fix_run2.log).

## 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.
