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

sleepq_wait/sleepq_wait_sig/sleepq_timedwait/sleepq_timedwait_sig NEVER sleep: _sleepq_wait_begin() drops the PDOMAIN_FBSD domain from the tsleep() flags, so tsleep's interlock validation always mismatches and returns immediately

Field Value
ID DF-2917
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:L/A:H
CWE CWE-628 Function Call With Incorrectly Specified Arguments
File sys/kern/subr_sleepqueue.c
Lines 327-336 (interlock :275-276; check kern_synch.c:660-663)
Area kern
Confidence certain
Discovered 2026-09-02
Pass 2 (GLM 5.3 second pass)
Bucket base:kern
Reported pending
Known CVE none
CVE match novel

Summary

sleepq_add() interlocks the sleeper with tsleep_interlock(wchan, PDOMAIN_FBSD0 + queuePDOMAIN_FBSDINC) (:275-276), recording td_wdomain = 0x01000000. _sleepq_wait_begin() computes exactly that domain into a local at :327 and then never uses it β€” the tsleep() calls at :331/:336 pass tflags alone, and PINTERLOCKED carries no domain bits. tsleep()'s interlock check (td->td_wdomain != (flags & PDOMAIN_MASK)) always mismatches (0x01000000 != 0) and takes the 'goto resume' fast path, so every sleepq_wait() returns immediately (ret 0) without ever descheduling. The dead 'domain' local is the smoking gun for the intended tflags | domain; the established in-tree pattern is tsleep(waddr, PCATCH | PINTERLOCKED | PDOMAIN_UMTX, ...) (kern_umtx.c:227). For any FreeBSD-compat/Linux-KPI consumer module every blocking wait degenerates into an immediate return: callers busy-spin at kernel priority (per-CPU livelock DoS), timeouts return instantly as success, and the API's blocking contract does not exist. Root-gated; no unprivileged trigger; no memory corruption.

Proof of contest

VERIFIED (findings/poc/DF-2917/): demonstrator sqe2e.ko round 1: sleepq_lock β†’ sleepq_add β†’ sleepq_wait with NO waker β€” 'returned ret=0 after 0 sbticks with NO wakeup issued' (instant return, zero blocking) on the DF-0139-masked + DF-2915-fixed kernel. Fix validated (tflags | domain, rebuilt): same sleeper 'STILL BLOCKED after ~200 ticks with no wakeup', wakes on broadcast; round 2 wakes in 1 tick.

tsleep(td->td_wchan, tflags | domain, ...) in both calls β€” validated fix.diff in findings/poc/DF-2917/.

Timeline

  • 2026-09-02 Discovered during pass-2 audit of subr_sleepqueue.c (GLM 5.3); reproduced + fix validated across the kernel ladder.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2917 Β· 8 files
FileTypeDescriptionSize
fix.diff β€” 1.2 KB view raw
run.sh β€” 317 B view raw
run.log β€” 380 B view raw
run.2.log β€” 664 B view raw
kbuild3_fix2917.log β€” 5.7 MB ↓ download
VERDICT.md β€” 2.3 KB ↓ raw
verdict.json β€” 4.8 KB view raw
manifest.json β€” 962 B view raw
VERDICT.md
↓ download raw

DF-2917 VERDICT β€” REPRODUCED (sleepq wait API never blocks)

status: reproduced / impact: dos / confidence: certain

Root cause

  • sleepq_add() (subr_sleepqueue.c:275-276) interlocks the sleeper with domain PDOMAIN_FBSD0 + queue*PDOMAIN_FBSDINC β†’ td->td_wdomain = 0x01000000 (kern_synch.c:446).
  • _sleepq_wait_begin() (subr_sleepqueue.c:327) computes exactly that domain into a local variable and never uses it β€” the tsleep() calls at :331/:336 pass tflags (= PINTERLOCKED|PCATCH, param.h:326: 0x400 β€” no domain bits).
  • tsleep() (kern_synch.c:660-663) validates the interlock: td->td_wdomain != (flags & PDOMAIN_MASK) β†’ 0x01000000 != 0 β†’ goto resume β†’ immediate return without descheduling.
  • Every sleepq_wait(), sleepq_wait_sig(), sleepq_timedwait(), sleepq_timedwait_sig() (subr_sleepqueue.c:387-458) is affected.

The correct pattern exists in-tree: tsleep(waddr, PCATCH | PINTERLOCKED | PDOMAIN_UMTX, ...) (kern_umtx.c:227).

Evidence

kernel state observation
#2 Sep 3 09:16:39 (DF-0139 mask + DF-2915 fix) DF-2917 live round1 (nowake): sleepq_wait() returned ret=0 after 0 sbticks with NO wakeup issued (run.log)
#3 Sep 3 09:26:13 (+ DF-2917 fix) fixed round1: sleeper STILL BLOCKED after ~200 ticks with no wakeup - sleepq_wait() really sleeps; broadcast wakes it; round2 (broadcast): returned after 1 sbticks (run.2.log)

