# DF-0019 — VERDICT

## Verdict: REPRODUCED (panic), FIXED (validated)

`usched_bsd4.queue_checks` accepts `<=0`, causing a NULL-deref / KASSERT panic in `bsd4_chooseproc_locked_cache_coherent()`. Root-only self-DoS. No escalation path (DoS only). The fix (reject `< 1` in a custom sysctl handler + defensive clamp in the consumer) was built into a single-fix kernel and validated: the patched kernel rejects `queue_checks=0` with `EINVAL` and does not panic; the unpatched `#0` kernel panics with the exact signature the finding predicted.

## Mechanism (trigger → primitive → effect)

1. **Sysctl has no lower bound.** `usched_bsd4_queue_checks` is registered `SYSCTL_ADD_INT(..., CTLFLAG_RW, ..., 5, ...)` at `sys/kern/usched_bsd4.c:2043-2047` with the default `sysctl_handle_int` handler. There is no range check — contrast `sysctl_usched_bsd4_stick_to_level` (`:1868-1882`) which validates its range.

2. **Setting `queue_checks=0` makes the loop body unreachable.** In `bsd4_chooseproc_locked_cache_coherent()` (`:1416`), the local `min_level_lwp` is initialized `NULL` (`:1428`) and is only assigned inside the `while (checks < usched_bsd4_queue_checks)` loop body (`:1483-1538`). With `queue_checks=0`, the guard `0 < 0` is false, so the loop is skipped entirely. `min_level_lwp` stays `NULL`.

3. **NULL is dereferenced.** After the loop, `lp = min_level_lwp` (`:1544`, NULL), then `KASSERT(lp, ("chooseproc: at least the first lp was good"))` (`:1548`) trips on INVARIANTS kernels (which `X86_64_GENERIC` is — `options INVARIANTS` at `sys/config/X86_64_GENERIC:56`). On non-INVARIANTS builds, execution continues to `lp->lwp_priority` (`:1560`) and `TAILQ_REMOVE(q, lp, lwp_procq)` (`:1572`) — both NULL dereferences.

4. **Call chain confirmed by stack trace:**
   ```
   bsd4_release_curproc+0x89a  (inlined bsd4_select_curproc → bsd4_chooseproc_locked_cache_coherent)
   bsd4_acquire_curproc+0x17d  (sys/kern/usched_bsd4.c:354 — user_resched_wanted() path)
   syscall2+0x213              (returning to userland from the sysctl write)
   ```

## Critical reachability detail (why the original PoC didn't fire)

The **default DragonFlyBSD user scheduler is `dfly`** (`sys/kern/kern_usched.c:72`: `if (defsched == NULL) return(&usched_dfly)`). The `bsd4` scheduler is compiled in and its sysctls are registered, but its code paths (`bsd4_release_curproc`, `bsd4_select_curproc`, `bsd4_chooseproc_locked_cache_coherent`) are **dormant** unless a process explicitly switches to it via `usched_set(USCHED_SET_SCHEDULER, "bsd4")` (syscall 481).

The original PoC (`queue_checks_panic.sh`) only wrote the sysctl and did not switch to the bsd4 scheduler, so the buggy code path was never reached. The refined PoC (`df0019.c`) calls `usched_set(0, USCHED_SET_SCHEDULER, "bsd4")` first, then sets `queue_checks=0`, which triggers the panic on the very next syscall return.

`bsd4_chooseproc_locked_cache_coherent` was also **inlined** by the compiler into `bsd4_release_curproc` (confirmed via `objdump`), which is why it doesn't appear as a separate symbol in `nm /boot/kernel/kernel` — but the code and the KASSERT panic string are present.

## Threat model & impact

- **Privileges required:** root (`SYSCAP_NOSYSCTL_WR` for the sysctl write; `SYSCAP_NOSCHED` for `usched_set`).
- **Impact:** kernel panic → full-system DoS. No integrity/confidentiality impact.
- **No escalation path:** this is a NULL-deref / KASSERT panic (DoS), not a write-capable corruption primitive. No slab grooming, no pointer forge, no `uid=0` chain is possible.
- **Severity:** Low (privileged trigger, DoS only).

## Exploit chain

None — this is a pure DoS (NULL-deref panic). No memory-corruption primitive, no escalation possible.

## PoC changes

- **`df0019.c`** (NEW): the working trigger. Calls `usched_set(0, USCHED_SET_SCHEDULER, "bsd4")` to switch to the bsd4 scheduler, then sets `kern.usched_bsd4.queue_checks=0` via sysctl, then forks CPU-bound children. Panics on the unpatched kernel; gets `EINVAL` on the fixed kernel.
- **`queue_checks_panic.sh`** (original, updated comment): kept for reference. The original only wrote the sysctl and did NOT switch to the bsd4 scheduler, so it could not trigger the panic on a default-configured kernel. Documented this limitation.
- **`load.c`** (auxiliary, from debugging): standalone heavy-workload launcher. Not needed for the final trigger but kept as a stress-test artifact.

## Fix validation (Phase 8)

**Fix:** `findings/poc/DF-0019/fix.diff` — three changes to `sys/kern/usched_bsd4.c`:
1. **New sysctl handler** `sysctl_usched_bsd4_queue_checks()` (mirrors `sysctl_usched_bsd4_stick_to_level` at `:1868`): rejects `new_val < 1` with `EINVAL`.
2. **Registration change**: `SYSCTL_ADD_INT` → `SYSCTL_ADD_PROC` at `:2043`, routing through the new handler.
3. **Defensive consumer clamp**: `while (checks < (usched_bsd4_queue_checks > 0 ? usched_bsd4_queue_checks : 1))` at `:1483` — belt-and-suspenders so even a directly-poked variable can't cause the NULL deref.

**Build:** `make -j6 nativekernel KERNCONF=X86_64_GENERIC` from `/usr/src` on the `with-src` snapshot. Produced `kernel.stripped` (15705824 bytes, BuildID `54000c0c...`, vs original 15705800 / `b18d2eb8...`).

**Install:** `cp kernel.stripped /boot/kernel/kernel` + `sync` + clean shutdown + reboot. Booted as `#1: Sun Jul 12 15:35:41 UTC 2026`.

**Before/after:**

| Kernel | PoC result | Panic? |
|--------|-----------|--------|
| `#0` unpatched baseline | `queue_checks: 5 -> 0`, then panic | **YES** — `panic: chooseproc: at least the first lp was good` |
| `#1` single-fix kernel | `sysctl: kern.usched_bsd4.queue_checks=0: Invalid argument`, exit 0 | **NO** |

Valid values (1, 5, 100) are still accepted on the patched kernel. Values <= 0 (0, -1, -999) are rejected with `EINVAL`. Fix is deterministic (tested multiple values, all consistent).

## References

- `sys/kern/usched_bsd4.c:1483` — unchecked loop bound (`while (checks < usched_bsd4_queue_checks)`).
- `sys/kern/usched_bsd4.c:1544-1548` — NULL `lp = min_level_lwp` → KASSERT panic.
- `sys/kern/usched_bsd4.c:1560,1572` — NULL deref on production (non-INVARIANTS) kernels.
- `sys/kern/usched_bsd4.c:2043-2047` — `SYSCTL_ADD_INT` registration (no lower bound).
- `sys/kern/usched_bsd4.c:1868-1882` — `stick_to_level` handler (validation pattern mirrored).
- `sys/kern/kern_usched.c:72` — default scheduler is `dfly` (why original PoC didn't fire).
- `sys/config/X86_64_GENERIC:56` — `options INVARIANTS` (KASSERT compiled in).
- CWE-20 Improper Input Validation.
