# DF-0894 — OOB read/write in `ffs_balloc` via crafted `fs_nindir` in UFS superblock

## Bug

`sys/vfs/ufs/ffs_balloc.c:296-297` — the indirect-block array index
`bap[indirs[i].in_off]` is computed using `in_off = bn % MNINDIR(ump)`,
where `MNINDIR(ump) = ump->um_nindir = fs->fs_nindir` (an on-disk
superblock field). The `bread()` at line 291 allocates the indirect-block
buffer as exactly `fs->fs_bsize` bytes (= `fs_bsize/sizeof(ufs_daddr_t)`
entries). The mount path `ffs_vfsops.c:642-646` validates only `fs_magic`
and the `fs_bsize` range; `fs_nindir` is copied verbatim into
`ump->um_nindir` at `ffs_vfsops.c:729` with **no check that it equals
`fs_bsize / sizeof(ufs_daddr_t)`**. A crafted `fs_nindir=8192` (vs the
correct `fs_bsize/4 = 4096`) allows `in_off` values up to 8191, driving
`bap[in_off]` up to ~16KB past the indirect-block buffer:

```
ffs_balloc.c:291   bread(vp, ..., (int)fs->fs_bsize, &bp);   // 16384-byte buffer
ffs_balloc.c:296   bap = (ufs_daddr_t *)bp->b_data;          // 4096 valid entries
ffs_balloc.c:297   nb = bap[indirs[i].in_off];               // OOB read (up to +16KB)
ffs_balloc.c:333   bap[indirs[i - 1].in_off] = nb;           // OOB write
ffs_balloc.c:378   bap[indirs[i].in_off] = nb;               // OOB write
ffs_balloc.c:479   bap[indirs[unwindidx].in_off] = 0;        // OOB write (unwind path)
```

Trigger path: mount crafted UFS image → write to a file at a large
offset → `ffs_balloc` → `ufs_getlbns` computes `in_off = (lbn-12) %
MNINDIR(ump)` → OOB index into the indirect-block buffer.

Same root cause as DF-0820 (unvalidated `fs_nindir`), different
exploitation path (single-indirect block via file write, vs direct block
path).

## Build / run / expected

```sh
# in guest as root
sh ./build.sh    # cc -o craft_img craft_img.c; cc -o harness harness.c
sh ./run.sh      # calls reproduce.sh
```

Expected on the **unpatched #0 GENERIC** kernel (INVARIANTS ON):
- `harness 16384 8192 8203` prints `OOB READ of 16380 bytes past the 16384-byte buffer`.
- Live trigger: **kernel panic** — `Fatal trap 12: page fault while in kernel mode` at
  `ffs_balloc+0x5e7: movl (%rbx,%rax,4),%eax` (the `bap[in_off]` instruction).

Expected on the **patched #1 single-fix kernel**: the crafted mount is **rejected**
with `mount_ufs: ... incorrect super block` and dmesg `ffs_mountfs: bad fs_nindir
8192 (expected 4096)`. No OOB, no panic.

## Impact ceiling

UFS is **not user-mountable** on DragonFly (`vfs.usermount=0`,
`SYSCAP_RESTRICTEDROOT`). This is a **root-context mount of attacker-supplied
media → kernel OOB read/write → page-fault panic** (DoS / hardening gap), **not**
an unprivileged→root escalation. On default GENERIC (INVARIANTS ON), the OOB
access (with `in_off=8191`, ~16KB past the buffer) deterministically page-faults
into unmapped kernel memory → panic. Smaller OOB offsets (4 bytes past) silently
corrupt adjacent kernel heap/buffers without an immediate panic.

## Files

| File           | Purpose                                                                  |
|----------------|--------------------------------------------------------------------------|
| `craft_img.c`  | UFS superblock `fs_nindir` patcher: sets the field to a forged value.    |
| `harness.c`    | Deterministic primitive characterizer (transcribes `ufs_getlbns` + the OOB index math with sentinels). |
| `reproduce.sh` | full live trigger: newfs → patch fs_nindir → mount RW → write → panic.   |
| `build.sh`     | `cc -o craft_img craft_img.c; cc -o harness harness.c`.                  |
| `run.sh`       | calls `reproduce.sh`.                                                    |
| `fix.diff`     | validated single-fix patch (validate `fs_nindir` at mount time).         |
