DDB db_set_breakpoints() fault-abort orphans live INT3s and poisons the saved original byte β permanent kernel-text corruption at an unrelated address plus unrecoverable crit-section panic
Summary
A single breakpoint at an unmapped address aborts the resume-time insert pass mid-list via nested trap: earlier (LIFO) breakpoints keep live INT3s while the inserted flag stays FALSE so clear never restores them, and any later insert re-reads the patched 0xCC as the 'original' - permanently corrupting the first byte of an instruction the operator did NOT type. Live-proven twice on stock: db_invltlb_cmd 0x0f->0xcc with flag FALSE, then crit-count-mismatch panic + dump. Save-byte-at-set-time + never-re-read fix validated on a rebuilt kernel.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5725 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.9 KB | β raw | |
| VERDICT.md | β | 6.1 KB | β raw | |
| serial_vm.sh | β | 1.9 KB | view raw | |
| serial_vm1.sh | β | 1.8 KB | view raw | |
| drive_ddb.py | β | 2.2 KB | view raw | |
| enter_ddb.sh | β | 867 B | view raw | |
| build.sh | β | 163 B | view raw | |
| run.sh | β | 275 B | view raw | |
| run1-orphan-panic.serial.log | β | 29.3 KB | view raw | |
| run2-orphan-panic.serial.log | β | 36.0 KB | view raw | |
| control-run.serial.log | β | 45.0 KB | view raw | |
| env.txt | β | 350 B | view raw | |
| fix.diff | β | 1.5 KB | view raw | |
| fix_build.log | β | 5.7 MB | β download | |
| fix_run.log | β | 15.2 KB | view raw | |
| verdict.json | β | 5.4 KB | view raw |
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:
- Breakpoints earlier in the list (list is LIFO: the last-set bp is inserted
first) already have INT3 written β but
db_breakpoints_insertedstays FALSE (the flag store atdb_break.c:178is never reached). The INT3 is now orphaned:db_clear_breakpoints()(db_break.c:187-196) restores onlyif (db_breakpoints_inserted), so no future code path ever restores that byte while the flag is desynced. - Any later
db_set_breakpoints()pass re-reads the patched byte (db_break.c:171) as the "original instruction" βbkpt_instis poisoned with 0xCC β and every subsequent clear restores 0xCC, permanently replacing the first byte of the instruction at that address. - Empirically (UP guest, stock kernel) the abort route is not the quiet
db_nofaultlongjmp 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 withpanic: 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
DF-5725 β VERDICT
reproduced (deterministic, twice) β impact: panic (plus a real, demonstrated
kernel-text corruption primitive: first byte of an instruction at a valid,
unrelated address permanently replaced with 0xCC while DDB believes no
breakpoints are inserted; the run then ends in panic: trap: critical section
count mismatch!, crash dump, reboot).
Environment
- DragonFly 6.5-DEVELOPMENT #0 Thu Jul 2 06:02:54 UTC 2026 x86_64,
stock
X86_64_GENERICkernel (guest/sys/ddb/db_break.cMD500d1f40d09dd788691f6224b391d3e26= audited tree). - QEMU/KVM, 1 vCPU (UP), 4 GB, socket-backed serial console driven from the
host (
serial_vm1.sh+drive_ddb.py, paced 8-byte chunks). - DDB entry: root-gated
sysctl debug.enter_debugger=ddb(unprivileged write β EPERM, verified by DF-5721's probes);debug.debugger_on_panic=1.
What was run (exact)
Run 1 and Run 2 (identical, run1-orphan-panic.serial.log,
run2-orphan-panic.serial.log):
db> x/bx db_invltlb_cmd # read pristine byte: 0x0f db> b 0xffffffff85000000 # BAD bp set FIRST (tail of list) db> b db_invltlb_cmd # GOOD bp set SECOND (head of list) db> show breaks # both listed db> c # resume: insert pass head-first 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 says NOT inserted) panic: trap: critical section count mismatch! -1892583040/-1 (dump, reboot)
Control (control-run.serial.log): GOOD bp alone β c β machine runs; re-enter
DDB β byte already restored to 0x0f at entry (db_stop_at_pc β
db_clear_breakpoints with flag TRUE); delete; c; machine alive. The defect is
specifically the fault-aborted insert pass.
Why (line-accurate)
db_set_breakpoint()(sys/ddb/db_break.c:88-111) never initializesbkpt_instand never validates the address.db_set_breakpoints()(db_break.c:161-180) walks the list head-first: for each bp it reads the original byte (db_break.c:171,db_get_value) and writesBKPT_SET(db_break.c:174-176). On x86_64db_read_bytes(sys/platform/pc64/x86_64/db_interface.c:241-251) accesses the raw address; an unmapped address faults. The fault is not fielded bydb_nofaulton this path β it routes totrap_fatal()β nested DDB entry (observed; the trap dump is in both run logs,current thread = pri 6 (CRIT)β the resume path runs in a critical section).- The abort leaves earlier-listed breakpoints patched with INT3 while
db_breakpoints_insertedremains FALSE (db_break.c:178unreached) β proven bydb_invltlb_cmd: ccat the nested prompt. - From that point every recovery path is broken:
-
db_clear_breakpoints()(db_break.c:187-196) restores onlyif (db_breakpoints_inserted)β the desynced flag makes it a no-op, so nothing ever restores the byte; - a later insert pass re-reads the patched byte as the original (db_break.c:171) βbkpt_instpoisoned with 0xCC β and the eventual clear writes back 0xCC: permanent first-byte corruption of the instruction at that address; - empirically the nested CRIT-context trap state is a hair-trigger: the next trap on that thread tripspanic: trap: critical section count mismatch!(observed in both runs; panic trace runs throughDebugger() β sysctl_debug_enter_debugger β userland_sysctlβ the original entry frame β confirming the abort happened mid-resume). - Because a stop requires
--bkpt->count == 0exactly (db_run.c:100), the corrupted state also silently swallows the skip-count bookkeeping.
With debug.debugger_on_panic=0 the fatal trap would panic-reboot directly at
step 2; with the default (1) the machine survives only until the first nested
trap, which panics anyway (both runs).
Threat model
DDB requires the root-gated console (serial break, panic, or
debug.enter_debugger). The corrupted byte and the panic are caused by an
operator typo (b at an unmapped address) β the corruption lands at an
address the operator did NOT type and persists after deleting every
breakpoint. Severity: Low (privileged surface), but it is genuine
kernel-text corruption (wrong-code execution primitive if the target address
is chosen) caused by a debugger state-machine defect, not by the by-design
debugger memory-write capability.
Fix validation
fix.diff: initialize+save bkpt_inst at db_set_breakpoint() time (before
linking; a bad address faults at the prompt β the pre-existing, recoverable
behavior for bad DDB reads β and nothing is added to the list), and use the
saved byte in db_set_breakpoints() (never re-read patched memory).
Validated on a single combined-fix kernel (this fix + DF-5726 + DF-5727,
fix_build.log / fix_run.log): b 0xffffffff85000000 is rejected at set
time, show breaks lists only the good breakpoint, c resumes with no
fatal trap, and db_invltlb_cmd reads 0x0f (never 0xcc) at every
observation point; no panic; machine stays up. Residual (documented): a page
that is valid at set time but unmapped later (module unload) can still abort
the insert pass mid-resume β the poisoning leg is gone by construction, the
orphan leg would need per-bp fault isolation in the MD layer.
Rig notes
- The SMP rig (
serial_vm.sh, 6 vCPU) wedged unrecoverably after a DDB resume (same failure DF-5721 recorded insmp-E1-wedge.serial.log); all clean sessions ran on the UP rig (serial_vm1.sh). - Crash dumps written by the two baseline panics were discarded with
vm.sh reset with-srcbefore the fix build.
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
fixedBaseline reproduced twice on stock #0 kernel (orphan 0xcc + crit-mismatch panic + dump + reboot). Patched kernel (#1, combined db_break/db_watch fixes): 'b 0xffffffff85000000' faults at SET time at the prompt (pre-existing recoverable bad-read behavior) and adds nothing ('No breakpoints set'); good bp insert/clear cycle restores 0x0f at entry; c resumes with no fatal trap; machine stays up. Bad behavior gone.
findings/poc/DF-5725/fix.diff; fix_build.log; fix_run.log (patched session: 'No breakpoints set' / 'db_invltlb_cmd: f' / clean resume); baseline: run1-orphan-panic.serial.log, run2-orphan-panic.serial.log; control-run.serial.log
Confirmed kernel references
Detail
Exploit chain
Root at DDB console: (1) b
Evidence (decisive lines)
run1-orphan-panic.serial.log + run2-orphan-panic.serial.log: 'db_invltlb_cmd: f' (0x0f pristine) -> 'b 0xffffffff85000000' + 'b db_invltlb_cmd' + 'c' -> 'Fatal trap 12 ... 0xffffffff85000000 ... pri 6 (CRIT)' -> 'db_invltlb_cmd: cc' (orphaned INT3) -> 'panic: trap: critical section count mismatch! -1892583040/-1' -> dump -> reboot. control-run.serial.log: same GOOD bp without the bad one restores 0x0f cleanly and resumes. fix_run.log (patched kernel): bad bp rejected at set time ('No breakpoints set'), c resumes with no trap, byte stays 0x0f, machine up.
PoC changes
No seed PoC. Built the socket-serial DDB rig (serial_vm.sh SMP / serial_vm1.sh UP + drive_ddb.py, adapted from the proven DF-5721 rig) plus enter_ddb.sh; discovered x/b alone is modifier-only (must use x/bx) and that the SMP instance wedges after DDB resume (same as DF-5721), so all clean runs are UP.
Verified recommended fix
Save and validate the original instruction byte in db_set_breakpoint() at set time (before linking) and make db_set_breakpoints() write BKPT_SET(saved) without re-reading patched memory (fix.diff).
Verdict
db_set_breakpoints() (sys/ddb/db_break.c:161-180) has no per-breakpoint fault isolation and defers reading the original instruction byte to resume time: a single breakpoint at an unmapped address (operator typo) aborts the insert pass mid-list via Fatal trap 12 -> nested DDB entry, leaving breakpoints earlier in the list patched with INT3 while db_breakpoints_inserted stays FALSE (proven live: db_invltlb_cmd byte 0x0f -> 0xcc at the nested prompt, twice, run1/run2 logs), after which no code path ever restores the byte (db_clear_breakpoints restores only when the flag says inserted) and any later insert pass re-reads the patched INT3 as the original (bkpt_inst poisoned at db_break.c:171) making the corruption permanent on clear. The nested CRIT-context trap state then panics deterministically ('panic: trap: critical section count mismatch!', crash dump, reboot) in both runs. Root-gated DDB console surface -> Low severity, but the demonstrated primitive is real kernel-text corruption at an address the operator did not type plus an unrecoverable panic.
No comments yet.