β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-0019

usched_bsd4.queue_checks accepts <=0 causing NULL-deref/panic in cache-coherent chooseproc

Field Value
ID DF-0019
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H
CWE CWE-20 Improper Input Validation
File sys/kern/usched_bsd4.c
Lines 1483, 1544-1548, 1560, 1572, 2043
Area kern
Confidence certain
Discovered 2026-06-29
Reported pending

Summary

kern.usched_bsd4.queue_checks is registered SYSCTL_ADD_INT(..., CTLFLAG_RW, ...) with the default sysctl_handle_int handler (no lower bound). Setting it to 0 (or negative) β€” a root operation β€” makes bsd4_chooseproc_locked_cache_coherent()'s while (checks < usched_bsd4_queue_checks) loop never execute, leaving min_level_lwp == NULL. The function then assigns lp = min_level_lwp (NULL) and trips KASSERT(lp) (INVARIANTS) or NULL-dereferences lp (production), panicking the kernel on the next user reschedule in cache_coherent mode with a non-empty runqueue.

Root cause

sys/kern/usched_bsd4.c:1483:

while (checks < usched_bsd4_queue_checks) {     /* body never runs if <= 0 */
    ...
}
...
lp = min_level_lwp;                             /* :1544  NULL */
q = min_q; which = min_which; pri = min_pri;
KASSERT(lp, ("chooseproc: at least the first lp was good"));   /* :1548 */
...
if (chklp) {
    if (chklp->lwp_priority < lp->lwp_priority + PPQ) { ... }  /* :1560 NULL deref */
}
...
TAILQ_REMOVE(q, lp, lwp_procq);                 /* :1572 NULL deref */

min_level_lwp is initialized NULL (~:1428) and is only assigned inside the loop body, which the loop guard (checks < queue_checks, with checks starting at 0) never enters when usched_bsd4_queue_checks <= 0. The sysctl registration (:~2043) uses SYSCTL_ADD_INT(..., CTLFLAG_RW, &usched_bsd4_queue_checks, 5, ...) with no handler-level bound β€” contrast sysctl_usched_bsd4_stick_to_level (:1868-1882) which validates its range, and bsd4_recalculate_estcpu which clamps usched_bsd4_decay to [1..1024] (:1071-1075). queue_checks has neither protection.

Threat model & preconditions

  • Attacker position: privileged β€” writing kern.usched_bsd4.* requires SYSCAP_NOSYSCTL_WR (root). So this is a privileged-user-triggered kernel panic (self-DoS / hardening defect), not a privilege escalation.
  • Privileges gained or impact: kernel panic (full-system DoS). No integrity/confidentiality impact.
  • Required config or capabilities: root; the bsd4 scheduler in use; kern.usched_bsd4.cache_coherent != 0 (boot default on multi-node/HT hardware) and a non-empty runqueue.
  • Reachability: sysctl kern.usched_bsd4.queue_checks=0, then any context switch that reaches bsd4_chooseproc_locked_cache_coherent.

Proof of concept

PoC source: findings/poc/DF-0019/queue_checks_panic.sh

Run (root, disposable VM)

sh findings/poc/DF-0019/queue_checks_panic.sh

Expected output

panic: chooseproc: at least the first lp was good     # INVARIANTS kernel
   (or) Fatal trap 12 ... in bsd4_chooseproc_locked_cache_coherent   # production

Impact

Root-only self-DoS. The kernel should not panic from a sysctl write of an in-range integer β€” it is a robustness/hardening defect and a violation of the scheduler's own invariants. Rated Low (privileged trigger).

Validate the sysctl with a custom handler (reject < 1), mirroring stick_to_level, and/or clamp in the consumer:

