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

s_inodes_count never validated against gcount*ipg β€” dirent inode numbers beyond the group-descriptor table OOB-read fs->e2fs_gd in ext2_vget

Field Value
ID DF-3048
Status new
Severity High
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:H
CWE CWE-125
File sys/vfs/ext2fs/ext2_vfsops.c
Lines 606-621 (sink :1358 via fs.h:108-113)
Area vfs/ext2fs
Confidence certain
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket memcorrupt
Reported pending
Known CVE none
CVE match novel

Summary

ext2_compute_sb_data() checks only ficount<=icount; nothing enforces s_inodes_count <= s_groups_counts_inodes_per_group. ext2_check_direntry and ext2_ei2i both bound inode numbers by the raw on-disk icount, but ext2_vget() first computes ino_to_fsba() = e2fs_gd_get_i_tables(&fs->e2fs_gd[(ino-1)/ipg]) β€” a crafted dirent with gcountipg < ino <= icount reads the gd array at an attacker-chosen 64-byte-strided offset (cg up to icount/ipg β‰ˆ 2^29). Crafted ext2 image (root mount or vfs.usermount=1); afterwards ANY unprivileged user triggers the wild read with one pathname lookup. VERIFIED: stat of a dirent with ino 0x40000002 (cg=1048576, 64MB past a 1KB gd allocation) β†’ fatal kernel page fault with RIP inside e2fs_gd_get_i_tables. The garbage i_tables read OOB also steers subsequent bread() β€” an observable read primitive. Fix validated (reject s_inodes_count > (uint64_t)gcount*ipg at mount): image rejected EINVAL, no panic, pristine mounts unaffected.

Timeline

  • 2026-09-02 Discovered during pass-2 audit of ext2_vfsops.c (GLM 5.3); wild gd read reproduced + fix validated.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3048 Β· 9 files
FileTypeDescriptionSize
craft.py β€” 3.2 KB view raw
vntool.c β€” 1.1 KB view raw
run.sh β€” 398 B view raw
build.sh β€” 248 B view raw
run.log β€” 1.1 KB view raw
panic.txt β€” 1.2 KB view raw
fix_run.txt β€” 609 B view raw
env.txt β€” 461 B view raw
verdict.json β€” 3.0 KB view raw

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

with the clamp the crafted image is rejected at mount (EINVAL 'inode count exceeds groups x inodes per group'); stat is harmless; guest healthy; identical-geometry reload and pristine RW mounts still work (regression checks in DF-3047/fix_run.txt console capture).

findings/poc/DF-3048/fix_run.txt; findings/poc/DF-3047/fix.diff (hunk 2); fix console capture findings/poc/DF-3047/fix_run.txt
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT #0 (same kernel; ext2fs.ko module rebuilt with fix.diff hunk 2, 2026-09-05)

Confirmed kernel references

Detail

Exploit chain

crafted ext2 image (icount=0x7ffffff0, dirent ino=0x40000002) -> root/usermount mount -> stat /mnt/lost+found -> ext2_lookup:809 passes (ino<=icount) -> VFS_VGET -> ext2_vget:1358 ino_to_fsba -> fs->e2fs_gd[1048576] (64MB past 1KB alloc) -> kernel page fault -> panic. Wild-read primitive: cg=(ino-1)/ipg attacker-chosen; unmapped->panic (demonstrated), mapped->garbage i_tables steers subsequent bread (not pursued to leak).

Evidence (decisive lines)

run.log (full session), panic.txt (console: RIP=e2fs_gd_get_i_tables), fix_run.txt (fixed module: mount EINVAL, guest alive)

PoC changes

seed sketch had no working image: crafted with mke2fs 1.47 (-O ^metadata_csum,^64bit, -I 128), patched s_inodes_count and the root-dir lost+found dirent inode directly; guest lacks vnconfig so vntool.c (VNIOCATTACH) was written; mount(8) needed no changes.

Verified recommended fix

reject s_inodes_count > (uint64_t)gcount * ipg in ext2_compute_sb_data()

Verdict

s_inodes_count is never validated against s_groups_counts_inodes_per_group at mount; a crafted dirent with ino in (gcountipg, icount] makes ext2_vget/ino_to_fsba index fs->e2fs_gd[(ino-1)/ipg] far out of bounds. Reproduced: fatal kernel page fault with RIP in e2fs_gd_get_i_tables from a single stat() of the patched 'lost+found' entry. Fixed module rejects the image at mount.