objcache_get() M_WAITOK lost wakeup: objects stranded in remote CPUs' partially-filled magazines cause a permanent, unkillable sleep
| Field | Value |
|---|---|
| ID | DF-2813 |
| Status | new |
| Severity | Medium |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-662 (liveness β CWE-835 unkillable block) |
| File | sys/kern/kern_objcache.c |
| Lines | 547-555 (sleep), 647/660 (mycpu wakeup), 682-703 (depot delivery), 1029-1034 (#if 0 rebalance) |
| Area | kern |
| Confidence | certain |
| Discovered | 2026-08-31 |
| Pass | 2 (GLM 5.3 second pass) |
| Bucket | base:kern |
| Reported | pending |
| Known CVE | none |
| CVE match | novel |
Summary
An exhausted M_WAITOK objcache_get() sleeps on the depot with flags=0 (unkillable) and timo=0 (no timeout). The hot-path wakeup in objcache_put is gated on the PUTTER's own cpucache->waiting and uses wakeup_mycpu, so it can never wake a sleeper on another CPU; objects freed into a remote CPU's partially-filled magazines are invisible to the sleeper and only reach the depot (the sole wakeup source it can observe) after 2Γ mag_capacity objects accumulate on that one CPU. The magazine rebalance callout that would eventually drain partial magazines is compiled out (#if 0). Result: freed objects exist and are locally allocatable on one CPU while a thread on another CPU sleeps in objcache_get forever.
Threat model & preconditions
Unprivileged local user. Blocking write(2)/send(2) bottoms out in sosend_generic β m_getl(M_WAITOK) β objcache_get on the nmbclusters-limited mbuf caches (guest: ~66MB β exhaustible via socket buffers across many sockets). m_get's reclaim-retry never runs because pure M_WAITOK never returns NULL. A stuck thread is signal-unkillable; repeated cross-CPU free patterns strand one victim thread each β thread/fd/memory exhaustion DoS; kernel threads equally susceptible. Not memory corruption; no escalation path.
Proof of contest
VERIFIED deterministically 3/3 (findings/poc/DF-2813/, KLD): private cache (cluster_limit=8, magcap=6); ARM drains 80 objects; sleeper on cpu5 sleeps 'objcache_get'; STRAND frees 6 objects on cpu0 β sleeper still STUCK after 11s while an M_NOWAIT probe on cpu0 returns an object and on cpu5 returns NULL; RESCUE (magazine fills β depot cycle) wakes it in 0.000s (positive control proving the delivery path is the defect). Fix (flush the loaded magazine to the depot when depot->waiting && !cpucache->waiting) validated on a rebuilt kernel: sleeper wakes at strand time, no mbuf regressions.
Recommended fix
Validated fix.diff in findings/poc/DF-2813/.
Timeline
- 2026-08-31 Discovered during pass-2 audit of kern_objcache.c (GLM 5.3); deterministic unkillable wedge reproduced 3/3 + fix validated same run.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2813 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.3 KB | β raw | |
| VERDICT.md | β | 6.0 KB | β raw | |
| ochang.c | β | 9.6 KB | view raw | |
| ochang_drv.c | β | 2.7 KB | view raw | |
| build.sh | β | 382 B | view raw | |
| run.sh | β | 639 B | view raw | |
| build.log | β | 403 B | view raw | |
| run.log | β | 2.0 KB | view raw | |
| run.2.log | β | 1.9 KB | view raw | |
| run.3.log | β | 1.9 KB | view raw | |
| run.patched.log | β | 1.9 KB | view raw | |
| env.txt | β | 463 B | view raw | |
| fix.diff | β | 1.9 KB | view raw | |
| verdict.json | β | 5.2 KB | view raw |
DF-2813 β objcache_get() M_WAITOK lost wakeup: objects stranded in remote per-cpu magazines cause a permanent, unkillable sleep
What
objcache_get() (sys/kern/kern_objcache.c:547-555) parks an exhausted
M_WAITOK caller in ssleep(depot, &depot->spin, 0, "objcache_get", 0) β
flags 0 (no PCATCH, not signal-interruptible) and timeout 0 (infinite).
An objcache_put() on the hot path (kern_objcache.c:644-651, 656-664) only
wakes sleepers on the putting CPU (wakeup_mycpu, gated on the putter's
own cpucache->waiting). Objects pushed into a per-cpu magazine are
invisible to every other CPU. They only become globally visible when a full
magazine is cycled into the depot (kern_objcache.c:682-703, which does
wakeup(depot)), i.e. only after 2 Γ mag_capacity objects accumulate on
one CPU. The magazine rebalance callout that would eventually drain partial
magazines is compiled out (#if 0, kern_objcache.c:1029-1034).
Consequence: a thread on cpu A sleeps forever inside objcache_get() while
freed objects sit in cpu B's partially-filled magazines. Nothing in the
kernel ever delivers them. The sleep has no timeout and no signal
interruption β a user process stuck here is unkillable (kill -9 sets
the flag; the thread never returns to userret).
Reproduce
On the audit QEMU guest (6-cpu DragonFly 6.5-DEVELOPMENT, stock INVARIANTS kernel #0):
./build.sh # on guest: builds ochang.ko + ochang_drv (as maxx, in ~/ochang) sudo sh run.sh # kldload ./ochang.ko ; ./ochang_drv ; dmesg tail ; kldunload
The KLD creates a private objcache (cluster_limit=8, mag_capacity=6, unallocated = 6Β·6Β·2+8 = 80), then:
- ARM β drain with
M_NOWAITgets: exactly 80 objects held. - SLEEPER β kernel thread pinned to cpu5 calls
objcache_get(oc, M_WAITOK)βWarning: objcache(ochang) exhausted on cpu5!β sleeps on the depot.wmesg="objcache_get",TDF_TSLEEPQ=1. - STRAND β free exactly 6 objects (one magazine) on cpu0 via IPI.
- Observe: sleeper still STUCK for 1102 ticks (11 s) while
PROBE cpu0 β OBJECT AVAILABLE (an
M_NOWAITget on cpu0 succeeds from its loaded magazine, ahead of any exhaustion check) and PROBE cpu5 β NULL. Same cache, same instant: the objects exist and are reachable from cpu0 but the sleeper cannot get them. - RESCUE (positive control) β free 12 more on cpu0: both magazines
fill, the depot cycle deposits a full magazine,
wakeup(depot)fires β sleeper acquired 0.000 s after RESCUE.
Reproduced 3/3 runs (run.log, run.2.log, run.3.log), deterministically.
Expected output (bug present)
sleeper: started=1 got=0 STUCK for 1102 ticks (11 s), TDF_TSLEEPQ=1 wmesg="objcache_get" ochang: PROBE cpu0 -> OBJECT AVAILABLE ochang: PROBE cpu5 -> NULL (exhausted) === RESCUE ... === sleeper acquired 0.000 s after RESCUE
Unprivileged reachability (no KLD needed in principle)
Every cluster-limited cache is reachable from an unprivileged user with
blocking syscalls: sosend_generic() allocates with m_getl(..., M_WAITOK,
...) (sys/kern/uipc_socket.c:1024, also :866, :1164), i.e. a blocking
write(2)/send(2) on any socket bottoms out in objcache_get(...,
M_WAITOK). The mbuf caches are limited by nmbufs/nmbclusters
(sys/kern/uipc_mbuf.c:797-851); on this guest nmbclusters=33296 (~66 MB of
2 KB clusters, or ~16 k mbufs) β exhaustible by an unprivileged user through
socket buffers across many sockets. Freeing fewer than 2Γmagcap objects on
a CPU different from the blocked one (CPU placement is not under user
control but is statistically spread across 6 CPUs and repeatable at will)
strands them; each attempt that lands cross-CPU wedges one thread of a
victim process permanently.
Fix
fix.diff (validated β see VERDICT.md): when the depot has remote waiters,
objcache_put() flushes its partially-filled loaded magazine to the depot
(making the objects globally visible and waking the sleeper) before caching
the new object. Zero behavior change when nobody waits.
Files
ochang.cβ KLD driver (the PoC)ochang_drv.cβ userspace orchestrator (pins to cpu2)build.sh/run.shbuild.log,run.log,run.2.log,run.3.log,run.patched.logenv.txt,fix.diff,manifest.json,verdict.json
DF-2813 β VERDICT
Status: reproduced (deterministic, 3/3 runs). Impact: local DoS (unkillable thread / permanent M_WAITOK block). Confidence: certain. Fix: validated on a rebuilt guest kernel (#1) β bad behavior gone.
Root cause (line-accurate)
objcache_get()exhaustion +M_WAITOKpath: sys/kern/kern_objcache.c:547-555 βssleep(depot, &depot->spin, 0, "objcache_get", 0). flags=0 β noPCATCH(tsleep is not signal-interruptible, the victim process cannot be killed); timo=0 β no timeout.- Hot-path wakeup gating: sys/kern/kern_objcache.c:647-648 and :660-661 β
if (cpucache->waiting) wakeup_mycpu(&oc->depot[...])wherecpucacheis the putter's per-cpu structure (objcache_put:622). A remote sleeper setcache_percpu[A]->waiting(objcache_get:548), which the putter on CPU B never checks;wakeup_mycpu(sys/kern/kern_synch.c:1189-1193) only scans the current CPU's sleep queue anyway. - Objects put on the hot path land in the putting CPU's magazine
(objcache_put:646, :659) and are invisible to other CPUs. They reach the
depot β the only place a remote sleeper can find them
(objcache_get:484-497) β only when a full magazine is cycled
(objcache_put:682-703,
wakeup(depot)at :700-701), i.e. after 2 Γ mag_capacity objects accumulate on one CPU. - The magazine rebalance callout that would eventually drain partial
magazines is disabled: sys/kern/kern_objcache.c:1029-1034 (
#if 0). - There is no other wakeup source:
objcache_dtor(:734), reclaim (:886-887, :896-897) andobjcache_set_cluster_limit(:371) all require depot-level events that never happen while objects sit in a remote partial magazine.
Net: thread on CPU A sleeps forever although objects are free and cached on CPU B. Verified interlocked-sleep correctness elsewhere (ssleep's spin-interlock at kern_synch.c:815-828 rules out same-visibility-window lost wakeups; the hole is purely the per-cpu-magazine hysteresis).
Reproduction narrative
Deterministic KLD harness (private cache: cluster_limit=8, mag_capacity=6,
unallocated_objects = 6 cpus Γ 6 Γ 2 + 8 = 80):
- ARM:
objcache_get(oc, M_NOWAIT)Γ80 β exhausted (nheld=80, magcap derived = 6 β matches kern_objcache.c:296-301 accounting exactly). - SLEEPER: kernel thread pinned to cpu5 (
lwkt_create(..., cpu=5)) callsobjcache_get(oc, M_WAITOK). dmesg:Warning: objcache(ochang) exhausted on cpu5!(kern_objcache.c:540). Thread state:TDF_TSLEEPQ=1 wmesg="objcache_get"β inside the ssleep. - STRAND: 6 puts (= exactly one magazine) on cpu0 via IPI. Observation over 10 s: STUCK for 1102 ticks (11 s).
- Behavioral proof of stranding:
PROBE cpu0runsobjcache_get(oc, M_NOWAIT)on cpu0 β OBJECT AVAILABLE (hot path pops from cpu0's loaded magazine before any exhaustion check, kern_objcache.c:445-450);PROBE cpu5β NULL (exhausted). Same cache, same instant, opposite outcomes on different CPUs. - RESCUE (positive control): 12 more puts on cpu0 β both magazines fill β
depot cycle β
wakeup(depot)β sleeper acquired 0.000 s after RESCUE (latency=1102 ticks total, i.e. it woke the instant the magazine was delivered). This proves the wakeup machinery itself works and the defect is precisely the delivery path.
Runs: run.log, run.2.log, run.3.log β identical results, 3/3.
Impact assessment
- Local DoS. Unprivileged reachability:
sosend_generic()βm_getl(..., M_WAITOK, ...)(sys/kern/uipc_socket.c:1024; also :866, :1164) β a blockingwrite(2)/send(2)on any socket blocks insideobjcache_get(M_WAITOK)when the mbuf caches are exhausted (sys/kern/uipc_mbuf.c:797-851 limits; guest nmbclusters=33296 β 66 MB of clusters / ~16 k+maxfiles mbufs β exhaustible by an unprivileged user through socket buffers).m_get's reclaim-retry (sys/kern/uipc_mbuf.c:1020-1032) never executes because pure M_WAITOK never returns NULL β it sleeps inside objcache. - The sleeping thread is not signal-interruptible: the owning process becomes unkillable; repeated attacks strand threads/fds/memory.
- Not a memory-safety bug; no escalation path identified (the sleep holds no spinlocks at the ssleep β ssleep releases the depot spin β and kernel callers do not hold critical tokens across M_WAITOK mbuf allocation paths in the audited call sites).
Fix validation
fix.diff: inobjcache_put(), before the hot-path push, if the depot has waiters (depot->waiting) and there is no local waiter (the existingwakeup_mycpucovers that case) and the loaded magazine is non-empty, flush the loaded magazine to the depot full list (swapping in an empty magazine) andwakeup(depot). Decision re-checked under the depot spinlock; unlockedwaitingprobe is benign (skip-or-flush).- Baseline kernel #0 (stock,
Thu Jul 2 06:02:54 UTC 2026): sleeper stuck 11 s with objects available on cpu0 (run.log/run.2.log/run.3.log). - Patched kernel #1 (
Tue Sep 1 16:06:47 UTC 2026, built in-guest withmake -j6 nativekernel KERNCONF=X86_64_GENERIC+ installkernel): sleeper woke 0.25 s after STRAND (first driver poll tick after the flush;latency=100 ticks= woke at strand time), no hang; both probes OBJECT AVAILABLE; DRAIN/DESTROY/kldunload clean; the whole SSH-driven run exercised the mbuf objcache paths on the patched kernel without incident (run.patched.log). - Guest reset to the clean
with-srcsnapshot afterwards (vm.sh reset with-src), stock kernel #0 back, guest up.
Why the other suspected issues in this file are NOT this finding
- Same-CPU putter vs sleeper: covered by
cpucache->waiting+wakeup_mycpuβ correct. - Depot-level deliveries (full mag cycle, no-empty-mag free, dtor,
reclaim, set_cluster_limit): all
wakeup(depot)β correct, and proven by the RESCUE control. - DF-0064 (stats race on total_objects) β unrelated, stats-only, known.
Artifacts
ochang.c, ochang_drv.c, build.sh, run.sh, build.log, run.log, run.2.log, run.3.log, run.patched.log, env.txt, fix.diff, manifest.json, verdict.json
Fix verification
fixedApplied fix.diff to /usr/src in-guest, make -j6 nativekernel + installkernel, rebooted into kernel #1. Exact same PoC: baseline stuck 11 s with stranded objects; patched wakes at strand time (0.25 s / latency=100 ticks), probes both AVAILABLE, clean teardown, no regressions observed in mbuf-heavy ssh operation.
["findings/poc/DF-2813/run.patched.log: '!!! sleeper woke unexpectedly after 0.25 s' + 'sleeper: started=1 got=1 latency=100 ticks' + clean DRAIN/DESTROY/unload", 'findings/poc/DF-2813/run.log:baseline kernel #0 stuck behavior for contrast', 'findings/poc/DF-2813/fix.diff: git-apply-able (verified --check against pristine sys/)']
Confirmed kernel references
Detail
Exploit chain
unpriv user: (1) exhaust a cluster-limited mbuf cache via socket buffers on many sockets (nmbclusters ~ 66 MB of clusters / nmbufs smaller still); (2) victim/attacker thread issues a blocking write(2) on a socket whose mbuf allocation hits objcache_get(M_WAITOK) -> permanent non-interruptible sleep when the frees that follow land in remote partially-filled magazines (< 2*magcap accumulated per cpu, no rebalance timer - callout is #if 0); (3) kill -9 cannot reclaim the process; repeat to strand threads/fds/memory.
Evidence (decisive lines)
['run.log / run.2.log / run.3.log: \'STUCK for 1102 ticks (11 s), TDF_TSLEEPQ=1 wmesg="objcache_get"\' with \'PROBE cpu0 -> OBJECT AVAILABLE\' and \'PROBE cpu5 -> NULL (exhausted)\', then \'sleeper acquired 0.000 s after RESCUE\'', 'run.patched.log (kernel #1 with fix.diff): sleeper woke 0.25 s after STRAND, latency=100 ticks, clean DRAIN/DESTROY', "dmesg in run logs: 'Warning: objcache(ochang) exhausted on cpu5!' (kern_objcache.c:540) while ARM drained exactly ncpus*2*magcap+cluster_limit = 80 objects", 'env.txt: DragonFly 6.5-DEVELOPMENT, hw.ncpu=6, nmbclusters=33296, source hashes']
PoC changes
PoC authored from scratch (no seed existed). Iterations: ipifunc3_t callback signature needed (void, int, struct intrframe); thread sleep-state read via TDF_TSLEEPQ+td_wmesg instead of nonexistent td_state; private-struct reach-in replaced with a behavioral probe (IPI objcache_get(M_NOWAIT)+put pair per cpu) which is stronger evidence anyway; driver clock_gettime -> gettimeofday for DFly userland visibility.
Verified recommended fix
In objcache_put's hot path, when depot->waiting != 0 and no local waiter exists, flush the partially-filled loaded magazine to the depot full list (swap in a depot empty magazine) and wakeup(depot) so remote M_WAITOK sleepers can reach the objects; see fix.diff.
Verdict
Deterministically reproduced (3/3) on the 6-cpu stock guest: a thread blocked in objcache_get(oc, M_WAITOK) on cpu5 slept permanently (11 s observation window, wmesg="objcache_get", TDF_TSLEEPQ=1, no PCATCH, timo=0) while 6 freed objects sat available in cpu0's loaded magazine (PROBE cpu0 -> OBJECT AVAILABLE vs PROBE cpu5 -> NULL on the same cache at the same instant). The positive control (RESCUE: filling both cpu0 magazines -> depot cycle -> wakeup(depot)) woke it in 0.000 s, proving the defect is exactly the remote per-cpu-magazine delivery path guarded by wakeup_mycpu on the putter's cpu. fix.diff (flush partial loaded magazine to the depot when depot->waiting && !cpucache->waiting) was validated on a rebuilt guest kernel #1: the sleeper now wakes at strand time (0.25 s, first poll tick) and the system ran normally. Unprivileged reachability: blocking write(2)/send(2) -> sosend_generic m_getl(M_WAITOK) (uipc_socket.c:1024) with the user-exhaustible nmbufs/nmbclusters-limited mbuf caches; the stuck thread is unkillable.
No comments yet.