DDB software watchpoints (watch) are inert on x86_64 β pmap protection silently evaporates, watchpoint trap classification is dead code
Summary
The watch command RO-protects the page, but a watched kernel write takes a plain T_PAGEFLT that vm_fault silently rewires writable - after the first hit the watch is permanently inert while still listed as armed. Live-proven: watch ticks + c left the machine running with the counter advancing through thousands of writes to the exact watched address, zero stops. Root cause chain: IS_WATCHPOINT_TRAP hardwired 0, the Mach classifier #ifdef notused dead, db_watchpoints_inserted desynced from the vanished protection. Operational risk only (operator believes a tripwire is armed when none exists); hwatch is the working flavor. refuse-until-classifier-exists fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5721 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.3 KB | β raw | |
| VERDICT.md | β | 4.3 KB | β raw | |
| watch-desync.serial.log | β | 687 B | view raw | |
| unpriv-probes.log | β | 2.0 KB | view raw | |
| probe_int3.c | β | 377 B | view raw | |
| probe_step.c | β | 1.4 KB | view raw | |
| probe_dbregs.c | β | 2.8 KB | view raw | |
| drive_ddb.py | β | 2.0 KB | view raw | |
| serial_vm.sh | β | 1.7 KB | view raw | |
| serial_vm1.sh | β | 1.7 KB | view raw | |
| smp-E1-wedge.serial.log | β | 13.9 KB | view raw | |
| run.watch-panic.serial.log | β | 30.2 KB | view raw | |
| env.txt | β | 626 B | view raw | |
| fix.diff | β | 1.2 KB | view raw | |
| verdict.json | β | 3.3 KB | view raw |
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)
sys/ddb/db_watch.c:218-233βdb_set_watchpoints()arms software watchpoints withpmap_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.- 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. 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"). Thereforedb_trap()(sys/ddb/db_trap.c:54) always computeswatchpt = FALSE, the "Watchpoint at" print (db_trap.c:63-64) is unreachable, and the step-over choreography indb_restart_at_pc()(sys/ddb/db_run.c:199-206, thewatchpt ||arm) can never engage.- 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_insertedwas 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. - 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 transcriptup-panics.fullserial.log(in DF-5722 pack) β full boot's serial historyserial_vm.sh/serial_vm1.sh/drive_ddb.pyβ reproducible rigprobe_*.c+unpriv-probes.logβ unprivileged reachability probes (all negative: user int3 β SIGTRAP; PT_STEP oversyscallβ SIGTRAP; PT_SETDBREGS with kernel addr β EINVAL)fix.diffβ refuse software watchpoints on x86_64verdict.json/manifest.json
Reproduce
./serial_vm1.sh up(UP guest, socket serial)ssh -F dfbsd-qemu/config dfbsd 'sysctl debug.enter_debugger=ddb' &- On the serial socket:
watch ticks, thenc - Watch the machine keep running (no stop), re-enter DDB,
x/x ticks(advancing),show watches(still listed).
DF-5721 β VERDICT
Status: reproduced (deterministic; single attempt on the stock X86_64_GENERIC snapshot kernel, UP guest). Impact: none (debugger correctness defect β no memory unsafety, no privilege boundary; DDB entry is console/root-gated, verified live). Confidence: certain β the inert-watch behavior is directly visible in the serial transcript and the root cause is line-verified in three files.
What was claimed / what reproduced
Claim: DDB software watchpoints (watch) never fire on x86_64 and the
protection silently evaporates after the first watched write.
Reproduced exactly: watch ticks + c β machine continues; ~36 s later
(second DDB session) ticks = 0xb342 (the counter advances on every clock
tick β i.e. thousands of kernel writes to the exact watched address
occurred), show watches still lists the watchpoint, zero watchpoint
stops, machine alive (watch-desync.serial.log).
Root cause chain (line-accurate)
sys/ddb/db_watch.c:226-229β watchpoints are armed by RO-protecting the page (pmap_protect(..., VM_PROT_READ)), so a watched write takes T_PAGEFLT (14), not a debug exception.sys/ddb/db_watch.c:241-273βdb_find_watchpoint(), the page-faultβ watchpoint classifier this design requires, is#ifdef notused(dead).sys/cpu/x86_64/include/db_machdep.h:63-67βIS_WATCHPOINT_TRAP()is hardwired0, sodb_trap.c:54always passeswatchpt=FALSE: the "Watchpoint at" print (db_trap.c:63-64) and the watchpoint step-over arm indb_restart_at_pc(sys/ddb/db_run.c:199-206) are unreachable on x86_64.- The page fault instead flows
sys/platform/pc64/x86_64/trap.c:629-631βtrap_pfault()βvm_fault(kernel_map, ...); the map entry is writable, so the PTE is rewired RW and the write completes. The pmap protection is gone;db_watchpoints_insertedremains TRUE while running (db_run.c:208βdb_watch.c:231), sodb_set_watchpoints()never re-protects on later resumes. The feature is inert.
Why Low / impact none
- DDB entry paths are all privileged: console (physical/serial access),
debug.enter_debuggersysctl (root; unprivileged write β EPERM, re-verified live this run), panic withdebug.debugger_on_panic=1, NMI+ddb_on_nmi. The three unprivileged probes (probe_int3.c,probe_step.c,probe_dbregs.cβ see pack) confirm no unprivileged route intodb_stop_at_pcon this kernel: user int3 β SIGTRAP (trap.c:490-495), PT_STEP-over-syscall artifact β filtered (trap.c:714-718), PT_SETDBREGS with kernel DR0 β EINVAL (machdep.c set_dbregs address validation). - No memory unsafety: the watch list is bounded (
db_watch.c:51, NWATCHPOINTS=100, static table), sizes are capped by command parsing. - Operational risk only: an operator debugging a data-corruption incident
with
watchbelieves a tripwire is armed when none exists.
Delimitation
- The kernel panic that co-occurred in one watch-armed session
(run with
b hardclockhistory) is DF-5722 and was proven watch-independent by bisect: identical panic with the watch removed (DF-5722 run #3/#4), no panic with the watch alone (this finding's run). hwatch/dhwatch(hardware DR watchpoints) work and are not implicated.- The SMP-guest resume starvation (DF-5720 lead) was cross-checked from
db_run.c's side this run and is NOT in db_run.c (see audit notes):
identical
c-resume code on a UP guest is clean.
Fix
fix.diff: make watch/dwatch refuse on x86_64 (the platform defines
DB_NO_SOFTWARE_WATCHPOINTS in db_machdep.h; db_watchpoint_cmd
prints "software watchpoints not supported on this platform (use hwatch)"
and returns). Not kernel-rebuild-validated: Low-severity feature-disable
fix, no panic/corruption involved, and a kernel rebuild was not feasible
in-run (UP guest). Full implementation (resurrect db_find_watchpoint
in the kernel page-fault path + a real IS_WATCHPOINT_TRAP) is the
upstream-quality fix but is a feature restoration, not a one-line repair.
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
not_testablefix.diff authored post-verification against the read-only sys/ tree; not applied/rebuilt (kernel rebuild infeasible in-run on the 1-vCPU validation guest; Low-severity feature-disable, not memory corruption, so rebuild validation is not mandatory per contract).
findings/poc/DF-5721/fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
watch-desync.serial.log (db> watch ticks / c / machine alive / second session: ticks: b342 advancing, show watches lists it, zero stops); README.md root-cause chain with path:line; unpriv-probes.log (all reachability probes negative); fix.diff refuses the feature on x86_64
PoC changes
No seed PoC existed. Built the rig from scratch: socket-serial QEMU guests (serial_vm.sh SMP / serial_vm1.sh UP), a paced serial-line DDB driver (drive_ddb.py), and three unprivileged reachability probes (probe_int3.c, probe_step.c, probe_dbregs.c) compiled on the guest.
Verified recommended fix
Refuse software watchpoints on x86_64 (define DB_NO_SOFTWARE_WATCHPOINTS in db_machdep.h, reject in db_watchpoint_cmd) until a real watchpoint trap classifier exists; point users at hwatch.
Verdict
DDB software watchpoints (watch) are inert on x86_64: live-verified that a 4-byte watch on the every-tick-written kernel global ticks never stops the kernel (ticks advanced to 0xb342 with the watch listed as armed, machine alive), because the pmap RO protection (db_watch.c:226-229) faults as a plain page fault that vm_fault() silently rewires writable, while the watchpoint classifier the resume choreography needs is dead code (db_find_watchpoint #ifdef notused, db_watch.c:241-273) and IS_WATCHPOINT_TRAP is hardwired 0 (db_machdep.h:67), making the watchpt arm of db_restart_at_pc (db_run.c:199-206) unreachable. Root-console-only surface (unprivileged probes all negative), no memory unsafety: Low severity, debugger-correctness defect that leaves an operator debugging with a dead instrument.
No comments yet.