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.
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-2917 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 domainPDOMAIN_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 passtflags(=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
fixedfix.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).
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).
No comments yet.