# DF-3233 — `ffs_blkatoff` hands directory code an out-of-bounds `*res`
pointer on crafted-geometry mounts — unprivileged `creat`/`touch` reads and
WRITES directory entries tens of KB past the buffer
(`sys/vfs/ufs/ffs_subr.c`)

## What

```c
/* sys/vfs/ufs/ffs_subr.c:80 (ffs_blkatoff) and :151 (ffs_blkatoff_ra) */
if (res)
	*res = (char *)bp->b_data + blkoff(fs, uoffset);
```

`blkoff(fs, loc)` is `loc & fs->fs_qbmask` (fs.h:487-488) and the buffer
was sized `bsize = blksize(fs, ip, lbn)` — an *independent* derivation
(`fs_bsize`, or `fragroundup(blkoff(i_size))` via the fsize masks).  Nothing
at mount validates that `fs_qbmask` matches `fs_bsize` (ffs_vfsops.c:642-646
checks only magic/bsize; `ffs_oldfscompat` recomputes masks only for
pre-4.4 inode formats, ffs_vfsops.c:812-815 — same validation gap as
DF-0903/DF-3212, but those verified the read/write syscall paths; this is
the **directory-lookup sink**).  With `fs_qbmask = 0x1ffff` on a stock
8192/1024 filesystem, `*res` can point up to 128 KB past an 8192-byte
buffer.

Callers dereference and write through `*res`:

* `ufs_direnter`  ufs_lookup.c:793-870 — reads `ep->d_ino/d_reclen/d_namlen`
  from `dirbuf`, then **bcopy's a new directory entry** (attacker-named,
  ~28+ bytes) at `dirbuf + dsize`
* `ufs_dirremove`/whiteout :941-948 — `ep->d_ino = UFS_WINO; ep->d_type = DT_WHT`
* `ufs_dirrewrite` :1023-1026 — `ep->d_ino = newinum; ep->d_type = newtype`
* `ufs_dirhash` dirhash.c:468 — entry scan through `dp`

## Reachability (deterministic)

`ufs_lookup`'s search loop (ufs_lookup.c:234-350) finds a "slot" when an
entry has slack (`reclen - DIRSIZ ≥ needed`); for `slotstatus == FOUND` it
returns `dp->i_offset = slotoffset` (:390-405) and `ufs_direnter` calls
`ffs_blkatoff(dvp, dp->i_offset, &dirbuf, &bp)` (:793).  So a directory
that contains **one crafted free-slot entry at a high offset** aims the
wild pointer at an attacker-chosen `offset & fs_qbmask`.

## PoC (verified on the stock INVARIANTS guest, X86_64_GENERIC #0)

`craft3233` builds it offline from a stock `newfs -b 8192 -f 1024` image:

* directory inode (the `mkdir`-created `/mnt/d`) rewritten to
  `i_size = 0x31000` (200704), 25 blocks (12 direct + 13 via one indirect),
  mode 0777;
* content: 6256 32-byte entries with `d_reclen == DIRSIZ` (zero slack — no
  mid-directory slot) plus ONE free-slot entry (`d_ino = 0`,
  `d_reclen = 512`) at offset **0x30e00**;
* superblock `fs_qbmask` patched `0x1fff → 0x1ffff`.

Root mounts; **maxx (uid 1001)** triggers.  Expected arithmetic:
`lbn(0x30e00) = 24` (≥ NDADDR ⇒ `bsize = fs_bsize = 8192`, bread of the
real block-24 data), and

```
dirbuf = b_data + (0x30e00 & 0x1ffff) = b_data + 0x10e00   (+69120)
       ⇒ 60928 bytes past the 8192-byte buffer
```

Observed, twice, from two fresh boots (`panic.txt`, `panic.2.txt`):

```
Fatal trap 12: page fault while in kernel mode
fault code = supervisor write data, page not present
instruction pointer = 0x8:0xffffffff80bca655
Stopped at      memset+0xd5:   repe stosq %es:(%rdi)     ← ufs_direnter compaction
```

* run 1: first `open(O_CREAT|O_EXCL)` returned `EEXIST` (the OOB state
  machine consuming wild bytes) and *survived*; the immediate unprivileged
  `touch /mnt/d/zz` → **fatal supervisor WRITE** in `memset` (the
  direnter entry-copy region).  Guest down.
* run 2 (fresh boot, identical image): identical outcome, identical faulting
  instruction (`memset+0xd5`), different fault VA (0xfffff8005c86a000 vs
  0xfffff8005a396e00) — the write target tracks buffer-cache KVA layout,
  exactly as a buffer-adjacent OOB write should.

When the slot lands on mapped neighbour-buffer pages the same code performs
a **silent** OOB read + write of attacker-named dirent bytes into another
buffer's data (the first-trigger EEXIST shows the read side consuming wild
data); when it lands on the unmapped remainder of the buffer's KVA
reservation it is the observed fatal trap.

## Preconditions

crafted FFS image + mount (root, or any user with `vfs.usermount=1`);
trigger is one `creat`/`touch` by any user with write permission on the
directory (mode 0777 in the crafted inode).

## Fix

Same mount-time geometry gate as DF-3232 (`fix.diff`): `fs_qbmask` must
equal `~(int64_t)fs_bmask` (and `fs_bmask == fs_bsize-1`, etc.) or the
mount is rejected — which also closes DF-3190/DF-3212/DF-0903's mask
family at the same checkpoint.
