# DF-0811 — VERDICT

## Verdict: REPRODUCED (panic / OOB kernel heap write). Fix VALIDATED.

## Bug summary

`ext2_get_group_number()` in `sys/vfs/ext2fs/ext2_alloc.c:855-861` divides the
block offset by `fs->e2fs_bsize` (block size in **bytes**, e.g. 4096) instead
of `fs->e2fs_bpg` (blocks per **group**, e.g. 32768). Mount enforces
`e2fs_bpg == e2fs_bsize * 8` (`ext2_vfsops.c:568`), so the divisor is wrong
by a factor of 8. The bug propagates into `ext2_block_in_group()` (line 864-868).

In `ext2_cg_block_bitmap_init()` (line 870-925), when `EXT2_BG_BLOCK_UNINIT`
is set and the filesystem has `EXT2F_ROCOMPAT_GDT_CSUM` (gate at :1004-1006),
the function calls:

```c
start = (uint64_t)cg * fs->e2fs_bpg + le32toh(fs->e2fs->e2fs_first_dblock);  /* :888 */
tmp = e2fs_gd_get_b_bitmap(&fs->e2fs_gd[cg]);                                 /* :892 */
if (!EXT2_HAS_INCOMPAT_FEATURE(fs, EXT2F_INCOMPAT_FLEX_BG) ||
    ext2_block_in_group(fs, tmp, cg))      /* BUGGY: returns TRUE for wrong group */
    setbit(bp->b_data, tmp - start);       /* :895 — WILD WRITE if tmp < start */
```

Under `EXT2F_INCOMPAT_FLEX_BG`, a group's block-bitmap pointer (`b_bitmap`)
may legitimately reside in a **different** group's block range — mount
validation (`ext2_vfsops.c:400-409`) uses **correct** bounds and accepts
this. But `ext2_block_in_group()` uses the **buggy** divisor. An attacker
crafts a group descriptor whose `b_bitmap` is physically in group 0 but
whose buggy group-number computes to the target cg (e.g. `b_bitmap=1025`
with bsize=1024: buggy `(1025-1)/1024 = 1`). Then:

- `start = cg * bpg + first_dblock` is large (e.g. 8193 for cg=1).
- `tmp - start = 1025 - 8193` wraps as `uint64_t` to `0xFFFFFFFFFFFFE400`.
- `setbit(bp->b_data, 0xFFFFFFFFFFFFE400)` indexes
  `bp->b_data[0xFFFFFFFFFFFFE400 / 8]` — a **wild out-of-bounds kernel heap
  write**.

The same buggy gate applies to the inode-bitmap (:898-900) and inode-table
(:906-908) setbit calls. `ext2_b_bitmap_validate()` (:927-974) would catch
this, but it is called **after** init (:1016 > :1006) and is **skipped under
FLEX_BG** (:934-942), so the wild write lands before any sanity check.

`setbit(a,i)` is `((a)[(i)/NBBY] |= 1<<((i)%NBBY))` (`sys/sys/param.h:390`).

## Confirmed citations

| Fact | Location |
|------|----------|
| Divisor bug: `/ e2fs_bsize` instead of `/ e2fs_bpg` | `sys/vfs/ext2fs/ext2_alloc.c:859-860` |
| `ext2_block_in_group` inherits the bug | `sys/vfs/ext2fs/ext2_alloc.c:864-868` |
| Wild setbit site (b_bitmap) | `sys/vfs/ext2fs/ext2_alloc.c:895` |
| Wild setbit site (i_bitmap) | `sys/vfs/ext2fs/ext2_alloc.c:900` |
| Wild setbit site (i_tables loop) | `sys/vfs/ext2fs/ext2_alloc.c:908` |
| Mount invariant `bpg == bsize*8` (wrong by factor 8) | `sys/vfs/ext2fs/ext2_vfsops.c:568` |
| Init gate: GDT_CSUM or METADATA_CKSUM | `sys/vfs/ext2fs/ext2_alloc.c:1004-1006` |
| BLOCK_UNINIT gate for init | `sys/vfs/ext2fs/ext2_alloc.c:876` |
| Validate called AFTER init | `sys/vfs/ext2fs/ext2_alloc.c:1016` (> :1006) |
| Validate SKIPPED under FLEX_BG | `sys/vfs/ext2fs/ext2_alloc.c:934-942` |
| `setbit(a,i)` macro = `a[i/NBBY]` | `sys/sys/param.h:390` |
| Correct divisor used elsewhere (`dtog` macro) | `sys/vfs/ext2fs/fs.h:122-123` |
| Mount validation uses CORRECT bounds (accepts crafted image) | `sys/vfs/ext2fs/ext2_vfsops.c:400-409` |
| Block allocator quadratic rehash (skips full cg → cg+1) | `sys/vfs/ext2fs/ext2_alloc.c:750-757` |
| `ext2_alloccg` returns 0 immediately if nbfree==0 | `sys/vfs/ext2fs/ext2_alloc.c:994-995` |