--- a/sys/kern/usched_bsd4.c
+++ b/sys/kern/usched_bsd4.c
@@ -1480,7 +1480,7 @@
     * minimize the contention (we are in a locked region
     */
-   while (checks < usched_bsd4_queue_checks) {
+   while (checks < (usched_bsd4_queue_checks > 0 ? usched_bsd4_queue_checks : 1)) {

plus a sysctl_usched_bsd4_queue_checks handler that returns EINVAL for new_val < 1 (registered via SYSCTL_ADD_PROC), matching the sysctl_usched_bsd4_stick_to_level pattern at :1868-1882.

References

Timeline

  • 2026-06-29 Discovered during automated file-by-file audit of sys/kern/usched_bsd4.c.
  • pending Reported to DragonFlyBSD security contact.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0019 Β· 16 files
FileTypeDescriptionSize
df0019.c trigger-source working trigger: usched_set(bsd4) + queue_checks=0 β†’ panic 3.7 KB view raw
load.c trigger-source auxiliary heavy-workload launcher (stress-test artifact) 1.9 KB view raw
queue_checks_panic.sh trigger-source original sysctl-only trigger (does NOT switch scheduler) 780 B view raw
trigger.sh trigger-source shell wrapper for load.c (intermediate debug version) 1014 B view raw
build.sh build-script cc -O2 -o df0019 df0019.c 190 B view raw
run.sh run-script runs df0019 as root 906 B view raw
fix.diff suggested-fix git-apply-able: sysctl handler rejects <1 + consumer clamp 1.7 KB view raw
VERDICT.md verdict full analysis: mechanism, reachability, fix validation 6.4 KB ↓ raw
README.md readme human-readable summary + reproduce instructions 2.1 KB ↓ raw
run.log run-log baseline #0 kernel: panic signature 1.1 KB view raw
fix_run.log run-log patched #1 kernel: EINVAL, no panic 1004 B view raw
panic.txt panic-signature panic: chooseproc: at least the first lp was good 481 B view raw
fix_build.log build-log full make -j6 nativekernel output 5.6 MB ↓ download
env.txt environment uname, cc, cpu_topology, sysctl state 689 B view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme human-readable summary + reproduce instructions
↓ download raw

DF-0019 β€” PoC

usched_bsd4.queue_checks=0 β†’ kernel panic (root-only self-DoS).

The bug

kern.usched_bsd4.queue_checks (sys/kern/usched_bsd4.c:2043) is SYSCTL_ADD_INT(..., CTLFLAG_RW, ...) with the default handler β€” no lower bound. Setting it to <=0 makes bsd4_chooseproc_locked_cache_coherent's while (checks < usched_bsd4_queue_checks) loop (:1483) never run, so min_level_lwp stays NULL; then :1544 lp = min_level_lwp (NULL) β†’ KASSERT(lp) (:1548, INVARIANTS) or NULL-deref (:1560/:1572, production). Kernel panic on the next user reschedule.

Important: default scheduler is dfly, not bsd4

The DragonFlyBSD default user scheduler is dfly (sys/kern/kern_usched.c:72). The bsd4 code path is dormant unless a process explicitly switches via usched_set(USCHED_SET_SCHEDULER, "bsd4"). The trigger PoC (df0019.c) does this; the original queue_checks_panic.sh did not and could not fire the panic.

Build (as root on the guest)

sh build.sh
# or: cc -O2 -o df0019 df0019.c

Run (as root on the guest, disposable VM)

sh run.sh
# or: ./df0019 24

Expected output

Bug present (unpatched #0 kernel):

panic: chooseproc: at least the first lp was good
bsd4_release_curproc() at bsd4_release_curproc+0x89a
bsd4_acquire_curproc() at bsd4_acquire_curproc+0x17d
syscall2() at syscall2+0x213

Bug fixed (patched #1 kernel):

sysctl: kern.usched_bsd4.queue_checks=0: Invalid argument
survived β€” no panic. exit 0.

Files

  • df0019.c β€” working trigger (switches to bsd4 scheduler, sets queue_checks=0).
  • load.c β€” auxiliary heavy-workload launcher (stress-test artifact).
  • queue_checks_panic.sh β€” original sysctl-only trigger (does NOT switch scheduler; kept for reference).
  • build.sh / run.sh β€” repro scripts.
  • fix.diff β€” the validated fix (git-apply-able).
  • VERDICT.md β€” full analysis.
  • run.log / fix_run.log β€” baseline panic / patched clean-exit logs.
  • panic.txt β€” panic signature from serial console.
  • fix_build.log β€” full kernel build output.
  • env.txt β€” guest environment.
VERDICT.md verdict full analysis: mechanism, reachability, fix validation
↓ download raw

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

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED. PoC panics on unpatched #0 (panic: chooseproc: at least the first lp was good, 2x from fresh reset) and does NOT panic on single-fix #1 (sysctl returns EINVAL for 0, PoC exits 0, guest stays up). Valid values still accepted.

BEFORE (#0): queue_checks: 5->0 / panic: chooseproc: at least the first lp was good / guest down. AFTER (#1): sysctl: queue_checks=0: Invalid argument / exit 0 / guest up. Regression: 1,5,100 OK; 0,-1,-999 EINVAL.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Jul 12 15:35:41 UTC 2026

Confirmed kernel references

Detail

Exploit chain

none -- this is a pure NULL-deref / KASSERT panic (DoS only). No memory-corruption primitive. Root writes sysctl -> kernel panics on next bsd4 scheduler context switch.

Evidence (decisive lines)

BASELINE (#0): kern.usched_bsd4.queue_checks: 5 -> 0; then SSH dies. Serial: 'panic: chooseproc: at least the first lp was good / bsd4_release_curproc+0x89a / bsd4_acquire_curproc+0x17d / syscall2+0x213 / Stopped at Debugger+0x7c / db>'. PATCHED (#1): 'sysctl: queue_checks=0: Invalid argument' / exit 0 / guest stays up. Valid values 1,5,100 still accepted; 0,-1,-999 rejected.

PoC changes

Wrote df0019.c (NEW working trigger): calls usched_set(0, USCHED_SET_SCHEDULER, "bsd4") first, then sets queue_checks=0. Original queue_checks_panic.sh only wrote the sysctl but never switched scheduler (default is dfly). Authored build.sh, run.sh, fix.diff (sysctl handler + consumer clamp).

Verified recommended fix

fix.diff makes three changes to sys/kern/usched_bsd4.c: (1) adds a sysctl handler that rejects new_val < 1 with EINVAL; (2) changes SYSCTL_ADD_INT to SYSCTL_ADD_PROC at :2043; (3) adds defensive clamp at :1483. Supersedes finding proposal.

Verdict

REPRODUCED. The bug is real: kern.usched_bsd4.queue_checks is registered SYSCTL_ADD_INT(CTLFLAG_RW) with no lower bound. Setting it to 0 makes the while loop at :1483 unreachable, so min_level_lwp stays NULL. Then lp=NULL at :1544 triggers KASSERT(lp) at :1548 on the INVARIANTS GENERIC kernel -- confirmed by the exact predicted panic signature 'panic: chooseproc: at least the first lp was good'. CRITICAL REACHABILITY DETAIL: the default DragonFlyBSD scheduler is 'dfly', NOT 'bsd4' -- the buggy code path is dormant unless a process explicitly switches via usched_set(USCHED_SET_SCHEDULER, "bsd4"). The refined PoC (df0019.c) calls usched_set(2) first, then sets queue_checks=0, which panics immediately on syscall return.