# DF-3229 — fuse_ipc_tx() error paths free a still-linked fuse_ipc

**Status: REPRODUCED (deterministic, 2/2 stock runs, 20/20 victims each) —
memcorrupt (free-while-linked, DF-3227's verified class, on the tx ERROR
paths) — fix VALIDATED in composition with DF-3227's fix.**

## Root cause (line-accurate)

`fuse_ipc_tx()` (sys/vfs/fuse/fuse_ipc.c:246-287) enqueues the fip on BOTH
`fmp->request_head` AND `fmp->reply_head` (:258-261) before waiting.
Two exits of `fuse_ipc_wait()` return WITHOUT `fuse_ipc_remove()`, after
which `fuse_ipc_tx`'s error path calls `fuse_ipc_put()` (:270) → refcnt
1→0 → `objcache_put()` (:110-117) — **the fip is freed while still
linked**:

* **PATH A** — `fuse_ipc.c:163-167`: dead set between tx:253's
  `fuse_test_dead` and wait:163's check (a ~µs window straddling the two
  queue inserts):
  ```c
  if (fuse_test_dead(fip->fmp)) {
          KKASSERT(!fuse_ipc_test_replied(fip));
          fuse_ipc_set_replied(fip);      /* <-- NO fuse_ipc_remove */
          return ENOTCONN;
  }
  ```
  → freed while linked on **both** queues. (Sibling exits all dequeue:
  :186 timeout, :193 tsleep-error, wait_sent :213/:240 — only :163 and
  :198 forget.)

* **PATH B** — `fuse_ipc.c:198-201`: a waiter sleeping at :175 when the
  mount is killed is NOT woken by `fuse_mount_kill()` (it wakes `fmp`,
  not fips).  If the daemon then WRITES a reply for the unread request,
  `fuse_device_write` (fuse_device.c:190-220 — note it has **no dead
  check**) dequeues reply_head, sets replied, wakes the waiter; the
  waiter hits `if (fuse_test_dead(...)) { KKASSERT(replied); return
  ENOTCONN; }` with **no remove** → tx:270 put() frees a fip still
  linked on **request_head**.

Distinct from the known findings: DF-0917 (device read/write no-ref
*timing* UAF vs the 35s timeout), DF-0918 (NULL `ohd` panic on the
early-return-0 race), DF-3227 (**success**-path missing dequeue for
blind replies — its verified fix only adds remove on the success path;
both paths here are error paths and remain broken with that fix
applied).  DF-3193 is the fmp (not fip) lifetime bug.

## Deterministic staging (df3229.c)

1. Daemon services INIT + in-mount STATFS + first OPENDIR, waits for a
   fully drained queue → `U0` = last unique.
2. Barrier release: N (=20) one-shot hammer threads each issue exactly
   ONE `fstatfs(fd)` (fd on the mount root; `kern_fstatfs` derives the
   mount from `fp->f_nchandle.mount` with NO namei and NO vnode ops, so
   traffic is not blocked by vflush) → N unread hung victims
   `U0+1 .. U0+N` linked on both queues.
3. Child `umount(MNT_FORCE)`: vflush vgones the root →
   `fuse_vop_inactive` issues FUSE_FSYNCDIR (`U0+N+1`) and
   FUSE_RELEASEDIR (`U0+N+2`), then `fuse_unmount` issues FUSE_DESTROY
   (`U0+N+3`).  The daemon blind-replies **exactly these three predicted
   uniques** (retry-polling: ENOMSG = not queued yet = harmless), so the
   umount advances to `fuse_mount_kill()` — dead=1 — **without ever
   touching the victims**.  (Those three fips complete via the tx
   SUCCESS path = the known DF-3227 blind-reply defect; they are
   excluded from the markers by unique.)
4. Kill confirmed (read → ENOTCONN, no pop: the read's dead check at
   fuse_device.c:130-131 fires before the queue walk).  Storm writes
   `[U0+1 .. U0+N]`: every MATCHED write (write(2) returns 0, or EPROTO
   — fuse_device_write completes the IPC even when the audit-length
   check fails) completes a victim via the **:198 error exit** → its
   put() at tx:270 frees it **still linked on request_head**.

