# DF-0894 — 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 `bap[indirs[i].in_off]` in `ffs_balloc.c:297`
up to ~16KB past the indirect-block buffer, because `in_off = bn %
MNINDIR(ump)` uses the forged value.

## Mechanism (trigger → primitive → effect)

1. **Trigger**: root mounts a crafted UFS image with forged `fs_nindir=8192`
   in the superblock (`ffs_vfsops.c:642-646` — no `fs_nindir` check; copied
   at `:729`). Then writes 1 byte at file offset `8203 * 16384 = 134283264`.

2. **Primitive**: `ffs_balloc` (`ffs_balloc.c:65`) enters the single-indirect
   path at `:290`. `ufs_getlbns` (`ufs_bmap.c:315`) computes
   `in_off = (8203-12) % 8192 = 8191`. At `:291`, `bread()` reads the
   indirect block into a buffer of exactly `fs_bsize=16384` bytes
   (= 4096 `ufs_daddr_t` entries). At `:296-297`:
   ```c
   bap = (ufs_daddr_t *)bp->b_data;      // 16384 bytes = 4096 entries
   nb = bap[indirs[i].in_off];            // bap[8191] = bp->b_data + 32764
   ```
   This reads 4 bytes at offset 32764 in a 16384-byte buffer — **16380
   bytes past the end**.

3. **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
   `ffs_balloc+0x5e7` (`movl (%rbx,%rax,4),%eax`), the exact
   `bap[in_off]` instruction. Kernel panic. Smaller OOB offsets (e.g.,
   `in_off=4096`, just 4 bytes past) silently corrupt adjacent kernel
   memory without an immediate fault — the OOB write paths at `:333`,
   `:378`, `:479` would corrupt adjacent buffers/heap objects.

## Confirmed evidence

- **Panic signature** (`panic.txt`): `Fatal trap 12: page fault while in
  kernel mode` at `ffs_balloc+0x5e7: movl (%rbx,%rax,4),%eax`.
  Instruction pointer `0xffffffff809062e7`, fault virtual address
  `0xfffff8006287dfe0`, fault code `supervisor read data, page not present`.
  Reproduced 3× (identical RIP, varying fault VA due to KVA allocation).

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

- **Mount succeeds** on unpatched `#0`: `MOUNT_RC=0` with forged
  `fs_nindir=8192`. The trigger write reaches `ffs_balloc` and faults.

## 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. This is a
  **DoS/hardening gap**, not an unprivileged→root LPE.
- **OOB extent**: up to ~16KB (forged `fs_nindir=8192`,
  `in_off_max=8191`). OOB read at `:297`; OOB write at `:333/:378/:479`
  when `nb==0`. Content is not attacker-controlled (it's disk block
  addresses being written), but the OOB *location* is controlled by
  `in_off`.
- **On GENERIC (INVARIANTS ON)**: deterministic page-fault panic for
  large OOB offsets. For small offsets (4 bytes past), silent heap/buffer
  corruption — potentially exploitable for further privesc if combined
  with heap grooming, but the root-only mount precondition makes this a
  root→kernel vector.

## Why this is NOT a uid0 escalation

The trigger requires `mount -t ufs`, which needs root privileges. 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).

## PoC changes

- `craft_img.c` — new: patches `fs_nindir` (4 bytes at SBOFF+116) in the
  UFS superblock to a forged value.
- `harness.c` — new: transcribes `ufs_getlbns` and the OOB index math
  with a poisoned allocator, proving the OOB extent deterministically.
- `reproduce.sh` — new: `newfs` default geometry → patch `fs_nindir`
  4096→8192 → mount RW → prime indirect block (lbn=12) → trigger write
  (lbn=8203, in_off=8191, ~16KB OOB) → page-fault panic.

## 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;
}
```

- **Before (unpatched #0)**: mount succeeds, write triggers `Fatal trap 12`
  at `ffs_balloc+0x5e7`.
- **After (patched #1)**: mount rejected — `ffs_mountfs: bad fs_nindir 8192
  (expected 4096)` in dmesg, `mount_ufs: ... incorrect super block`,
  `MOUNT_RC=1`. No OOB access, no panic, guest stays up. Reproduced 2×.
