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)
PoC verification
Evidence pack
findings/poc/DF-3048 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
fixedwith 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
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.
No comments yet.