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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5571 Β· 16 files
FileTypeDescriptionSize
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-entered db>: 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 (guard if (db_active == 0))
  • after c: /bin/fault handled 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; clean Caught 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 consoles
  • vk_ddb_session.sh β€” the phase driver (trigger sequence)
  • mk_image.sh, vkinit.c, fault.c β€” root-image rig
  • linkfix-df5474.diff β€” prerequisite DF-5474 link fix
  • fix.diff β€” the fix; build.log β€” patched rebuild
  • env.txt, hashes.txt
  • VERDICT.md, manifest.json, verdict.json
VERDICT.md
↓ download raw

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)

  1. 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 via sysctl debug.enter_debugger=ddb β†’ db_sysctl.c:73 and via any panic.)
  2. 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.
  3. Nested fault inside ddb: call 0x10() executes in the vkernel process; unmapped β†’ host SIGSEGV β†’ exc_segfault β†’ kern_trap β†’ db_active nonzero β†’ trap.c:605 β†’ trap_fatal (prints "Fatal trap 12 ... instruction pointer = 0x2b:0x10") β†’ nested kdb_trap(12, ...); switch default β†’ db_printf("kernel: type 12 trap, code=0") (:136) β†’ db_nofault NULL (db_fncall arms no guard) β†’ db_global_jmpbuf_valid TRUE β†’ longjmp(db_global_jmpbuf, 1) (:150-151).
  4. The longjmp restores the FIRST invocation's setjmp context (:167); execution continues: db_active++ (:169) β†’ 2; the nested invocation's frame (and its would-be db_active-- at :177) is abandoned. Re-entered db_trap() prompt.
  5. c: db_active-- (:177) β†’ 1 (leaked), restart_cpus (:192), regs restored from ddb_regs (first trap's frame β€” the nested invocation longjmp'd before ddb_regs = *regs at :156, so ddb_regs still holds the original frame), return 1 (:225) β†’ the SIGQUIT entry unwinds normally.
  6. 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 reached panic("vconsgetc: EOF") which itself re-entered Debugger("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*)addr both fail ("Symbol not found") β€” read the counter by literal address with x/x (address from nm 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 -a is not supported by the guest /bin/sh.
  • With -e init_path=/bin/sh the 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=VKERNEL64 does 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_level balances 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

fixed
baseline reproduced→ patch + rebuild →patched clean

Applied 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).
↓ fix.diffper-fix-DF-5571

Confirmed kernel references

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.