ext2_gd_csum OOB heap read via unvalidated on-disk group descriptor size
Summary
ext2_csum.c:684-686 if(offset<le16toh(e3fs_desc_size)) calculate_crc32c(gd+offset, e3fs_desc_size-offset). e3fs_desc_size on-disk u16 UNVALIDATED when METADATA_CKSUM set without INCOMPAT_64BIT. struct ext2_gd=64B. desc_size=0xFFFF reads 65503B past 64B struct in e2fs_gd array. Mount-time ext2_gd_csum_verify (vfsops.c:688). Attacker recompute sb crc32c after edit. Panic or heap over-read.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0876 Β· 21 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | deterministic C harness transcribing ext2_gd_csum verbatim; proves OOB read length 65503 + page-boundary SIGSEGV | 11.4 KB | view raw |
| craft_img.py | trigger-source | host-side image crafter: mke2fs -O metadata_csum,^64bit + binary-patch s_desc_size=0xFFFF + recompute superblock CRC32C | 5.4 KB | view raw |
| ext2_bad.img | crafted-input | 4MB crafted ext2 image (s_desc_size=0xFFFF, metadata_csum on, 64bit off) | 4.0 MB | β download |
| ext2_bad_tiny.img | crafted-input | 1MB crafted ext2 image variant | 1.0 MB | β download |
| build.sh | build-script | cc -O2 -o harness harness.c | 360 B | view raw |
| run.sh | run-script | ./harness; documents in-kernel reproduce steps | 1.1 KB | view raw |
| build.log | build-log | full harness compile output | 321 B | view raw |
| run.log | run-log | full harness run output (CASE A/B/C/D + SIGSEGV) | 931 B | view raw |
| run.baseline.raw.log | run-log | baseline (unpatched ext2fs.ko) in-kernel reproduction: EIO + csum-diff info leak, 3 runs | 737 B | view raw |
| fix_run.log | run-log | patched ext2fs.ko in-kernel result: EINVAL on bad image (no OOB read), 3 runs | 774 B | view raw |
| fix_run.raw.log | run-log | raw patched run capture | 774 B | view raw |
| fix_build.log | build-log | full patched ext2fs.ko module build output (rc=0) | 21.2 KB | view raw |
| panic.txt | panic-signature | no kernel panic observed; the OOB read leaks silently via csum diff on this heap layout; harness demonstrates the page-fault path deterministically | 1.8 KB | view raw |
| fix.diff | suggested-fix | git-apply-able two-hunk fix: vfsops.c mount-time desc_size validation + ext2_csum.c defense-in-depth clamp | 2.6 KB | view raw |
| env.txt | environment | guest uname, compiler version, kernel build | 580 B | view raw |
| env.raw.txt | environment | raw env capture | 517 B | view raw |
| VERDICT.md | verdict | full narrative: mechanism, impact, fix validation | 11.7 KB | β raw |
| README.md | readme | summary + reproduce instructions | 3.5 KB | β raw |
| manifest.json | manifest | this file | 4.1 KB | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0876 β ext2_gd_csum OOB heap read via unvalidated group descriptor size
Severity: High (CWE-125 OOB Read)
File: sys/vfs/ext2fs/ext2_csum.c:684-686
Reproduction: deterministic C harness + crafted ext2 image (root mount)
Summary
ext2_gd_csum() reads e3fs_desc_size - offset bytes from gd + offset
when computing a group descriptor checksum. e3fs_desc_size is the on-disk
s_desc_size u16 taken straight from the attacker-controlled superblock,
without validation for filesystems that have METADATA_CKSUM set
without INCOMPAT_64BIT. With s_desc_size = 0xFFFF and offset = 32,
the read length is 65503 bytes β 65471 bytes past the 64-byte
struct ext2_gd in the e2fs_gd slab allocation.
This is a heap over-read. Reachable from any crafted ext2 image at mount
time (ext2_gd_csum_verify, called from ext2_compute_sb_data).
Root-only mount (SYSCAP_RESTRICTEDROOT), so not a local-privesc
vector β the realistic threat is root mounting attacker-supplied media
(USB image, downloaded filesystem image) for DoS / info leak.
Reproduce
1) Deterministic C harness (no kernel required)
./build.sh # cc -O2 -o harness harness.c ./run.sh # demonstrates OOB length 65503 + page-boundary SIGSEGV
Expected (run.log):
[C] desc_size=0xFFFF (attacker-controlled, METADATA_CKSUM only):
csum=0x2c9a read length = 65503 bytes
OOB read: gd+32 .. gd+65535 (length 65503)
struct ext2_gd ends at gd+64
-> read extends 65471 bytes PAST the 64-byte struct ext2_gd.
[D] ...
[!] SIGSEGV caught during csum read at addr 0x0000000800474000
HARNESS_RC=133
2) In-kernel (root required)
Craft the image (host with mke2fs):
python3 craft_img.py ext2_bad.img 4096 scp ext2_bad.img dfbsd:/root/poc/DF-0876/
On the guest (root):
kldload ext2fs vnconfig -c vn0 /root/poc/DF-0876/ext2_bad.img mkdir -p /mnt/t1 mount_ext2fs /dev/vn0 /mnt/t1 # -> "Input/output error" dmesg | tail -2 # expect: WARNING: mount of vn0 denied due bad gd=0 csum=0x????, expected=0x???? - run fsck
The expected=0x???? value incorporates the 65503 leaked bytes; it varies
across mounts as heap layout changes (info-leak signature).
Fix
fix.diff applies two changes (both required for defense-in-depth):
sys/vfs/ext2fs/ext2_vfsops.c: extend the existing INCOMPAT_64BIT desc_size check with a non-64bit branch that rejects anything other than0orE2FS_REV0_GD_SIZE.sys/vfs/ext2fs/ext2_csum.c:684-686: clamp the csum read length tosizeof(struct ext2_gd) - offset.
Validated by hot-swapping a patched ext2fs.ko (see VERDICT.md Β§ "Fix
validation"): bad image now returns EINVAL at mount (no OOB read, no
csum leak); legitimate images mount identically to the unpatched module.
Files
| File | Purpose |
|---|---|
harness.c |
deterministic C harness (transcribes ext2_gd_csum) |
craft_img.py |
host-side image crafter (mke2fs + patch s_desc_size + recompute SB crc32c) |
ext2_bad.img |
crafted 4MB ext2 image, s_desc_size=0xFFFF |
ext2_bad_tiny.img |
crafted 1MB variant |
build.sh/run.sh |
exact reproduce commands |
fix.diff |
git-apply-able two-hunk fix |
VERDICT.md |
full narrative + fix validation |
manifest.json |
artifact catalog |
run.log, build.log, fix_run.log, fix_build.log, panic.txt, env.txt |
full untrimmed logs |
See VERDICT.md for the complete root-cause analysis, impact ceiling, and
fix-validation evidence.
DF-0876 β ext2_gd_csum OOB heap read via unvalidated on-disk group descriptor size
Verdict
REPRODUCED. The bug is real and the OOB read of 65503 bytes past the
64-byte struct ext2_gd is provable both statically and dynamically. The
fix.diff validates e3fs_desc_size at mount and clamps the csum read length,
and is VALIDATED by hot-swapping a patched ext2fs.ko and confirming
the bad image is now rejected at mount with EINVAL (no OOB read, no csum
side-channel leak).
Mechanism (trigger β primitive β effect)
The ext2_gd_csum() function computes a CRC32C over a group descriptor:
/* sys/vfs/ext2fs/ext2_csum.c:666-704 */
static uint16_t
ext2_gd_csum(struct m_ext2fs *fs, uint32_t block_group, struct ext2_gd *gd)
{
size_t offset;
uint32_t csum32;
uint16_t crc, dummy_csum;
offset = offsetof(struct ext2_gd, ext4bgd_csum); /* = 30 */
block_group = htole32(block_group);
if (EXT2_HAS_RO_COMPAT_FEATURE(fs, EXT2F_ROCOMPAT_METADATA_CKSUM)) {
csum32 = calculate_crc32c(fs->e2fs_csum_seed,
(uint8_t *)&block_group, sizeof(block_group));
csum32 = calculate_crc32c(csum32, (uint8_t *)gd, offset);
dummy_csum = 0;
csum32 = calculate_crc32c(csum32, (uint8_t *)&dummy_csum,
sizeof(dummy_csum));
offset += sizeof(dummy_csum); /* = 32 */
if (offset < le16toh(fs->e2fs->e3fs_desc_size))
csum32 = calculate_crc32c(csum32, (uint8_t *)gd + offset,
le16toh(fs->e2fs->e3fs_desc_size) - offset); /* <-- BUG */
...
fs->e2fs->e3fs_desc_size is the on-disk s_desc_size u16, read straight
from the superblock of the to-be-mounted image. With
offset = 32 and e3fs_desc_size = 0xFFFF = 65535, the calculate_crc32c
call reads 65535 - 32 = 65503 bytes starting at gd + 32.
The in-memory gd is one element of the fs->e2fs_gd[] array
(sys/vfs/ext2fs/ext2fs.h:181). struct ext2_gd is exactly 64 bytes
(verified: sizeof(struct ext2_gd) = 64, offsetof(ext4bgd_csum) = 30).
The 65503-byte read walks past the 64-byte struct into adjacent heap.
Mount-time reachability
ext2_gd_csum() is reached from ext2_gd_csum_verify()
(sys/vfs/ext2fs/ext2_csum.c:708), which is called by
ext2_compute_sb_data() (sys/vfs/ext2fs/ext2_vfsops.c:688) for any
filesystem with EXT2F_ROCOMPAT_METADATA_CKSUM set. So any crafted image
with metadata_csum reaches the bug at mount.
Why the size is unvalidated
The only e3fs_desc_size validation in ext2_compute_sb_data is at
sys/vfs/ext2fs/ext2_vfsops.c:549-554:
if (EXT2_HAS_INCOMPAT_FEATURE(fs, EXT2F_INCOMPAT_64BIT) &&
le16toh(es->e3fs_desc_size) != E2FS_64BIT_GD_SIZE) {
SDT_PROBE1(ext2fs, , vfsops, ext2_compute_sb_data_error,
"unsupported 64bit descriptor size");
return (EINVAL);
}
This only fires when INCOMPAT_64BIT is set. For filesystems with
METADATA_CKSUM but not INCOMPAT_64BIT, e3fs_desc_size is taken
directly from disk with no bounds check β and ext2_gd_csum's METADATA_CKSUM
branch reads it unconditionally.
The ext2_gd_csum() GDT_CSUM branch immediately below is symmetric but
correctly gated behind INCOMPAT_64BIT:
} else if (EXT2_HAS_RO_COMPAT_FEATURE(fs, EXT2F_ROCOMPAT_GDT_CSUM)) {
...
if (EXT2_HAS_INCOMPAT_FEATURE(fs, EXT2F_INCOMPAT_64BIT) && /* <-- gated */
offset < le16toh(fs->e2fs->e3fs_desc_size))
crc = ext2_crc16(crc, ...);
The METADATA_CKSUM branch lacks that gate. That is the bug.
Image-crafting attacker model
Attacker controls an ext2 filesystem image. To trigger the OOB read they:
- Build an ext2 image with
metadata_csumset and64bitclear (mke2fs -O metadata_csum,^64bit). This activates the METADATA_CKSUM branch and skips the desc_size validation. - Binary-patch the on-disk
s_desc_sizefield (superblock offset 254) to0xFFFF. - Recompute the superblock CRC32C (Castagnoli, no inversion, over
[0 .. 1020)of the superblock) soext2_sb_csum_verify(ext2_csum.c:87) accepts the patched superblock.
craft_img.py performs all three steps. The resulting ext2_bad.img mounts
just far enough to reach ext2_gd_csum_verify and trigger the OOB read.
Reproduction
A) Deterministic C harness (no kernel required)
harness.c transcribes ext2_gd_csum verbatim and runs it against a
64-byte struct ext2_gd placed at the end of a page, with the next page
PROT_NONE. Three cases:
| Case | desc_size | Result |
|---|---|---|
| A | 0 | read length 0 (legitimate rev0) |
| B | 64 | read length 32 (legitimate 64bit, 32..64 within GD) |
| C | 0xFFFF | read length 65503 β 65471 bytes past the 64-byte struct |
| D | 0xFFFF | full read faults at predicted page boundary (SIGSEGV) |
Output (run.log):
[*] sizeof(struct ext2_gd) = 64 bytes
[*] offsetof(ext4bgd_csum) = 30 bytes
[A] desc_size=0 (legitimate rev0 GD):
csum=0x8cb0 OOB read length = 0 bytes (expected 0)
[B] desc_size=64 (legitimate 64bit GD):
csum=0x2c9a read length = 32 bytes (32..64 within GD)
[C] desc_size=0xFFFF (attacker-controlled, METADATA_CKSUM only):
csum=0x2c9a read length = 65503 bytes
OOB read: gd+32 .. gd+65535 (length 65503)
struct ext2_gd ends at gd+64
-> read extends 65471 bytes PAST the 64-byte struct ext2_gd.
[D] Invoking ext2_gd_csum_faulting (full ext2_csum.c:684-686 read):
GD at 0x800473fc0 (end of page 1); next page PROT_NONE at 0x800474000
Expecting SIGSEGV at 0x800474000 (= gd+64 = start of PROT_NONE page).
[!] SIGSEGV caught during csum read at addr 0x0000000800474000
HARNESS_RC=133
B) In-kernel manifestation (root only β see privilege note)
Mount the crafted image on the default 6.5-DEVELOPMENT #0 GENERIC kernel:
# kldload ext2fs # vnconfig -c vn0 /root/poc/DF-0876/ext2_bad.img # mount_ext2fs /dev/vn0 /mnt/t1 mount_ext2fs: /dev/vn0: Input/output error # EIO from csum verify # dmesg | tail -2 vn0: MBR magic not found; ... WARNING: mount of vn0 denied due bad gd=0 csum=0x8300, expected=0x4e72 - run fsck
The expected=0x???? value is the kernel-computed csum incorporating the
65503 bytes that were read (32 from the GD + 65471 from adjacent heap). It
varies across mounts as heap layout changes
(0xcc5e / 0x3a24 / 0xb94 / 0x3d5b / 0x4e72 / 0x72a observed), proving the
read incorporates varying adjacent heap content. This is the info-leak
manifestation: 16 bits of noisy heap-state summary per group descriptor.
No kernel panic was observed on the test slab layout (the read walks through adjacent mapped slab pages). On a different heap layout β or with a larger read β the read would cross an unmapped page and page-fault (panic). Both outcomes are valid manifestations; the bug class is heap over-read / OOB.
Impact ceiling
- Class: OOB heap read (CWE-125). Read-only primitive β there is no write, no UAF, no type confusion. No escalation chain is possible.
- Info leak: the 16-bit csum diff in
dmesgis a noisy summary of adjacent heap state. Practically hard to weaponize for KASLR-defeat on this kernel (KASLR is already OFF in the audit guest anyway), but a real disclosure channel. - Possible panic: heap-layout-dependent; on production kernels with adjacent unmapped pages, the OOB read page-faults and DoSes.
- Privilege boundary: ext2 mount requires
SYSCAP_RESTRICTEDROOT(sys/kern/vfs_syscalls.c:318and the per-fsmount(2)path), i.e. root only. An unprivileged user cannot mount a crafted ext2 image, so this is not a local-privesc vector. The realistic threat is root mounting attacker-supplied media (USB, downloaded image, mount-on-connect appliance), where the bug yields DoS / info leak. - No escalation attempted: read-only primitive β no
uid=0chain to develop. This is a valid hard blocker per the procedure's Phase 6 list ("the primitive is genuinely read-only").
PoC changes
Built the entire evidence pack from scratch:
harness.cβ deterministic C harness transcribingext2_gd_csumverbatim. Three cases (legitimate rev0, legitimate 64bit, attacker 0xFFFF) plus a faulting variant that proves the read crosses a page boundary at the predicted address. Built withcc -O2.craft_img.pyβ host-side image crafter.mke2fs -O metadata_csum,^64bitfollowed by binary-patchings_desc_size=0xFFFFat SB offset 254 and recomputing the superblock CRC32C. Producesext2_bad.img.build.sh/run.shβ exact reproduce commands.fix.diffβ git-apply-able two-hunk fix (see below).ext2_bad.img,ext2_bad_tiny.imgβ crafted images (different geometries, same exploit).
Recommended fix (fix.diff)
Two complementary changes:
- Mount-time validation (
sys/vfs/ext2fs/ext2_vfsops.c, the existing "Check group descriptors" block): extend the existing INCOMPAT_64BIT desc_size check with a non-64bit branch that rejects anything other than0orE2FS_REV0_GD_SIZE(= 32). ReturnsEINVALfor crafted images. - Defense-in-depth clamp (
sys/vfs/ext2fs/ext2_csum.c:684-686): clamp thecalculate_crc32clength tosizeof(struct ext2_gd) - offsetso any caller that does reach the loop with an unchecked desc_size cannot read past the in-memory struct.
This supersedes the finding markdown's initial proposal (which suggested
either approach in isolation): doing both is correct because (1) is the
user-visible behavior fix (reject bad images cleanly with EINVAL) and (2)
guarantees that no future code path that re-introduces an unchecked
desc_size can resurrect the OOB read.
Fix validation (Phase 8)
Built a single-fix ext2fs.ko module by applying fix.diff to the
in-guest /usr/src and running make in sys/vfs/ext2fs/ (~12 s, gcc 8.3).
Hot-swapped the patched module via kldunload ext2fs && kldload <patched.ko>.
| Test | Unpatched ext2fs.ko |
Patched ext2fs.ko |
|---|---|---|
Bad image (s_desc_size=0xFFFF) |
Input/output error (EIO) + dmesg: WARNING: mount ... csum=0x8300, expected=0x4e72 (OOB read happened) |
Invalid argument (EINVAL) + no csum verify message (rejected at mount) |
Legitimate image (s_desc_size=0) |
mounts OK (RW writes have a pre-existing unrelated EIO) | mounts OK (same behavior β no regression) |
Patch determinism: 3/3 patched runs return EINVAL with no csum verify
message. The pre-existing ext2_mountfs: trying to free NULL pointer
warnings on the EINVAL cleanup path are not introduced by the fix β
they fire on any early ext2_compute_sb_data failure (including the
pre-existing INCOMPAT_64BIT+bad-desc_size case) because the out: cleanup
unconditionally calls free(... e2fs_gd ...).
Patched module SHA256: 6dd7daa1eb89dc4e11b40a726d834a8c5b636907f687732e45cf33e5c5694c15
Unpatched module SHA256: 497134c238ec6f4b42bd04f9c49d658e3698dc6f4ba7948bc68a1f48eedf29fb
fix_status: fixed (bad behavior gone on patched module, present on baseline,
legitimate-image regression test clean).
Kernel references
sys/vfs/ext2fs/ext2_csum.c:666-704βext2_gd_csum(the bug)sys/vfs/ext2fs/ext2_csum.c:684-686β the unchecked readsys/vfs/ext2fs/ext2_csum.c:698-700β the symmetric but correctly-gated GDT_CSUM branchsys/vfs/ext2fs/ext2_csum.c:708-726βext2_gd_csum_verify(the mount-time caller)sys/vfs/ext2fs/ext2_vfsops.c:548-554β the existing (insufficient) desc_size validationsys/vfs/ext2fs/ext2_vfsops.c:688βext2_gd_csum_verifycall sitesys/vfs/ext2fs/ext2_vfsops.c:647βe2fs_gdallocationsys/vfs/ext2fs/ext2fs.h:372-396βstruct ext2_gddefinitionsys/vfs/ext2fs/ext2fs.h:94βe3fs_desc_sizefieldsys/kern/vfs_syscalls.c:318βSYSCAP_RESTRICTEDROOTmount check (root-only boundary)
Fix verification
fixedVALIDATED the fix. Baseline (unpatched ext2fs.ko, sha256 497134...) + ext2_bad.img: mount_ext2fs returns 'Input/output error' (EIO) AND dmesg prints 'WARNING: mount of vn0 denied due bad gd=0 csum=0x8300, expected=0x????' -- the 'expected' value varies across mounts (0xcc5e/0x3a24/0xb94/0x3d5b/0x4e72/0x72a) because the 65503-byte OOB read incorporates varying adjacent heap content. Patched ext2fs.ko (sha256 6dd7da...) + same ext2_bad.img: mount_ext2fs returns 'Invalid argument' (EINVAL) and NO csum-verify message appears in dmesg -- the bad s_desc_size is now rejected at mount (ext2_compute_sb_data) before ext2_gd_csum_verify is ever called, so the OOB read does not happen. Deterministic across 3/3 runs. Legitimate-image regression test: a freshly-mke2fs'd image with s_desc_size=0 mounts successfully on both patched and unpatched modules (mount_rc=0) -- no regression introduced by the fix. (Note: the 'ext2_mountfs: trying to free NULL pointer' warnings on the patched EINVAL cleanup path are PRE-EXISTING -- they fire on any early ext2_compute_sb_data failure because the out: cleanup at line 1011 unconditionally frees fs->e2fs_gd, which is NULL if the failure happened before allocation; not introduced by this fix.) => fix closes the bug.
BASELINE (unpatched ext2fs.ko + ext2_bad.img): mount_ext2fs: /dev/vn0: Input/output error / mount_rc=71 / WARNING: mount of vn0 denied due bad gd=0 csum=0x8300, expected=0x4e72 - run fsck (3/3 runs reproduce; 'expected' varies 0xcc5e/0x3a24/0xb94/0x3d5b/0x4e72/0x72a across heap layouts -- info-leak signature of the OOB read). PATCHED (fix.diff applied, hot-swapped ext2fs.ko + same ext2_bad.img): mount_ext2fs: /dev/vn0: Invalid argument / mount_rc=71 / (NO csum-verify message; bad s_desc_size rejected at ext2_compute_sb_data -- OOB read never executes) / 3/3 runs deterministic. REGRESSION (patched ext2fs.ko + legitimate ext2_ok.img with s_desc_size=0): mount_rc=0 /dev/vn0 on /mnt/t1 (ext2fs, local) -- mounts identically to unpatched module.
Confirmed kernel references
Detail
Exploit chain
Read-only primitive -- no escalation chain developed or attempted. The bug is a heap OVER-READ (CWE-125), not a write: ext2_gd_csum calls calculate_crc32c over a buffer that extends past the 64-byte struct ext2_gd, but only reads (never writes). The only attacker-observable output is the 16-bit 'expected' csum value printed to dmesg, which is a noisy summary of 65503 bytes of adjacent heap state (info leak class). Per Phase 6's valid hard blockers, 'the primitive is genuinely read-only' is a documented stop point with no chain to develop. Additionally, ext2 mount requires SYSCAP_RESTRICTEDROOT (sys/kern/vfs_syscalls.c:318) so an unprivileged user cannot trigger this -- the realistic threat is root mounting attacker-supplied media (USB / downloaded image) for DoS/info-leak, not local privesc. The harness (harness.c CASE C/D) and the in-kernel csum-diff leak (run.baseline.raw.log) are the deliverables.
Evidence (decisive lines)
Harness run.log: [*] sizeof(struct ext2_gd)=64 bytes / [*] offsetof(ext4bgd_csum)=30 bytes / [C] desc_size=0xFFFF: csum=0x2c9a read length=65503 bytes / OOB read: gd+32 .. gd+65535 (length 65503) / struct ext2_gd ends at gd+64 / -> read extends 65471 bytes PAST the 64-byte struct ext2_gd. / [!] SIGSEGV caught during csum read at addr 0x0000000800474000 / HARNESS_RC=133. In-kernel baseline (unpatched ext2fs.ko + ext2_bad.img), 3 mounts: WARNING: mount of vn0 denied due bad gd=0 csum=0x8300, expected=0x4e72 - run fsck (csum 'expected' varies across mounts: 0xcc5e/0x3a24/0xb94/0x3d5b/0x4e72/0x72a -- info-leak signature).
PoC changes
Built the entire findings/poc/DF-0876/ evidence pack from scratch. harness.c -- deterministic C harness transcribing ext2_gd_csum (ext2_csum.c:666-704) verbatim with a Castagnoli CRC32C implementation matching libkern/icrc32.c; three cases (desc_size=0/64/0xFFFF) plus a faulting variant that runs the full ext2_csum.c:684-686 read against a poison-padded PROT_NONE buffer to demonstrate the page-fault at the predicted address. Iterated on harness compile (added sys/endian.h for htole32) and on observable-side-effect (made csum32 volatile + returned it so the compiler doesn't elide the unused OOB read). craft_img.py -- host-side image crafter using mke2fs -O metadata_csum,^64bit followed by binary-patch of s_desc_size at SB offset 254 (verified via /tmp/sb_off.c on guest) and CRC32C recompute over SB [0..1020).
Verified recommended fix
fix.diff applies two complementary changes. (1) sys/vfs/ext2fs/ext2_vfsops.c (the 'Check group descriptors' block at line 548): extend the existing INCOMPAT_64BIT desc_size check with a non-64bit branch that rejects s_desc_size values other than 0 or E2FS_REV0_GD_SIZE, returning EINVAL -- this is the user-facing fix that rejects crafted images cleanly at mount. (2) sys/vfs/ext2fs/ext2_csum.c:684-686: clamp the calculate_crc32c length to sizeof(struct ext2_gd) - offset so any future caller that reaches the loop with an unchecked desc_size cannot read past the in-memory struct -- defense in depth. Supersedes the finding markdown's initial proposal (which suggested either approach in isolation): doing both is correct. The full git-apply-able diff lives in findings/poc/DF-0876/fix.diff.
Verdict
REPRODUCED. The bug is real: ext2_gd_csum (sys/vfs/ext2fs/ext2_csum.c:684-686) computes calculate_crc32c(csum32, gd + 32, le16toh(fs->e2fs->e3fs_desc_size) - 32) in the METADATA_CKSUM branch, and e3fs_desc_size is taken straight from the on-disk superblock WITHOUT validation when INCOMPAT_64BIT is clear (the only validation, at ext2_vfsops.c:549, fires only when INCOMPAT_64BIT is set). sizeof(struct ext2_gd)=64, offsetof(ext4bgd_csum)=30. With s_desc_size=0xFFFF the read length is 0xFFFF-32=65503 bytes from gd+32, walking 65471 bytes past the 64-byte struct ext2_gd in the e2fs_gd slab allocation (allocated at ext2_vfsops.c:647). The deterministic C harness transcribes ext2_gd_csum verbatim and proves the read length is exactly 65503 bytes plus SIGSEGVs at the predicted page boundary when the read is actually performed against a poison-padded buffer. The in-kernel manifestation is reproducible from a crafted ext2 image (craft_img.py: mke2fs -O metadata_csum,^64bit, binary-patch s_desc_size=0xFFFF at SB offset 254, recompute superblock CRC32C so ext2_sb_csum_verify accepts it): mount_ext2fs returns EIO and dmesg prints 'WARNING: mount ... csum=0x????, expected=0x????' where the 'expected' value incorporates the 65503 leaked bytes and VARIES across mounts (0xcc5e / 0x3a24 / 0xb94 / 0x3d5b / 0x4e72 / 0x72a observed) -- proving the read really happens and incorporates varying adjacent heap content. No kernel panic was observed on this slab layout (the read walked through adjacent mapped slab pages), but the harness demonstrates the page-fault path deterministically.
No comments yet.