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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3062 Β· 14 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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 β†’ passes sys/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; ls OK; 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:1319 ibytes = fs->e2fs_ipg / 8; β€” integer division; 0 for ipg ∈ [1,7].
  • sys/vfs/ext2fs/ext2_alloc.c:1320 memset(bp->b_data, 0, ibytes - 1); β€” ibytes - 1 is 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-587 checks only ipg >= ipb(=bsize/isize) and ipg <= 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 -p0 in 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

fixed
baseline reproduced→ patch + rebuild →patched clean

same 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
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0 Jul 2 2026 + rebuilt /boot/kernel/ext2fs.ko (fix.diff)

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.