# DF-3035 — VERDICT

**Status: reproduced** (impact: kernel memory corruption, attacker-offset
OOB read + write; panic used as the observable). **Fix validated: fixed.**

## What was tested

`sys/vfs/ufs/ffs_alloc.c` — every allocation/free path in the FFS
allocator reads the on-disk cylinder-group header and uses its fields
(`cg_cgx`, `cg_freeoff`, `cg_iusedoff`, `cg_boff`, `cg_btotoff`,
`cg_clustersumoff`, `cg_clusteroff`, `cg_rotor`, `cg_frotor`,
`cg_irotor`, `cg_nclusterblks`) as raw kernel-memory offsets/indexes
after nothing more than `cg_chkmagic()` (`fs.h:398` — a magic
comparison).  Mount-time validation does not exist for these fields
(`ffs_vfsops.c:642-646` checks only `fs_magic` and `fs_bsize` bounds).

## Baseline runs (stock INVARIANTS kernel, guest
`DragonFly dfbsd 6.5-DEVELOPMENT ... #1: Mon Jun 29 14:18:01 UTC 2026`)

All runs use a 128 MB image built by the guest's own `newfs`
(UFS1, 1 cylinder group, bsize 16384 / fsize 2048 / frag 8,
contigsumsize 7), with **one 32-bit field of cg0's on-disk header
patched** by `cgxtool` (offsets computed from the image's own
superblock).  The mount itself never reads cg headers, so the crafted
image mounts read-write without complaint; the first allocation on it
executes the poisoned path.

| run | patched field | value | result |
|---|---|---|---|
| 0 (control) | — | — | first `creat`+16 KiB write **succeeds**, clean unmount (`run.log`) |
| 1 | `cg_cgx` | 0x40000000 | `Fatal trap 12`, **supervisor write**, page not present, `ffs_clusteracct.isra.0+0x1eb: movl %eax,(%rdx,%rcx,4)` @ `0xfffff8021776a200` (`panic1.txt`) |
| 2 | `cg_cgx` | 0x40040000 | same trap + same stopped instruction @ `0xfffff8018dce3600` (`panic2.txt`) |
| 3 | `cg_cgx` | 0x40000000 | same trap + same stopped instruction @ `0xfffff80218519500` (`panic3.txt`) |
| 4 | `cg_iusedoff` | 0x20000000 | `Fatal trap 12`, **supervisor read**, page not present, `ffs_nodealloccg+0x13f: movzbl (%rdi,%rax,1),%ecx` @ `0xfffff8007c136000` (`panic4.txt`) |

## Why this is a write/read primitive, not "just a panic"

* The stopped instruction in runs 1–3 is the single indexed store
  `fs->fs_maxcluster[cgp->cg_cgx] = i` (`ffs_alloc.c:1963`):
  `movl %eax,(%rdx,%rcx,4)` — address = base + 4 × (int32 index).
  The index is the on-disk `cg_cgx` (here 0x40000000 → +4 GiB).  The
  fault is *only* observed because the chosen offset lands on an
  unmapped page; any `cg_cgx` whose target is mapped yields a **silent
  attacker-offset int32 store/inc/dec** (`fs_cs(cg_cgx).cs_nbfree--`
  at `ffs_alloc.c:1213` is the same class, ±8 GiB reach).  With the
  heap base ~`0xfffff801xxxxxxxx`, the reachable window covers the
  whole kernel heap and then some — signature: the three write faults
  sit at `bootbase + 0x100000000` with boot-varying bases
  (`0xfffff8011776a200`, `0xfffff8008dbe3600`, `0xfffff80118519500`),
  matching `fs_maxcluster + 4·cg_cgx` in every run.
* Run 4's stopped instruction is `inosused[ipref]` (`ffs_alloc.c:1415`/
  `1393`) — a byte read at `cgbuf + cg_iusedoff`, here +512 MiB.  The
  same pointer is later written through `setbit(inosused, ipref)`
  (`ffs_alloc.c:1463`) when a zero bit is found: read primitive first,
  write primitive on the same field.
* `cg_freeoff`/`cg_boff`/`cg_btotoff`/`cg_clustersumoff`/`cg_clusteroff`
  are the identical pattern for the block bitmap and cylinder/cluster
  arrays (bit-level writes at ±2 GiB, int16/int32 incs/decs) — traced in
  the finding table, not separately run: one field per boot is the
  observability limit (the first wild access traps), and the mechanism
  is line-for-line identical.

## Exploitability assessment

* Trigger is a crafted filesystem image + one `creat`/`write` — the
  mount itself needs root (or `vfs.usermount=1` setups / auto-mounting
  removable media, where the trigger becomes unprivileged).
* The primitive is a *relative* kernel-memory write with a ±8–32 GiB
  32-bit-controlled displacement (csum dec/inc at 16-byte stride,
  `fs_maxcluster` store at 4-byte stride) plus bit-level writes through
  the bitmap offsets.  No KASLR/SMAP/SMEP on this guest; heap base is
  boot-stable within a session, so a repeatable offset to a target
  object is realistic (e.g. point `cg_cgx` so that
  `fs_csp[16·cg_cgx]` lands on a neighbouring mount's `fs_csp`,
  function-pointer-bearing structures, or refcounts).  A full uid0
  chain was not developed because the *mount* already requires root on
  the default configuration — the ceiling is arbitrary kernel memory
  corruption from a malicious disk image (or unprivileged corruption
  wherever users can get their image mounted).
* The fail-stop `panic()`s reachable with *in-range* inconsistent cg
  metadata (`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) make the same image a reliable
  local DoS even without an out-of-range field.

## Fix validation

`fix.diff` adds `ffs_chkcg()` — validating `cg_cgx` against the cg's
real index, rotors against their map sizes, and every map offset/size
against `fs_cgsize` (with int64 math, ordered like the on-disk layout) —
called after `cg_chkmagic()` in `ffs_fragextend`, `ffs_alloccg`,
`ffs_clusteralloc`, `ffs_nodealloccg`, `ffs_blkfree_cg`, and
`ffs_freefile`; plus a clamp of the preference-derived cg in
`ffs_hashalloc()` before it indexes `fs_csp[]`.

Applied to the guest's `/usr/src`, `make nativekernel KERNCONF=X86_64_GENERIC`
(`ffs_alloc.c` compiled `-Werror`-clean), installed, rebooted
(`run.patched.log` for the full sequence):

* identical `cgx 0x40000000` image → **no panic**; `dd` fails
  `No space left on device` (poisoned cg rejected → allocator returns 0
  → ENOSPC);
* identical `iusedoff 0x20000000` image → **no panic**; same graceful
  ENOSPC;
* control (unpatched image) → first allocation **still succeeds**
  (`legit filesystem unaffected`), clean unmount.

Baseline bad behaviour GONE; legitimate allocation behaviour preserved.

## Non-goals / notes

* DF-0793 (async-TRIM lifetime) and DF-0794 (`fs_ncg==0`) were given and
  are not re-verified here.
* Superblock-geometry div-by-zeros reachable from this file
  (`fs_maxbpg==0` at `ffs_blkpref:805`, `fs_spc`/`fs_nsect`/`fs_npsect`
  in `cbtocylno`/`cbtorpos`, `fs_frag>8` indexing `fragtbl`/`around`/
  `inside` in `ffs_mapsearch`) are the DF-0820/DF-0794 family (mount-time
  superblock validation) and are not re-filed.
