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

## Summary
The finding claims a heap OOB write in `ffs_write` via a crafted `fs_qbmask`
loaded from disk without validation. **The validation gap is real and
reproduced**, but the finding's specific OOB-write mechanism (negative
`xfersize` reaching `uiomove` as a wrapped `size_t`) is **defeated by a
`size_t`/`int` promotion at `ufs_readwrite.c:294-295`** that clamps the
negative `xfersize` to a small positive value first. The actual observed
effect is an unconditional `panic("ffs_balloc: blk too big")` — a kernel
DoS, not a memory-corruption primitive.

**Net impact:** root→kernel DoS (panic). UFS mount is root-only
(`vfs.usermount=0`), so this is a hardening gap, not an unpriv LPE.

## How to reproduce

### Build (host)
```
./build.sh           # builds craft_img + trigger + harness inside the guest
```

### Run (host)
```
./run.sh             # runs the harness (deterministic math) AND the live trigger
```

### Manual reproduction inside the guest (as root)
```
sh /root/reproduce.sh qbmask3fff
```
This will:
1. `newfs -b 8192 -f 1024` a base image (so normal `fs_qbmask=0x1FFF`)
2. Patch `fs_qbmask` to `0x3FFF` via `craft_img`
3. Mount RW
4. `pwrite(fd, buf, 16, 10000)` to a file on the mount

**Expected on unpatched `#0` GENERIC:** kernel panic
`panic: ffs_balloc: blk too big` (guest dies).

**Expected on patched `#1` kernel:** mount rejected
(`mount_ufs: ... incorrect super block`), no panic.

## The bug (confirmed in source)
- `ufs_readwrite.c:292` — `blkoffset = blkoff(fs, uio_offset)` = `offset & fs_qbmask`
- `ufs_readwrite.c:293` — `xfersize = fs->fs_bsize - blkoffset` (negative if `blkoffset > fs_bsize`)
- `ufs_readwrite.c:294-295` — `if (uio->uio_resid < xfersize) xfersize = uio->uio_resid;`
  **DEFEATS** the negative-xfersize chain via `size_t`/`int` promotion
  (`uio_resid` is `size_t`, `xfersize` is `int`).
- `ffs_balloc.c:91` — `panic("ffs_balloc: blk too big")` fires when
  `blkoffset + xfersize > fs_bsize`.

## Why it is NOT a heap OOB write (as the finding claims)
The finding overlooked that `uio->uio_resid` is `size_t` (unsigned). When
the `int xfersize` is negative, the comparison `uio_resid < xfersize`
promotes `xfersize` to a huge `size_t`, making the comparison TRUE, and
`xfersize` is clamped to `uio_resid` (a small positive). The negative
value never reaches `uiomove`. See `promote_test.c` and the Conclusion
section of `harness.c` output.

## Fix
`fix.diff` adds geometry validation at `ffs_mountfs` mount time. Validated
on a single-fix `#1` kernel: crafted image rejected with `EINVAL`,
legitimate filesystems unaffected.

## Threat model
`vfs.usermount=0` — root-only mount. This is a **root→kernel** DoS / hardening
gap, not an unprivileged LPE.
