Resuming a DDB session entered from interrupt context (any breakpoint in clock/interrupt-path code) deterministically panics: td_critcount underflow at session exit
Summary
b hardclock + c + d hardclock + c panics 3/3 boots with 'td_critcount is/would-go negative' at the session-exit crit_exit; thread-context sessions on the same kernel resume cleanly - the trigger is specifically interrupt-context entry. One decrement goes unmatched between kdb_trap's crit_enter and the exit; the db_run engine is exonerated (identical state in the surviving controls). Debugging ANY interrupt-context code with the standard breakpoint workflow converts the first continue into a guaranteed panic+dump+reboot. Also cross-checked the DF-5720 SMP resume wedge: identical path on UP resumes cleanly, so the wedge is the SMP stop_cpus/restart_cpus exit layer. save/restore-critcount fix proposed (3 vmcores preserved for the upstream unwind).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5722 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.3 KB | β raw | |
| VERDICT.md | β | 4.5 KB | β raw | |
| run4-minimal-panic.serial.log | β | 1.7 KB | view raw | |
| run5-threadctx-control.serial.log | β | 1.5 KB | view raw | |
| up-panics.fullserial.log | β | 29.2 KB | view raw | |
| panic.watch.serial.log | β | 30.2 KB | view raw | |
| fix.diff | β | 1.3 KB | view raw | |
| verdict.json | β | 3.5 KB | view raw |
DF-5722 β Resuming DDB entered from interrupt context panics the kernel
Severity: Low (root-console trigger; deterministic kernel panic β any attempt to debug interrupt-path code with breakpoints crashes the box)
Setting a DDB breakpoint in any code that runs in interrupt context (e.g.
b hardclock) and then continuing (c) deterministically panics on
resume:
panic: td_critcount is/would-go negative! 0xfffff8008f313f00 -1 cpuid = 0 Trace beginning at frame 0xfffff801154ab548 crit_panic() at crit_panic+0x2f 0xffffffff80672cff crit_panic() at crit_panic+0x2f 0xffffffff80672cff crit_exit_wrapper() at crit_exit_wrapper+0x3c 0xffffffff806734ac
Reproduced 3/3 boots (with variations: + software watchpoint, + invisible-
step roundtrip, and the minimal single-session recipe). Thread-context DDB
sessions on the same kernel resume cleanly (multiple controls, incl. a
breakpoint hit in sys_kill resumed with c β machine alive), so the
trigger is specifically the interrupt-context session.
Minimal recipe (run4-minimal-panic.serial.log)
db> b hardclock db> c CPU0 restarting CPUs: 0x0000000000000000 restarted CPU0 stopping CPUs: 0x00000000 stopped Breakpoint at hardclock: pushq %rbp db> d hardclock db> c CPU0 restarting CPUs: 0x0000000000000000 restarted panic: td_critcount is/would-go negative! 0xfffff8008f313f00 -1
One DDB session, entered from the clock-interrupt path via the INT3, no
step-over, no watchpoint, no other state. The panic fires as the session
exits (crit_exit() at db_interface.c:201 β crit_exit_wrapper() β
_crit_exit β crit_panic, sys/kern/lwkt_thread.c:1766-1781): the
thread's td_critcount was decremented once too often across the session.
Bisect matrix (all on the same stock kernel)
| # | session context | sequence | result |
|---|---|---|---|
| E1-UP | thread (sysctl Debugger) | enter, c |
ALIVE |
| E2 | thread | enter, s, c (TF roundtrip) |
ALIVE |
| run5a | thread | b sys_kill, c, hit, c |
ALIVE |
| run5b | thread | b sys_kill,0, c, hits |
stops each hit, ALIVE |
| run2 | thread | watch ticks, c |
ALIVE (watch inert: DF-5721) |
| run1 | interrupt (hardclock bp) + watch | 2 sessions + watch + c |
PANIC |
| run3 | interrupt (hardclock bp) | 2 sessions (with invisible step), c |
PANIC |
| run4 | interrupt (hardclock bp) | 1 session, d, c |
PANIC |
db_run.c's engine executes identical, context-agnostic code in every row
(state captured live: db_run_mode transitions, rfl TF-clear at every
stop, DR registers idle) β the defect is in the machine layer's crit
accounting across kdb_trap() for interrupt-context sessions
(sys/platform/pc64/x86_64/db_interface.c:160-201 crit_enter/crit_exit,
in interaction with the interrupt entry/exit paths'
TD_CRITCOUNT bookkeeping, ipl.s/exception.S/interrupt stubs).
Filed from the sys/ddb/db_run.c pass-2 audit (where it was discovered and characterized via db_run.c's step/continue/breakpoint machinery); the fix owner is db_interface.c / ipl.s.
Threat model
Requires a DDB breakpoint in interrupt-context code β console/root only (unprivileged reachability probes all negative, see DF-5721 pack). Not an escalation; it is a debugger-robustness defect: any root operator who sets a breakpoint in a clock/interrupt/ITHREAD path gets a guaranteed panic + crash dump + reboot instead of a debugging session. Three crash dump sets were taken on the guest (/var/crash info.0-2, kern.0-2, vmcore.0-2) for offline analysis.
Files
run4-minimal-panic.serial.logβ minimal recipe + panicrun5-threadctx-control.serial.logβ thread-context controls (alive)up-panics.fullserial.logβ full serial history (all runs on final boot)panic.watch.serial.logβ run1 (watch + interrupt sessions) panicfix.diffβ defensive kdb_trap critcount save/restoreverdict.json/manifest.json
Reproduce
./../DF-5721/serial_vm1.sh up(UP guest β on SMP guests the resume path wedges earlier in stop_cpus/restart_cpus, DF-5720 lead, and the recipe does not complete)ssh -F dfbsd-qemu/config dfbsd 'sysctl debug.enter_debugger=ddb' &- On serial:
b hardclock,c, wait for "Breakpoint at hardclock", thend hardclock,cβ panic + dump + auto-reboot.
DF-5722 β VERDICT
Status: reproduced (deterministic, 3/3 boots, stock X86_64_GENERIC snapshot kernel on a 1-vCPU QEMU/KVM guest with socket serial console). Impact: panic (root-console-triggerable kernel panic; debugger robustness defect, not privilege escalation). Confidence: certain for the trigger and the layer attribution; the exact leak site inside the session window is narrowed to db_interface.c:160-201 + interrupt entry/exit bookkeeping and would need dump analysis to pin to a single instruction (three vmcores are preserved on the guest /var/crash).
Narrative
During the pass-2 audit of sys/ddb/db_run.c I live-verified the whole
step/continue engine on a UP guest (thread-context sessions all clean:
TF set/clear symmetry β rfl=0x246 TF-clear at every stop after s;
breakpoint dispatch and count handling β "Breakpoint at" banners, count
reset, STEP_INVISIBLE invisible-step roundtrips; continue paths). While
verifying the breakpoint dispatch in interrupt context (b hardclock),
every resume panicked:
panic: td_critcount is/would-go negative! <td> -1
crit_panic β crit_panic β crit_exit_wrapper
(sys/kern/lwkt_thread.c:1772-1781, called from crit_exit() at
sys/platform/pc64/x86_64/db_interface.c:201 β the C wrapper, the only
crit_exit_wrapper call site on this path)
Bisect (full matrix in README):
- thread-context sessions: sysctl-entry, single-step, breakpoint-hit in
sys_kill resumed with c β ALL alive.
- interrupt-context session (breakpoint in hardclock, the clock path):
PANIC on resume β minimal recipe = b hardclock, c, d hardclock,
c (run4-minimal-panic.serial.log).
- The DF-5721 software-watchpoint was exonerated by run #3 (panic without
any watchpoint).
- The SMP-guest starvation (DF-5720 lead) is a separate failure: on the
SMP guest the second kdb_trap entry's stop_cpus hangs before any
resume completes; on UP (stop_cpus no-op) the resume completes and the
interrupt-context panic is what surfaces. Both live-captured; neither
is db_run.c (see below).
Why db_run.c is exonerated (this audit's file)
- The engine is context-agnostic: db_stop_at_pc/db_restart_at_pc ran
identical code in the surviving thread-context controls and the
panicking interrupt-context runs; captured state identical
(
db_run_mode0/4/5 transitions per code, TF clear at stops, DRs idle, breakpoint list operations symmetric). - The panic fires in the machine layer's critical-section accounting around/inside kdb_trap (db_interface.c:160-201) in interaction with interrupt entry/exit TD_CRITCOUNT bookkeeping (ipl.s doreti/doreti_soft temporary decls at ipl.s:281-283/304-306, interrupt stub guarded inc/dec msi_vector.s:56-79): across an interrupt-context session one td_critcount decrement goes unmatched, and the session-exit crit_exit() underflows (0 β -1 β crit_panic).
Impact
Root console only (all unprivileged entry routes closed β probes in the
DF-5721 pack: user int3 β SIGTRAP; PT_STEP artifact filtered;
PT_SETDBREGS kernel-addr EINVAL; sysctl door EPERM). Consequence for the
privileged operator: any breakpoint in interrupt-context code (clock,
ITHREAD, driver interrupt paths β the bread and butter of kernel
debugging) converts the first c into a panic + dump + reboot. Local
DoS requires root, so: Low severity, panic/dos impact class.
Fix
fix.diff (defensive, authored against the read-only sys/ tree):
kdb_trap saves curthread->td_critcount after its crit_enter() and
force-restores it before returning (replacing the bare crit_exit() at
db_interface.c:201). This makes the debugger session transparent to the
interrupted context's crit accounting regardless of where inside the
session the count leaked, and directly prevents the observed
would-go negative exit. NOT validated by rebuild+rerun: the panic
recipe requires a UP guest (SMP guests wedge in stop_cpus first, a
different defect) and a kernel rebuild on the 1-vCPU guest was not
feasible in-run. fix_status=not_testable. The upstream-quality fix is to
find the unmatched decrement (dump analysis of the preserved vmcores);
the save/restore is the safe belt that stays correct even then.
Fix status (batch validation)
Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.
Fix verification
not_testablefix.diff authored post-verification against the read-only sys/ tree; not applied/rebuilt β the panic recipe needs a UP guest and a kernel rebuild on the 1-vCPU guest was not feasible in-run. Baseline (panic) reproduced 3/3.
findings/poc/DF-5722/fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
run4-minimal-panic.serial.log (b hardclock / c / Breakpoint at hardclock / d hardclock / c / panic + crit_panic trace); run5-threadctx-control.serial.log (thread-context bp hit + c = alive); panic.watch.serial.log (run1 variant); README.md bisect matrix; three vmcores preserved in guest /var/crash
PoC changes
No seed PoC. Built from scratch: UP/SMP socket-serial QEMU rigs, paced serial DDB driver, root-sysctl DDB entry, and the bisect battery across 6 boots (3 panic runs, 3 controls).
Verified recommended fix
kdb_trap: save curthread->td_critcount after crit_enter() and force-restore it at session exit instead of bare crit_exit() (see fix.diff); upstream-quality fix is to find the unmatched decrement in the interrupt-entry/polled-console path.
Verdict
Resuming a DDB session that was entered from interrupt context (e.g. any breakpoint in clock/interrupt-path code such as 'b hardclock') deterministically panics the kernel with 'td_critcount is/would-go negative' at the session-exit crit_exit() (db_interface.c:201 -> crit_exit_wrapper -> crit_panic, lwkt_thread.c:1772): one td_critcount decrement goes unmatched across the session. 3/3 boots on the stock snapshot kernel (UP guest); thread-context controls on the same kernel (sysctl-entry, single-step roundtrip, breakpoint hit in sys_kill resumed with 'c') are all clean, and the DF-5721 software watchpoint was exonerated by a no-watch bisect run. db_run.c's engine (the audited file) is context-agnostic and exonerated by identical captured state in surviving vs panicking runs; the defect lives in the machine layer's crit accounting around kdb_trap for interrupt-context sessions. Root-console-only trigger (all unprivileged routes probed closed), so: Low severity, panic class β the practical cost is that debugging any interrupt-path code with DDB crashes the box.
No comments yet.