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

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.

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2915 Β· 19 files
FileTypeDescriptionSize
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
VERDICT.md
↓ download 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

  1. 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 like sleepq_wc_cache and 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.

  1. End-to-end panic (run.2.log sequence, panic.txt): on a kernel with only the (known, separate) DF-0139 hash bug masked (test_df0139_mask.diff, test-only β€” without it sleepq_lock() GPFs inside lock xaddl %edx,sleepq_chains(%r13) first, see panic_df0139_stockkernel.txt), loading sqe2e.ko executes the real API sequence sleepq_lock β†’ sleepq_add β†’ sleepq_wait for 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.

  1. Production-kernel manifestation (no INVARIANTS): the KKASSERT is compiled out, so the garbage wc is linked into sc_wchead (subr_sleepqueue.c:212-213) with wc_wchan != NULL. It can then never be found as the wchan (needs pointer equality) nor as a free slot (needs wc_wchan == NULL), so the for(;;) 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 and sc_free_count is inflated past SLEEPQ_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 garbage wc_wchan != NULL simply 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 through sleepq_lock / sleepq_add / sleepq_wait / sleepq_broadcast and 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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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.
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Thu Sep 3 09:16:39 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

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 , sbticks is already declared in (redeclaring trips -Werror=redundant-decls); (4) M_SLEEPQ is 'struct malloc_type M_SLEEPQ[1]' so modules must extern it as an array; (5) module .ko lands in the obj dir (/usr/obj/tmp/...) and /tmp is wiped on reboot - copy to /root.

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.