ext2_dirbad panic()s a writable-mounted ext2 on any mangled directory entry β one flipped byte = reboot-loopable local DoS
Summary
ext2_dirbad (:771-784) calls panic() whenever the mount is not MNT_RDONLY and a walked dirent fails ext2_check_direntry (rec_len<12, %4!=0, <REC_LEN(namlen), cross-block, ino>icount) - invoked from ext2_search_dirblock's mangled-entry path on every linear and htree lookup and the i_size-too-small fixup (:532-537). A single patched byte (rec_len 16->17) in a crafted image panics the stock kernel on the first stat/open that walks past it; the RDONLY branch plus the walker's skip-to-next-block recovery prove non-fatal handling is already implemented. Also the automatic follow-on of DF-3082 on valid images. VERIFIED guest: RW mount + stat past corrupt byte -> panic; RO mount survives identical lookups; fix validated (printf instead of panic): ENOENT + console log, no panic.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3083 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.9 KB | β raw | |
| VERDICT.md | β | 2.2 KB | β raw | |
| craft.py | β | 2.1 KB | view raw | |
| run.sh | β | 913 B | view raw | |
| fix_run.sh | β | 644 B | view raw | |
| fix.diff | β | 1.4 KB | view raw | |
| run.log | β | 164 B | view raw | |
| panic.txt | β | 770 B | view raw | |
| fix_run.log | β | 325 B | view raw | |
| verdict.json | β | 3.5 KB | view raw | |
| manifest.json | β | 967 B | view raw | |
| env.txt | β | 772 B | view raw |
DF-3083 β ext2_dirbad() panics a writable-mounted ext2 on any mangled directory entry (local DoS from a crafted image)
Summary
ext2_dirbad() (sys/vfs/ext2fs/ext2_lookup.c:771-784) reacts to a
directory-entry consistency failure with panic() whenever the mount is
not read-only:
if ((mp->mnt_flag & MNT_RDONLY) == 0)
panic("ext2_dirbad: %s: bad dir ino %ju at offset %ld: %s\n", ...);
else
SDT_PROBE4(...); /* read-only: log and continue */
It is called from the dirent walkers whenever ext2_check_direntry()
(ext2_lookup.c:794-819) rejects an on-disk entry β i.e. any of:
rec_len < EXT2_DIR_REC_LEN(1),rec_len % 4 != 0,rec_len < EXT2_DIR_REC_LEN(namelen), entry crosses block boundary,ino > s_inodes_count(ext2_lookup.c:801-810);- the "i_size too small" fixup path (ext2_lookup.c:532-537).
Call sites: ext2_search_dirblock() "mangled entry"
(ext2_lookup.c:698-706), reached from the linear searchloop
(ext2_lookup.c:444) and from ext2_htree_lookup()
(sys/vfs/ext2fs/ext2_htree.c:399). Any stat/open/ls -l whose lookup
walks past the corrupt byte panics the kernel.
A single flipped byte in a directory block (e.g. rec_len 16 β 17) turns
any writable-mounted ext2 image into an immediate kernel panic; the
read-only branch proves the non-fatal handling already exists.
Reachability
- crafted ext2 image (one corrupt dirent byte) mounted read-write
β by root, or by an unprivileged user with
vfs.usermount=1(the image may also arrive on removable media); - trigger is a plain path lookup β unprivileged, e.g.
ls -lorstatof any name at/after the corrupt entry; - also fires as the follow-on of DF-3082 on a valid image (freed leaf blocks read back zero-filled β "mangled entry" β panic).
Reproduced (baseline, stock INVARIANTS kernel #0)
mount -t ext2fs -o ro /dev/vn0 /mnt/e2 ; ls -l /mnt/e2/d # control: survives umount /mnt/e2 mount -t ext2fs /dev/vn0 /mnt/e2 # read-write stat /mnt/e2/d/file4 panic: ext2_dirbad: /mnt/e2: bad dir ino 12 at offset 56: mangled entry ext2_dirbad() at ext2_dirbad+0x32 ext2_search_dirblock() at ext2_search_dirblock+0x194 ext2_lookup() at ext2_lookup+0x1fe vop_compat_nresolve() at vop_compat_nresolve+0x82
offset 56 = the patched entry (rec_len 16β17, rec_len % 4 != 0).
Fix
fix.diff (shared with DF-3082's pack) β always log, never panic;
the walker already contains the recovery (skip to the next block,
ext2_lookup.c:702-706). Validated on a patched kernel: the RW mount
survives the same stat (printf on console instead of panic).
Contents
mangle.patch.py(embedded in craft steps) β flips one rec_len byterun.shβ guest root: RO control + RW triggerrun.log,panic.txtβ baseline evidencefix.diff,fix_run.logβ fix validationverdict.json,manifest.json,VERDICT.md
DF-3083 VERDICT
Bottom line
REPRODUCED. A single corrupted byte in a directory entry (rec_len
16 β 17, violating rec_len % 4 == 0) turns any read-write mounted
ext2 filesystem into an immediate kernel panic: one unprivileged path
lookup that walks past the entry (e.g. stat /mnt/e2/d/file4) hits
ext2_check_direntry() failure β ext2_search_dirblock() calls
ext2_dirbad() (sys/vfs/ext2fs/ext2_lookup.c:701) β panic()
(ext2_lookup.c:777-780) because the mount is not MNT_RDONLY.
The identical lookup on a read-only mount survives (the non-fatal
SDT branch), and the walker already contains the recovery
(skip to next block, ext2_lookup.c:702-706).
Baseline run (stock #0)
RO control survived (mount/ls/umount all completed); RW mount:
stat /mnt/e2/d/file4 panic: ext2_dirbad: /mnt/e2: bad dir ino 12 at offset 56: mangled entry ext2_dirbad() at ext2_dirbad+0x32 ext2_search_dirblock() at ext2_search_dirblock+0x194 ext2_lookup() at ext2_lookup+0x1fe vop_compat_nresolve() at vop_compat_nresolve+0x82
(panic.txt; offset 56 = the patched entry in dir inode 12.)
Trigger caveat (poc_changes)
The first attempt used ls -l as the trigger; the mangled entry also
breaks readdir, which EIOs before emitting names, so no lstat ever walked
into the corrupt entry. A direct stat of a name positioned after the
corrupt entry is the deterministic trigger (run.sh updated accordingly).
Fix validation (kernel #1, single-fix build shared with DF-3082)
Same image, RW mount, same lookup:
stat file4 rc=1 (ENOENT expected, NO panic) stat file1 ok ext2_dirbad: : bad dir ino 12 at offset 56: mangled entry <- console printf FIX-OK: RW mount survived mangled-entry lookup
The console line proves the walker reached the same code point and now logs instead of panicking; entries before the corrupt byte remain resolvable.
Impact
Local kernel panic (DoS) from a crafted image on a writable mount β by an
admin mounting untrusted media, or fully unprivileged with
vfs.usermount=1 (0 on this guest, but a supported configuration). Also
the automatic follow-on of DF-3082 on valid images (freed htree leaf
blocks read back zero-filled β "mangled entry" β panic), which is how
DF-3082's demonstration ended.
Fix verification
fixedSame mangled image, RW mount, same lookup on the single-fix kernel: ENOENT returned, console prints 'ext2_dirbad: : bad dir ino 12 at offset 56: mangled entry', entries before the corrupt byte still resolve (stat file1 ok), guest stays up (fix_run.log).
['fix_run.log', 'fix.diff', 'build.log.gz (in DF-3082 pack, shared build)']
Confirmed kernel references
Detail
Exploit chain
crafted image (1 flipped byte) + RW mount (root, or unpriv with vfs.usermount=1) -> stat/open/ls -l of any name at/after the corrupt entry -> ext2_search_dirblock:701 -> ext2_dirbad:778 panic -> reboot-loopable local DoS; also the automatic follow-on of DF-3082 on valid images (zero-filled freed htree leaves)
Evidence (decisive lines)
['panic.txt: panic ext2_dirbad /mnt/e2 ino 12 offset 56 via ext2_lookup<-ext2_search_dirblock<-vop_compat_nresolve', 'run.log: RO control survived; RW trigger transcript', "fix_run.log: 'stat file4 rc=1 (ENOENT expected, NO panic)' + console ext2_dirbad printf + FIX-OK", 'fix.diff (shared with DF-3082)']
PoC changes
Initial trigger used ls -l; readdir EIOs before emitting names on this image so no lstat reached the corrupt entry - replaced with a direct stat of a name positioned after the corrupt entry (deterministic). run.sh updated; craft.py in pack regenerates the image.
Verified recommended fix
replace the panic with printf+SDT in ext2_dirbad (fix.diff, validated in-guest)
Verdict
One corrupted dirent byte (rec_len 16->17, so rec_len % 4 != 0) in a crafted ext2 image, mounted read-write, panics the stock kernel on the first unprivileged path lookup that walks past it: ext2_check_direntry rejects the entry, ext2_search_dirblock calls ext2_dirbad (ext2_lookup.c:701), which panics on any non-MNT_RDONLY mount (ext2_lookup.c:777-780). The identical lookup on a read-only mount survives (control run), and the walker already skips to the next block, so the panic is pure fail-stop policy. Single-fix kernel (printf instead of panic): same lookup returns ENOENT, console shows the ext2_dirbad message, no panic.
No comments yet.