# DF-0914 — VERDICT

## Verdict: REPRODUCED (panic), fix VALIDATED

The bug is real: `fs_nindir` (an on-disk UFS superblock field) is copied
verbatim into `ump->um_nindir` at `ffs_vfsops.c:729` with no validation
that it equals `fs_bsize / sizeof(ufs_daddr_t)`. The mount-time check at
`ffs_vfsops.c:642-646` validates only `fs_magic` and the `fs_bsize` range.
A forged `fs_nindir=8192` (vs the correct 4096 for `fs_bsize=16384`) drives
`daddr = ((ufs_daddr_t *)bp->b_data)[xap->in_off]` at `ufs_bmap.c:221` up to
~16KB past the indirect-block buffer, because `in_off = bn % MNINDIR(ump)`
uses the forged value. This is the **READ-path** OOB (vs DF-0894's write path).

## Mechanism (trigger → primitive → effect)

1. **Trigger (Phase A — correct image)**: root `newfs` a UFS1 image
   (`fs_bsize=16384`, `fs_nindir=4096`). Mount RW, write 1 byte at lbn=12
   (offset 196608) to allocate `i_ib[0]` (single indirect block) with correct
   `in_off=0` (in bounds). `ftruncate` the file to 134283265 bytes (covers
   lbn=8203). Unmount cleanly.

2. **Trigger (Phase B — forged image)**: patch `fs_nindir` 4096→8192 in the
   superblock (`craft_img.c`). Mount RW — the forged `fs_nindir` is accepted
   (`ffs_vfsops.c:729` copies it with no check). Read 1 byte at lbn=8203
   (offset 134283264). `ffs_read` (`ufs_readwrite.c:110`) calls
   `ffs_blkatoff_ra(vp, offset, ...)` → `bread(vp, lbn=8203, ...)` →
   `VOP_BMAP` → `ufs_bmap` → `ufs_bmaparray(vp, 8203, ...)`.

3. **Primitive**: `ufs_getlbns(vp, 8203)` at `ufs_bmap.c:315` computes
   `off = (8191 / 1) % 8192 = 8191` (with forged `MNINDIR=8192`).
   Back in `ufs_bmaparray` at `:221`:
   ```c
   daddr = ((ufs_daddr_t *)bp->b_data)[xap->in_off];
   // bap[8191] = bp->b_data + 32764
   ```
   The indirect-block buffer is `fs_bsize=16384` bytes (4096 entries, indices
   0..4095). `bap[8191]` reads at offset 32764 — **16380 bytes past the end**
   of the 16384-byte buffer.

4. **Effect**: On default GENERIC (INVARIANTS ON), the 16KB OOB read crosses
   into unmapped kernel virtual memory → **page fault** →
   `Fatal trap 12: page fault while in kernel mode` at
   `ufs_bmaparray+0x15c: movslq (%rsi,%rax,4),%r12` — the exact `bap[in_off]`
   instruction. **Supervisor read data, page not present** — confirming it is
   a READ-path OOB, distinct from DF-0894's write-path OOB at `ffs_balloc`.

## Confirmed evidence

- **Panic signature** (`panic.txt`): `Fatal trap 12: page fault while in
  kernel mode` at `ufs_bmaparray+0x15c: movslq (%rsi,%rax,4),%r12`.
  Fault virtual address `0xfffff80055cbdfe0`, fault code
  `supervisor read data, page not present`. Instruction pointer
  `0xffffffff80913b7c`. This is **distinct from DF-0894** which panics at
  `ffs_balloc+0x5e7: movl (%rbx,%rax,4),%eax` (write-path).

- **Two-phase isolation**: Phase A writes on the CORRECT image (fs_nindir=4096,
  all `ffs_balloc` operations in-bounds). Phase B only reads on the FORGED
  image. This cleanly separates DF-0914's read-path OOB from DF-0894's
  write-path OOB.

- **Harness** (`harness.c`): transcribes `ufs_getlbns` + the OOB index math,
  confirming `in_off=8191` indexes `bap[8191]` at `bp->b_data + 32764` —
  16384 bytes past the 16384-byte buffer.

## Impact ceiling

- **Privilege**: UFS is **not user-mountable** 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**, not an unprivileged→root LPE.

- **OOB extent**: up to ~16KB (forged `fs_nindir=8192`, `in_off_max=8191`).
  OOB READ at `ufs_bmap.c:221`. The OOB value becomes `daddr`, used as a
  disk block address at `:241` → **confused-deputy read** of an arbitrary
  disk block, or page-fault panic (INVARIANTS).

- **On GENERIC (INVARIANTS ON)**: deterministic page-fault panic for large
  OOB offsets. For small offsets (4 bytes past), the OOB read silently
  returns adjacent kernel memory (info leak) and the confused-deputy read
  targets an arbitrary disk block.

## Why this is NOT a uid0 escalation

The trigger requires `mount -t ufs`, which needs root privileges
(`vfs.usermount=0`). A non-root user cannot mount UFS on DragonFly.
Therefore this is root→kernel corruption (a hardening gap), not
unprivileged→root. The Phase 6 escalation chain is not applicable: there is
no privilege boundary to cross (root→kernel is game-over by definition).

## Fix validation (Phase 8)

**fix.diff** adds a check at `ffs_vfsops.c:647` (after the existing
magic/bsize validation):
```c
if (fs->fs_nindir != fs->fs_bsize / sizeof(ufs_daddr_t)) {
    kprintf("ffs_mountfs: bad fs_nindir %d (expected %d)\n", ...);
    error = EINVAL;
    goto out;
}
```

This is **identical to DF-0894's fix** — the same mount-time `fs_nindir`
check closes both bugs because both depend on the forged `fs_nindir` being
accepted at mount.

- **Before (unpatched #0)**: Phase B mount succeeds (`MOUNT_B_RC=0`),
  read at lbn=8203 → `Fatal trap 12` at `ufs_bmaparray+0x15c`.
- **After (patched #1)**: Phase B mount **rejected** —
  `ffs_mountfs: bad fs_nindir 8192 (expected 4096)` in dmesg,
  `mount_ufs: incorrect super block`, `MOUNT_B_RC=1`. No OOB, no panic.

## Relationship to DF-0894

**Same root cause, same fix, different sink.** Both bugs exploit the
unvalidated `fs_nindir` at `ffs_vfsops.c:729`:

| | DF-0894 | DF-0914 |
|---|---|---|
| Sink | `ffs_balloc.c:297` (write path) | `ufs_bmap.c:221` (read path) |
| Panic RIP | `ffs_balloc+0x5e7` | `ufs_bmaparray+0x15c` |
| Instruction | `movl (%rbx,%rax,4),%eax` | `movslq (%rsi,%rax,4),%r12` |
| Trigger | file write (extension) | file read (bmap on read) |
| Fix | validate `fs_nindir` at mount | **same** (validate `fs_nindir` at mount) |

DF-0894's fix.diff already contains this check. DF-0914's fix.diff is the
same check (with a comment referencing both finding IDs). Applying either
fix closes both bugs.

## PoC changes

- `craft_img.c` — new: patches `fs_nindir` (reuses DF-0894 pattern with
  DF-0914-specific comments).
- `harness.c` — new: transcribes `ufs_getlbns` + OOB-read math for the
  READ path (`ufs_bmaparray:221`).
- `trigger.c` — new: Phase A (write 1 byte at lbn=12 + ftruncate on correct
  image).
- `trigger_ro.c` — new: Phase B (read at lbn=8203 on forged image).
- `reproduce.sh` — new: two-phase flow (write on correct → patch → read on
  forged) that cleanly isolates the READ-path OOB from DF-0894's write path.
- `fix.diff` — mount-time `fs_nindir` validation (same as DF-0894).
