# 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.
