β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-3063

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3063 Β· 7 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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)

  1. sys/vfs/ext2fs/ext2_vfsops.c:306 β€” feature-mask check only runs for rev > E2FS_REV0 β†’ REV0 + GDT_CSUM is mountable.
  2. sys/vfs/ext2fs/ext2_vfsops.c:522-546 β€” inode_size validated only in the REV1 branch; REV0 leaves the raw field arbitrary.
  3. sys/vfs/ext2fs/ext2_alloc.c:1251 β€” all_blks computed 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.
  4. sys/vfs/ext2fs/ext2_alloc.c:1258-1267 β€” getblk/clrbuf/bawrite loop over all_blks - used_blks blocks 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_testable
baseline reproduced→ patch + rebuild →patched clean

fix.diff authored and applies cleanly; not rebuilt/validated in-guest (device-corruption class, non-mandatory)

fix.diff
↓ fix.diffper-fix-DF-3063

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.