DDB trap funnel defeats the db_nofault guard: recoverable DDB memory probes β including show registers' unconditional disasm of the DDB-writable $rip at db_print.c:68 β are routed through trap_fatal(), producing a spurious 'Fatal trap' crash banner with kernel state disclosure, a leaked gd_trap_nesting_level, and corrupted session-resume state
Summary
db_show_regs ends with an unconditional disasm of tf_rip - a raw DDB-settable register: disassembling a noncanonical rip faults inside db_read_bytes under the db_nofault guard, but trap()'s db_active funnel sends every such trap to trap_fatal() (full crash banner + register disclosure) BEFORE kdb_trap's nofault longjmp recovers the prompt - and the longjmp abandons the funnel's ++gd_trap_nesting_level and corrupts session resume. Reproduced live 2x (set $rip=0xdead... + show registers; x/i 0xdead...): trap 9 banner at the db_read_bytes loop then recovered db> prompt. Operator at a crashed system (the exact population DDB serves - tf_rip may already be noncanonical) gets a second crash dump instead of quiet recovery. honor-nofault-before-the-funnel fix (exported pending/take helpers, both platforms). File otherwise clean (the prompt's db_print_cmd map was stale; modifier/format paths killed with citations).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5732 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.8 KB | β raw | |
| VERDICT.md | β | 6.0 KB | β raw | |
| drive.py | β | 2.2 KB | view raw | |
| serial_vm.sh | β | 1.8 KB | view raw | |
| run.sh | β | 312 B | view raw | |
| env.txt | β | 460 B | view raw | |
| run4-decisive-setrip-showregisters.log | β | 1.2 KB | view raw | |
| panic.txt | β | 480 B | view raw | |
| session0-sweep.log | β | 6.4 KB | view raw | |
| run1-control-valid-xi.log | β | 117 B | view raw | |
| run3-control-held-connection.log | β | 117 B | view raw | |
| run3-sshpoll.log | β | 249 B | view raw | |
| serial-full.log | β | 14.2 KB | view raw | |
| fix.diff | β | 3.7 KB | view raw | |
| verdict.json | β | 5.7 KB | view raw | |
| manifest.json | β | 1.7 KB | view raw | |
| code_hashes.txt | β | 148 B | view raw |
DF-5732 β DDB trap funnel defeats db_nofault on recoverable memory probes (trigger: show registers via db_print.c:68)
What
sys/ddb/db_print.c:68 β db_show_regs() ends with
db_print_loc_and_inst(PC_REGS(DDB_REGS), DDB_REGS) β db_disasm() dereferences
ddb_regs.tf_rip. $rip is a DDB-settable variable (db_regs[] entry
{ "rip", &ddb_regs.tf_rip, NULL }, sys/platform/pc64/x86_64/db_trace.c:107;
db_set_cmd writes it raw, sys/ddb/db_variables.c:120-129).
When tf_rip (or, for the same class, any address x/i probes) is
noncanonical, db_disasm β db_get_value β db_read_bytes faults under the
db_nofault guard (sys/platform/pc64/x86_64/db_interface.c:242). But
trap()'s DDB funnel (sys/platform/pc64/x86_64/trap.c:414-427) routes every
trap taken while db_active straight into trap_fatal() β full "Fatal trap"
crash banner + register/segment dump β before kdb_trap() finally takes the
db_nofault longjmp (db_interface.c:140-144) back to the db> prompt.
Side effects beyond the noise: the funnel's ++gd_trap_nesting_level
(trap.c:422) is skipped by the longjmp and leaks; and the abandoned nested
dispatch corrupts session-resume state β the DF-5727 evidence pack captured
panic: trap: critical section count mismatch! -1892583040/-1 on cont after
exactly this funnel (findings/poc/DF-5727/baseline-up.serial.log:547-549).
Reproduce (decisive run β this file's trigger)
# 1. boot guest with socket serial console findings/poc/DF-5732/serial_vm.sh up # 2. enter DDB as root (blocks; run in background) ssh -F dfbsd-qemu/config root@dfbsd 'sysctl debug.enter_debugger=ddb' & # 3. at the db> prompt on the serial socket: set $rip = 0xdead000000000000 show registers # Expected (bug): full register dump, then from db_print.c:68's disasm of $rip: # -0x2153000000000000: # Fatal trap 9: general protection fault while in kernel mode # ... full trap_fatal register/segment dump ... # kernel: type 9 trap, code=0 # db> <- recovered via db_nofault longjmp (the "quiet" recovery # the guard exists for was NOT quiet)
Automated: python3 findings/poc/DF-5732/drive.py 'set $rip = 0xdead000000000000' 3 'show registers' 6
(with DF5732_SOCK=/tmp/opencode/df-5732-ser.sock).
Sibling trigger, same funnel: x/i 0xdead000000000000 (see session0-sweep.log).
Files
run4-decisive-setrip-showregisters.logβ the decisive run (banner reproduced)session0-sweep.logβ full modifier/format sweep +x/inoncanonical triggerrun1-control-valid-xi.log,run3-control-held-connection.log(+ ssh poll) β controlsserial-full.logβ complete serial console captureenv.txtβ guest identityfix.diffβ funnel fix (authored post-verification, never applied to sys/)verdict.json/manifest.jsonβ machine verdict
DF-5732 VERDICT β reproduced (Low, dos)
Bottom line
sys/ddb/db_print.c in this tree contains only db_show_regs() (69 LOC);
the "print/p modifier dispatch" the task described lives in db_examine.c
(db_print_cmd, db_examine.c:204) and db_hex_value does not exist (its role
is db_format_radix/db_format_hex, db_output.c:260/286, and
db_num_to_str, db_lex.c:127). The one live defect found on this file's own
code path is at db_print.c:68: the unconditional
db_print_loc_and_inst(PC_REGS(DDB_REGS), DDB_REGS) disassembles
ddb_regs.tf_rip, which any DDB console user can set to an arbitrary 64-bit
value via set $rip = ... (db_variables.c:120-129 writes the raw
db_expr_t). Disassembling a noncanonical (or unmapped) rip faults inside
db_read_bytes under the db_nofault guard (db_interface.c:242) β but
trap()'s db_active funnel (trap.c:414-427) sends every trap (except
T_DNA) to trap_fatal() first: a full "Fatal trap 9: general protection
fault" crash banner with register/segment disclosure, then the quiet
db_nofault longjmp recovery (db_interface.c:140-144) back to db>. The
longjmp also abandons the funnel's ++gd_trap_nesting_level (trap.c:422),
leaking it, and the discarded nested trap dispatch leaves the session-resume
path corrupted β independently corroborated by the DF-5727 pack, which
captured panic: trap: critical section count mismatch! -1892583040/-1 on
cont immediately after this exact funnel
(findings/poc/DF-5727/baseline-up.serial.log:525-549: c β Fatal trap 12 at
the same 0xffffffff80bccb70 (db_read_bytes loop) β c β crit-count panic).
Reproduction (all on the stock snapshot kernel, live guest)
Run 4 (decisive, this file's trigger β run4-decisive-setrip-showregisters.log):
sysctl debug.enter_debugger=ddb β set $rip = 0xdead000000000000 β
show registers β all 25 registers print cleanly (the value-formatting path
is fine) β -0x2153000000000000: printed by db_printsym β Fatal trap 9
banner (cpuid 3, RIP 0xffffffff80bccb70 = the db_read_bytes read loop) β
db> recovers via the nofault longjmp. Banner reproduced, twice total:
- run 4:
set $rip+show registers(db_print.c:68 trigger) - run 0 (session0-sweep.log):
x/i 0xdead000000000000(sibling trigger, same funnel, trap 9 at the same RIP)
Honest caveats (why Low and not higher)
- Trigger privilege: DDB console access is already root-equivalent
(serial console / kbd break /
debug.enter_debugger). The bug degrades an already-privileged recovery tool; it is not an escalation. - The post-
contwedge is NOT cleanly attributable to the fault: my controls show that on this snapshot even a perfectly cleandebug.enter_debuggerDDB session (x/ion a valid address, thencont, driver connected or not) leaves the guest dead (silent console, ssh down, no panic output) β see run1-control-valid-xi.log and run3-control-held-connection.log (+run3-sshpoll.log: DOWN for 110 s with the connection held). Prior packs' "control" logs end identically at "restarted" (e.g. DF-5728 run3-controls.serial.log), so post-resume liveness was never actually established there either. Therefore I claim only: banner + register disclosure (deterministic, reproduced), trap-nesting leak (static, trap.c:422 vs longjmp), and resume-state corruption (captured in DF-5727's pack; my runs' resume failures are consistent with it but confounded by the snapshot-level resume problem).
Pass-2 checklist results (negative results, with weight)
- (a) modifier dispatch: db_show_regs ignores
modifentirely (dummy4, db_print.c:46);show registers/x,/99verified harmless live (session0-sweep.log). The stale-modifier default path (p/16 1) is consumer-level harmless: db_print_cmd's switch (db_examine.c:211-250) has no default β prints a bare newline. DF-5720 territory, not re-reported. - (b) number formatting: db_num_to_str (static buf[25], ksnprintf-bounded,
worst case "-%llo" of INT64_MIN = 24 bytes β fits); db_format_radix/_hex
ksnprintf-bounded (db_output.c:279/296); db_print.c:60-63 tbuf[24] can only
see offset β [0,2^32) because db_search_symbol stores diff into an
unsigned int(db_sym.c:210,221,225) β worst octal "01000000000000000000000" = 23 chars + NUL fits.val = -valon INT64_MIN (db_output.c:274-275, 291-292) is UB-in-theory, wraps in practice, cosmetic only. - (c) console reachability:
show registers= db_show_cmds entry (db_command.c:88).$maxoff=-1(DF-5730's unvalidated write) on db_print.c:56's(unsigned long)db_maxoffcast produces only cosmetic garbage attribution (kernphys+0xffdf0ff0, session0-sweep.log) β DF-5730's blast radius, not re-reported.db_find_xtrn_sym_and_offsetnever leavesname/offsetuninitialized (db_sym.c:225, 237-239) and never dereferences the probe address. Register table type-safety verified: alltf_*fields areregister_t(cpu/x86_64/include/frame.h:79-83) ==db_expr_t-sized. - Killed candidates: stale-modifier strcpy OOB (unreachable β every dispatch
re-lexes a tIDENT first, db_command.c:308,318; "String too long" path
longjmps via db_error, db_command.c:481-487, before any strcpy);
db_num_to_str static-buffer aliasing (single use per db_printf);
unmapped-rip disasm hang (guarded by db_nofault longjmp β just noisily,
which IS the filed bug);
offset != valueheuristic (upstream FreeBSD heritage, cosmetic).
Fix
Honor db_nofault before the trap_fatal funnel β see fix.diff
(trap.c + db_interface.c + ddb.h). Not kernel-build-validated (Low/Info-class
robustness finding; fix-validation kernel builds are reserved for
memory-corruption findings per contract).
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 (trap.c funnel pre-check + db_interface.c accessors + ddb.h decls, pc64 and vkernel64). Not kernel-build-validated: Low-severity dos/robustness finding, and fix-validation kernel builds are contract-reserved for memory-corruption findings. Baseline reproduction is deterministic (banner on both triggers).
fix.diff; baseline = run4-decisive-setrip-showregisters.log / panic.txt
Confirmed kernel references
Detail
Exploit chain
root/serial-console DDB session -> set $rip = 0xdead000000000000 (raw db_expr_t write, no validation) -> show registers -> db_print.c:68 db_print_loc_and_inst -> db_disasm dereferences noncanonical rip under db_nofault -> #GP -> trap.c:420 funnel -> trap_fatal banner + register disclosure + leaked gd_trap_nesting_level -> kdb_trap nofault longjmp recovers db> with corrupted resume state (crit-count panic on cont per DF-5727 capture). No unprivileged path; console access is root-equivalent.
Evidence (decisive lines)
run4-decisive-setrip-showregisters.log (register dump then 'Fatal trap 9' banner, recovered db>); panic.txt (banner extract); session0-sweep.log (x/i noncanonical sibling trigger at same RIP + full modifier/format sweep incl. harmless /x /99 modifiers, $maxoff=-1 cosmetic attribution, radix 8/10 formatting); run1-control-valid-xi.log + run3-control-held-connection.log + run3-sshpoll.log (honest controls: clean DDB enter/cont also fails to resume on this snapshot, so the post-cont wedge is not claimed as fault-attributed); serial-full.log (complete console capture); VERDICT.md (full narrative + killed candidates); fix.diff (funnel fix, never applied to sys/)
PoC changes
No seed PoC existed (pass-2 discovery). Built the rig fresh: socket-serial VM (serial_vm.sh, adapted from proven DF-5728 rig) + paced serial driver (drive.py); DDB entry via backgrounded 'sysctl debug.enter_debugger=ddb'.
Verified recommended fix
In trap()'s DDB funnel, honor db_nofault BEFORE trap_fatal(): export db_nofault_pending()/db_nofault_take() from db_interface.c and longjmp quietly for guarded probes instead of running the crash banner and abandoning the nesting bookkeeping (fix.diff).
Verdict
Reproduced live on the stock snapshot kernel: db_show_regs (sys/ddb/db_print.c:46-68) ends with db_print_loc_and_inst(PC_REGS(DDB_REGS)) which disassembles ddb_regs.tf_rip; tf_rip is DDB-writable via set $rip (db_trace.c:107 + db_variables.c:120-129). After set $rip = 0xdead000000000000, show registers prints the register dump then hits 'Fatal trap 9: general protection fault' - trap()'s db_active funnel (trap.c:414-427) routes every trap taken under the db_nofault guard (db_interface.c:242) into trap_fatal() (full crash banner + register disclosure) before kdb_trap's db_nofault longjmp (db_interface.c:140-144) quietly recovers to db>. The longjmp also skips the funnel's --gd_trap_nesting_level (leak, trap.c:422-424) and the abandoned nested dispatch corrupts session-resume state (DF-5727's pack independently captured 'panic: trap: critical section count mismatch!' on cont after this exact funnel). Same funnel hit via x/i 0xdead000000000000 (trap 9 at the identical RIP 0xffffffff80bccb70 = db_read_bytes read loop). Rated Low: trigger requires the already-privileged DDB console; the defect is a debugger-robustness/information-noise bug in the kernel's primary crash-response tool, not an escalation.
No comments yet.