Hello,

This is a plain bug report (a filesystem correctness issue), not a
security advisory. OpenBSD's ext4 extent reader in
sys/ufs/ext2fs/ext2fs_extents.c validates only eh_magic on the extent
header and trusts every other on-disk field. A merely corrupt (not
even malicious) extent inode therefore gets parsed with no bounds
checks, and the result is a file that silently reads the wrong
filesystem blocks while returning success. FreeBSD -- the upstream for
this file -- added the missing validation in 2021; OpenBSD's copy
predates it.

DEMONSTRATED BEHAVIOUR (stock 7.9 GENERIC.MP amd64)

Take an ext4 file with a single extent and set its on-disk eh_entries
to 2 (leaving everything else intact). The file still "has" one real
extent, but the header now claims two, and the second extent slot is
the zero-initialised tail of the in-inode block array.
ext4_ext_binsearch() converges on that all-zero extent, so
ext4_bmapext() (ext2fs_bmap.c) computes

pos = bn - ep->e_blk + ((ep->e_start_hi << 32) | ep->e_start_lo)
    = bn -    0      +                0                 = bn

i.e. physical block == logical block. Reading the file then returns
filesystem blocks 0,1,2,... (the boot block, the SUPERBLOCK, the group
descriptors, the inode table) instead of the file's own data --
silently, with exit status 0.

I confirmed this live: reading such a file returned the ext4
superblock (magic 0xEF53 and the volume label) as the file contents,
read(2) succeeding. The file does not own block 1. FreeBSD returns EIO
for exactly this case (see below).

(The same missing validation also allows the original out-of-bounds
case: with eh_entries = 0xffff the binary search walks ~768 KB past
the ~156-byte dinode pool object. On 7.9 amd64 that over-read does not
fault -- the kernel heap is densely mapped, so it returns garbage and
the read ends in EINVAL rather than a panic -- but it is still an
out-of-bounds read, and on a different heap layout or arch it could
fault.)

WHAT IS UNVALIDATED (sys/ufs/ext2fs/ext2fs_extents.c)

ext4_ext_find_extent() checks eh_magic only, then:

ext4_ext_binsearch()/_binsearch_index() bound the search directly on
the on-disk eh_entries (eh_ecount) with no check that eh_ecount <=
eh_max, that eh_max fits the 60-byte in-inode area or one block, or
that the chosen extent's block range is valid;
the descent loop for (i = eh_depth; i != 0; --i) trusts eh_depth with
no upper bound and no cycle detection;
child/index blocks fetched via bread() are not re-checked for
eh_magic, and the leaf/index block number is passed to bread() with no
range check against fs->e2fs_bcount.

UPSTREAM ALREADY FIXED THIS

ext2fs_extents.c carries "$FreeBSD: ... 254260 2013-08-12 ... pfg $".
FreeBSD added the validation to sys/fs/ext2fs/ext2_extents.c in commit
f1d5e2c862ef (2021-12-30, "Improve extents verification logic", review
D33375, PR 259112): ext4_ext_check_header() /
ext4_validate_extent_entries() reject eh_max == 0, eh_max >
ext4_ext_max_entries(ip, depth), eh_ecount > eh_max, eh_depth too
deep, and validate each extent's physical block range (start_blk <=
first data block, or start_blk + count > fs->e2fs_bcount -> EIO).
OpenBSD's copy predates that work and was never resynced, so none of
these checks are present. Linux treats the class as security-relevant
(e.g. CVE-2026-31449, crafted eh_entries -> OOB read).

IMPACT (honest)

Local and root-gated: mount(2) is privileged on OpenBSD and there is
no unprivileged-mount path, so this is not an LPE the way it can be on
Linux. The realistic exposure is a privileged context mounting
untrusted ext-family media (removable media, forensic/backup/appliance
hosts). I rate it Low and consider it primarily a correctness bug: a
corrupt extent inode yields silent wrong-data reads (returning
filesystem metadata as file contents) and, in the large-eh_entries
case, an out-of-bounds read -- where the validated path would return
EIO. The information disclosed by the silent-wrong-read is the mounted
image's own metadata region, so the security value is limited; the
substantive issue is that OpenBSD returns wrong data with success
status where FreeBSD/Linux return an error.

REPRODUCTION (the demonstrated case)

Build an ext4 image without the 64bit/metadata_csum features (OpenBSD
won't mount those), write a small file, then corrupt only its
eh_entries:
mke2fs -F -q -b 1024 -L LEAKTEST -O extent,^64bit,^metadata_csum img 16384
dd if=/dev/zero bs=1024 count=64 | tr '\0' A > /tmp/f
debugfs -w -R "write /tmp/f target" img
debugfs -w -R "sif target block[0] 0x0002f30a" img # eh_magic=0xf30a,
eh_entries=2
debugfs -w -R "sif target block[1] 0x00000002" img # eh_max=2, eh_depth=0
Then on OpenBSD:
vnconfig vnd0 img
mount -t ext2fs -o ro /dev/vnd0c /mnt
hexdump -C /mnt/target | head # returns block 0 then the SUPERBLOCK (0xEF53 +
# "LEAKTEST"), read succeeds; NOT the file's data
(For the out-of-bounds variant, set block[0]=0xfffff30a,
block[1]=0x0000ffff: the read returns EINVAL on 7.9 amd64.)

SUGGESTED FIX

Port FreeBSD's ext4_ext_check_header()/ext4_validate_extent_entries()
(or an equivalent): on every header load (root and child), enforce
eh_ecount <= eh_max, bound eh_max by the block/inode capacity, cap
eh_depth, reject the zero/invalid extent, and range-check extent block
numbers against the filesystem size before use. Happy to provide the
test images or a userland reproducer.

Regards,
Stuart Thomas
TriageForge research lab

Reply via email to