# DF-3106 — VERDICT

**Status: reproduced (memory corruption, then panic). Impact at PoC level: panic
with a fully-characterized kernel heap OOB write. Confidence: certain.**

## Root cause

`ufs_lookup()`'s slot search validates each walked entry only for *forward
progress* — `d_reclen != 0 && d_reclen <= DIRBLKSIZ - (off & (DIRBLKSIZ-1))`
(sys/vfs/ufs/ufs_lookup.c:264-266). Full validation (`ufs_dirbadentry()`:
`d_reclen >= DIRSIZ(fmt,ep)`, `namlen <= MAXNAMLEN`, NUL-termination) runs only
when `debug.dircheck` (= `dirchk`) is nonzero, and `dirchk` is **0** on stock
kernels (`#ifdef DIAGNOSTIC` only; sys/vfs/ufs/ufs_lookup.c:64-68; verified
`debug.dircheck: 0` on the audit guest).

`ufs_direnter()`'s compaction loop (sys/vfs/ufs/ufs_lookup.c:808-844) then
re-walks that region **trusting `d_namlen`**:

```c
dsize = DIRSIZ(OFSFMT(dvp), nep);        /* :831 — up to 264 for namlen 255 */
spacefree += nep->d_reclen - dsize;
...
bcopy((caddr_t)nep, (caddr_t)ep, dsize); /* :843 — ep advances by dsize */
```

A crafted entry with `d_reclen=4/8` and `d_namlen=255` passes the walk
(reclen small but nonzero and within the dirblock), contributes nothing to
the slot accounting (`size = reclen - DIRSIZ <= 0`, skipped at :287), yet in
direnter each such entry copies **264 bytes** and advances the destination
pointer **264 bytes** while the source chain advances only 4-8. The
destination runs `(region/4)*264` bytes past the directory buffer.

The `panic("ufs_direnter: compact1/compact2")` safety nets (:857-862) are
checked only **after** the loop — the OOB writes are unconditional, and with
>= 2 poison entries `spacefree = Σreclen − Σdsize ≤ 512 − 528 < newentrysize`,
so the panic is *guaranteed after* the corruption (corruption-then-panic).

## Reproduction (baseline kernel #0, stock INVARIANTS X86_64_GENERIC)

Crafted image: `newfs -b 4096 -f 512`, directory `/d` = one **512-byte
fragment** (`di_size=512`, `di_db[0]`), dirblock poisoned by `patch_dir`:

* variant A (precise): `[e0 free reclen 168][e1 poison reclen 8 namlen 255]
  [e2 free reclen 4][W-fill {04 00 00 FF}*]` with a chain that ends exactly
  at 512; region = [0,180) chosen COMPACT by a 160-char create name
  (DIRECTSIZ(160)=172).
* variant B (massive): W-fill poisons across [168,496), free trigger @500
  reclen 12; region = [0,512); ~86 live iterations.

Observed:

* **Variant A** — `panic: ufs_direnter: compact2`, backtrace
  `ufs_direnter ← ufs_makeinode ← ufs_create ← vop_old_create`
  (panicA.txt, runA.log). Derivation of the write: iter1 bcopy lands at
  `dirbuf+0..264` (in-bounds); iter2 lands at `dirbuf+264..528` in a
  **512-byte** buffer → **16 bytes OOB write**, then the loop exits
  (loc=180=i_count) and the post-loop check panics compact2
  (`spacefree = 168+(8−264)+(4−264) = −348 < 172`; ep->d_ino != 0 because
  the OOB bcopy itself wrote W bytes there). The panic is only reachable
  through >= 2 compacted entries with DIRSIZ >> reclen — impossible on a
  valid filesystem.
* **Variant B** — `Fatal trap 12: page fault while in kernel mode`,
  `fault code = supervisor write data, page not present`, stopped at
  `memmove+0x32` (panicB.txt, runB.log) — the compaction bcopy itself
  faulted writing at `0xfffff80053b07000` after walking ~22.7KB
  (86 × 264B) past the 512-byte fragment buffer. The write walked through
  adjacent kernel heap (buffer-cache memory) until an unmapped page.

Trigger ran as the unprivileged user `maxx` (root only mounts the image;
the only authorization is VOP_EACCESS(VWRITE) at :378, satisfied by mode
0777 on /d).

## Primitive characterization

* **What**: repeated 264-byte `bcopy`s of attacker-crafted on-disk bytes
  (arbitrary content, NULs allowed — raw disk data).
* **Where**: forward from the end of a kernel buffer-cache fragment buffer
  (512B, 1KB, ... — attacker picks the fs geometry); deterministic offsets
  (264-byte multiples).
* **Extent**: attacker-chosen number of iterations (region layout) × 264
  bytes; variant B demonstrated ~22.7KB past a 512B buffer.
* **Limit**: the guaranteed post-loop panic means the corrupting thread
  dies right after the writes — exploitation to uid=0 would need a second
  CPU to consume the corrupted heap objects in the window before the panic
  completes (SMP consumers of buffer-cache neighbors), or the corruption to
  land in data whose consumer runs concurrently. Not pursued to uid=0 in
  this run; the primitive (control + extent + content) is fully established.

## Fix validation

`fix.diff` adds entry validation to the compaction loop (reclen != 0,
reclen within region, reclen >= DIRSIZ for live entries; on failure
`B_INVAL` + `brelse` + `EIO`). Applied to the guest's /usr/src,
`make nativekernel` (KERNEL_RC=0) + `installkernel` (INSTALL_RC=0), reboot
into kernel #1:

* Variant A: `touch: ...: Input/output error` (EIO), **no panic, guest up**
  (runA.patched.log).
* Variant B: same clean EIO, **no page fault, no panic, guest up**
  (runB.patched.log).
* Regression on the patched kernel: 40 create / 20 delete / 30 create in a
  normal UFS dir (exercises the compaction path) → 50 files, all good —
  valid-directory compaction is unaffected.

## Notes

* The walk itself has a smaller adjacent flaw demonstrated during PoC
  development: an entry header starting within 8 bytes of the buffer end
  makes `ep->d_reclen` (checked at :264) read up to 6 bytes past the
  buffer (OOB read feeding only the bounds check). Same trust-gap family;
  not separately IDed.
* `ufs_dirbad()` panicking on RW-mounted corrupted directories is tracked
  separately as DF-3083's UFS instance (DF-3108).

## Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest `make -j6 nativekernel KERNCONF=X86_64_GENERIC` completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
