# DF-2990 — UFS softdep `newblk` hash: lock-free insert vs locked remove

## What

`newblk_lookup()` (sys/vfs/ufs/ffs_softdep.c:1003-1036) maintains the
global `newblk_hashtbl` hash chains with **no** softdep lock (`lk`):
both the lookup walk (`newblk_find`, :991) and the insertion
(`LIST_INSERT_HEAD`, :1032) run lock-free. The two *removal* sites,
`softdep_setup_allocdirect()` (:1317) and `setup_allocindir_phase2()`
(:1633), remove entries from the **same chains** while holding `lk`
(and kfree them at :1318/:1634). Every other hash in this file
(pagedep :874/:1968/:3815, inodedep :966/:2122) is consistently
protected by `lk`; the newblk table is the one structure with mixed
discipline.

## Attack surface / reachability

* insert: `ffs_alloccg` / `ffs_alloccgfrag` / `ffs_alloccgblk` →
  `softdep_setup_blkmapdep` (ffs_alloc.c:987, :1098, :1220) — runs on
  every block/frag allocation on a softdep UFS mount, i.e. on every
  unprivileged `write(2)` that extends a file.
* remove: `ffs_balloc` → `softdep_setup_allocdirect`
  (ffs_balloc.c:135, :188, :207, :268) / `softdep_setup_allocindir_*`
  — runs microseconds later in the same allocation sequence.

Two processes writing two different files on two CPUs both mutate the
65-chain table (only `hashinit(64)`, :1062) concurrently with no common
lock. The `newblk_in_progress` semaphore does not help: the remove path
never takes it.

## Failure modes

1. Interleaved insert/remove stores on one chain → lost head update →
   orphaned `newblk` → later consumer panics
   `softdep_setup_allocdirect: lost block` (:1302) /
   `setup_allocindir: lost block` (:1621).
2. Inserter's `[oldfirst]->le_prev = &new->le_next` fixup landing on an
   entry another CPU has already unlinked+freed (kfree at :1318) →
   **use-after-free write into M_NEWBLK memory**; symmetrically, a
   later `LIST_REMOVE` writing through a stale `le_prev` is a stray
   heap-pointer write into a live object (weakly controlled).
3. Lock-free `newblk_find` traversal stepping into an entry freed under
   `lk` → UAF read; after the freed slab is recycled (M_ZERO) the walk
   truncates early → false `lost block` panic.
4. On INVARIANTS kernels the corrupted chain is usually caught as
   `Bad link elm %p next->prev != elm` / `Bad list head` panics from
   `queue.h` QMD checks.

## Amplifier

An I/O-completion interrupt preempting the inserter between its 2nd and
3rd store (between `oldfirst->le_prev = &new->le_next` and
`head->lh_first = new`) stretches the effective race window from ~ns to
the interrupt service time (~µs), during which any other CPU's locked
remove of the old head completes the corruption deterministically.
Hence heavy async metadata I/O + parallel allocation churn is the
trigger.

## Reproduce (in-guest)

```
# root: softdep UFS fs on vn0 (softdep is the newfs -U superblock flag;
# NOTE: mount -o softdep is NOT a DragonFly mount option)
dd if=/dev/zero of=/root/df2990.img bs=1m count=2048
vnconfig -c vn0 /root/df2990.img
newfs -U /dev/vn0
mount /dev/vn0 /mnt/df2990        # mount shows "(ufs, soft-updates)"
chmod 1777 /mnt/df2990
cc -O2 -o /tmp/df2990_churn churn.c
cc -O2 -o /tmp/df2990_frag frag_churn.c
/tmp/df2990_churn /mnt/df2990 900   # as any unprivileged user
/tmp/df2990_frag  /mnt/df2990 600   # frag-path variant
```

Success criterion: kernel panic (`lost block` / `Bad link` /
`found block`) with the churn running, or orphaned-newblk corruption
observable in dmesg. See VERDICT.md for the honest outcome of three
bounded attempts on the 6-vCPU guest (~30 min saturated churn: no
manifestation; the static defect is certain, the window is ns-scale).

## Fix

Protect `newblk_hashtbl` with `lk` exactly like the pagedep/inodedep
tables (see fix.diff).
