# DF-0914 — ufs_bmaparray OOB read of indirect-block buffer via unvalidated fs_nindir

## Summary

`ufs_bmaparray()` at `ufs_bmap.c:221` reads `daddr = ((ufs_daddr_t *)bp->b_data)[xap->in_off]`
where `in_off` is computed by `ufs_getlbns()` at `ufs_bmap.c:315` as
`off = (bn / blockcnt) % MNINDIR(ump)`. `MNINDIR(ump)` equals `ump->um_nindir`,
which is copied verbatim from the on-disk `fs->fs_nindir` field at
`ffs_vfsops.c:729` with **no validation** that it equals
`fs_bsize / sizeof(ufs_daddr_t)`.

The indirect-block buffer is `fs_bsize` bytes = `fs_bsize/4` entries. A forged
`fs_nindir` larger than `fs_bsize/4` (e.g. 8192 vs 4096 for `fs_bsize=16384`)
drives `in_off` up to `fs_nindir-1` (8191), reading up to ~16KB past the
buffer — an OOB heap READ. The OOB value is then used as a disk block address
at `ufs_bmap.c:241` (`daddr = blkptrtodb(ump, daddr)`), a confused-deputy read.

**Same root cause as DF-0894** (unvalidated `fs_nindir`), but a different
exploitation path: DF-0914 is the READ path (`ufs_bmaparray` on read/seek),
DF-0894 is the WRITE path (`ffs_balloc` on write). The same mount-time fix
(validating `fs_nindir`) closes both.

## Build / Run

**Build** (in guest as root):
```sh
cd /root/poc && sh ./build.sh
```
Builds `craft_img`, `harness`, `trigger` (Phase A write), `trigger_ro` (Phase B read).

**Run** (in guest as root):
```sh
cd /root/poc && sh ./reproduce.sh
```

**Expected on unpatched #0 GENERIC**: Phase A succeeds (write+ftruncate on
correct fs_nindir). Phase B mounts the forged image, reads at lbn=8203 →
`Fatal trap 12: page fault while in kernel mode` at `ufs_bmaparray+0x15c`
(`movslq (%rsi,%rax,4),%r12` — the `bap[in_off]` OOB read).

**Expected on patched #1 kernel**: Phase B mount rejected —
`ffs_mountfs: bad fs_nindir 8192 (expected 4096)` → `MOUNT_B_RC=1`,
no OOB, no panic.

## Threat model / access

UFS mount is **root-only** on DragonFly (`vfs.usermount=0`,
`SYSCAP_RESTRICTEDROOT`). The attack vector is root-context mount of
attacker-supplied media → kernel OOB read. This is a **root→kernel
hardening gap / info leak / DoS**, not an unprivileged→root LPE.

## Relationship to DF-0894

| Aspect | DF-0894 | DF-0914 |
|--------|---------|---------|
| Root cause | unvalidated `fs_nindir` | unvalidated `fs_nindir` |
| Sink | `ffs_balloc.c:297` (`bap[in_off]` R/W) | `ufs_bmap.c:221` (`bap[in_off]` READ) |
| Path | WRITE (file extension) | READ (file read/seek) |
| Impact | OOB write/read → panic or heap corruption | OOB read → panic or confused-deputy |
| Fix | validate `fs_nindir` at mount | **same fix** (validate `fs_nindir` at mount) |

DF-0894's validated `fix.diff` already validates
`fs_nindir != fs_bsize/sizeof(ufs_daddr_t)`. This check closes DF-0914 too.
DF-0914's `fix.diff` is the same check.

## Files

- `craft_img.c` — patches `fs_nindir` in UFS superblock (reuses DF-0894 pattern)
- `harness.c` — deterministic OOB-read characterization (transcribes `ufs_getlbns` + `bap[in_off]`)
- `trigger.c` — Phase A: write at lbn=12 + ftruncate (on correct image)
- `trigger_ro.c` — Phase B: read at lbn=8203 (on forged image)
- `reproduce.sh` — full two-phase flow (Phase A correct → Phase B forged)
- `fix.diff` — mount-time `fs_nindir` validation (closes DF-0894 + DF-0914)
- `panic.txt` — Fatal trap 12 at `ufs_bmaparray+0x15c` (READ-path OOB proof)
