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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5722 Β· 8 files
FileTypeDescriptionSize
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 + panic
  • run5-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) panic
  • fix.diff β€” defensive kdb_trap critcount save/restore
  • verdict.json / manifest.json

Reproduce

  1. ./../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)
  2. ssh -F dfbsd-qemu/config dfbsd 'sysctl debug.enter_debugger=ddb' &
  3. On serial: b hardclock, c, wait for "Breakpoint at hardclock", then d hardclock, c β†’ panic + dump + auto-reboot.
VERDICT.md
↓ download raw

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_mode 0/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_testable
baseline reproduced→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-5722

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.