# DF-3120 — unsynchronized UFS quotaon/quotaoff state machine

Unsynchronized `ufs_quotaon()` / `ufs_quotaoff()` double-execution race in
`sys/vfs/ufs/ufs_quota.c`. Two threads holding `SYSCAP_NOQUOTA_WR` (any
quotactl-capable principal, e.g. root or a delegated quota manager) that both
pass the `um_quotas[type] == NULLVP` check at `ufs_quota.c:503` run the whole
quotaoff teardown twice:

* double `vn_close(qvp)` at `:519` → vnode refcount underflow / UAF
  (INVARIANTS: `KKASSERT` in `vrele`, `sys/kern/vfs_lock.c:354`)
* loser's `crfree(ump->um_cred[type])` at `:521` after the winner stored
  `NOCRED` (`(struct ucred *)0`, `sys/sys/ucred.h:82`) at `:522`
  → **NULL pointer deref in `crfree`** (`sys/kern/kern_prot.c`)
* loser's scan-side `dqrele→dqsync` after `um_quotas[type]` cleared at `:520`
  → `panic("dqsync: file")` (`ufs_quota.c:909`) or `VOP_WRITE` on a closed
  quota file
* loser's `um_quotas[type] = NULLVP` store can clobber a concurrent
  re-quotaon's live quota-file pointer (leaked vp + later `dqsync: file`)

There is no lock: `QTF_CLOSING` is set at `:505` but *read only by
`ufs_dqget`* (`:762`) — `ufs_quotaoff` itself never tests it, and
`vfs_busy()` in `ufs_quotactl` (`sys/vfs/ufs/ufs_vfsops.c:141`) is a shared
unmount pin, not mutual exclusion.

## PoC

`qt_race.c` — 6 quotactl threads (2×Q_QUOTAON rotating 8 quota files,
4×Q_QUOTAOFF, each using a *distinct* control path), plus unprivileged
children (uid 65534) doing Q_SYNC/Q_GETQUOTA and file churn so dquots exist
and stay dirty. The distinct control paths matter: when every thread
nlookup()ed the *same* control file, the shared namecache entry serialized
the whole syscall and the race could not manifest (measured: 1.5M quotactls,
zero overlaps, instrumented kernel). With distinct paths the overlap happens
within seconds.

## Build (in guest, as root)

    cc -O -pthread -o /root/qt_race /root/qt_race.c

## Guest setup (stock kernel has no `options QUOTA` — a QUOTA kernel
must be built first, see VERDICT.md "not_testable on stock config" note)

    dd if=/dev/zero of=/qt.img bs=1m count=64
    vnconfig -c /dev/vn0 /qt.img
    newfs -m 0 /dev/vn0
    mkdir -p /qtmnt && mount -t ufs /dev/vn0 /qtmnt
    (PoC creates /qtmnt/ctrlN and /qtmnt/q0..q7 itself)

## Run

    /root/qt_race 100

## Expected

Kernel panic on the serial console within ~10-100 s:

    Fatal trap 12: page fault while in kernel mode
    fault virtual address = 0x0
    Stopped at crfree+0xc: cmpq $0,(%rdi)

(the `crfree(NOCRED)` NULL-deref). SSH dies / `vm.sh status` => down.
With the INVARIANTS vrele assert or a dq-holding interleaving you can also
see `panic("dqsync: file")` / `vrele` KKASSERT variants.
