# DF-5725 — DDB `db_set_breakpoints()` fault-abort orphans live INT3s / poisons the saved original byte → kernel-text corruption + unrecoverable panic

**Severity: Low (DDB console is root-gated), but the primitive is real kernel-text
memory corruption (byte → 0xCC) caused by state-machine desync, plus a
deterministic `panic: trap: critical section count mismatch!` with crash dump —
live-proven twice on the stock X86_64_GENERIC kernel.**

## What breaks

`db_set_breakpoints()` (`sys/ddb/db_break.c:161-180`) reads the original byte and
writes the INT3 for every listed breakpoint **without any per-breakpoint fault
isolation**, and without initializing `bkpt_inst` at breakpoint-set time
(`db_set_breakpoint()`, `db_break.c:88-111`, never touches `bkpt_inst`).  When one
breakpoint in the list sits at an unmapped address (an operator typo — `b
0xffffffff85000000`), the resume path (`c` → `db_restart_at_pc()` →
`db_set_breakpoints()`) aborts mid-list via the kernel page-fault path:

1. Breakpoints earlier in the list (list is LIFO: the last-set bp is inserted
   first) already have INT3 written — but `db_breakpoints_inserted` stays FALSE
   (the flag store at `db_break.c:178` is never reached).  **The INT3 is now
   orphaned**: `db_clear_breakpoints()` (`db_break.c:187-196`) restores only
   `if (db_breakpoints_inserted)`, so no future code path ever restores that
   byte while the flag is desynced.
2. Any later `db_set_breakpoints()` pass re-reads the patched byte
   (`db_break.c:171`) as the "original instruction" — `bkpt_inst` is poisoned
   with 0xCC — and every subsequent clear *restores* 0xCC, permanently replacing
   the first byte of the instruction at that address.
3. Empirically (UP guest, stock kernel) the abort route is not the quiet
   `db_nofault` longjmp but **Fatal trap 12 → trap_fatal() → nested DDB entry**
   from a CRIT-priority thread; the nested session is a hair-trigger: the next
   trap on that corrupted-context thread panics with
   `panic: trap: critical section count mismatch!` → crash dump → reboot.

## Live proof (twice, deterministic; stock kernel, UP rig)

`run1-orphan-panic.serial.log` / `run2-orphan-panic.serial.log`:

```
db> x/bx db_invltlb_cmd          # pristine first byte (a never-executed DDB cmd handler)
db_invltlb_cmd: f                # 0x0F
db> b 0xffffffff85000000         # BAD breakpoint set FIRST (list tail)
db> b db_invltlb_cmd             # GOOD breakpoint set SECOND (list head)
db> c                            # insert pass: INT3->db_invltlb_cmd, then read fault
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0xffffffff85000000
current thread          = pri 6 (CRIT)
db> x/bx db_invltlb_cmd
db_invltlb_cmd: cc               # <-- ORPHANED INT3; flag db_breakpoints_inserted==FALSE
panic: trap: critical section count mismatch! -1892583040/-1
... dump ... Rebooting...
```

Control (`control-run.serial.log`): the same GOOD breakpoint **without** the bad
one behaves correctly — byte restored to 0x0F at DDB entry, machine resumes
clean. The defect is specifically the fault-aborted insert pass.

## Fix

Save the original byte at breakpoint-set time (validating the address while the
console can still react) and never re-read patched memory in
`db_set_breakpoints()` — see `fix.diff` (validated: patched kernel refuses to
add the bad breakpoint, insert pass no longer faults at resume, no orphan, no
panic; byte restored cleanly).

## Reproduce

```
./serial_vm1.sh up                       # UP guest, socket serial (SMP wedges after DDB resume)
./enter_ddb.sh                           # root sysctl debug.enter_debugger=ddb
DF5725_SOCK=/tmp/opencode/df-5725-ser-up.sock ./drive_ddb.py \
   "x/bx db_invltlb_cmd" 3 \
   "b 0xffffffff85000000" 2 "b db_invltlb_cmd" 2 "show breaks" 3 "c" 8 \
   "x/bx db_invltlb_cmd" 6
# expect: 0x0f ... Fatal trap 12 ... db_invltlb_cmd: cc ... panic + dump + reboot
```