The no-wake round is decisive: nothing in the system could legitimately wake the sleeper, yet on the unfixed kernel the "wait" returned instantly with ret=0.

Impact

For any FreeBSD-compat / Linux-KPI consumer module: every blocking wait degenerates into an immediate return β€” callers busy-spin at kernel priority (per-CPU livelock / DoS), timeouts return instantly as success, and the wakeup-blocking contract of the API does not exist. No memory corruption.

Fix validation

fix.diff (tflags | domain in both tsleep calls) β€” built as kernel #3 (kbuild3_fix2917.log, rc=0): the same trigger that returned in 0 ticks now blocks ~200 ticks and wakes on broadcast within 1 tick. fix_status: fixed.

Reachability

Zero in-tree callers; the API exists for kld-loaded compat modules (root-only). Not reachable by unprivileged users.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied in the guest /usr/src on top of mask+DF-2915-fix, kernel rebuilt (kbuild3_fix2917.log, rc=0) and installed. The identical no-wake demonstrator that returned in 0 ticks on the baseline now blocks for ~200 ticks until a broadcast is issued (and a subsequent round wakes in 1 tick). Blocking semantics fully restored; fix_status: fixed.

run.2.log on kernel #3 ('STILL BLOCKED after ~200 ticks ... really sleeps'; 'round2 (broadcast): returned after 1 sbticks'); kbuild3_fix2917.log (full build log).
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #2: Thu Sep 3 09:26:13 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

Confirmed kernel references

Detail

Evidence (decisive lines)

["run.log - kernel #2 (mask+DF-2915 fix, DF-2917 live): 'SQDEMO e2e: round1 (nowake): sleepq_wait() returned ret=0 after 0 sbticks with NO wakeup issued' - instantaneous return, zero blocking", "run.2.log - kernel #3 (both fixes): 'round1: sleeper STILL BLOCKED after ~200 ticks with no wakeup - sleepq_wait() really sleeps (DF-2917 fixed); issuing broadcast' and 'round2 (broadcast): sleepq_wait() returned after 1 sbticks'", 'fix.diff - tflags | domain in both tsleep() calls (variable already existed, dead, at :327)', 'VERDICT.md/README.md - full source trace kern_synch.c:660-663 -> goto resume']

PoC changes

Authored from scratch. The demonstrator is sqe2e.c (shared with the DF-2915 pack): round 1 runs lock/add/wait with no waker and times how long sleepq_wait() blocks; round 2 verifies broadcast wakeups. Required the DF-0139 test mask + DF-2915 fix kernels (see ../DF-2915) so the code path is reachable at all.

Verified recommended fix

Pass the already-computed domain into tsleep(): tsleep(td->td_wchan, tflags | domain, td->td_wmesg, ...) in both branches of _sleepq_wait_begin() (subr_sleepqueue.c:331/:336).

Verdict

REPRODUCED end-to-end. _sleepq_wait_begin() (sys/kern/subr_sleepqueue.c:317-340) computes 'domain = PDOMAIN_FBSD0 + td->td_sqqueue * PDOMAIN_FBSDINC' at :327 into a local that is then never used; tsleep() is called at :331/:336 with tflags alone. PINTERLOCKED (0x400, sys/sys/param.h:326) carries no domain bits, while sleepq_add() interlocked the thread with tsleep_interlock(wchan, PDOMAIN_FBSD0+...) recording td_wdomain = 0x01000000 (subr_sleepqueue.c:275-276, kern_synch.c:446). tsleep()'s interlock validation (kern_synch.c:660-663, td->td_wdomain != (flags & PDOMAIN_MASK)) therefore always mismatches and takes the 'goto resume' fast path: every sleepq_wait(), sleepq_wait_sig(), sleepq_timedwait() and sleepq_timedwait_sig() (subr_sleepqueue.c:387-458) returns immediately (ret 0) WITHOUT ever descheduling. Demonstrated on kernel #2 (DF-0139 mask + DF-2915 fix): a kthread executed sleepq_lock/sleepq_add/sleepq_wait with NO waker at all and sleepq_wait() returned ret=0 after 0 sbticks ('round1 (nowake): sleepq_wait() returned ret=0 after 0 sbticks with NO wakeup issued'). For any FreeBSD-compat/Linux-KPI consumer this turns every blocking wait into a busy-spin at kernel priority (per-CPU livelock) and every timeout into an instant success return - the blocking primitive the API promises does not exist. Fix validated on kernel #3: with tflags|domain (fix.diff, the established umtx pattern kern_umtx.c:227) the same no-wake sleeper genuinely blocked for ~200 ticks until the harness issued sleepq_broadcast() (woke in 1 tick on the follow-up round). Reachability: zero in-tree callers; kldload-gated (root).