# DF-0847 — VERDICT

**Status**: NOT REPRODUCED on the default kernel. (Real-but-latent source-level
concurrency defect; the cited code path is dead on `X86_64_GENERIC`.)

## The claim

`sys/vfs/ufs/ufs_quota.c`: the global dquot hash table (`ufs_dqhashtbl`),
free list (`ufs_dqfreelist`), and counter (`ufs_numdquot`) are mutated without
any lock in `ufs_dqget()`/`ufs_dqrele()`/`ufs_dqflush()`. Concurrent quota
operations could double-unlink / corrupt the lists → UAF / panic.

## Why it does NOT reproduce on the default kernel

The unlocked manipulation is **dead code** on the stock `X86_64_GENERIC`
kernel. Two compile-time gates close the path:

1. **`ufs_quotactl()` early-returns `EOPNOTSUPP` unless `options QUOTA` is set**
   (`sys/vfs/ufs/ufs_vfsops.c:77-78`):
   ```c
   ufs_quotactl(...)
   {
   #ifndef QUOTA
       return (EOPNOTSUPP);
   #else
   ```
   `X86_64_GENERIC` does **not** carry `options QUOTA` (0 occurrences — only
   `VKERNEL64` and `LINT64` do, per `sys/config/`). So `quotaon` always fails
   and `ump->um_quotas[type]` is never set.

2. **Every in-kernel caller of `ufs_dqget()` is `#ifdef QUOTA`-gated**, so the
   internal write()/creat()/chown() paths never reach it:
   - `sys/vfs/ufs/ffs_alloc.c:110,131,150,217,303` (`ufs_chkdq`)
   - `sys/vfs/ufs/ffs_inode.c:176,195,448` (`ufs_chkdq`/`ufs_getinoquota`)
   - `sys/vfs/ufs/ffs_balloc.c:490` (`ufs_chkdq`)
   - `sys/vfs/ufs/ufs_vnops.c:290,545,568,589,1285,1301,1319,1330,2027,...`
   - `sys/vfs/ufs/ufs_inode.c:78,113,150`

   Even a hypothetical direct caller would hit the guard inside `ufs_dqget()`
   itself (`ufs_quota.c:762-765`): `dqvp = ump->um_quotas[type]; if (dqvp ==
   NULLVP ...) return (EINVAL);` — early-returns before any list mutation.

`ufs_quota.c` is compiled whenever FFS is (`sys/conf/files:1994`:
`vfs/ufs/ufs_quota.c  optional ffs`, **not** `optional quota`), so the
functions exist in the binary (`nm /boot/kernel/kernel` shows `ufs_dqget`,
`ufs_dqrele`, `ufs_quotaon`) — but with **zero callers**. Unreachable.

### Runtime proof

`quota_unreachable` calls `quotactl(Q_QUOTAON)` on a UFS mount as the
unprivileged `maxx` user:

```
quotactl(Q_QUOTAON, "/mnt/q", uid=0, "/mnt/q/quota.user") -> rc=-1 errno=45 (Operation not supported)
PATH DEAD: EOPNOTSUPP => ufs_quotactl() #ifndef-QUOTA early-return at
           sys/vfs/ufs/ufs_vfsops.c:77-78.
```

## The source-level bug is REAL (latent)

Although unreachable on the default kernel, the concurrency defect is genuine
and would be live on any kernel built with `options QUOTA`. Reading
`ufs_quota.c` (master DEV, unpatched):

| Site | Operation | Lock |
|------|-----------|------|
| `ufs_dqget:770` | `LIST_FOREACH` hash walk | none |
| `ufs_dqget:779` | `TAILQ_REMOVE` free-list (hit path) | none |
| `ufs_dqget:804` | `TAILQ_REMOVE` free-list (recycle) | none |
| `ufs_dqget:806` | `LIST_REMOVE` hash (recycle) | none |
| `ufs_dqget:813` | `LIST_INSERT_HEAD` publish | none |
| `ufs_dqget:841` | `LIST_REMOVE` hash (error path) | none |
| `ufs_dqrele:890` | `TAILQ_INSERT_TAIL` free-list | none |
| `ufs_dqflush:956-966` | full hash sweep + `LIST_REMOVE` per entry | none |

The only serialization is `vn_lock(dqvp)` (`:812`), which is *per quota-file
vnode* and is acquired **after** the list mutation — it cannot protect the
**global** lists against two CPUs on different mounts. This matches the
`ext2_ihash_token` / `msdosfs` / `nfs` analogue in the tree, all of which use
an `lwkt_token` to guard their global hash tables — `ufs_quota.c` is the
odd one out with no token at all.