## Reproduction — two proofs

### 1. Deterministic harness (`harness.c`)
Transcribes `ext2_get_group_number`, `ext2_block_in_group`, and the setbit
index math **verbatim** from the kernel source. Shows:
- BUGGY `ext2_block_in_group(4096, cg=1)` returns **1** (TRUE) — because
  `(4096-0)/4096 = 1`.
- CORRECT `ext2_block_in_group(4096, cg=1)` returns **0** (FALSE) — because
  `(4096-0)/32768 = 0`.
- setbit bit_index = `4096 - 32768` = `0xFFFFFFFFFFFF9000` (uint64 wrap).
- byte_index = `0x1FFFFFFFFFFFF200` — **2305843009213690368** bytes past the
  4096-byte buffer.
- The write faults (SIGSEGV) against a guard-paged mmap — proving OOB.

### 2. In-kernel panic on #0 GENERIC (real ext2 mount)
Crafted ext2 image (`craft_img.py`): 16MB, bsize=1024, FLEX_BG + GDT_CSUM,
2 groups. Group 0's nbfree zeroed (allocator skips to cg=1). Group 1's
`b_bitmap` set to block 1025 (in group 0; buggy group = 1), BLOCK_UNINIT set,
GDT checksum recomputed. Mount + `dd` → **Fatal trap 9: general protection
fault** at `ext2_alloccg+0x811: orb %sil,(%rdx)` (the setbit instruction
dereferencing the wild address). Guest **DOWN**.

```
Fatal trap 9: general protection fault while in kernel mode
instruction pointer = 0x8:0xffffffff82602ff1
current process = 979
Stopped at ext2_alloccg+0x811:  orb %sil,(%rdx)
db>
```

## Impact ceiling

Wild kernel heap write from a **crafted ext2/ext4 filesystem image** mount +
file write. The write offset is a non-canonical x86-64 address (~2^60 bytes
past the buffer), so on the default GENERIC kernel (INVARIANTS ON) it manifests
as an immediate **panic** (trap 9 GPF). On a kernel without INVARIANTS, the
same primitive is a controllable OOB write whose target depends on the buffer's
kernel virtual address + the wrapped offset. The trigger is root-only (mount),
but the realistic threat model is an admin mounting an attacker-supplied image
(USB, VM disk, container layer, downloaded filesystem image).

## Fix (`fix.diff`)

Two changes in `sys/vfs/ext2fs/ext2_alloc.c`:
1. **Root cause**: fix the divisor `e2fs_bsize → e2fs_bpg` in
   `ext2_get_group_number()` (line 860).
2. **Defense in depth**: add a bounds check `tmp >= start && tmp - start <
   (uint64_t)fs->e2fs_bsize * NBBY` before each of the three setbit calls
   (:894, :900, :909), so even if the group-membership test is wrong, the bit
   index is clamped to the bitmap buffer size.

## Fix validation (Phase 8)

| Phase | Kernel/module | Trigger | Result |
|-------|---------------|---------|--------|
| Baseline | stock `ext2fs.ko` (#0, INVARIANTS ON) | mount crafted image + dd | **PANIC** (trap 9 at `ext2_alloccg+0x811`) |
| Patched | rebuilt `ext2fs.ko` with fix.diff | same crafted image + dd | **NO PANIC**, dd RC=0, guest alive, files created |

Clean before/after: the fix closes the bug.
