# DF-4148 — cuse client-open vs server-teardown UAF/double-free race

## What (code trace — not reproduced on the guest)

`cuse_client_open()` (`cuse.c:1497-1531`) loads `pcsd = dev->si_drv1`
**unlocked** (1509), then dereferences `pcsd->server`/`pcsd->user_dev`
(1511-1512), takes `pcs->mtx` and does `pcs->refs++` (1521).

The final `cuse_server_unref()` (`cuse.c:685-732`) decrements `refs` to 0
under `pcs->mtx` (692) with **no re-check afterwards** and tears down:

* `cuse_server_free_dev()` NULLs `si_drv1` under `pcs->mtx` (668-669),
  calls `destroy_dev(pcsd->kern_dev)` (680) — on DragonFly this is
  **asynchronous** (`devfs_destroy_dev` → `lwkt_sendmsg`,
  `sys/vfs/devfs/devfs_core.c:755-763`; the "destroy device synchronously"
  comment at `cuse.c:679` is wrong for DFly) — and then `kfree(pcsd)` (682)
  **without waiting for in-flight `cuse_client_open` dispatches**;
* `kfree(pcs)` at 731 after the last unlock at 723.

Interleaving A (refs race): a client open that loaded `si_drv1` before the
NULL write but executes `refs++` after the final `--refs` (0→1) proceeds on
the dying server: `TAILQ_INSERT_TAIL(&pcs->hcli, pcc)` (1561) into memory
that is kfree'd at 731, and since `is_closing` was already set its error
path (1564-1574) runs `devfs_clear_cdevpriv` → `cuse_client_free` →
`cuse_server_unref` (refs 1→0) → a **second full teardown**: second
`TAILQ_REMOVE` from `cuse_server_head` (already removed at 701, stale
tqe pointers → global-list corruption), re-walk of `hdev` racing thread 1
→ double `destroy_dev`/`kfree(pcsd)`, and double `kfree(pcs)`.

Interleaving B (pcsd UAF): open loads `pcsd`, is preempted before `pcs =
pcsd->server`; teardown completes `kfree(pcsd)` (682) → open dereferences
freed memory, then `cuse_server_lock()` on freed/uninited `pcs->mtx`
(lockmgr writes into freed memory).

Trigger surface: an unprivileged user can hammer `open()` on a world-writable
cuse device node; the server side must be exiting at the same moment (daemon
restart/crash).  Both sides were driven as root in the PoC below.

## Guest attempt — honest result

`cuse_race.c`: 6 cycler processes doing tight open/close loops against a
pumper that services OPEN/CLOSE commands for ~1 s and then closes `/dev/cuse`
to start the teardown (whose `cuse_server_free` wait-loop polls refs at 1 Hz,
so each round's teardown races the cyclers' in-flight opens).

* run 1: 120 rounds — `all 120 rounds done, guest alive`
* run 2: 300 rounds — `all 300 rounds done, guest alive`

**No panic, no guest death in 420 teardown races** ⇒ `not_reproduced`.
The per-round hit window is only a few µs wide against a teardown that
itself takes ~µs; the trace stands but the practical hit-rate on this
6-vCPU guest was too low.  Two supporting observations were recorded:
a pumper parking in `cuse_server_free`'s wait loop for >60 s mid-run
(transient ref oscillation), and — during harness debugging — wedged
D-state clients that keep an inherited `/dev/cuse` fd alive, which also
demonstrates that teardown of a cuse server with blocked clients never
completes (operational DoS amplifier, see DF-4149).

## Fix

`fix.diff` (authored, **not** guest-validated — no reproducer exists):

1. serialize the final decrement and the open-side increment under
   `cuse_global_mtx` and add a `dying` flag set before teardown starts;
   `cuse_client_open` checks `dying`/NULL under the same lock **before**
   touching `pcsd->server` or bumping refs;
2. NULL `si_drv1` in `cuse_server_free_dev` under `cuse_global_mtx`
   (matching the open-side read).

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