# DF-0793 — Use-after-free in async TRIM block-free path

## Verdict (one line)

**NOT REPRODUCED (false-positive as filed): the async-TRIM store/defer/UAF code
path in `ffs_blkfree` is unreachable on current master, because its `MNT_TRIM`
gate reads the wrong mount pointer.** The finding's underlying *concern*
(missing `taskqueue_drain` in `ffs_unmount`) is a real latent defect, but it is
masked by a separate bug that makes the entire FFS async-TRIM feature
non-functional. A fix for **both** was authored and validated.

---

## 1. The claim

`ffs_blkfree` (`sys/vfs/ufs/ffs_alloc.c:1667-1697`), when the mount has
`MNT_TRIM`, `kmalloc`s a `struct ffs_blkfree_trim_params`, stores **raw,
un-refcounted** copies of `ip->i_fs`, `ip->i_devvp`, `ip->i_dev`, and submits a
`BUF_CMD_FREEBLKS` bio whose completion callback (`ffs_blkfree_trim_completed`,
:1646) defers the real free work (`ffs_blkfree_trim_task` :1633) onto
`taskqueue_swi`. `ffs_unmount` (`ffs_vfsops.c:824`) frees `fs`/`ump`/`devvp`
with **no `taskqueue_drain`** of the pending TRIM tasks anywhere in
`sys/vfs/ufs/`. Alleged impact: delete a large file under an `MNT_TRIM` mount,
then `umount` while TRIM bios are in-flight -> the deferred task dereferences
freed `M_UFSMNT` heap / freed device vnode -> UAF.

The "no drain" half of the claim is **factually correct**: `grep -rn
taskqueue_drain sys/vfs/ufs/` returns nothing, and
`taskqueue_enqueue(taskqueue_swi,&tp->task)` at `ffs_alloc.c:1655` is the only
swi reference in the whole UFS tree.

## 2. Why it does not reproduce — the gate uses the wrong mount pointer

`ffs_blkfree` decides whether to TRIM with:

```c
struct mount *mp = ip->i_devvp->v_mount;          /* ffs_alloc.c:1670 */
...
if (!(mp->mnt_flag & MNT_TRIM)) {                 /* ffs_alloc.c:1673 */
        ffs_blkfree_cg(...);                       /* synchronous */
        return;
}
/* ... TRIM path: kmalloc tp, store raw ptrs, submit async FREEBLKS bio ... */
```

`ip->i_devvp` is the **block-device vnode** the filesystem lives on. Its
`v_mount` is the **devfs** mount of `/dev` (or NULL), **not** the ufs mount.
`ffs_mountfs` never sets `devvp->v_mount`; it sets
`dev->si_mountpoint = mp` (`ffs_vfsops.c:734`) — a field on the *cdev* that
`ffs_blkfree` does **not** consult. The real ufs mount is reachable from the
inode as `ip->i_vnode->v_mount`.

So `mp->mnt_flag & MNT_TRIM` is evaluated against the devfs mount, which never
has `MNT_TRIM`. The gate is therefore **always TRUE** (TRIM never selected),
`ffs_blkfree_cg` runs synchronously, and the store/defer/UAF code at
:1679-1697 is **dead code**.

### Proof (kernel instrumentation)

A diagnostic kernel with a `kprintf` at the top of `ffs_blkfree`, on a ufs
mount mounted with `MNT_TRIM` (`mount | grep` shows `ufs, trim, local`),
printing for 48 `ffs_blkfree` calls:

```
devvp->v_mount      = 0xfffff8008db3c800      (devfs mount)
ip->i_vnode->v_mount= 0xfffff8008eddb800      (the ufs mount — DIFFERENT object)
devvp_vmtrim        = 0                       (devfs mount has NO MNT_TRIM)
ipvmount_trim       = 1                       (ufs mount HAS MNT_TRIM)
```

The flag is set on the ufs mount (`ipvmount_trim=1`) but never seen, because
the check reads the device vnode's mount (`devvp_vmtrim=0`).

### Behavioural proof (async-disk KLD)

To remove the "QEMU has no slow async TRIM device" excuse, a small root-loaded
KLD (`asyncd.c`, modelled on `sys/dev/disk/md/md.c`) provides a 256 MB
memory-backed disk whose `READ`/`WRITE` complete synchronously but whose
`BUF_CMD_FREEBLKS` completion is deferred 2 s via a callout — exactly mimicking
a real SSD. Mounting ufs+trim on it (via a direct `mount(2)` syscall, which the
kernel accepts without a device-capability check — only the *userland*
`mount_ufs` gates on `kern.cam.da.*.trim_enabled`, `sbin/mount_ufs/mount_ufs.c:101`)
and deleting a file:

| kernel                                   | FREEBLKS bios sent | panic |
|------------------------------------------|-------------------:|:-----:|
| unpatched baseline (`#0` 06:02:54)       | **0**              | no    |
| "expose" (mount-pointer fix only)        | 405                | no*   |
| full-fix (mount-pointer fix + drain)     | 675 / 810          | no    |

\* see §4.

**0 FREEBLKS on the unpatched kernel** is the definitive confirmation that the
TRIM branch is never taken: `ffs_blkfree` always returns through the
synchronous `ffs_blkfree_cg` path. The `struct ffs_blkfree_trim_params`, the
bio, the callback, and the swi task are never instantiated.

## 3. Reachability for an unprivileged user

Even setting aside the dead-code problem, the async-TRIM path is **not**
reachable by an unprivileged user on the default guest:

