# DF-0888 — ext2_indirtrunc bzero heap OOB write via truncate past indirect-block structural limit

## Verdict

**REPRODUCED** (kernel panic / heap OOB write) on the default GENERIC kernel
(`6.5-DEVELOPMENT #0`, INVARIANTS ON). The fix is **VALIDATED** on a single-fix
`#1` kernel: the oversized truncate is rejected with `EFBIG` and no panic.

## The bug (confirmed, line-by-line)

`ext2_truncate()` (`sys/vfs/ext2fs/ext2_inode.c:465`) guards `length` only
against `length < 0` — there is **no structural-limit check** analogous to FFS's
`if (length > fs->fs_maxfilesize) return (EFBIG)` at `ffs_inode.c:157`. The only
`e2fs_maxfilesize` check in the ext2 truncate path is at `ext2_inode.c:251`, and
it sits on the **lengthen** branch (`if (osize < length)`) — so it never fires on
a *shorten* (the case where `length < i_size`). Worse, under `HUGE_FILE`
(`ext2_vfsops.c:703`) `e2fs_maxfilesize == INT64_MAX`, so even that check is
useless for classic indirect-block inodes.

When a regular file's on-disk `i_size` claims a value in the triple-indirect
range and `ftruncate()` targets a length that also lands in / past that range,
`ext2_ind_truncate()` computes (`ext2_inode.c:310-313`):

```
lastblock       = lblkno(fs, length + bsize - 1) - 1
lastiblock[TRIPLE] = lastblock - 12 - NINDR - NINDR^2
```

For `length = 5 TiB` / 4 KiB blocks: `lastiblock[TRIPLE] = 1341127667 >
NINDR^3 = 1073741824` → the value is positive, so it is **not** normalized to
-1 at `:322-327` and is passed straight into
`ext2_indirtrunc(level=TRIPLE, lastbn=lastiblock[TRIPLE])` at `:365`.

Inside `ext2_indirtrunc` (`:135-173`):

```
factor = NINDR^2                              # :135-137
last   = lastbn / factor  = 1278              # :138-140   (> NINDR-1 = 1023)
bzero(&bap[last+1],                          # :172
      (NINDR - (last+1)) * sizeof(daddr_t))   # :173  -> (1024-1279)*4
```

`&bap[1279]` is already past the end of `bap[]` (which has 1024 entries in a
4096-byte buffer), and the `(NINDR-(last+1))*sizeof` length **underflows size_t
to ≈2^64**. The resulting `bzero` is an effectively-unbounded zero-fill past
`bp->b_data` — a kernel heap OOB write.

## Proof

### 1. Deterministic arithmetic harness (`harness.c`)
Transcribes `ext2_ind_truncate`/`ext2_indirtrunc` verbatim and prints:

```
lastiblock[TRIPLE] = 1341127667   > NINDR^3 (1073741824) ? YES -> OOB
last = 1278   > NINDR-1 (1023) ? YES -> OOB index
bzero start: &bap[1279]  (bap has 1024 entries)  start OOB ? YES
bzero len = 18446744073709550596 bytes   len underflow ? YES
== RESULT: BUG CONFIRMED -- OOB heap bzero in ext2_indirtrunc ==
```

### 2. Kernel reproduction on `#0` GENERIC
An ext2 image is crafted (`craft_img.py`) whose regular file inode has
`e2di_size = 6 TiB`, `e2di_blocks[14]` (triple-indirect) = a valid block, no
extents flag. Mounted ext2fs (root-only), `ftruncate(fd, 5 TiB)` on the file:

```
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0xfffff8007a6673fc
fault code            = supervisor write data, page not present
instruction pointer   = 0x8:0xffffffff80bca670
current process       = 984
Stopped at  memset+0xf0:  movq  %r10,(%rdi)
```

`memset+0xf0` is the kernel `bzero` (resolve to memset) faulting on the first
out-of-bounds page of the underflowed zero-fill. Guest dies in DDB. (See
`panic.txt`.)

### 3. Fix validation on `#1` patched kernel
With `fix.diff` applied, the same trigger returns cleanly:

```
[trigger] ftruncate returned rc=-1 errno=27 (File too large)
```

3/3 runs deterministic, guest stays up, no panic. (See `fix_run.log`.)

## Reachability / impact ceiling

- ext2, like UFS, is **NOT user-mountable** on DragonFly (`get_fscap` →
  `SYSCAP_RESTRICTEDROOT`; `vfs.usermount=0`). Mounting a crafted ext2 image
  requires **root**. There is therefore **no unprivileged→root (uid0) chain**
  from this bug: the privilege boundary (root→kernel) is already crossed by the
  time the vulnerable code runs. This is a **root→kernel hardening gap /
  crafted-media DoS / heap-corruption** issue, not an LPE.
- On the default GENERIC kernel (INVARIANTS ON) the OOB write immediately
  page-faults → reliable DoS (panic). The heap write itself (zeroing, attacker
  cannot control the byte value beyond "zero") is not a classic
  attacker-controlled-content corruption, so even on `noinv` the primitive is a
  large heap zero-out rather than a controlled-value overwrite. Impact is
  **panic / DoS from crafted ext2 media**, ceiling = memory corruption requiring
  root to trigger.

## Files

| file | purpose |
|---|---|
| `harness.c`     | deterministic arithmetic proof (OOB index + size_t underflow) |
| `trigger.c`     | kernel trigger: open + ftruncate(fd, 5 TiB) on the patched file |
| `craft_img.py`  | HOST-side ext2 image crafter (mke2fs + debugfs + raw inode patch) |
| `df0888.ext2`   | the crafted 8 MiB image (inode 12: size=6TiB, TIND=block 510) |
| `build.sh`/`run.sh` | exact build/run commands |
| `build.log`     | guest cc build of trigger+harness |
| `run.log`       | baseline (#0) decisive run incl. panic |
| `panic.txt`     | fatal-trap-12 page-fault signature from `boot.log` |
| `fix.diff`      | git-apply-able fix: structural-limit check in ext2_truncate + indirtrunc clamp |
| `fix_build.log` | full nativekernel build output of the patched kernel |
| `fix_run.log`   | patched-kernel (#1) run: EFBIG, 3× deterministic, no panic |
| `env.txt`       | guest uname, cc, sysctl, ext2 module, image features |
| `VERDICT.md`    | this file |
| `manifest.json` | machine-readable catalog |

## How to reproduce

On the HOST (Linux, needs `mke2fs`, `debugfs`, `python3`):
```
python3 craft_img.py df0888.ext2
scp df0888.ext2 dfbsd:/root/df0888.ext2
scp trigger.c   dfbsd-maxx:poc/DF-0888/
```
On the GUEST (build as maxx, run as root):
```
ssh dfbsd-maxx 'cd poc/DF-0888 && cc -O2 -Wall -o trigger trigger.c'
ssh dfbsd      'cp /home/maxx/poc/DF-0888/trigger /root/trigger && \
                vnconfig -c /dev/vn0 /root/df0888.ext2 && \
                mount -t ext2fs /dev/vn0 /mnt/df0888 && \
                /root/trigger /mnt/df0888/target'
```
Unpatched kernel: fatal trap 12 in `memset+0xf0`. Patched kernel: `errno=27 (EFBIG)`.
