# DF-2915 VERDICT — REPRODUCED (kernel panic on first use; DoS on production kernels)

**status: reproduced / impact: panic (INVARIANTS) + unbounded-allocation livelock
(production) / confidence: certain**

## Root cause (source)

`sys/kern/subr_sleepqueue.c:145-146` creates the wc cache:

```c
sleepq_wc_cache = objcache_create_simple(M_SLEEPQ, sizeof(struct sleepqueue_wchan));
```

`objcache_create_simple()` (sys/kern/kern_objcache.c:378-391) registers
`objcache_malloc_alloc` as the backing allocator with **no ctor**.
`objcache_malloc_alloc()` (kern_objcache.c:571-577) does
`kmalloc(size, mtype, ocflags & OC_MFLAGS)` — **no M_ZERO** (the zeroing
sibling `objcache_malloc_alloc_zero` at :582-589 is never used here).

`sleepq_lock()` (subr_sleepqueue.c:208-209) then *assumes* zeroed memory:

```c
wc = objcache_get(sleepq_wc_cache, M_WAITOK);
KKASSERT(wc->wc_wchan == NULL && wc->wc_refs == 0);
```

The assumption holds only for objects that previously round-tripped
through the release paths (which reset `wc_wchan`/`wc_type` themselves).
It is FALSE for every **freshly kmalloc'd** object — and the first use of
the API on any hash chain always takes that path (chains start empty,
init_sleepqueue :139-142).

## Runtime evidence

1. **Stock kernel root-cause probe** (`run.log`, `sqprobe.c`): polluted
   the M_SLEEPQ zone with 0xAA-filled 48-byte chunks (spray + free), then
   built a cache exactly like `sleepq_wc_cache` and pulled objects:

   ```
   SQDEMO probe: 256/256 objcache_get() objects violate the KKASSERT
   precondition at subr_sleepqueue.c:209 (uninitialized
   wc_wchan/wc_refs/wc_blocked)
   ```

   256/256 — not luck; the allocator hands back recycled garbage.

2. **End-to-end panic** (`run.2.log` sequence, `panic.txt`): on a kernel
   with only the (known, separate) DF-0139 hash bug masked
   (`test_df0139_mask.diff`, test-only — without it `sleepq_lock()`
   GPFs inside `lock xaddl %edx,sleepq_chains(%r13)` first, see
   `panic_df0139_stockkernel.txt`), loading `sqe2e.ko` executes the real
   API sequence `sleepq_lock → sleepq_add → sleepq_wait` for the first
   time on a chain, after polluting M_SLEEPQ with 0xAA:

   ```
   SQDEMO e2e: M_SLEEPQ polluted with 0xAA, running sleepq lock/add/wait round trip
   panic: assertion "wc->wc_wchan == NULL && wc->wc_refs == 0" failed in sleepq_lock at /usr/src/sys/kern/subr_sleepqueue.c:209
   sq_sleeper() at sq_sleeper+0x10
   ```

   Guest dead at `db>` — kernel panic from the very first API call.

3. **Production-kernel manifestation (no INVARIANTS)**: the KKASSERT is
   compiled out, so the garbage wc is linked into `sc_wchead`
   (subr_sleepqueue.c:212-213) with `wc_wchan != NULL`.  It can then
   never be found as the wchan (needs pointer equality) **nor** as a free
   slot (needs `wc_wchan == NULL`), so the `for(;;)` loop (:185-215)
   re-searches, finds neither, unlocks and allocates *another* wc —
   repeat — unbounded M_SLEEPQ growth / live-lock of the calling thread
   until a coincidentally-zero chunk happens to come back; every garbage
   entry is permanently stuck in the chain and `sc_free_count` is
   inflated past `SLEEPQ_FREEPERSLOT`, which also disables the
   free-entry recycling in `_sleepq_wait_complete` (:359-371).
   (Not executed: would require a non-INVARIANTS build; the mechanism is
   fully determined by the source — a garbage `wc_wchan != NULL` simply
   fails both probes of the re-search loop.)

## Fix validation

`fix.diff` (`bzero(wc, sizeof(*wc))` after `objcache_get`):

- kernel #1 (mask only): `kldload sqe2e.ko` → **panic at :209** (above).
- kernel #2 (mask + fix.diff): same `kldload sqe2e.ko`, same 0xAA
  pollution → **no panic**; module proceeds through `sleepq_lock` /
  `sleepq_add` / `sleepq_wait` / `sleepq_broadcast` and completes
  (`DF-2917/run.log`: `kldload-rc=0`, round trips finish, guest stays
  up).

**fix_status: fixed** — the exact trigger that panicked the baseline is
clean on the patched kernel.

## Reachability / threat

No in-tree callers exist (the file is compiled by sys/conf/files:1453 but
`sleepq_*` is referenced only by its own header).  The API exists for
FreeBSD-compat / Linux-KPI kld modules (comment at :38), so the trigger
is loading any consumer module — a root-only action (kldload needs
privilege).  Not reachable by unprivileged users; severity Medium
(kernel panic + allocation live-lock from the first use of a shipped
kernel API).
