# DF-0843 — Missing global lock on dirhash list (UAF + list corruption)

**Severity:** High · **CWE:** CWE-416 (UAF), CWE-362 (race) · **File:** `sys/vfs/ufs/ufs_dirhash.c`

## Summary

`ufs_dirhash.c` has no mutex/lockmgr/spinlock anywhere. `ufsdirhash_recycle()`
(:927-970) frees a *victim* inode's `dh_hash` from the global list with no lock
on the victim; concurrent `ufsdirhash_lookup()` (:318/:356) double-derefs that
freed `dh_hash` (UAF) and all list mutations (`build`/`free`/`lookup`) are
unsynchronized (list corruption). FreeBSD's `ufsdirhash_lock` was never ported.

## How to reproduce

The live race is narrow (recycle's score-decay gate + tiny window), so the
**deterministic harness** is the accepted proof.

```sh
./build.sh && ./run.sh
```

- `build.sh` compiles `harness.c` (and the live `trigger.c`).
- `run.sh` runs the harness **UNLOCKED** (models the current kernel → race
  fires) and **LOCKED** (models the fix → race closes).

### Expected output

```
UNLOCKED: Ran 100 race iterations; UAF(stale-ptr)=53-86%  NULL-deref=53-86%
          VERDICT: RACE REPRODUCED ...
LOCKED:   Ran 100 race iterations; UAF(stale-ptr)=0%      NULL-deref=0%
          VERDICT: RACE CLOSED ...
```

### Live kernel trigger (optional, bonus)

The harness is self-contained. To also exercise the live kernel path (needs a
UFS mount; the finding's threat model = an admin-mounted UFS image owned by the
attacker; `vfs.usermount=0` so root must mount it):

```sh
# as root:
dd if=/dev/zero of=/root/ufs.img bs=1m count=256
vnconfig vn0 /root/ufs.img && newfs -i 1024 /dev/vn0
mkdir -p /ufstest && mount -t ufs /dev/vn0 /ufstest && chown maxx:maxx /ufstest
sysctl vfs.ufs.dirhash_maxmem=16384
# as maxx:
( cd /ufstest && for i in 0 1 2 ...; do mkdir d$i; for j in ...; do : > d$i/f$j; done; done )
./trigger /ufstest 40 8 30      # 8 threads, 30s; race is narrow, may not panic
```

## Impact

Local unprivileged DoS (panic) on default GENERIC (INVARIANTS catches slab
reuse); silent UAF read + slab-groom candidate on a no-INVARIANTS build (not
demonstrated to uid0 — read-primary primitive + race too narrow to win live).

## Fix

See `fix.diff` — adds a global `struct lock ufsdirhash_lock`, exclusive in
recycle/free/build-list-mutation, exclusive→downgrade-shared in lookup with
`dh_hash` re-validation. Validated by Phase 8 (built, booted #1, 53M-iteration
trigger, no panic). `VERDICT.md` has the full analysis.
