DF-3216 / verdict.json
{ "finding_id": "DF-3216", "status": "reproduced", "reproduced": 1, "impact": "panic", "confidence": "certain", "verdict": "ext2_reload() adopts a re-validated superblock with a new block size but never re-derives ump->um_nindir/um_bptrtodb/um_seqinc (set only in ext2_mountfs, ext2_vfsops.c:991-993) nor mnt_stat.f_iosize (readindirs buffer size, ext2_bmap.c:126). After a 4096->1024 reload the live mount walks indirect blocks with MNINDIR=1024 against 256-slot 1KB blocks: ext2_getlbns (ext2_bmap.c:354) emits in_off 256..1023; ext2_bmaparray reads bap[in_off] OOB (ext2_bmap.c:255); blkptrtodb mis-maps all data I/O 4x (deterministic ext2_dirbad panic on the first post-reload lookup on the pure-stock kernel); and after mount -u -o rw, ext2_balloc writes bap[in_off]=newb (ext2_balloc.c:267) as 4-byte OOB heap writes up to +3068 bytes past a 1KB buffer-cache buffer at write()-offset-chosen positions with allocated-block-number values. Demonstrated: stock panic (run 1), mis-mapped reads + ~741 OOB-read-sourced block pointers + 15 documented OOB writes + 0/256-entry on-disk indirect with 757KB of orphaned allocated blocks (instrumented run 3 + on-disk forensics). fix.diff (re-derive um_* + f_iosize in ext2_reload) eliminates all of it: correct markers, zero OOB prints, consistent on-disk mapping.", "exploit_chain": "vfs.usermount=1 unprivileged user mounts own crafted 4096-bsize ext2 read-only on user-owned vn device; rewrites backing file with 1024-bsize ext2; mount(2) MNT_UPDATE|MNT_RELOAD (permitted to the original mounter, vfs_syscalls.c:259-264) adopts new bsize while um_* stay stale; (a) any lookup => ext2_dirbad panic; or with a decoy dir block: (b) mount -u -o rw passes the non-root VOP_EACCESS check on the users device; sparse pwrite at lbn>=268 drives ext2_getlbns to in_off 256..1023: OOB reads return heap garbage consumed as block numbers (data written to heap-chosen blocks) and, where adjacent memory is zero, 4-byte OOB heap writes bap[in_off]=<allocated block#> at b_data+1024..+4044 (offset chosen by write offset, value groomable via allocation order, repeatable hundreds of times). Not escalated to uid=0 in this run; primitive fully characterized (no-SMAP/SMEP guest, adjacent 1KB buffer-cache chunks confirmed as victims).", "evidence": [ "panic.txt: panic: ext2_dirbad: /mnt/e2: bad dir ino 2 at offset 0: mangled entry from ext2_lookup on first post-reload lookup (pure stock kernel, run 1)", "run.2.log lines ~53+: live DF3216[WRITE-OOB]: bap[997] <- 1351 at b_data+4*997=3988 (buffer size 1024, b_data=0xfffff80079376000) x15 plus ~741 balloc-OOB lines (instrumented run 3)", "run.2.log lines 32-39: post-reload reads of lbn 100/101/268-271 return zero content (NOT-A-MARKER) vs correct markers pre-reload (lines 14-19)", "VERDICT.md on-disk forensics: post-run image victim file /w single-indirect block 609 has 0/256 entries while i_size=1MB and 0xAA data sits orphaned at blocks 611+ (pointers exist only past the kernel buffer)", "fix_run.log: post-reload reads return correct markers (lbn 100,101,268-271), zero live DF3216 prints (the visible ones are dmesg|tail-30 replay of the pre-fix run; they start after the post-run dmesg tail marker); fixed-image forensics: /w maps 609->610->256 entries->0xAA correctly" ], "kernel_refs": [ "sys/vfs/ext2fs/ext2_bmap.c:126", "sys/vfs/ext2fs/ext2_bmap.c:210", "sys/vfs/ext2fs/ext2_bmap.c:255", "sys/vfs/ext2fs/ext2_bmap.c:257", "sys/vfs/ext2fs/ext2_bmap.c:276", "sys/vfs/ext2fs/ext2_bmap.c:354", "sys/vfs/ext2fs/ext2_balloc.c:198", "sys/vfs/ext2fs/ext2_balloc.c:233", "sys/vfs/ext2fs/ext2_balloc.c:267", "sys/vfs/ext2fs/ext2_vfsops.c:821", "sys/vfs/ext2fs/ext2_vfsops.c:991", "sys/vfs/ext2fs/ext2_vfsops.c:992", "sys/vfs/ext2fs/ext2_vfsops.c:993", "sys/vfs/ext2fs/ext2_vfsops.c:1126", "sys/vfs/ext2fs/ext2_vfsops.c:183" ], "poc_changes": "Finding authored in this pass-2 run (no prior seed). Harness reuses DF-3047s vntool.c/vn(4) approach; new craft.py builds the bsize 4096->1024 pair with equal gcount (1) so DF-3047s cluster-array OOB does not fire first, adds /f with per-1KB-block markers (misdirection oracle) and empty /w (write victim), and plants a decoy copy of the first root-dir block at 4x its block number so post-reload lookups survive the stale-shift mis-map and reach the indirect-walk OOB layers. trigger.c performs reload+statfs (f_iosize refresh), marker reads, and the sparse 0xAA write/readback scan. Instrumentation (instrument.diff) is printf-only probes in ext2_bmap/ext2_balloc/ext2_reload; fix.diff is the real fix without probes.", "attempts": 4, "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64", "runtime_sec": 3300, "guest_dirty": 0, "build_cmd": "sh /root/build.sh (host: python3 craft.py <dir>; images + sources scpd to /root; guest: cc -O2 -o /root/vntool vntool.c; cc -O2 -o /root/trigger trigger.c)", "run_cmd": "sh /root/run.sh (kldload ext2fs; vntool attach 0 /root/dev64.img; mount -t ext2fs -o ro; marker reads; cat imgB8.img > dev64.img; ./trigger reload /mnt/e2; reads; mount -u -o rw; ./trigger write /mnt/e2/w; dmesg)", "code_hash": "5594942602a20ccdddd270f5561f776fadc93ac061e2e143f9182b1373fd0ccc", "notes": "Run 2 (run.log, uninstrumented) showed stale pre-reload 4KB buffers serving correct content for ranges read before the swap - a buffer-cache artifact; cold reads and run 1s panic are the true behavior. The stock-kernel observable is a deterministic panic; the memcorrupt primitive (OOB read + offset/value-controlled 4-byte OOB heap write) is proven on the instrumented stock-source module and by on-disk forensics (0/256-entry indirect block + orphaned 0xAA blocks at heap-chosen addresses). Guest reset with-src performed after validation (guest_dirty=0). Related: DF-3047 (same MNT_RELOAD trigger, gcount/cluster-array sinks; its identical-geometry fix also blocks this vector as defense-in-depth), DF-0894 (ffs nindir class - killed at fresh mount here by the log_bsize<=2 gate).", "recommended_fix": "In ext2_reload(), after ext2_compute_sb_data() succeeds, re-derive ump->um_nindir/um_bptrtodb/um_seqinc from the adopted geometry and set mp->mnt_stat.f_iosize = fs->e2fs_bsize (fix.diff); alternatively/redundantly gate reload on identical geometry as DF-3047s fix does.", "fix_status": "fixed", "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64 (stock kernel; ext2fs.ko rebuilt from patched /usr/src)", "fix_baseline_reproduced": 1, "fix_patched_reproduced": 0, "fix_verdict": "fix.diff applied on the guests clean /usr/src (instrumentation re-applied for observability), ext2fs.ko rebuilt via make -j4 nativekernel and installed; identical PoC sequence: reload rc=0, post-reload reads return the CORRECT BLK markers (baseline returned zeros/garbage), write demo completes with ZERO live OOB prints (baseline: 15 OOB writes + ~741 OOB-read-sourced block pointers), on-disk victim file maps correctly (609->610->256 entries->0xAA data vs baseline 0/256 entries + orphaned blocks), no panic, clean unmount.", "fix_evidence": "fix_run.log (correct markers lines 14-19 & 32-43; all DF3216 lines appear only after the post-run dmesg tail marker = dmesg replay of the pre-fix run, not live); VERDICT.md fixed-image forensics paragraph" } |