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

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.

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2813 Β· 14 files
FileTypeDescriptionSize
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:

  1. ARM β€” drain with M_NOWAIT gets: exactly 80 objects held.
  2. 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.
  3. STRAND β€” free exactly 6 objects (one magazine) on cpu0 via IPI.
  4. Observe: sleeper still STUCK for 1102 ticks (11 s) while PROBE cpu0 β†’ OBJECT AVAILABLE (an M_NOWAIT get 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.
  5. 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.sh
  • build.log, run.log, run.2.log, run.3.log, run.patched.log
  • env.txt, fix.diff, manifest.json, verdict.json
VERDICT.md
↓ download raw

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_WAITOK path: sys/kern/kern_objcache.c:547-555 β€” ssleep(depot, &depot->spin, 0, "objcache_get", 0). flags=0 β‡’ no PCATCH (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[...]) where cpucache is the putter's per-cpu structure (objcache_put:622). A remote sleeper set cache_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) and objcache_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):

  1. ARM: objcache_get(oc, M_NOWAIT) Γ—80 β†’ exhausted (nheld=80, magcap derived = 6 β€” matches kern_objcache.c:296-301 accounting exactly).
  2. SLEEPER: kernel thread pinned to cpu5 (lwkt_create(..., cpu=5)) calls objcache_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.
  3. STRAND: 6 puts (= exactly one magazine) on cpu0 via IPI. Observation over 10 s: STUCK for 1102 ticks (11 s).
  4. Behavioral proof of stranding: PROBE cpu0 runs objcache_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.
  5. 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 blocking write(2)/send(2) on any socket blocks inside objcache_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: in objcache_put(), before the hot-path push, if the depot has waiters (depot->waiting) and there is no local waiter (the existing wakeup_mycpu covers that case) and the loaded magazine is non-empty, flush the loaded magazine to the depot full list (swapping in an empty magazine) and wakeup(depot). Decision re-checked under the depot spinlock; unlocked waiting probe 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 with make -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-src snapshot 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

fixed
baseline reproduced→ patch + rebuild →patched clean

Applied 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/)']
↓ fix.diffDragonFly dfbsd 6.5-DEVELOPMENT DragonFly 6.5-DEVELOPMENT #1: Tue Sep 1 16:06:47 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

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.