Dead spin_lock_test_mode: debug.spin_lock_test=1 wedges a CPU in a critical section for 60s then panics INVARIANTS kernels (test-mode escape never fires)
| Field | Value |
|---|---|
| ID | DF-2928 |
| Status | new |
| Severity | Low |
| CVSS 3.1 | CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H |
| CWE | CWE-670 Always-Incorrect Control Flow Implementation |
| File | sys/kern/kern_spinlock.c |
| Lines | 88-91, 409, 417-424 (escape: indefinite2.h:171-176) |
| 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
The INVARIANTS sysctl handler for debug.spin_lock_test=1 ('Test the indefinite wait code') performs a deliberate recursive spin_lock and sets the file-static spin_lock_test_mode expecting the contested wait to break immediately β but no code in sys/ reads that variable; indefinite_check() honors only kern_lock.c's lock_test_mode. The recursive acquisition therefore spins the full 60 seconds with preemption/interrupts masked on that CPU and then panics via the unconditional 'indefinite wait' panic (indefinite2.h:183-186). Broken since the 2017 indefinite2.h rewrite; still present in upstream master (checked 2026-09-03). Root-capable only (SYSCAP_RESTRICTEDROOT β unprivileged writes verified denied on the guest); availability-only, debug kernels only. It also documents the fragile break-without-acquire contract of _spin_lock_contested. Verified 3/3: root write β CPU frozen in crit section, msgbuf 'indefinite wait' per-second accumulation, panic at exactly T0+60s.
Proof of contest
VERIFIED (findings/poc/DF-2928/): unpriv write 'Operation not permitted'; root write β 60s freeze + panic, 3/3 independent runs, full serial logs. Fix validated conceptually (fix.diff: make spin_lock_test_mode non-static + honor it for 'S'/'s' wait types in indefinite_check; alternative: delete the broken branch).
Timeline
- 2026-09-02 Discovered during pass-2 audit of kern_spinlock.c (GLM 5.3); reproduced 3/3 as root on INVARIANTS.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2928 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.5 KB | β raw | |
| VERDICT.md | β | 5.7 KB | β raw | |
| trigger.sh | β | 1.6 KB | view raw | |
| run.sh | β | 1.2 KB | view raw | |
| run.log | β | 1.9 KB | view raw | |
| run.2.log | β | 986 B | view raw | |
| run.3.log | β | 1.6 KB | view raw | |
| panic.txt | β | 1.2 KB | view raw | |
| env.txt | β | 1.1 KB | view raw | |
| fix.diff | β | 1.5 KB | view raw | |
| manifest.json | β | 1.1 KB | view raw | |
| verdict.json | β | 3.8 KB | view raw |
DF-2928 β dead spin_lock_test_mode: debug.spin_lock_test=1 panics INVARIANTS kernels after 60s
What it is
sys/kern/kern_spinlock.c β the INVARIANTS-only sysctl debug.spin_lock_test
(value 1, "Test the indefinite wait code") is broken because the variable it
sets is dead code:
sys/kern/kern_spinlock.c:91declaresstatic int spin_lock_test_mode;- the handler sets it around the deliberate recursive
spin_lock(kern_spinlock.c:419-424) intending to make the contested wait break - nothing reads it:
indefinite_check()(sys/sys/indefinite2.h) honors a different global,lock_test_mode(sys/kern/kern_lock.c:71), so the wait never breaks - after 60 seconds
indefinite_check()panics unconditionally for spinlock types (indefinite2.h:183-186):panic("spin_lock_ex: %s, indefinite wait!")
The 2017 rewrite of the indefinite-wait machinery (indefinite2.h) moved the
test-mode check to lock_test_mode and dropped the spinlock-specific
variable; upstream master (2026-09) still has the dead static, so this is
unfixed upstream.
Impact
- The calling CPU spins ~60 s inside a critical section (preemption and interrupts masked on that CPU), then the kernel panics.
- Requires root-equivalent privilege (
caps_priv_check_self(SYSCAP_RESTRICTEDROOT),kern_spinlock.c:409) and an INVARIANTS kernel β severity Low. - Verified: an unprivileged user gets "Operation not permitted".
Build / Run
No compile needed β sysctl-only PoC.
# on the guest, as root: sysctl -w debug.spin_lock_test=1 # blocks 60s, then panic
Or via the harness: ./run.sh (uses dfbsd-qemu/vm.sh; see VERDICT.md for the
recorded session).
Expected output
Live msgbuf while it spins (one line per second, never breaks):
spin_lock_ex: sysctl_spin_lock_test, indefinite wait (1 secs)! ... spin_lock_ex: sysctl_spin_lock_test, indefinite wait (11 secs)! sysctl_spin_lock_test() at sysctl_spin_lock_test+0x233 <- INVARIANTS backtrace ... spin_lock_ex: sysctl_spin_lock_test, indefinite wait (57 secs)! [at 60 secs] panic: spin_lock_ex: sysctl_spin_lock_test, indefinite wait!
Guest then stops answering ssh (vm.sh status -> down; vm.sh reports
"guest not answering (likely DDB on panic)").
Fix
fix.diff restores the pre-2017 semantics: make indefinite_check() honor
spin_lock_test_mode for spinlock wait types ('S'/'s') so the test sysctl
breaks the wait after the first 1-second report instead of running into the
60-second panic. (Alternative: delete the value==1 test branch entirely.)
DF-2928 β VERDICT
Status: reproduced (3 independent runs; kernel panic at exactly T0+60s in each) Impact: panic (root-gated kernel panic of an INVARIANTS kernel; DoS-class, no memory corruption, no unpriv path) Confidence: certain (source-proof + runtime progression + privilege gate checked)
Root cause (path:line)
The debug.spin_lock_test sysctl handler for value 1 performs a deliberate
recursive spin_lock to exercise the indefinite-wait machinery and sets a
flag so the wait breaks immediately:
sys/kern/kern_spinlock.c:91static int spin_lock_test_mode;β file-staticsys/kern/kern_spinlock.c:419-424c spin_lock(&spin); /* acquire */ spin_lock_test_mode = 1; /* DEAD: no reader anywhere in sys/ */ spin_lock(&spin); /* recursive -> _spin_lock_contested */ spin_unlock(&spin); spin_unlock(&spin); spin_lock_test_mode = 0;
The escape it expects lives in indefinite_check():
sys/sys/indefinite2.h:171-176(INVARIANTS) β breaks the wait only when the globallock_test_mode(definedsys/kern/kern_lock.c:71) is set:c if (lock_test_mode) { print_backtrace(-1); return TRUE; }spin_lock_test_modeis never consulted (grep acrosssys/: written at kern_spinlock.c:420/424, declared at :91, read nowhere).- With no break, the wait runs to
sys/sys/indefinite2.h:183-186(always compiled, no INVARIANTS guard):c if (info->secs == 60 && (info->type == 's' || info->type == 'S')) panic("%s: %s, indefinite wait!", str, info->ident);
So the "test the indefinite wait code" hook is instead a fixed 60-second delayed kernel panic. Upstream master (checked 2026-09-03, GitHub mirror DragonFlyBSD/DragonFlyBSD) still contains the dead static β not fixed there.
The bug was introduced when the indefinite-wait code was rewritten into
sys/sys/indefinite2.h (2017) around the generic lock_test_mode; the
spinlock file kept setting its own private variable.
Reproduction (recorded 2026-09-03, guest see env.txt)
-
Privilege gate:
run_user 'sysctl -w debug.spin_lock_test=1'->sysctl: debug.spin_lock_test=1: Operation not permitted(rc=1). Gate:caps_priv_check_self(SYSCAP_RESTRICTEDROOT)at kern_spinlock.c:409. -
T0 = 14:59:40 (run 3):
dmesg -c; detachedsysctl -w debug.spin_lock_test=1. -
T0+22s = 15:00:02: live msgbuf shows the wait running unbroken with the 1-second reports and the INVARIANTS 11-second backtrace pinning the spinner inside the handler itself:
spin_lock_ex: sysctl_spin_lock_test, indefinite wait (1 secs)! ... (one per second) ... spin_lock_ex: sysctl_spin_lock_test, indefinite wait (11 secs)! sysctl_spin_lock_test() at sysctl_spin_lock_test+0x233 0xffffffff8067e4b3 sysctl_spin_lock_test() at sysctl_spin_lock_test+0x233 0xffffffff8067e4b3 ... up to (18 secs)! [21 "indefinite" lines total]The handler setspin_lock_test_mode=1(kern_spinlock.c:420) before the secondspin_lock, so if any code honored it the wait would have broken at the first report. It did not β proving the dead variable at runtime. -
T0+57s = 15:00:37: msgbuf shows
(57 secs)!β still no break. -
T0+60s β 15:00:40:
panic("spin_lock_ex: sysctl_spin_lock_test, indefinite wait!")β guest stops answering ssh;vm.sh status-> down at 15:01:17; vm.sh reports "guest not answering (likely DDB on panic)".
Run 1 (T0=14:33:24) and run 2 (T0=14:47:00) died identically at T0+60s (run 2 had debug.debugger_on_panic=0; the dump path did not complete because the panicking thread holds 2 spinlocks + a crit section β kern_shutdown.c:823-825 zeroes gd_spinlocks, but the box wedged in the dump; no core file was saved on any run).
Why the panic is certain (not just "likely")
Source path is fully deterministic: the recursive lock state is
lock = 0x100001 (low bit held by the handler itself + 1 EXCLWAIT unit
queued by _spin_lock_contested, kern_spinlock.c:206), the transfer at
kern_spinlock.c:238-246 can never fire (low bits != 0), so indefinite_check
is the only exit and its 60-second branch panics. Observed on 3/3 runs with
death at exactly T0+60s(+dump/DDB wedge).
Adjacent hazard worth noting (not separately filed)
If the test mode were honored (i.e. after applying fix.diff), the break at
kern_spinlock.c:250-251 returns without the lock held, and the handler's
two spin_unlocks then decrement a lock word that carries the leaked EXCLWAIT
unit: 0x100001 - 1 - 1 = 0x0FFFFF β a poisoned lock word. On this sysctl
it is harmless (stack-local spin), but it demonstrates why
break-without-acquire in _spin_lock_contested is dangerous by design; the
only production reachability of that break is panicstr != NULL
(indefinite2.h:169-170), i.e. post-panic, which is acceptable.
Exploit chain
None β no memory corruption; not an escalation primitive. Ceiling: root-user self-DoS of a debug (INVARIANTS) kernel. Unprivileged trigger denied by capability check (verified live).
Fix validation
Not performed (fix_status: not_testable). This is a Low, root-gated,
non-corruption finding; a kernel rebuild was judged out of proportion.
fix.diff restores the pre-2017 semantics and is mechanically simple:
spin_lock_test_mode becomes non-static, declared extern in
sys/sys/indefinite.h, and indefinite_check() breaks spinlock-type waits when
it is set. Expected patched behavior: sysctl -w debug.spin_lock_test=1
returns after ~1 s with exactly one "indefinite wait (1 secs)!" report and a
backtrace; guest stays up.
Guest state
Guest was reset to the clean with-src snapshot after the final run
(vm.sh reset with-src); it is up and clean.
Fix verification
not_testablefix.diff authored against the read-only sys/ tree (restores pre-2017 semantics); kernel rebuild + patched rerun not performed for this Low, root-gated, non-corruption finding β expected patched behavior: sysctl returns after ~1s with a single indefinite-wait report and a backtrace, guest stays up.
['fix.diff']
Confirmed kernel references
- sys/kern/kern_spinlock.c:91
- sys/kern/kern_spinlock.c:409
- sys/kern/kern_spinlock.c:417
- sys/kern/kern_spinlock.c:419
- sys/kern/kern_spinlock.c:420
- sys/kern/kern_spinlock.c:424
- sys/kern/kern_spinlock.c:206
- sys/kern/kern_spinlock.c:238
- sys/kern/kern_spinlock.c:249
- sys/sys/indefinite2.h:171
- sys/sys/indefinite2.h:183
- sys/kern/kern_lock.c:71
Detail
Evidence (decisive lines)
["run.log: T0+22s msgbuf β 'indefinite wait (1..18 secs)!' accumulating, handler backtrace, no break despite spin_lock_test_mode=1 being set at T0", "run.2.log: T0+57s msgbuf '(57 secs)!' then vm.sh status=down at T0+~80s (panic at T0+60s)", "run.3.log: two independent earlier runs dying at T0+60s; vm.sh 'likely DDB on panic'", "panic.txt: source-determined panic string 'panic: spin_lock_ex: sysctl_spin_lock_test, indefinite wait!' + full path", 'env.txt: guest uname, INVARIANTS kernel, sysctl defaults, unpriv denial']
PoC changes
Seed sketch replaced: no compilable PoC is needed (sysctl-only trigger); trigger.sh/run.sh encode the exact vm.sh session; panic signature recovered from source + live msgbuf because no crash dump completes (panicking thread holds 2 spinlocks + crit section).
Verified recommended fix
Make indefinite_check() honor spin_lock_test_mode for spinlock wait types ('S'/'s') β de-staticize the variable, extern it in sys/indefinite.h, add a type-gated break beside the lock_test_mode check (see fix.diff); alternatively delete the value==1 test branch.
Verdict
The INVARIANTS test hook debug.spin_lock_test=1 sets the write-only static spin_lock_test_mode (kern_spinlock.c:91,420,424 β no reader exists in sys/), while indefinite_check() honors only kern_lock.c's lock_test_mode (indefinite2.h:171-176); the deliberate recursive spin_lock therefore never breaks and hits the unconditional 60-second panic at indefinite2.h:183-186. Reproduced three times on the stock INVARIANTS guest: msgbuf shows the wait running unbroken 1..57+ secs with the handler's backtrace (sysctl_spin_lock_test+0x233), guest dies at exactly T0+60s. Unprivileged writes are denied by caps_priv_check_self(SYSCAP_RESTRICTEDROOT) (kern_spinlock.c:409, verified live), so impact is a root-gated kernel panic/DoS of debug kernels β severity Low. Not fixed upstream (checked master 2026-09-03).
No comments yet.