kdb_trap() longjmp re-entry double-increments db_active (DF-5561 twin): the permanent leak routes every subsequent trap into trap_fatal()+kdb_trap() β permanent all-CPU ddb wedge of the whole vkernel, surviving even panic
Summary
The exact DF-5561 pc64 defect: a trap in a live ddb session longjmps back to the setjmp, re-executing db_active++ while the abandoned invocation's decrement never runs - after session exit db_active is stuck >=1 forever and user_trap/kern_trap/trap_fatal divert EVERY trap (ordinary user page faults, ASTs) into trap_fatal+all-CPU stop. The vkernel is one host process hosting every tenant, so the wedge bricks the entire VM: live-vkernel repro with counter 1->2 proven at the nested prompt, post-continue user-trap storm (type 7, PID 1 sh), nested-fault cascade, shell input eaten by ddb, and the exit-EOF panic re-entering Debugger() - wedge survives panic. count-only-first-entry fix validated (counter 1->1, fault delivered as normal SIGSEGV, clean shutdown). No memory corruption, operator/root-triggered deterministic DoS.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5571 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 5.4 KB | β raw | |
| VERDICT.md | β | 7.7 KB | β raw | |
| run.baseline.log | β | 4.4 KB | view raw | |
| run.fixed.log | β | 2.6 KB | view raw | |
| run.log | β | 4.4 KB | view raw | |
| fix_run.log | β | 2.6 KB | view raw | |
| build.log | β | 933.6 KB | β download | |
| fix_build.log | β | 933.6 KB | β download | |
| vk_ddb_session.sh | β | 1.8 KB | view raw | |
| mk_image.sh | β | 795 B | view raw | |
| vkinit.c | β | 328 B | view raw | |
| fault.c | β | 210 B | view raw | |
| linkfix-df5474.diff | β | 532 B | view raw | |
| fix.diff | β | 1014 B | view raw | |
| hashes.txt | β | 233 B | view raw | |
| verdict.json | β | 6.0 KB | view raw |
DF-5571 β vkernel64 kdb_trap() longjmp re-entry double-increments db_active (DF-5561 twin); the leak routes every subsequent trap into trap_fatal()+kdb_trap() β permanent all-CPU ddb wedge of the whole vkernel
What (twin of DF-5561, pc64)
kdb_trap() in sys/platform/vkernel64/x86_64/db_interface.c:
150: if (db_global_jmpbuf_valid)
151: longjmp(db_global_jmpbuf, 1); /* nested trap while ddb is live */
...
167: setjmp(db_global_jmpbuf); /* re-entry lands here */
168: db_global_jmpbuf_valid = TRUE;
169: db_active++; /* runs AGAIN on re-entry */
...
177: db_active--; /* the abandoned outer call's
decrement NEVER runs */
A fault taken while a ddb session is live (e.g. the ddb call 0x10() β
db_fncall arms no fault guard) re-enters kdb_trap(), which longjmps back
to the setjmp at :167; the increment at :169 executes a second time while
the abandoned outer invocation's db_active-- at :177 never runs. After the
session finally exits, db_active is stuck at >= 1 forever.
Consequence (vkernel64 trap.c routing):
- user_trap() β trap.c:366: if (db_active) { trap_fatal(frame, TRUE, eva); ... } β every USER-mode trap
- kern_trap() β trap.c:605: same for kernel traps
- trap_fatal() β trap.c:959: if ((debugger_on_panic || db_active) && kdb_trap(...)) return;
so with the leaked counter, every ordinary trap (user page faults, ASTs,
forced exceptions) is diverted into trap_fatal() + kdb_trap() β
stop_cpus() (all-CPU stop via pthread_kill on the vcpu threads,
mp.c:230-244) β db> prompt. The vkernel β a single host process hosting
every guest "process" β wedges permanently in ddb; even the panic path
re-enters ddb.
Trust model
Identical to DF-5561: requires an interactive ddb session in the vkernel
(console operator, sysctl debug.enter_debugger=ddb as root inside the
vkernel, SIGQUIT on the vkernel process = exc_debugger() β Debugger(),
or a panic with debugger_on_panic). A ddb command that faults (e.g.
call <bad addr>()) then poisons the session; every subsequent continue
wedged the entire machine. Medium / local-DoS, deterministic.
Reproduce (as run on the QEMU guest, 2026-10-07)
Build prerequisites:
1. stock make nativekernel KERNCONF=VKERNEL64 fails to link (DF-5474:
__build_id_start/end undefined) β apply linkfix-df5474.diff in the
guest scratch tree first (validated link+boot, same as DF-5474).
2. sh mk_image.sh β pristine 96MB UFS /vkroot.img:
/sbin/init=vkinit (opens /dev/console, execs /bin/sh),
/bin/sh, /bin/sleep, /bin/fault (derefs 0x10),
/lib/libc.so.8, /libexec/ld-elf.so.2.
(vkinit is required: with -e init_path=/bin/sh the kernel-exec'd sh
exits instantly β "init died (signal 0, exit 0)".)
sh vk_ddb_session.sh /usr/obj/usr/src/sys/VKERNEL64/kernel.debug baseline 0x11150a8
The driver (one ssh session) boots the vkernel (-m 256m -n 2 -s, fifo
console), waits for userspace, then performs the trigger sequence:
kill -QUIT <child> (the vkernel re-forks at startup, init.c:203; the
parent ignores SIGQUIT) β at db>:
x/x 0x11150a8 # &db_active from nm β vkernel ddb has no symtab call 0x10() # fault inside ddb -> longjmp re-entry x/x 0x11150a8 # counter now 2 c # continue; counter decrements to 1 (leaked) /bin/fault # from the shell: first user trap after continue echo POSTCONT-ALIVE # never executes in baseline
Expected β baseline (run.baseline.log)
- first prompt:
0x11150a8: 1 - after
call 0x10()β "Fatal trap 12 ... ip=0x2b:0x10" β "kernel: type 12 trap" β re-entereddb>:0x11150a8: 2β the double-increment - after
c:Fatal trap 7: system forced exception while in user mode, current process = 1 (sh)βkernel: type 7 trapβ CPU0 stopping CPUs βStopped at ...(no prompt echo), then a nested Fatal trap 12 cascade echo POSTCONT-ALIVEβNo such command(input now owned by ddb β the shell/whole OS is dead)- driver exit closes the fifo β
panic: vconsgetc: EOFβ which re-enters ddb AGAIN (Debugger("panic")) β the wedge survives even a panic.
Expected β fixed (run.fixed.log)
- first prompt:
0x11150a8: 1 - after
call 0x10()re-entry:0x11150a8: 1(guardif (db_active == 0)) - after
c:/bin/faulthandled as a normal user SIGSEGV:pid 2 (fault), uid 0: exited on signal 11 / Segmentation fault echo POSTCONT-ALIVEβ printed by the shell- "Fatal trap" count: 1 (only the intentional
call 0x10()one) vs 5 in baseline; cleanCaught SIGTERM ... Shutting down...
Fix validation
fix.diff (authored after verification, never applied to the audit tree):
guard the re-entry increment. Applied in the guest scratch tree, vkernel
rebuilt (build.log, RC=0, db_interface.o recompiled), identical sequence
re-run: counter stays 1, no storm, no wedge. Fixed.
Files
run.baseline.log/run.fixed.logβ full untrimmed vkernel consolesvk_ddb_session.shβ the phase driver (trigger sequence)mk_image.sh,vkinit.c,fault.cβ root-image riglinkfix-df5474.diffβ prerequisite DF-5474 link fixfix.diffβ the fix;build.logβ patched rebuildenv.txt,hashes.txtVERDICT.md,manifest.json,verdict.json
DF-5571 VERDICT β REPRODUCED (deterministic, live vkernel64 on the QEMU guest)
Bottom line
The DF-5561 lead is confirmed byte-for-byte on the vkernel64 twin.
kdb_trap()'s db_global_jmpbuf re-entry path re-executes db_active++
(sys/platform/vkernel64/x86_64/db_interface.c:169) while the abandoned
outer invocation's db_active-- (:177) never runs, permanently leaking
db_active >= 1; user_trap() (trap.c:366) and kern_trap()
(trap.c:605) then route every subsequent trap β including ordinary
user-mode traps β into trap_fatal() + kdb_trap(), wedging the entire
vkernel in ddb. Verified on a live vkernel booted inside the QEMU guest:
counter read 1 -> call 0x10() fault -> counter read 2 at the re-entered
prompt -> c -> user-mode trap 7 from PID 1 (sh) taken to trap_fatal +
all-CPU stop + ddb + nested-fault cascade; echo POSTCONT-ALIVE eaten by
ddb ("No such command"); even the driver-exit EOF panic re-entered ddb.
With the one-line guard (if (db_active == 0) db_active++;), the same
sequence keeps the counter at 1, /bin/fault gets normal SIGSEGV
delivery, the shell answers, and the machine shuts down cleanly. Fix
validated on a rebuilt vkernel.
Mechanism (path:line)
- Entry: SIGQUIT on the vkernel child process β
exc_debugger()(exception.c:277) βDebugger()(db_interface.c:340) βbreakpoint()β host SIGTRAP βexc_segfault()βkern_trap()β T_BPTFLT βkdb_trap(3, 0, frame)(trap.c:714 area). (Also reachable viasysctl debug.enter_debugger=ddbβ db_sysctl.c:73 and via any panic.) - First entry:
ddb_regs = *regs(:156),crit_enter()(:158),stop_cpus(:163),setjmp(db_global_jmpbuf)(:167),db_global_jmpbuf_valid = TRUE(:168),db_active++(:169) β 1,db_trap()prompt. - Nested fault inside ddb:
call 0x10()executes in the vkernel process; unmapped β host SIGSEGV βexc_segfaultβkern_trapβdb_activenonzero β trap.c:605 βtrap_fatal(prints "Fatal trap 12 ... instruction pointer = 0x2b:0x10") β nestedkdb_trap(12, ...); switch default βdb_printf("kernel: type 12 trap, code=0")(:136) βdb_nofaultNULL (db_fncall arms no guard) βdb_global_jmpbuf_validTRUE βlongjmp(db_global_jmpbuf, 1)(:150-151). - The longjmp restores the FIRST invocation's
setjmpcontext (:167); execution continues:db_active++(:169) β 2; the nested invocation's frame (and its would-bedb_active--at :177) is abandoned. Re-entereddb_trap()prompt. c:db_active--(:177) β 1 (leaked),restart_cpus(:192), regs restored fromddb_regs(first trap's frame β the nested invocation longjmp'd beforeddb_regs = *regsat :156, so ddb_regs still holds the original frame),return 1(:225) β the SIGQUIT entry unwinds normally.- Leak in action: the next user-mode trap (observed: "Fatal trap 7:
system forced exception while in user mode, current process = 1 (sh)")
hits trap.c:366 β
trap_fatalβkdb_trapβ stop_cpus β ddb β no path back to userland. Subsequent faults cascade inside the wedged ddb (nested Fatal trap 12 while printing the stop line), and the driver-exit EOF reachedpanic("vconsgetc: EOF")which itself re-enteredDebugger("panic")β the wedge survives panic.
Reproduction runs (guest: DragonFly 6.5-DEVELOPMENT, QEMU/KVM)
Baseline (run.baseline.log, stock vkernel built from the audited tree + DF-5474 link fix):
db> x/x 0x11150a8 # &db_active (nm: 00000000011150a8 B db_active)
0x11150a8: 1
db> call 0x10()
Fatal trap 12: page fault while in kernel mode ... ip=0x2b:0x10
kernel: type 12 trap, code=0
Stopped at 0x71cad9 ...
db> x/x 0x11150a8
0x11150a8: 2 # <-- DOUBLE INCREMENT
db> c
CPU0 restarting CPUs ... restarted
Fatal trap 7: system forced exception while in user mode
current process = 1 (sh) # ordinary user trap -> trap_fatal
kernel: type 7 trap, code=4
CPU0 stopping CPUs ... stopped # all-CPU stop on a user AST-class trap
[nested Fatal trap 12 cascade]
db> echo POSTCONT-ALIVE
No such command # shell dead; ddb owns the console
[driver exit -> panic: vconsgetc: EOF -> Debugger("panic") -> ddb again]
Fixed (run.fixed.log, fix.diff applied + rebuilt, RC=0):
db> x/x 0x11150a8 0x11150a8: 1 db> call 0x10() Fatal trap 12 ... kernel: type 12 trap, code=0 db> x/x 0x11150a8 0x11150a8: 1 # <-- GUARD HELD db> c /bin/fault fault: about to deref 0x10 pid 2 (fault), uid 0: exited on signal 11 Segmentation fault # normal user SIGSEGV delivery # echo POSTCONT-ALIVE POSTCONT-ALIVE # shell alive Caught SIGTERM from host system. Shutting down...
"Fatal trap" occurrences: baseline 5 vs fixed 1 (the intentional one).
Rig notes (what had to be fixed vs the naive sketch)
- vkernel ddb has no symbol table:
print *db_active/print *(int*)addrboth fail ("Symbol not found") β read the counter by literal address withx/x(address fromnm kernel.debug, stable across the patched relink: 0x11150a8 both builds). - The vkernel re-forks at startup (init.c:203) and the parent sets SIGQUITβSIG_IGN (init.c:210): signal the CHILD, not the launcher pid.
exec -ais not supported by the guest /bin/sh.- With
-e init_path=/bin/shthe kernel-exec'd sh dies instantly ("init died (signal 0, exit 0)") β use a /sbin/init (vkinit.c) that opens /dev/console itself and execs sh. - The vkernel console input in ddb is
vconsgetc()= blocking pread(0) (console.c:404-420): drive it through a fifo whose write end is held open for the whole session, or EOF βpanic("vconsgetc: EOF"). - The vkernel boot wipes the MBR of its root image (vkd writes): rebuild the image pristine (mk_image.sh) before every phase.
- Stock
make nativekernel KERNCONF=VKERNEL64does not link on this tree (DF-5474): apply linkfix-df5474.diff first. - Fault/trap host signals are SA_NODEFER + empty mask (exception.c:223-228) so the abandoned nested handler leaves no signal-mask residue β the only lasting corruption is the db_active leak itself.
Severity / impact ceiling
Medium (local DoS), matching DF-5561: the trigger needs an interactive ddb session (console operator / root inside the vkernel / panic-time debugger_on_panic), but a single faulting ddb command then bricks the whole virtual machine with no dump, no reboot, and no remote access. No memory corruption is involved (the primitive is a leaked integer flag), so there is no uid-0 route; impact ceiling is permanent wedge of the vkernel process and everything hosted in it.
Adjacent observations (not filed β different file)
- trap.c:368/607
gd_trap_nesting_levelbalances its own ++/-- around trap_fatal, so the longjmp abandonment does NOT leak it in vkernel64 (unlike pc64's trap.c noted in DF-5561). - vkernel64 lacks pc64's
if (panicstr == NULL)guard around the stopped_cpus mask check / restart_cpus (db_interface.c:185-192 vs pc64): a stop_cpus partial failure during a panic-time ddb entry panics recursively. Triggerability marginal (stop_cpus fails only on pthread_kill error, mp.c:240). vconsgetc()panics on console EOF (console.c:416) β rig fragility, not attacker-reachable beyond the wedge itself.- db_read_bytes/db_write_bytes (:231-307): identical nofault-guard shape to pc64, coherent; the RW-text remap code is #if 0'd in both.
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
fixedApplied fix.diff (guard) in the guest scratch tree, rebuilt the vkernel (nativekernel RC=0, db_interface.o recompiled), re-ran the identical trigger sequence: counter 1 -> 1 across the nested re-entry (baseline 1 -> 2), no post-continue trap storm, /bin/fault delivered as a normal user SIGSEGV, shell alive (POSTCONT-ALIVE printed), clean shutdown. Leak signature eliminated.
fix_run.log (= run.fixed.log): '0x11150a8: 1' at both prompts, 'pid 2 (fault), uid 0: exited on signal 11', 'POSTCONT-ALIVE', 'Caught SIGTERM ... Shutting down...'; fix_build.log (= build.log): build completed RC=0; run.baseline.log for the contrast (counter 2, trap storm, wedge).
Confirmed kernel references
- sys/platform/vkernel64/x86_64/db_interface.c:150
- sys/platform/vkernel64/x86_64/db_interface.c:151
- sys/platform/vkernel64/x86_64/db_interface.c:167
- sys/platform/vkernel64/x86_64/db_interface.c:169
- sys/platform/vkernel64/x86_64/db_interface.c:177
- sys/platform/vkernel64/x86_64/trap.c:366
- sys/platform/vkernel64/x86_64/trap.c:605
- sys/platform/vkernel64/x86_64/trap.c:959
- sys/platform/vkernel64/x86_64/exception.c:277
- sys/platform/vkernel64/platform/console.c:404
Detail
Exploit chain
ddb session in the vkernel (SIGQUIT -> exc_debugger -> Debugger(), or sysctl debug.enter_debugger=ddb as root inside the vkernel, or panic with debugger_on_panic) -> ddb command faults outside db_nofault (call 0x10(), db_fncall has no fault guard) -> nested kdb_trap default case longjmps to db_global_jmpbuf -> setjmp returns, db_active++ runs a second time -> c exits only the re-entered session (single db_active--) -> db_active stuck at 1 -> trap.c:366/605 divert every subsequent trap to trap_fatal/kdb_trap -> all-CPU stop_cpus on each trap, nested faults cascade inside ddb -> permanent db> wedge of the whole vkernel process (no dump, no reboot, no access; even panic re-enters ddb).
Evidence (decisive lines)
run.baseline.log (== run.log) lines 50-69: x/x 0x11150a8 -> 1, call 0x10() -> 'Fatal trap 12 ... ip=0x2b:0x10' -> 'kernel: type 12 trap, code=0' -> re-entered db> -> x/x -> 2; lines 70-105: post-`c` 'Fatal trap 7 ... user mode ... current process = 1 (sh)' -> 'kernel: type 7 trap' -> CPU0 stopping CPUs -> nested Fatal trap 12 cascade -> 'echo POSTCONT-ALIVE' -> 'No such command' -> EOF panic re-entering ddb. run.fixed.log (== fix_run.log): counter 1 -> 1 across the re-entry, '/bin/fault ... exited on signal 11 / Segmentation fault', 'POSTCONT-ALIVE' printed, clean SIGTERM shutdown; 'Fatal trap' count 5 (baseline) vs 1 (fixed).
PoC changes
Trigger is a vkernel ddb console sequence (SIGQUIT entry via the re-forked CHILD pid; vkernel ddb has no symbol table so the counter is read by literal address with x/x 0x11150a8 from nm, not print *db_active), driven through a fifo console (vconsgetc is a blocking pread(0); the write end must be held open or EOF panics). Rig built: minimal UFS root image (vkinit /sbin/init opening /dev/console then exec'ing sh β kernel-exec'd init_path=/bin/sh dies instantly; /bin/fault for the post-continue differential), pristine image rebuilt each phase (vkernel boot wipes the MBR), DF-5474 link fix required to build VKERNEL64 at all.
Verified recommended fix
Guard the re-entry increment: if (db_active == 0) db_active++; after the setjmp in kdb_trap() (mirrors the validated DF-5561 pc64 fix).
Verdict
DF-5561 twin confirmed on the vkernel64 copy of kdb_trap(): the db_global_jmpbuf re-entry path re-executes db_active++ (db_interface.c:169) while the abandoned outer invocation's db_active-- (:177) never runs, permanently leaking db_active >= 1; user_trap()/kern_trap() (trap.c:366/605) then route every subsequent trap, including ordinary user-mode traps, into trap_fatal()+kdb_trap(), wedging the entire vkernel in ddb. Reproduced deterministically in a live vkernel booted on the QEMU guest: counter read 1 at the first prompt, call 0x10() faulted, counter read 2 at the re-entered prompt, c leaked it to 1, and the next user-mode trap (type 7, PID 1 sh) was taken to trap_fatal + all-CPU stop + ddb + nested-fault cascade; the shell never ran again (input eaten by ddb) and even the EOF panic re-entered Debugger(). With the one-line guard (if (db_active == 0) db_active++), the same sequence on a rebuilt vkernel keeps the counter at 1, /bin/fault gets normal SIGSEGV delivery, the shell answers POSTCONT-ALIVE, and shutdown is clean β fix validated.
No comments yet.