* `vfs.usermount = 0` — the user cannot call `mount(2)` at all.
* The only `MNT_TRIM`-accepting mount path (`mount_ufs` userland) restricts
  TRIM to CAM `da` disks with `trim_enabled`; the guest has no `da` disk
  (root is `vbd0`/hammer2). The kernel `mount(2)` would accept `MNT_TRIM` on
  any device, but issuing it still requires root.
* `vn` completes `BUF_CMD_FREEBLKS` as a synchronous no-op
  (`sys/dev/disk/vn/vn.c:304`), so even a root-mounted vn+trim would never
  produce an in-flight TRIM bio.

So: on the default GENERIC guest an unprivileged user has **no** path to the
TRIM block-free path. There is no escalation surface to develop.

## 4. The latent UAF is real (demonstrated by un-masking it)

Fixing **only** the masking mount-pointer bug (`mp = ip->i_vnode->v_mount`)
makes the TRIM path reachable — this is the "expose" kernel above, where **405
FREEBLKS bios** flow and the deferred `ffs_blkfree_trim_task` dereferences the
stored `tp->i_fs`/`tp->i_devvp`/`tp->i_dev`. That confirms the finding's
*mechanism* is genuine (the store-then-defer pattern runs, and the deferred
task does deref pointers that `ffs_unmount` will free).

No panic was observed on the expose kernel, because of an **implicit**
synchronization: `umount` latency tracks the async bio delay *exactly*
(2 s delay → 2129 ms umount; 4 s delay → 3931 ms umount). Something in the
unmount path (the device buf drain on `vinvalbuf(devvp,V_SAVE)` / `vrele(devvp)`)
waits for the in-flight `FREEBLKS` bios to `biodone()`, which lets the enqueued
swi tasks run while `fs` is still valid. The residual UAF window —
*biodone (task enqueued) → umount unblocks → kfree(fs) → swi task finally runs*
— is real but very tight and did not fire in these runs. It would be widened
under heavy load / a backlogged swi taskqueue. The missing `taskqueue_drain`
is therefore a legitimate latent defect that should be closed.

## 5. The fix (`fix.diff`)

`fix.diff` (validated, `git apply --check` clean) makes two changes:

1. **Correctness — consult the right mount** (`ffs_alloc.c:1670`):
   `mp = ip->i_vnode->v_mount;` (was `ip->i_devvp->v_mount`). This is what
   actually enables FFS async TRIM; without it the feature is dead.

2. **Safety — drain pending TRIM before tearing down the mount**
   (`ufsmount.h` + `ffs_alloc.c` + `ffs_vfsops.c`):
   * add `u_int um_trim_pending;` to `struct ufsmount`;
   * `atomic_add_int(&ump->um_trim_pending,1)` when a TRIM bio is submitted,
     `atomic_subtract_int(...)` + `wakeup()` in `ffs_blkfree_trim_task`
     (also carries `tp->i_ump` so the task can find the counter);
   * in `ffs_unmount`, immediately before `kfree(fs->fs_csp)`:
     `while (ump->um_trim_pending) tsleep(&ump->um_trim_pending,0,"ufstrim",hz);`

   This turns the implicit "umount usually waits long enough" into a hard
   guarantee: `kfree(fs/ump)` cannot run until every deferred
   `ffs_blkfree_trim_task` has completed and decremented the counter.

### Phase 8 validation (built + booted the fixed kernel)

* **Baseline (unpatched `#0`):** mount ufs+trim, write/unlink/umount →
  **0 FREEBLKS** (TRIM dead), no panic.
* **Full-fix kernel** (`kern.version` 13:01:29, sha256
  `a0b2864db1c8836382acbf48ff4f1556ce4d3dd7cb67a675158263accbe7f746`):
  mount ufs+trim, write/unlink/umount ×5 → **675 FREEBLKS** flow (TRIM now
  operational), every `umount` returns `rc=0`, **no panic / no kernel
  warning**, deferred completions == FREEBLKS count (810 incl. timed run).
  Timed: `unlink+unmount = 2932 ms` (drain holds umount until the deferred
  tasks finish, then completes cleanly).

The fix therefore (a) makes async TRIM functional and (b) provably closes the
latent UAF. `recommended_fix`: **supersedes** the finding's proposal (the
finding proposed only the drain; the drain is insufficient on its own because
without the mount-pointer fix TRIM stays dead, and the mount-pointer fix
without the drain would *expose* the UAF — both must ship together).

## 6. PoC changes

* `asyncd.c` (+ `Makefile`): new — root-loaded KLD providing the slow async
  TRIM disk that QEMU hardware lacks; needed to exercise the TRIM bio path
  at all.
* `trim_uaf.c`, `trim_race.c`: syscall harnesses that mount ufs+trim directly
  (bypassing the userland da-only gate), create/delete files, and race umount.
* `fix.diff`: the validated two-part fix described above.

## 7. Honest impact / classification

* **As filed (reachable UAF on current master): false positive.** The path is
  dead code; the UAF cannot occur. Severity should be downgraded or the
  finding re-filed as "latent UAF + dead async-TRIM feature".
* **Real bugs confirmed during verification:** (1) `ffs_blkfree` MNT_TRIM gate
  uses the wrong mount pointer — FFS async TRIM is completely non-functional;
  (2) missing `taskqueue_drain` of swi TRIM tasks in `ffs_unmount` — a latent
  UAF that becomes live if/when (1) is fixed. Both are closed by `fix.diff`.
* **No privilege boundary is crossed** (the path is root-only on the default
  guest); no escalation chain applies.
