# DF-2797 — VERDICT

**Status: reproduced** (kernel panic on stock INVARIANTS guest + production-kernel
arithmetic demonstrated standalone). **Impact: panic** on the audit guest
(INVARIANTS); on production (non-INVARIANTS) kernels: silent capacity theft /
permanent system hang (see below). No memory corruption, no privilege
escalation — this is a domain-validation/hardening finding (DF-2790 class,
the blist sibling of the same defect family).

## How reproduced

### 1. Kernel mode (stock INVARIANTS guest, fresh `vm.sh reset with-src`)

KLD `kld_adomain/adomain.ko` creates its **own** alist via `alist_create(65536,
M_TEMP)` (legitimate allocator unit-testing pattern; the live
`vm_contig_alist` is never touched), frees it all, then calls
`alist_alloc(bl, 0, 0)`:

```
adomain: creating own alist(65536)
adomain: bl_free=65536, calling alist_alloc(bl,0,0) -- expect KKASSERT(count) panic
panic: assertion "count" failed in alist_alloc at /usr/src/sys/kern/subr_alist.c:250
cpuid = 2
Trace: alist_alloc+0x129 <- adomain_modevent+0x92 <- module_register_init ...
```

Full capture: `panic.txt`. This proves the count==0 guard is
INVARIANTS-only — there is no release-kernel validation whatsoever.

### 2. Production-kernel arithmetic (standalone, `-DNDEBUG`, identical code)

`run.log` (decisive lines):

* **T1 silent leaf-wipe** — `alist_alloc(bl, 0, 0)` on a fully-free leaf:
  `KKASSERT(count)` gone; the power-of-2 test `(count|(count-1)) != (count<<1)-1`
  evaluates `0|0xFFFFFFFF == 0xFFFFFFFF` → TRUE-equal → count=0 treated as a
  power of 2; `alst_leaf_alloc` computes `n = 32-0 = 32`, `mask = (u32)-1 >> 32`
  (C UB; x86 `shrl %cl` folds to `>> 0` → 0xFFFFFFFF), the `j`-loop matches the
  fully-free leaf, "allocates 0 blocks" by clearing **all 32 bits**, and
  returns blk. `bl_free -= 0`. 2048 calls wipe the entire 65536-block alist
  while `bl_free` still reports 65536 free; a subsequent 1-block allocation
  returns NONE. For the vm_contig consumer this silently strands the whole
  256MB DMA reserve (bookkeeping-full, bookkeeping-lying).
* **T2 infinite loop** — count=0 on a *partially*-free leaf:
  `for (j = 0; j <= n; j += count)` with `j += 0` never advances; verified
  spinning >5s (SIGALRM kill, exit 42). In-kernel the caller holds
  `vm_contig_spin` (`vm_page.c:2808/2875`) → every later
  `vm_page_alloc_contig`/`vm_page_free_contig` on any CPU spins forever →
  permanent system hang (impact class: local DoS given any caller passing 0).
* **T3 radix wrap** — `alist_create(3000000000)`: `radix *= ALIST_META_RADIX`
  overflows `alist_blk_t` (u32) → 0 → `while (radix < blocks)` never exits
  (verified >5s spin). Same loop in `alist_init` would hang at boot. The
  header advertises "2 billion blocks" but the arithmetic caps at 2^29
  (32*16^6); every blocks value in (2^29, 2^32) wraps.
* **Off-by-one (legal-input availability bug, same family)** — top-level
  guard `count < bl->bl_radix` (`:264`) rejects `count == bl_radix`, so a
  root-leaf alist (blocks ≤ 32) can never allocate its full capacity even
  when completely free (`alist_free(bl, 0, 32)` works — asymmetric). Found by
  the differential fuzzer (STRICT failure at geometry 32; see fuzz log in
  this pack's discovery history / task_log). vm_contig (65536 blocks) is
  unaffected; tiny alists only.

## Why this is not higher severity

The sole in-kernel consumer is `vm_contig_alist` (`vm_page.c:435`) and both
parameters reaching `alist_alloc` are kernel-computed page counts
(`vm_page.c:815, 2809`); no in-tree caller passes 0 (verified by grep over
`vm_page_alloc_contig`/`kmem_alloc_contig`/`contigmalloc` call sites). The
 alist is also exported KLD API (`sys/alist.h`) — out-of-tree kernel code
gets none of the INVARIANTS protection on production kernels.

## Exploit chain

None (hardening). The strongest primitive is the T2 spin-with-lock-held:
any future/out-of-tree caller passing a rounded-to-0 size converts a driver
bug into a permanent whole-system hang.

## Fix validation

`fix.diff` applied to the guest's `/usr/src`, kernel rebuilt
(`make nativekernel KERNCONF=X86_64_GENERIC`), A/B rerun — see
`fix_validation.log` in this pack: T1 wipes drop to 0 and the 1-block alloc
succeeds; T2/T3 return instantly (no hang); the KLD now prints
`SURVIVED r=ffffffff` (clean `ALIST_BLOCK_NONE`) instead of panicking; the
production-emulation standalone shows identical results.
