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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0876 Β· 21 files
FileTypeDescriptionSize
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
README.md readme summary + reproduce instructions
↓ download 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):

  1. sys/vfs/ext2fs/ext2_vfsops.c: extend the existing INCOMPAT_64BIT desc_size check with a non-64bit branch that rejects anything other than 0 or E2FS_REV0_GD_SIZE.
  2. sys/vfs/ext2fs/ext2_csum.c:684-686: clamp the csum read length to sizeof(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.

VERDICT.md verdict full narrative: mechanism, impact, fix validation
↓ download raw

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:

  1. Build an ext2 image with metadata_csum set and 64bit clear (mke2fs -O metadata_csum,^64bit). This activates the METADATA_CKSUM branch and skips the desc_size validation.
  2. Binary-patch the on-disk s_desc_size field (superblock offset 254) to 0xFFFF.
  3. Recompute the superblock CRC32C (Castagnoli, no inversion, over [0 .. 1020) of the superblock) so ext2_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 dmesg is 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:318 and the per-fs mount(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=0 chain 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 transcribing ext2_gd_csum verbatim. 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 with cc -O2.
  • craft_img.py β€” host-side image crafter. mke2fs -O metadata_csum,^64bit followed by binary-patching s_desc_size=0xFFFF at SB offset 254 and recomputing the superblock CRC32C. Produces ext2_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).

Two complementary changes:

  1. 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 than 0 or E2FS_REV0_GD_SIZE (= 32). Returns EINVAL for crafted images.
  2. Defense-in-depth clamp (sys/vfs/ext2fs/ext2_csum.c:684-686): clamp the calculate_crc32c length to sizeof(struct ext2_gd) - offset so 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

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED 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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 baseline + hot-swapped patched ext2fs.ko (SHA256 6dd7daa1eb89dc4e11b40a726d834a8c5b636907f687732e45cf33e5c5694c15, built from /usr/src/sys/vfs/ext2fs with gcc 8.3 in ~12s)

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.