ext2_nodealloccg INODE_UNINIT: memset(b_data, 0, ipg/8 - 1) underflows size_t for mount-legal ipg < 8 β unbounded kernel-heap memset on first create
Summary
s_inodes_per_group is only validated as ipg >= ipb and ipg <= bsize*8 (ext2_vfsops.c:582-587), so bsize=4096/inode_size=1024 (ipb=4) admits ipg=4. On the first inode allocation in a group whose descriptor carries EXT2_BG_INODE_UNINIT on a GDT_CSUM/METADATA_CKSUM filesystem, ibytes = ipg/8 == 0 and memset(bp->b_data, 0, ibytes-1) receives (size_t)-1: an unbounded zero-fill of the kernel heap starting at the inode-bitmap buffer. VERIFIED: Fatal trap 12, supervisor WRITE fault, RIP memset+0xd5 (repe stosq), first attempt, from a mount-legal crafted image via unprivileged touch(). uid0 escalation judged infeasible (unbounded length, fixed value 0, inevitable fatal fault) - panic-class memory corruption, same threat bar as DF-0811. Fix validated (howmany(ipg,NBBY) + full memset): same trigger completes rc=0, no panic.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3062 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| craft3062.py | β | 3.0 KB | view raw | |
| trigger.sh | β | 592 B | view raw | |
| vntool.c | β | 1.1 KB | view raw | |
| fix.diff | β | 782 B | view raw | |
| panic.txt | β | 737 B | view raw | |
| run.log | β | 340 B | view raw | |
| run.2.log | β | 469 B | view raw | |
| fix_run.txt | β | 468 B | view raw | |
| panic.bgetvp-followup.txt | β | 612 B | view raw | |
| dmesg.console.txt | β | 13.6 KB | view raw | |
| env.txt | β | 470 B | view raw | |
| build.sh | β | 245 B | view raw | |
| run.sh | β | 462 B | view raw | |
| VERDICT.md | β | 4.0 KB | β raw |
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 β passessys/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;lsOK;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:1319ibytes = fs->e2fs_ipg / 8;β integer division; 0 for ipg β [1,7].sys/vfs/ext2fs/ext2_alloc.c:1320memset(bp->b_data, 0, ibytes - 1);βibytes - 1is 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-587checks onlyipg >= ipb(=bsize/isize)andipg <= 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:
- 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 -p0in 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).
Fix verification
fixedsame trigger on patched module: no panic, touch rc=0, guest healthy (run.2.log/fix_run.txt); memset fault eliminated
run.2.log, fix_run.txt
Confirmed kernel references
Detail
Exploit chain
crafted image (ipg=4 + INODE_UNINIT + GDT_CSUM, crc16-corrected gds) -> mount RW -> touch -> ext2_valloc -> ext2_nodealloccg -> memset(b_data,0,SIZE_MAX) -> fatal kernel page fault. Primitive fully characterized; escalation to uid0 blocked by unbounded/uncontrolled write.
Evidence (decisive lines)
['panic.txt (Fatal trap 12, RIP memset+0xd5 repe stosq, supervisor WRITE)', 'run.log (baseline: session dies at touch)', 'run.2.log + fix_run.txt (fixed module: AFTER-TRIGGER-NO-PANIC, rc=0)', 'panic.bgetvp-followup.txt (separate DF-3066 chain, not the memset)']
PoC changes
seed-less; first run reproduced. craft mirrors kernel ext2_crc16 for gd checksums.
Verified recommended fix
ibytes = howmany(fs->e2fs_ipg, NBBY); memset(bp->b_data, 0, ibytes); plus mark reserved inodes used after regenerating an uninit bitmap
Verdict
ext2_nodealloccg's EXT2_BG_INODE_UNINIT branch computes ibytes=ipg/8 and memsets ibytes-1 bytes; mount-legal ipg<8 (bsize=4096,isize=1024,ipg=4) makes that (size_t)-1, so the first create() after mounting a crafted GDT_CSUM image runs an unbounded kernel-heap zero-fill. Fatal trap 12 supervisor-write fault in memset+0xd5 (repe stosq) reproduced on first attempt from a mount-legal image; uncontrolled length/value makes a uid0 chain infeasible (inevitable fatal fault), so impact recorded as panic-class memory corruption. fix.diff (howmany(ipg,NBBY)) validated: rebuilt ext2fs.ko, same trigger completes with no panic.
No comments yet.