## What ran on the guest (stock INVARIANTS kernel #0)

* Run 1 and Run 2 (fresh `with-src` state, stock fuse.ko):
  unique prediction exact every time (e.g. U0=13 → victims 14..33,
  FSYNCDIR 34 / RELEASEDIR 35 / DESTROY 36), kill confirmed, and
  **all 20/20 victim uniques MATCHED** post-kill (`W u=14..33 err=92
  MATCHED`), sanity probes beyond (34..37) ENOMSG.
  `storm done: matched=20 of 20 victims (+4 probes)`.
* Userspace wake evidence: each storm-matched victim's fstatfs later
  returns ENOTCONN (threads run at nice +19; end-events land after the
  correlate window — never_returned=20 at +0.9s; the child process was
  externally SIGKILL'd at umount-return in run 2 taking its per-thread
  dlogs — recorded honestly in run.2.log / fix_run.log).
* The freed-while-linked state itself is kernel-internal on a
  dead-but-alive fmp (no post-kill code walks request_head:
  fuse_device_read is dead-gated at :130-131, and fuse_device_clear's
  only caller is that same gated path), so the corruption is **latent**
  on the stock kernel — same object state DF-3227 proved destructive
  (`Bad tailq` panic / stale-queue-head pops / recycled-chunk writes)
  whenever the list IS walked.

## Kernel-level proof the linked-freed chunks are real

Fix-validation run with **only** DF-3229's fix applied (DF-3227 still
unfixed): the fix's own `fuse_ipc_remove()` (now walking request_head to
dequeue each victim) **panicked**:

```
panic: Bad link elm 0xfffff80117ccf420 next->prev != elm
fuse_ipc_remove() at fuse_ipc_remove+0x239
fuse_ipc_tx() at fuse_ipc_tx+0x581
fuse_statfs() ... kern_fstatfs() ... sys_fstatfs()
```

The INVARIANTS queue check found actually-corrupted links — the list
contained freed-but-linked chunks created by the still-unfixed DF-3227
success path (the three umount-internal blind replies).  This is direct
kernel confirmation that free-while-linked chunks exist on that list in
this staging — and demonstrates the two findings' fixes must compose.

## Fix validation (fix.diff = fuse_ipc_remove on both error exits)

* BOTH fixes (DF-3229 + DF-3227's verified success-path fix) + a
  disclosed witness `kprintf` in `fuse_ipc_remove`:
  * module rebuilt in-guest (`make` in /usr/src/sys/vfs/fuse), loaded,
  * identical PoC rerun: kill confirmed, storm matched 20/20 (protocol
    unchanged for the daemon — the fix only changes lifetime, not
    semantics),
  * **witness fired for the victims: `DF3229FIX: removed fip=...
    done=1 sent=1`** (done=1 = the :198 path; 11 removes in dmesg) —
    the fix dequeues each victim BEFORE the put, so they are freed
    clean,
  * **guest survived, no panic** (vs. the my-fix-only panic above).
* Baseline vs patched: stock = 20/20 victims freed while linked
  (code-certain, and the linked-freed state kernel-confirmed via the
  my-fix-only panic); patched = same 20/20 completions but every victim
  dequeued before the free (witness), lists left consistent, no panic.

## Exploitability

Same trust boundary as DF-3227/0915/0917: the attacker is the fuse
daemon / mount owner (uid 0 on stock config; operator+usermount expands
it).  The primitive is DF-3227's verified corruption class (freed
type-stable objcache chunk linked in a live TAILQ: recycled-chunk
corruption, 16-byte `fip->reply = fb` UAF write through a blind reply
matching a PATH A victim still on reply_head).  The 35s-per-request
no-PCATCH tsleep (DF-3227's verified secondary) also applies to these
waiters.

## Files

* `df3229.c` — PoC (deterministic staging)
* `run.log`, `run.2.log` — the two stock 20/20 runs (session-captured
  console+log output; the guest's /tmp logs from those boots were not
  preserved across resets — the decisive lines are quoted verbatim)
* `fix_run.log`, `fix_run_serial.txt` — both-fixes validation run
* `fix.diff` — the verified fix (two hunks)
* `verdict.json` / `manifest.json`

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