# DF-3035 — FFS allocator trusts unvalidated on-disk cylinder-group headers
(`sys/vfs/ufs/ffs_alloc.c`) → attacker-offset OOB kernel read/write on a
crafted filesystem image

## What

Every allocator in `sys/vfs/ufs/ffs_alloc.c` reads a cylinder-group block
with `bread()`, applies only `cg_chkmagic()` — a **magic comparison**
(`sys/vfs/ufs/fs.h:398`) — and then uses raw on-disk cg-header fields as
**byte offsets into the cg buffer and array indexes into kernel memory**:

| on-disk field | sink in ffs_alloc.c | primitive |
|---|---|---|
| `cg_cgx` | `fs->fs_cs(fs, cgp->cg_cgx).cs_nbfree--` — `ffs_alloccgblk:1213`; `fs->fs_maxcluster[cgp->cg_cgx] = i` — `ffs_clusteracct:1963`; `blkno = cg_cgx * fs_fpg + bno` — `:1218` | OOB **write** (int32 inc/dec/store) at `fs_csp + 16·cg_cgx + 4` / `fs_maxcluster + 4·cg_cgx` — signed 32-bit index ⇒ ±8/±32 GiB around the heap objects |
| `cg_freeoff` | `cg_blksfree(cgp)` base for `ffs_clrblock`/`setbit`/`ffs_isblock`/`scanc` — `ffs_alloccgblk:1208-1209`, `ffs_alloccg:1081-1082`, `ffs_mapsearch:1837-1847`, `ffs_blkfree_cg:1532-1609` | OOB **read+write** of single bits at `cgbuf + cg_freeoff + bit/8` (32-bit offset ⇒ ±2 GiB) |
| `cg_iusedoff` | `cg_inosused(cgp)` base for the inode-bitmap scan and `setbit` — `ffs_nodealloccg:1383-1463`, `ffs_freefile:1791-1799` | OOB **read+write** (`movzbl` scan, then `setbit`) |
| `cg_boff` / `cg_btotoff` | `cg_blks(fs,cgp,cylno)[...]--`, `cg_blktot(cgp)[cylno]--` — `ffs_alloccgblk:1215-1216`, `ffs_blkfree_cg:1550-1551` | OOB int16/int32 **write** |
| `cg_clustersumoff` / `cg_clusteroff` | `cg_clustersum(cgp)[len]` rw-loop, `sump[i] += cnt` — `ffs_clusteralloc:1257-1275`, `ffs_clusteracct:1893-1955` | OOB int32 **read/write** |
| `cg_rotor` / `cg_frotor` / `cg_irotor` | scan start offsets — `ffs_alloccgblk:1129`, `ffs_mapsearch:1836`, `ffs_nodealloccg:1410` | OOB **read** (`scanc` length underflow) |
| `cg_nclusterblks` | loop bound reading `*mapp++` — `ffs_clusteralloc:1294-1308`, `ffs_clusteracct:1907` | OOB **read** |
| inode `i_db[]` → `ffs_blkpref` → `cg = dtog(bpref)` | `fs->fs_cs(fs, cg)` guard before `bread` — `ffs_alloccg:1010`, `ffs_nodealloccg:1370` | OOB **read** at `fs_csp + 16·cg` |

The mount path never validates a single one of these fields
(`ffs_mountfs` checks only `fs_magic`/`fs_bsize`, `ffs_vfsops.c:642-646`),
and nothing re-checks them when the cg block is read at allocation time.
Nothing checks them at free time either (`ffs_blkfree_cg`, `ffs_freefile`).

Additionally, inconsistent-but-in-range cg metadata (per-cg `cg_cs` vs
`fs_cstotal`, bitmap vs counters) converts the kernel's own consistency
`panic()`s into fail-stop DoS from a crafted image: `ffs_valloc:630`
(dup alloc), `ffs_nodealloccg:1451` (block not in map),
`ffs_blkfree_cg:1542/1581` (freeing free block/frag),
`ffs_clusteralloc:1317` (map mismatch).

## Threat model

Crafted FFS media mounted by root (same model as DF-0820 / DF-3015).
The unprivileged trigger afterwards is a single `creat`/`write` in the
mounted filesystem — the very first allocation touches the poisoned cg.
On systems with `vfs.usermount=1` (or in any scenario where an
unprivileged user can get a crafted image mounted — e.g. removable media
auto-mounting), this becomes unprivileged-user → arbitrary-offset kernel
memory corruption.

## Primitive (verified on the guest, stock INVARIANTS kernel)

* `cg_cgx = 0x40000000` → deterministic `Fatal trap 12`, **supervisor
  write**, page not present, in `ffs_clusteracct`:
  `Stopped at ffs_clusteracct.isra.0+0x1eb: movl %eax,(%rdx,%rcx,4)`
  — the store `fs->fs_maxcluster[cg_cgx] = i` at `base + 4·cg_cgx`
  (`panic1/2/3.txt`). The same value three times → three fault addresses
  `base + 0x1_0000_0000`, with boot-varying heap bases.
* `cg_iusedoff = 0x20000000` → deterministic `Fatal trap 12`,
  **supervisor read**, in `ffs_nodealloccg`:
  `Stopped at ffs_nodealloccg+0x13f: movzbl (%rdi,%rax,1),%ecx`
  — the `inosused[ipref]` bitmap read at `cgbuf + 0x20000000`
  (`panic4.txt`).
* Control run (identical image, **no** field patched) → allocation
  succeeds, no panic (`run.setup.log`, `run.log`).

A cg_cgx that lands on *mapped* memory instead of the guard page yields a
silent single-int32 inc/dec at an attacker-chosen ±offset — a corruption
primitive, not a crash.

## Reproduce (baseline, stock kernel)

```
# on the host, from the repo root:
tar czf /tmp/p.tgz -C findings/poc/DF-3035 cgxtool.c setup3035.sh fs.h dinode.h
ssh -F dfbsd-qemu/config dfbsd '/bin/sh -c "mkdir -p /root/poc && cat > /tmp/p.tgz && tar xzf /tmp/p.tgz -C /root/poc && cc -O -o /tmp/cgxtool /root/poc/cgxtool.c"'

# in the guest (root):
sh /root/poc/setup3035.sh cgx 0x40000000      # panics in ffs_clusteracct (write)
sh /root/poc/setup3035.sh iusedoff 0x20000000 # panics in ffs_nodealloccg (read)
sh /root/poc/setup3035.sh cgx none            # control: no panic (see script)

# panic evidence: dfbsd-qemu/vm.sh log  (serial console)
```

Expected baseline: `Fatal trap 12: page fault while in kernel mode`,
fault code `supervisor write data, page not present` (cgx) or
`supervisor read data, page not present` (iusedoff), guest drops to `db>`.

Expected with `fix.diff` (validated, `run.patched.log`): no panic;
`dd: /mnt/t0: No space left on device` — the poisoned cg is rejected
(`ffs_chkcg`) and the allocator fails gracefully with ENOSPC; a clean
filesystem still allocates normally.
