# DF-2798 — out-of-domain free discipline: production kernels silently poison meta bits and hand out blocks beyond bl_blocks

## What this pack contains

| artifact | what it is |
|---|---|
| `trigger_oobfree.c` | standalone trigger (`-DNDEBUG`, production emulation) of the full silent-corruption chain |
| `kld_aoobfree/` | KLD calling `alist_free(own_alist, 128, 32)` with `bl_blocks=100` in kernel mode |
| `panic.txt` | serial-console capture of the stock-kernel (INVARIANTS) panic at `subr_alist.c:286` |
| `run.log` | full standalone trigger output |
| `build.sh`, `run.sh` | exact commands |
| `fix.diff` | git-apply-able fix (validated: see VERDICT.md) |

## Build

    cc -O2 -g -DNDEBUG -I <srcroot>/sys -o trigger_oobfree trigger_oobfree.c

## Run

    ./trigger_oobfree
    # kernel-mode (root): cd kld_aoobfree && make && kldload ./aoobfree.ko

## Expected output (baseline, unfixed)

```
after alist_free(bl,128,32): bl_free=132 (capacity=100!) root bitmap=0000037f  <-- NO PANIC, bits 8..9 set (child4=11)
3x alist_alloc(bl,0,32) -> 64 (in-domain exhausted)
alist_alloc(bl,0,32) returned 128  <-- bl_blocks=100: OUT-OF-DOMAIN block number handed out
```

plus the contrast case: a *partial* out-of-domain free panics loudly at
`alst_meta_free:784` (`attempt to free block beyond limit`), demonstrating the
asymmetry — the **full-cover branch (`:745-753`) has no `bl_blocks` check at
all**, and the top-level bound (`alist_free:286`) is an INVARIANTS-only
`KKASSERT`. On a production kernel the caller's range slip is accepted
silently, the parent's meta bits for the out-of-domain child are set to
ALL-FREE (`11`), `bl_free` over-counts (132 > capacity 100), and
`alst_meta_alloc`'s direct path (`:552-568`, also unbounded) later *matches*
those poisoned bits and returns block numbers ≥ `bl_blocks`.

For the sole in-kernel consumer (`vm_contig_alist`, `vm_page.c:435`) the
returned block feeds `PHYS_TO_VM_PAGE(blk << PAGE_SHIFT)` (`vm_page.c:2835`)
— a `vm_page_t` beyond `vm_page_array`, i.e. a wild kernel pointer handed to
whichever driver asked for contiguous DMA memory. (In-tree callers are
bug-free — this is caller-bug amplification, not a directly reachable bug;
see threat model in the finding.)

`alist_create` compounds the exposure: `bl_root` is allocated **without
`M_ZERO`** (`:166`) while `alst_radix_init` deliberately over-allocates slot
space to radix-16 granularity, leaving uninitialized slots inside
`bl_rootblks` that only the `blk >= bl->bl_blocks` guards (`:581`, `:754`)
keep untouched. The randomized differential fuzz (4.4M legal ops, see
DF-2797 pack) verified those guards hold under all legal sequences
(poison-persistence check H7) — but any future off-by-one walks raw heap
memory. The `M_ZERO` fix removes that ammunition.

## Fixed behavior (see VERDICT.md)

`alist_free` bounds-check panics **unconditionally** (loud, present on
production kernels, replacing the silent path); the direct-allocation path
additionally refuses spans beyond `bl_blocks` (defense in depth); `bl_root`
is zeroed.
