sleepq_lock() trusts uninitialized objcache memory: first use of the API on any chain panics (INVARIANTS) or corrupts the chain with a garbage wchan entry and live-locks the allocator loop (production)
| Field | Value |
|---|---|
| ID | DF-2915 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-665 Improper Initialization |
| File | sys/kern/subr_sleepqueue.c |
| Lines | 145-146, 205-215 (KKASSERT :209) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-09-02 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | memcorrupt |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
sleepq_wc_cache is created with objcache_create_simple() whose allocator kmalloc()s WITHOUT M_ZERO and with no ctor. sleepq_lock() assumes zeroed objects (KKASSERT at :209) which only holds for objects that previously round-tripped the release paths β every FRESH allocation (i.e. the first use of the API on any hash chain, since chains start empty) returns garbage. On INVARIANTS kernels the KKASSERT panics; on production kernels the garbage wc (wc_wchan != NULL) is linked into sc_wchead where it can never match a wchan nor serve as a free slot, so the for(;;) re-search loop fails both probes and allocates again β unbounded M_SLEEPQ growth/live-lock until a coincidentally-zero chunk appears, with permanently stuck entries and inflated sc_free_count. Root-gated: the sleepq API has zero in-tree callers (FreeBSD-compat/Linux-KPI kld surface only); no unprivileged trigger; no privesc chain.
Proof of contest
VERIFIED (findings/poc/DF-2915/): (1) stock-kernel probe module showed
256/256 freshly-objcache_get() 48-byte chunks (M_SLEEPQ pre-polluted
with 0xAA) violating the :209 precondition; (2) end-to-end on a kernel
with only the known DF-0139 hash bug masked: kldload sqe2e.ko β
panic: assertion "wc->wc_wchan == NULL && wc->wc_refs == 0" failed in
sleepq_lock at subr_sleepqueue.c:209, guest dead in db>; (3) fix
(bzero at allocation) validated by rebuild: identical trigger loads
clean and completes lock/add/wait/broadcast.
Recommended fix
bzero(wc, sizeof(*wc)) after objcache_get β validated fix.diff in
findings/poc/DF-2915/.
Timeline
- 2026-09-02 Discovered during pass-2 audit of subr_sleepqueue.c (GLM 5.3); reproduced across a 4-kernel ladder + fix validated.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2915 Β· 19 files| File | Type | Description | Size | |
|---|---|---|---|---|
| sqprobe.c | β | 3.0 KB | view raw | |
| sqe2e.c | β | 5.1 KB | view raw | |
| Makefile.sqprobe | β | 85 B | β download | |
| Makefile.sqe2e | β | 81 B | β download | |
| build.sh | β | 739 B | view raw | |
| run_probe.sh | β | 149 B | view raw | |
| run_e2e.sh | β | 706 B | view raw | |
| build.log | β | 7.5 KB | view raw | |
| run.log | β | 280 B | view raw | |
| run.2.log | β | 1003 B | view raw | |
| panic.txt | β | 566 B | view raw | |
| panic_df0139_stockkernel.txt | β | 702 B | view raw | |
| test_df0139_mask.diff | β | 1.1 KB | view raw | |
| fix.diff | β | 1.3 KB | view raw | |
| kbuild2_fix2915.log | β | 5.7 MB | β download | |
| env.txt | β | 566 B | view raw | |
| VERDICT.md | β | 4.4 KB | β raw | |
| verdict.json | β | 6.1 KB | view raw | |
| manifest.json | β | 1.4 KB | view raw |
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:
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:
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
- 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 likesleepq_wc_cacheand 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.
- End-to-end panic (
run.2.logsequence,panic.txt): on a kernel with only the (known, separate) DF-0139 hash bug masked (test_df0139_mask.diff, test-only β without itsleepq_lock()GPFs insidelock xaddl %edx,sleepq_chains(%r13)first, seepanic_df0139_stockkernel.txt), loadingsqe2e.koexecutes the real API sequencesleepq_lock β sleepq_add β sleepq_waitfor 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.
- Production-kernel manifestation (no INVARIANTS): the KKASSERT is
compiled out, so the garbage wc is linked into
sc_wchead(subr_sleepqueue.c:212-213) withwc_wchan != NULL. It can then never be found as the wchan (needs pointer equality) nor as a free slot (needswc_wchan == NULL), so thefor(;;)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 andsc_free_countis inflated pastSLEEPQ_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 garbagewc_wchan != NULLsimply 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 throughsleepq_lock/sleepq_add/sleepq_wait/sleepq_broadcastand 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).
Fix verification
fixedfix.diff (bzero(wc, sizeof(*wc)) replacing the load-bearing KKASSERT) applied to the guest /usr/src tree on top of the DF-0139 test mask; kernel rebuilt (kbuild2_fix2915.log, rc=0) and installed. The exact trigger that panicked the baseline kernel (same sqe2e.ko, same 0xAA pollution of M_SLEEPQ, same first-use sleepq_lock) loads and completes the full lock/add/wait/broadcast sequence with no panic and the guest stays up. fix_status: fixed.
kbuild2_fix2915.log (full build log); ../DF-2917/run.log lines: 'kldload-rc=0', 'SQDEMO e2e: M_SLEEPQ polluted with 0xAA...' followed by completed round trips on the fixed kernel; panic.txt shows the same trigger on the unfixed kernel.
Confirmed kernel references
Detail
Evidence (decisive lines)
["run.log - stock-kernel probe: 'SQDEMO probe: 256/256 objcache_get() objects violate the KKASSERT precondition at subr_sleepqueue.c:209'", 'panic.txt - end-to-end on mask-only kernel #1: panic assertion wc->wc_wchan == NULL && wc->wc_refs == 0 at subr_sleepqueue.c:209, backtrace sleepq_lock+0x200 <- sq_sleeper+0x10', "panic_df0139_stockkernel.txt - stock kernel: Fatal trap 9 GPF at sleepq_lock+0x54 'lock xaddl %edx,sleepq_chains(%r13)' re-demonstrating why DF-0139 must be masked to reach the DF-2915 path", '../DF-2917/run.log - kernel #2 (mask+fix2915): same kldload + 0xAA pollution, kldload-rc=0, NO panic, module completes - fix validated', 'VERDICT.md - full narrative incl. production-kernel (non-INVARIANTS) manifestation analysis']
PoC changes
Authored from scratch (no seed PoC - new pass-2 finding). Key hurdles: (1) DF-0139's wild hash index panics spin_lock() before the DF-2915 path is reachable, so a TEST-ONLY mask patch (test_df0139_mask.diff) was applied in the guest's /usr/src; (2) 'make nativekernel' builds but does NOT install - 'make installkernel' is required or the box keeps booting the old kernel; (3) kmod build details: kthread_create needs
Verified recommended fix
Zero the freshly allocated wc in sleepq_lock(): replace the KKASSERT at subr_sleepqueue.c:209 with bzero(wc, sizeof(*wc)) (or create sleepq_wc_cache with a zeroing allocator/ctor).
Verdict
REPRODUCED, both as root-cause and end-to-end. sleepq_lock() (sys/kern/subr_sleepqueue.c:208-209) assumes objcache_get() returns zeroed sleepqueue_wchan objects, but sleepq_wc_cache is an objcache_create_simple() cache whose allocator objcache_malloc_alloc() (kern_objcache.c:571-577) kmalloc()s without M_ZERO and registers no ctor (kern_objcache.c:386-391). On the stock kernel a probe that pollutes the M_SLEEPQ zone with 0xAA and rebuilds the identical cache saw 256/256 objcache_get() objects violating the KKASSERT precondition (uninitialized wc_wchan/wc_refs/wc_blocked). End-to-end: on a kernel with only the known DF-0139 hash bug masked (test-only precondition; without it sleepq_lock GPFs in 'lock xaddl %edx,sleepq_chains(%r13)' first - captured in panic_df0139_stockkernel.txt), the first real sleepq_lock/sleepq_add/sleepq_wait sequence panics with 'assertion "wc->wc_wchan == NULL && wc->wc_refs == 0" failed in sleepq_lock at subr_sleepqueue.c:209' (panic.txt). On production (non-INVARIANTS) kernels the KKASSERT is compiled out and the garbage wc (wc_wchan != NULL) enters sc_wchead where it can never match a wchan nor serve as a free slot, so the for(;;) re-search loop allocates again - unbounded M_SLEEPQ growth/livelock plus permanently stuck entries and inflated sc_free_count. Fix validated: with fix.diff (bzero after objcache_get) the identical trigger runs clean through lock/add/wait/broadcast on the rebuilt kernel. Reachability: the file is compiled into every kernel (sys/conf/files:1453) but has ZERO in-tree callers; it exists for FreeBSD-compat/Linux-KPI kld modules, so triggering requires loading a consumer module (root-only). No unprivileged path; no privesc chain exists for this bug class.
No comments yet.