ext2_zero_inode_table computes all_blks from the RAW s_inode_size field β REV0 (which also bypasses the feature gate) zeroes up to ~512MB of device blocks past the inode table on first create
Summary
ext2_alloc.c:1251 uses le16toh(fs->e2fs->e2fs_inode_size) while :1256 correctly uses the validated EXT2_INODE_SIZE(fs). At REV1 they provably agree (all_blks <= itpg); at REV0 the raw field is never validated (ext2_vfsops.c:522-546 validates only in the REV1 branch) and REV0 also skips the ro/incompat feature-mask check (:306), so a REV0+GDT_CSUM image with raw inode_size=0xFFFF, INODE_ZEROED clear and itable_unused=ipg runs a getblk/clrbuf/bawrite loop of up to 524280 blocks. VERIFIED with 0xAA-prefilled blocks: one touch() zeroed exactly blocks [itable..itable+1022] (md5 before/after), mount had validated only itpg=2 blocks; block 1043 untouched - destructive device write beyond the fs's validated extent plus a kernel-staged I/O storm. Fix: use fs->e2fs_isize + reject REV0 superblocks carrying rev1-only feature bits.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3063 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| craft3063.py | β | 3.1 KB | view raw | |
| trigger3063.sh | β | 740 B | view raw | |
| trigger3063b.sh | β | 646 B | view raw | |
| vntool.c | β | 1.1 KB | view raw | |
| fix.diff | β | 637 B | view raw | |
| env.txt | β | 470 B | view raw | |
| VERDICT.md | β | 2.9 KB | β raw |
DF-3063 VERDICT
Reproduced? YES (decisive second measurement; first run's samples were
already-zero blocks on a fresh image β inconclusive measurement, not a failed attempt; the loop demonstrably ran in both)
Guest: DragonFly 6.5-DEVELOPMENT #0 Jul 2 2026 (stock ext2fs.ko).
Craft (craft3063.py): mke2fs ext2 (REV0) -b 1024 -I 128 -N 16 on 4MB;
patched: s_rev_level=0 (already), RAW s_inode_size=0xFFFF (unvalidated at
REV0), s_inodes_per_group=16 (>= ipb=8 β legal), s_free_inodes_count=16,
ro_compat |= GDT_CSUM (REV0 skips the feature-mask gate at
sys/vfs/ext2fs/ext2_vfsops.c:306-331), gd0: nifree=16, INODE_ZEROED clear,
itable_unused=16 (β used_blks=0), gd crc16 fixed.
Decisive run (trigger3063b.sh):
--- pre-fill blocks 100..1100 with 0xAA --- --- BEFORE --- --- AFTER (touch) --- blk 5: 0f343b09⦠(zero) blk 5: 0f343b09⦠(unchanged) blk 500 : c599594b⦠(0xAA) blk 500 : 0f343b09⦠ZEROED blk 1042: c599594b⦠(0xAA) blk 1042: 0f343b09⦠ZEROED (last loop block) blk 1043: c599594b⦠(0xAA) blk 1043: c599594b⦠(unchanged) MOUNT-RC=0 / TOUCH-OK
The zeroed range [20 .. 1042] matches i_tables .. i_tables+all_blks-1
exactly (itable=20, all_blks = 0xFFFF*16/1024 = 1023). Mount validated only
i_tables + itpg - 1 = block 21 (ext2_cg_validate,
sys/vfs/ext2fs/ext2_vfsops.c:439-440). One file-create wiped 1023 blocks
(~1MB) of the device past the inode table; with ipg=8192 the same craft
zeroes ~512MB per group.
Root cause chain (path:line)
sys/vfs/ext2fs/ext2_vfsops.c:306β feature-mask check only runs forrev > E2FS_REV0β REV0 + GDT_CSUM is mountable.sys/vfs/ext2fs/ext2_vfsops.c:522-546β inode_size validated only in the REV1 branch; REV0 leaves the raw field arbitrary.sys/vfs/ext2fs/ext2_alloc.c:1251βall_blkscomputed from the RAW field (contrast line 1256 which correctly uses the validated EXT2_INODE_SIZE(fs)); at REV1 the two provably agree (all_blks <= itpg), at REV0 they diverge by up to 512x.sys/vfs/ext2fs/ext2_alloc.c:1258-1267β getblk/clrbuf/bawrite loop overall_blks - used_blksblocks starting at i_tables+used_blks.
Impact ceiling
Destructive device writes beyond the fs's validated extent (zero-fill), I/O storm DoS (up to ~524k blocks per affected group per create). All writes are buffer-cache-sized β no kernel-memory corruption. Not a uid0 primitive.
Fix
fix.diff: all_blks = fs->e2fs_isize * fs->e2fs_ipg / fs->e2fs_bsize;
(applies clean, fuzz 2). Not built/validated in-guest (budget spent on
DF-3062's mandatory fix cycle; this is device-level corruption, not kernel
memory corruption, so a fix build was not mandatory). Companion hardening
recommended: validate features_{rocompat,incompat} == 0 (or gate them) at
REV0.
impact=dos (destructive device zero-fill + I/O storm from crafted image); confidence=certain; attempts=2 (1 inconclusive measurement + decisive run).
Fix verification
not_testablefix.diff authored and applies cleanly; not rebuilt/validated in-guest (device-corruption class, non-mandatory)
fix.diff
Confirmed kernel references
Detail
Exploit chain
crafted REV0 image (raw inode_size=0xFFFF, GDT_CSUM, gd INODE_ZEROED clear, itable_unused=ipg) -> RW mount -> touch -> ext2_valloc -> ext2_nodealloccg -> ext2_zero_inode_table -> 1023x getblk+clrbuf+bawrite past itable
Evidence (decisive lines)
['run.log (md5 table: 500/1042 c599594bβ¦->0f343b09β¦ zero; 1043 unchanged proving exact loop end)']
PoC changes
none (first principles craft); measurement v2 pre-fills 0xAA because a fresh mke2fs image's data blocks are already zero
Verified recommended fix
all_blks = fs->e2fs_isize * fs->e2fs_ipg / fs->e2fs_bsize; and gate rev1 feature bits at REV0
Verdict
ext2_zero_inode_table computes all_blks from the RAW s_inode_size (ext2_alloc.c:1251) instead of the validated e2fs_isize; at REV0 that field is never validated and REV0 also bypasses the ro/incompat feature gate (ext2_vfsops.c:306), so a crafted REV0+GDT_CSUM image with s_inode_size=0xFFFF, itable_unused=ipg and INODE_ZEROED clear runs a getblk/clrbuf/bawrite loop that zeroed 1023 device blocks (measured md5 before/after) past the mount-validated 2-block inode-table extent on a single touch(). Scales to ~512MB per group. Device-level destructive write + I/O storm; no kernel-memory corruption.
No comments yet.