### Characterization on an `options QUOTA` kernel (non-default config)

To prove the bug isn't theoretical, a kernel was built with `options QUOTA`
added to `X86_64_GENERIC`. On it, `quotaon` succeeds and `ufs_dqget()` becomes
reachable. `dq_race.c` (root-driven, N threads calling `Q_GETQUOTA` with
cycling uids → repeated cache-miss recycle + free-list races) was used to
exercise the unlocked lists.

**Note on unprivileged reachability**: even with `options QUOTA`, an
**unprivileged** user can only trigger a dquot cache-miss for their *own* uid
(via file create/write — a single dquot), giving a negligible race window.
Driving the race requires the privileged `Q_GETQUOTA`-for-arbitrary-uid path
(`caps_priv_check RESTRICTEDROOT`, `ufs_vfsops.c:123`). So the realistic
unprivileged impact ceiling is negligible; the bug is a privileged-DoS /
latent corruption on a non-default config.

This characterization is labelled **non-default config** per the audit's
bright-line rule and does **not** affect the default-kernel verdict.

## Exploit-chain / escalation assessment

No escalation chain was developed, because:

- On the **default kernel** the primitive is **unreachable** (dead code) —
  there is nothing to escalate. This is a valid hard blocker (the cited
  vulnerable code path is dead at runtime on the default kernel AND on the
  default config).
- On an **`options QUOTA` kernel** the primitive is a concurrent-list-
  corruption race whose unprivileged reachability is negligible (single
  own-uid cache entry). Realistically it is a privileged-DoS, not an
  unprivileged privesc. Spending 15–30 grooming attempts would not change
  the *unprivileged*-reachability verdict, which is the gating property.

Honest impact: **none on the default kernel**; latent concurrency defect
(privileged-DoS ceiling) on a non-default `options QUOTA` kernel.

## PoC changes

The finding shipped with no on-disk PoC folder (`findings/poc/DF-0847/` did
not exist). I authored the full evidence pack:

- `quota_unreachable.c` — default-kernel reachability probe (the PoC).
- `dq_race.c` — root-driven race characterization harness for the
  `options QUOTA` kernel.
- `build.sh` / `run.sh` — exact build/run.
- `fix.diff` — the `lwkt_token` fix.

## Fix

`fix.diff` adds `static struct lwkt_token ufs_dq_token` (mirroring
`ext2_ihash_token` in `sys/vfs/ext2fs/ext2_ihash.c`) and wraps every hash /
free-list mutation in `ufs_dqget`/`ufs_dqrele`/`ufs_dqflush` in
`lwkt_gettoken`/`lwkt_reltoken`. The token is held only across list
manipulation and is explicitly **released around sleeping calls**
(`kmalloc(M_WAITOK)`, `vn_lock`, `VOP_READ`) so it cannot deadlock or serialize
I/O. It compiles cleanly and `git apply --check` passes against the audit tree.

This **supersedes** the finding's one-line proposal ("lwkt_token around all
hash/free-list mutations, I/O outside token") by implementing exactly that with
correct token-scope boundaries around the two sleeping call sites in `ufs_dqget`.

### Fix validation

`fix_status: not_testable`. The race-triggering PoC cannot run on the default
guest because the path is latent (`options QUOTA` off). I validated that
`fix.diff` (a) `git apply --check` passes against the read-only `sys/` tree,
(b) compiles into a kernel (built alongside the `options QUOTA` characterization
kernel — no new warnings/errors), and (c) by source tracing closes every cited
unlocked mutation site. On the default kernel the path is dead both before and
after the fix, so there is no runtime before/after to observe.

## Kernel references (confirmed during verification)

- `sys/vfs/ufs/ufs_quota.c:731-732` — global free list + counter, no lock
- `sys/vfs/ufs/ufs_quota.c:769-866` — `ufs_dqget`, all mutations unlocked
- `sys/vfs/ufs/ufs_quota.c:890` — `ufs_dqrele` free-list enqueue unlocked
- `sys/vfs/ufs/ufs_quota.c:956-966` — `ufs_dqflush` hash sweep unlocked
- `sys/vfs/ufs/ufs_vfsops.c:77-78` — `ufs_quotactl` `#ifndef QUOTA return EOPNOTSUPP`
- `sys/config/X86_64_GENERIC` — no `options QUOTA`
- `sys/conf/files:1994` — `ufs_quota.c` is `optional ffs` (compiled) not `optional quota`
- `sys/vfs/ufs/ffs_alloc.c:110` (and siblings) — `#ifdef QUOTA` caller gate
- `sys/vfs/ext2fs/ext2_ihash.c:67` — the `lwkt_token` analogue the fix mirrors
