# DF-0847 — Global dquot hash/free-list manipulated with no lock

**Finding**: `sys/vfs/ufs/ufs_quota.c` — `ufs_dqhashtbl` / `ufs_dqfreelist` /
`ufs_numdquot` are global across all mounts and mutated (hash lookup, recycle,
insert, remove, free-list insert) **without any token/mutex/spinlock** in
`ufs_dqget()`, `ufs_dqrele()`, and `ufs_dqflush()`. Two concurrent quota
operations could corrupt the lists / double-unlink / UAF.

**Severity filed**: Medium (CWE-362 race condition).

## Verification outcome

**NOT REPRODUCED on the default kernel; confirmed real-but-latent source bug.**

The cited unlocked manipulation is **dead code on the stock
`X86_64_GENERIC` kernel**:

1. `ufs_quotactl()` early-returns `EOPNOTSUPP` unless the kernel was built
   with `options QUOTA` (`sys/vfs/ufs/ufs_vfsops.c:77-78`).  `X86_64_GENERIC`
   does **not** set `options QUOTA` (0 occurrences — only `VKERNEL64` and
   `LINT64` carry it).
2. Every in-kernel caller of `ufs_getinoquota()`/`ufs_chkdq()`/`ufs_chkiq()`/
   `ufs_dqrele()` — the only paths that reach `ufs_dqget()` — is wrapped in
   `#ifdef QUOTA` (`ffs_alloc.c:110/131/150/217/303`, `ffs_inode.c:176/195/448`,
   `ffs_balloc.c:490`, `ufs_vnops.c:290/545/568/589/1285/...`, `ufs_inode.c:78/113/150`).
   On a non-`QUOTA` kernel these are compiled out.

So `ufs_dqget()`/`ufs_dqrele()`/`ufs_dqflush()` are present in the binary
(`ufs_quota.c` is `optional ffs`, per `sys/conf/files:1994`, not
`optional quota`) but have **zero callers** — unreachable.

### Runtime proof

`quota_unreachable` calls `quotactl(Q_QUOTAON)` on a UFS mount and prints:

```
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. `options QUOTA` is absent from
           X86_64_GENERIC, so every caller of ufs_dqget() is compiled out.
           => DF-0847 unreachable on this (default) kernel.
```

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

Even though unreachable on the default kernel, the concurrency defect is
genuine. Reading `ufs_quota.c`:

- `ufs_dqget()` (`ufs_quota.c:769-866`): `LIST_FOREACH` hash walk, `TAILQ_REMOVE`
  free-list dequeue (hit path `:779` and recycle path `:804`), `LIST_REMOVE`
  hash removal (`:806`), `LIST_INSERT_HEAD` publish (`:813`) — **no lock**.
- `ufs_dqrele()` (`ufs_quota.c:890`): `TAILQ_INSERT_TAIL` free-list enqueue —
  **no lock**.
- `ufs_dqflush()` (`ufs_quota.c:956-966`): full hash-table sweep with
  `LIST_REMOVE` per matching entry — **no lock**.

The only serialization the original code has is `vn_lock(dqvp)` (`:812`), which
is *per quota-file vnode* (per-mount-per-type) and is taken **after** the list
mutation — it does nothing to protect the global lists against two CPUs on
different mounts (or two missers on the same mount).

`dq_race.c` is a root-driven characterization harness (calls `Q_GETQUOTA` with
cycling uids from N threads) used to exercise the race on a custom
`options QUOTA` kernel. Note: even on an `options QUOTA` kernel, an
**unprivileged** user can only trigger a cache-miss for their *own* uid
(a single dquot via file create/write), giving a negligible race window;
driving the race needs the privileged `Q_GETQUOTA`-for-arbitrary-uid path. So
even where the code is live, unprivileged exploitation is not realistic.

## How to reproduce (default-kernel reachability probe)

Root setup (once) — create a UFS mount with quota files:

```
ssh dfbsd                          # root
dd if=/dev/zero of=/root/q.img bs=1m count=64
vnconfig -c -s labels vn0 /root/q.img
newfs /dev/vn0s0
mkdir -p /mnt/q
mount -o userquota,groupquota /dev/vn0s0 /mnt/q
touch /mnt/q/quota.user /mnt/q/quota.group
chmod 1777 /mnt/q
```

Then as the unprivileged user:

```
ssh dfbsd-maxx                     # maxx (uid 1001)
cd poc/DF-0847 && ./build.sh && ./run.sh /mnt/q
# expect: errno=45 (EOPNOTSUPP)  => PATH DEAD
```

## Fix

`fix.diff` adds a single `lwkt_token ufs_dq_token` (mirroring
`ext2_ihash_token` in `sys/vfs/ext2fs/ext2_ihash.c`) and wraps every
hash/free-list mutation in `lwkt_gettoken`/`lwkt_reltoken`. The token is held
only across list manipulation and is **released around sleeping calls**
(`kmalloc(M_WAITOK)`, `vn_lock`, `VOP_READ`) so it cannot deadlock. This
closes the race on any kernel that enables `options QUOTA`.

## Files

| file                  | purpose                                                        |
|-----------------------|----------------------------------------------------------------|
| `quota_unreachable.c` | default-kernel reachability probe (the PoC)                    |
| `dq_race.c`           | root-driven race characterization harness (options-QUOTA only) |
| `build.sh`/`run.sh`   | exact build/run                                                |
| `fix.diff`            | the lwkt_token fix (git-apply-able)                            |
| `VERDICT.md`          | full narrative + before/after                                  |
| `manifest.json`       | artifact catalog                                               |
