# DF-0903 — ffs_write heap OOB via crafted fs_qbmask (PARTIAL REPRODUCTION)

## Verdict (one line)
**REPRODUCED as kernel PANIC (DoS) — but the finding's specific OOB-write
mechanism is INCORRECT.** The crafted `fs_qbmask` IS loaded verbatim and
drives `xfersize` negative as claimed, but a `size_t`/`int` promotion at
`ufs_readwrite.c:294-295` silently clamps the negative `xfersize` to a
small positive value *before* it can reach `uiomove`. The actual observed
effect is an unconditional `panic("ffs_balloc: blk too big")` at
`ffs_balloc.c:91`, **not** a heap OOB write.

## Mechanism (what actually happens)

### The real validation gap (CONFIRMED)
`ffs_vfsops.c:ffs_mountfs` reads the on-disk superblock into `ump->um_fs`
via `bcopy` at line 673. The only geometry validation is at lines 642-646:
```c
if (fs->fs_magic != FS_MAGIC || fs->fs_bsize > MAXBSIZE ||
    fs->fs_bsize < sizeof(struct fs)) {
    error = EINVAL;
    goto out;
}
```
`fs_qbmask` is NOT validated. `ffs_oldfscompat` (line 796) re-derives
`fs_qbmask = ~fs_bmask` ONLY inside `if (fs->fs_inodefmt < FS_44INODEFMT)`
(line 802-816). Modern `newfs` sets `fs_inodefmt = FS_44INODEFMT (=2)`, so
the recompute is skipped and the on-disk `fs_qbmask` is used verbatim.
**A crafted `fs_qbmask` larger than `fs_bsize-1` is accepted.** Confirmed
empirically: `craft_img` patches `fs_qbmask` from `0x1FFF` to `0x3FFF`
(`fs_bsize=8192`) and the image mounts RW.

### What ffs_write actually does with the poisoned mask
At `ufs_readwrite.c:290-295`:
```c
for (error = 0; uio->uio_resid > 0;) {
    lbn = lblkno(fs, uio->uio_offset);
    blkoffset = blkoff(fs, uio->uio_offset);          /* = offset & 0x3FFF */
    xfersize = fs->fs_bsize - blkoffset;              /* e.g. 8192-10000 = -1808 */
    if (uio->uio_resid < xfersize)                    /* size_t < int ! */
        xfersize = uio->uio_resid;                    /* clamp */
```
**`uio->uio_resid` is `size_t`** (unsigned, `sys/sys/_uio.h:69`).
**`xfersize` is `int`** (signed, `ufs_readwrite.c:220`). The comparison
`size_t < int` triggers the C "usual arithmetic conversions": the `int` is
promoted to `size_t`, so a NEGATIVE `xfersize` becomes a huge `size_t`
(`(size_t)-1808 = 0xFFFFFFFFFFFFF8F0`). The comparison
`16 < 0xFFFFFFFFFFFFF8F0` is **TRUE**, so line 295 **clamps** `xfersize`
to `uio_resid` (16). The negative `xfersize` never survives to line 356.

### Why the finding's OOB-write chain is unreachable
With the clamped `xfersize=16` and `blkoffset=10000`, the call at line 329:
```c
error = VOP_BALLOC(vp, uio->uio_offset, xfersize, ...);
```
enters `ffs_balloc` (`ffs_balloc.c:88-91`):
```c
size = blkoff(fs, ap->a_startoffset) + ap->a_size;   /* 10000 + 16 = 10016 */
if (size > fs->fs_bsize)
    panic("ffs_balloc: blk too big");                 /* 10016 > 8192 -> PANIC */
```
The unconditional `panic` (NOT INVARIANTS-gated) fires. Control never
reaches `uiomovebp` at `ufs_readwrite.c:356`. Reproduced empirically:

```
panic: ffs_balloc: blk too big
ffs_balloc() at ffs_balloc+0xfff
vop_balloc() at vop_balloc+0xaf
ffs_write() at ffs_write+0x16a           <- the VOP_BALLOC call site
vop_write() at vop_write+0x9d
vn_write() at vn_write+0x134
```

