# 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.
