# DF-5721 — DDB software watchpoints (`watch`) are inert on x86_64

**Severity: Low (debugger correctness / operational; DDB console is root-gated)**

The `watch` command (software watchpoint, `sys/ddb/db_watch.c`) silently arms
nothing that can ever fire on x86_64, and the pmap-based protection it does
install evaporates on the first write fault. A kernel debugging session that
relies on `watch` during incident response is debugging with a dead
instrument: writes to the watched address proceed, no stop occurs, and the
watchpoint list still shows it as armed.

## Live proof (UP guest, stock X86_64_GENERIC kernel)

Session transcript (watch-desync.serial.log, from the socket-serial console):

```
db> dwatch ticks
db> watch ticks
db> c
... machine continues running normally ...
(36 s later, second DDB session)
db> x/x db_watchpoints_inserted
db_watchpoints_inserted:        0
db> x/x ticks
ticks:  b342                       <-- thousands of writes HAPPENED to the
db> show watches                     exact watched 4-byte address
*ffffffff8159eea0  ffffffff810529c8  4    <-- watch still listed as armed
```

`ticks` (a hot kernel global written by the clock code on every tick)
advanced freely while a 4-byte software watchpoint sat on it. Zero
watchpoint stops. Machine alive throughout.

## Root cause (line-accurate)

1. `sys/ddb/db_watch.c:218-233` — `db_set_watchpoints()` arms software
   watchpoints with `pmap_protect(map->pmap, ..., VM_PROT_READ)` — a RO
   downgrade of the watched kernel page. A kernel write then takes a plain
   **T_PAGEFLT**, not a debug trap.
2. The Mach-era classifier that would turn that page fault into a
   watchpoint event — `db_find_watchpoint()` (`sys/ddb/db_watch.c:241-273`)
   — is `#ifdef notused`: dead code. Nothing in the x86_64 trap path calls
   it.
3. `sys/cpu/x86_64/include/db_machdep.h:67` —
   `#define IS_WATCHPOINT_TRAP(type, code) 0` (comment: "Watchpoints are
   not supported. The debug exception type is in %dr6 and not yet in the
   args to this macro"). Therefore `db_trap()` (`sys/ddb/db_trap.c:54`)
   always computes `watchpt = FALSE`, the "Watchpoint at" print
   (`db_trap.c:63-64`) is unreachable, and the step-over choreography in
   `db_restart_at_pc()` (`sys/ddb/db_run.c:199-206`, the `watchpt ||` arm)
   can never engage.
4. The faulting write instead flows `trap.c:629-631 → trap_pfault()`;
   `vm_fault()` on the kernel_map entry (which is writable) re-installs the
   PTE writable, the write completes, and the watchpoint has silently
   evaporated. `db_watchpoints_inserted` was set TRUE at resume
   (`db_run.c:208` → `db_watch.c:231`), so no later resume re-arms it —
   the flag is desynced from the (vanished) pmap protection while running.
5. Hardware watchpoints (`hwatch`, `db_watch.c:296` →
   `db_md_set_watchpoint`, DR0-3/DR7) are the only working flavor; they are
   unaffected.

Related (same family, same session, kept out of this finding's scope): when
a `watch`-armed continue happened in a boot that also had DDB sessions in
interrupt context, the kernel panicked — but a dedicated bisect (DF-5722)
proved the watch is *not* the panic's cause.

## Threat model

DDB is reachable only from the console or root-gated sysctl
(`debug.enter_debugger` — DF-2246 verified the EPERM gate; re-verified
live this run: an unprivileged `sysctl debug.enter_debugger=ddb` gets
"Operation not permitted"). So this is not an unprivileged attack surface;
the risk is operational: a root operator debugging a kernel-data-write bug
with `watch` gets a silently dead instrument. No memory-safety impact, no
info leak, no privilege boundary.

## Files

- `watch-desync.serial.log` — decisive transcript
- `up-panics.fullserial.log` (in DF-5722 pack) — full boot's serial history
- `serial_vm.sh` / `serial_vm1.sh` / `drive_ddb.py` — reproducible rig
- `probe_*.c` + `unpriv-probes.log` — unprivileged reachability probes
  (all negative: user int3 → SIGTRAP; PT_STEP over `syscall` → SIGTRAP;
  PT_SETDBREGS with kernel addr → EINVAL)
- `fix.diff` — refuse software watchpoints on x86_64
- `verdict.json` / `manifest.json`

## Reproduce

1. `./serial_vm1.sh up` (UP guest, socket serial)
2. `ssh -F dfbsd-qemu/config dfbsd 'sysctl debug.enter_debugger=ddb' &`
3. On the serial socket: `watch ticks`, then `c`
4. Watch the machine keep running (no stop), re-enter DDB, `x/x ticks`
   (advancing), `show watches` (still listed).