The `promote_test.c` helper proves the C promotion semantics that defeat
the chain:
```
xfersize (int)     = -1808
(size_t)xfersize   = 0xfffffffffffff8f0
uio_resid          = 16
LINE 294: uio_resid < xfersize is TRUE (size_t promotion)
LINE 295: xfersize = uio_resid = 16
xfersize after clamp = 16 (POSITIVE -- OOB path defeated)
balloc: blkoffset + xfersize = 10000 + 16 = 10016 > fs_bsize=8192 => PANIC
```

## Threat model
`vfs.usermount = 0` on the audit guest (verified). UFS mounts are
**root-only**. This is a **root→kernel** hardening gap, NOT an unprivileged
LPE. The realistic impact ceiling is: **root can panic the kernel** (DoS)
via a crafted filesystem image. The finding's "heap OOB write → slab
grooming → priv-esc" chain is not reachable, and even if it were, root can
already corrupt kernel memory via `/dev/mem` or `kldload`, so the
additional impact would be marginal.

## Exploit chain
**None.** Not a memory-corruption primitive on master — the negative
`xfersize` is clamped by the `size_t`/`int` promotion at line 294-295
before it reaches `uiomove`. The only effect is the unconditional
`ffs_balloc: blk too big` panic. This is a DoS (root→kernel), not a
write primitive. No escalation attempt is warranted — the cited primitive
does not exist as described.

## Fix (validated)

`findings/poc/DF-0903/fix.diff` adds a geometry validation block in
`ffs_vfsops.c:ffs_mountfs` immediately after the existing magic/bsize
check (line 646). It rejects the mount with `EINVAL` if:
- `fs_fsize <= 0` or `fs_fsize > fs_bsize`
- `fs_bsize` or `fs_fsize` is not a power of 2
- `fs_qbmask != (int64_t)(fs_bsize - 1)` (the critical invariant for
  `blkoff()` to stay in `[0, fs_bsize)`)
- `fs_qfmask != (int64_t)(fs_fsize - 1)`

Built as a single-fix `#1` kernel and validated:
- **Baseline `#0` (unpatched):** crafted image mounts RW; `pwrite` at
  offset 10000 → `panic: ffs_balloc: blk too big`, guest dies.
- **Patched `#1`:** crafted image is rejected at mount
  (`mount_ufs: ... incorrect super block`, `MOUNT_RC=1`); trigger never
  runs; guest stays up.
- **No regression:** a legitimate `newfs -b 8192 -f 1024` image still
  mounts, accepts writes, and reads back correctly on `#1`. The guest's
  own `/boot` (bsize=16384) and root filesystem boot normally.

This fix supersedes the finding markdown's proposal (which suggested
adding the missing `xfersize <= 0` panic in `ffs_write`); the mount-time
validation is the cleaner root-cause fix because it rejects the malformed
superblock before any blkoff/xfersize arithmetic can run, and it cannot
be bypassed by also crafting `fs_bmask` consistently.

## PoC files (under findings/poc/DF-0903/)
- `craft_img.c` — superblock `fs_qbmask` patcher (8-byte LE write at file
  offset `SBOFF + offsetof(struct fs, fs_qbmask) = 8192 + 1336 = 9528`).
- `trigger.c` — `pwrite(fd, buf, 16, 10000)` against a file on the
  crafted mount; on `#0` this triggers the panic.
- `harness.c` — deterministic transcription of the blkoff/xfersize
  arithmetic with the line 294-295 size_t-promotion clamp modeled
  accurately. Proves the OOB write is unreachable.
- `promote_test.c` — minimal C program demonstrating the size_t/int
  promotion that defeats the finding's chain.
- `reproduce.sh` — guest-side orchestrator (build, newfs, patch, mount,
  trigger).
- `build.sh` / `run.sh` — host-runnable build/run entry points.
- `panic.txt` — `ffs_balloc: blk too big` panic signature from `boot.log`.
- `fix.diff` — git-apply-able unified diff (validated).
- `fix_build.log` / `fix_run.log` — full Phase 8 logs.
