# DF-3062 VERDICT

## Reproduced? YES — first attempt, deterministic

- Guest: DragonFly 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 UTC 2026
  (X86_64_GENERIC, INVARIANTS), stock ext2fs.ko.
- Image: `craft3062.py` — mke2fs base (ext2, -b 4096, -I 1024, 4MB) patched
  to s_inodes_per_group=4 (>= ipb=4 → passes `sys/vfs/ext2fs/ext2_vfsops.c:583`),
  s_free_inodes_count=4, ro_compat|=GDT_CSUM(0x10) (inside
  EXT2F_ROCOMPAT_SUPP → RW mount allowed), gd0: nifree=4,
  flags|=EXT2_BG_INODE_UNINIT(0x1), gd crc16 recomputed with the kernel's
  ext2_crc16 (uuid || cg || gd[0:30]).
- Run: `sh /root/trigger3062.sh` → kldload ext2fs; vn attach; RW mount OK;
  `ls` OK; `touch /mnt/e2/x` → **Fatal trap 12: page fault while in kernel
  mode, supervisor WRITE data, page not present, RIP = memset+0xd5
  (`repe stosq`)** — panic.txt lines 1-20. The write-faulting RIP is the
  memset itself: it walked off the end of bp->b_data writing zeroes until it
  hit an unmapped kernel page. Guest dead in ddb.

## Root cause (path:line)

- `sys/vfs/ext2fs/ext2_alloc.c:1319` `ibytes = fs->e2fs_ipg / 8;` — integer
  division; 0 for ipg ∈ [1,7].
- `sys/vfs/ext2fs/ext2_alloc.c:1320` `memset(bp->b_data, 0, ibytes - 1);` —
  `ibytes - 1` is int −1 → converted to size_t SIZE_MAX → unbounded kernel
  heap memset with byte value 0.
- Geometry gate that makes ipg<8 mount-legal:
  `sys/vfs/ext2fs/ext2_vfsops.c:582-587` checks only
  `ipg >= ipb(=bsize/isize)` and `ipg <= bsize*8`; e.g. bsize=4096,
  inode_size=1024 → ipb=4 → ipg=4 accepted.
- Reachability: ext2_valloc (ext2_alloc.c:385) → ext2_hashalloc →
  ext2_nodealloccg (ext2_alloc.c:1291) — feature gate at 1315-1316
  (GDT_CSUM || METADATA_CKSUM), INODE_UNINIT branch at 1317.

## Exploitability assessment (primary objective: uid=0?)

Primitive: unbounded-length, fixed-value(0) kernel heap write starting at a
buffer-cache buffer of attacker-chosen population context. The length
(SIZE_MAX) guarantees the store sequence runs past every mapped region in
its path and faults — observed deterministically. There is no attacker
control over the length or the written value, and the fault occurs before
any scheduled code can observe the corruption in a useful way. A uid=0 chain
from *this* primitive alone is therefore judged infeasible (hard blocker:
unbounded, uncontrolled write terminates in an inevitable fatal fault).
The value of the finding is: certain kernel-memory corruption / guaranteed
panic from a mounted crafted image by an unprivileged create(). Same threat
bar as DF-0811 (High). The related reserved-inode allocation consequences
that *do* lead to further state confusion are filed separately (DF-3066).

## Fix validation (mandatory for memcorrupt)

fix.diff:
```diff
-			ibytes = fs->e2fs_ipg / 8;
-			memset(bp->b_data, 0, ibytes - 1);
+			ibytes = howmany(fs->e2fs_ipg, NBBY);
+			memset(bp->b_data, 0, ibytes);
```
- Baseline reproduced on stock (panic.txt).
- Applied with `patch -p0` in guest /usr/src, rebuilt
  (`cd /usr/src/sys/vfs/ext2fs && make`), installed to /boot/kernel/ext2fs.ko.
- Re-ran the EXACT trigger: `AFTER-TRIGGER-NO-PANIC`, `TRIGGER-RC=0`,
  guest up (run.2.log / fix_run.txt). The memset fault is GONE.
- Follow-up regression on the same (still inconsistent) crafted image hit
  the separate pre-existing reserved-inode chain
  (`bgetvp - overlapping buffer`, panic.bgetvp-followup.txt) — that chain is
  the DF-3066 class (also reachable without fix.diff whenever INODE_UNINIT
  bitmap regeneration frees reserved-inode bits, e.g. ipg in 8..15 where the
  stock memset(…, ibytes−1==0) leaves garbage byte 0 in force); it is not
  caused by the fix. Recommended companion hardening (not in fix.diff):
  after regenerating an uninit inode bitmap, set bits for reserved inodes
  [1, EXT2_FIRST_INO(fs)) and all bits >= ipg, and reject creates that
  resolve to reserved inode numbers.

impact=panic (memcorrupt class; primitive = unbounded zero-fill kernel heap
write); confidence=certain; attempts=1 (decisive first run).
