{
 "finding_id": "DF-3154",
 "status": "reproduced",
 "reproduced": 1,
 "impact": "leak",
 "confidence": "certain",
 "verdict": "REPRODUCED. ext2_ei_csum_verify() (sys/vfs/ext2fs/ext2_csum.c:589) memcmps sizeof(struct ext2fs_dinode)==160 bytes against the zero template, but the inode slot is EXT2_INODE_SIZE(fs), mount-legal at 128 (ext2_vfsops.c:539-545), and ei points into a 1024-byte bread() buffer (ext2_vfsops.c:1364-1374). For any non-last inode the last 32 compared bytes belong to the NEXT inodes slot (cross-inode read); for the last inode of a block the 32 bytes lie PAST the bp->b_data allocation (kernel heap). Proven in-kernel on the stock guest kernel: two crafted metadata_csum images (bsize=1024, isize=128, all inode gens set nonzero to defeat ext2_vgets gen-0 randomization) differing ONLY in the first 32 bytes of inode 16 -- never statd -- flip an unprivileged stat of inode 15 between EIO (neighbor bytes 0xAA, memcmp!=0) and EBADF (neighbor bytes 0x00, csum error suppressed). The deciding bytes are outside the statd inodes slot. B-variant (victim last-in-block) decision comes from bytes that do not exist in the image (kernel heap; all-zero on this layout, suppression). Deterministic guard-page harness proves the [896,1056) span SIGSEGVs at buffer+1024. Impact: 32-byte kernel-heap OOB read per crafted bad-csum inode read + 1-bit all-zero oracle (EIO vs accept), post-mount unprivileged; Low severity. fix.diff (imin(isize, sizeof(dinode))) validated: A_outcome becomes variant-independent (EIO->EBADF) under the patched module.",
 "exploit_chain": "crafted ext2 image (metadata_csum, isize=128) with zeroed bad-csum victim inode reachable via dirent -> unprivileged stat -> ext2_vget -> ext2_ei2i -> ext2_ei_csum_verify -> memcmp 160B over 128B slot -> EIO/EBADF outcome decided by next-inode bytes or 32B of kernel heap past the 1024B buffer (1-bit all-zero oracle, repeatable per inode). No write primitive; no escalation.",
 "evidence": [
  "findings/poc/DF-3154/run.log: A_nonzero stat -> EIO vs A_zero stat -> EBADF (only difference: bytes of inode 16, never statd)",
  "findings/poc/DF-3154/build.log: harness slot-7 memcmp SIGSEGVs at buffer+1024 (guard page); slot-6 memcmp=170 with 0xAA next-slot head",
  "findings/poc/DF-3154/fix_run.log: patched module -> A_nonzero -> EBADF (variant-independence restored)",
  "findings/poc/DF-3154/panic.txt: stock-kernel vrele assertion panic on umount after failed vget (separate cross-file bug)",
  "findings/poc/DF-3154/craft_df3154.py: image construction incl. gen-reassignment + inode/dirent-tail csum recompute"
 ],
 "kernel_refs": [
  "sys/vfs/ext2fs/ext2_csum.c:589",
  "sys/vfs/ext2fs/ext2_csum.c:565",
  "sys/vfs/ext2fs/ext2_dinode.h:127",
  "sys/vfs/ext2fs/ext2_vfsops.c:539",
  "sys/vfs/ext2fs/ext2_vfsops.c:1364",
  "sys/vfs/ext2fs/ext2_vfsops.c:752"
 ],
 "poc_changes": "Seed PoC was conceptual only. Built from scratch: (1) discovered ext2_vget randomizes i_gen for gen==0 inodes (ext2_vfsops.c:1420) which breaks ALL metadata_csum verification vs stock e2fsprogs images (initial all-EIO red herring, root-caused via instrumented ext2fs.ko kprintfs); craft now assigns gen=0x13579BDF to all allocated inodes and recomputes every inode csum + the root dir tail csum; (2) buffer-cache staleness across vn re-attach forced one vn unit per image; (3) ext2_vgets ei2i-EIO path panics umount (vrele assertion, stock) so all mounts precede all stats; (4) harness rebuilt with fork+guard-page for the fault proof.",
 "attempts": 5,
 "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": 3600,
 "guest_dirty": 0,
 "build_cmd": "python3 craft_df3154.py /tmp/opencode/df3154 (host); cc -O2 -o harness_df3154 harness.c (guest); make in /usr/src/sys/vfs/ext2fs + kldload ./ext2fs.ko (fix validation)",
 "run_cmd": "sh /root/run_baseline.sh (guest root; stats as uid=1001 maxx); ./harness_df3154",
 "code_hash": "craft_df3154.py sha256=c8b4b59a78786d93bd75e09f9a236a788c19766a3e1470eac61cca70c3b1ea7c; harness.c sha256=c273e96e6487144eb85d50885c1b755e1a42029a8fb72ec379ecc11c3d804e39",
 "notes": "Cross-file bugs surfaced during verification (for orchestrator, NOT this finding): (1) ext2_vget ei2i-EIO error path leaves devvp ref imbalance -> stock panic assertion (count & VREF_MASK) > 0 in vrele on ANY umount after statting a bad-csum inode (backtrace findings/poc/DF-3154/panic.txt); (2) zero-inode vget SUCCESS path -> panic: unmount: dangling vnode; (3) ext2_readdir parses the metadata_csum dirent tail as an entry -> EIO on EVERY readdir of a valid Linux metadata_csum image, and ext2_dirbad -> RW-mount panic from a pristine image (ext2_lookup.c:198-203,777); (4) i_gen randomization breaks metadata_csum interop on gen-0 images (ext2_vfsops.c:1420-1424).",
 "recommended_fix": "Bound the zero-inode memcmp by the slot: imin(EXT2_INODE_SIZE(fs), sizeof(struct ext2fs_dinode)) at ext2_csum.c:589 (validated in fix.diff)",
 "fix_status": "fixed",
 "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #0 (same kernel, ext2fs.ko module rebuilt with fix.diff and kldloaded)",
 "fix_baseline_reproduced": 1,
 "fix_patched_reproduced": 0,
 "fix_verdict": "Module-level validation: stock A_nonzero=EIO/A_zero=EBADF (variant-dependent, bug signature); patched A_nonzero=EBADF/A_zero=EBADF (variant-independent) -- out-of-slot consumption eliminated. Panic-on-umount class remains (separate ext2_vget lifecycle bug).",
 "fix_evidence": [
  "findings/poc/DF-3154/fix_run.log",
  "findings/poc/DF-3154/fix.diff"
 ]
}