Unvalidated $tabstops DDB variable: rounddown(x,0) divide-by-zero trap cascade bricks the kernel; negative tabstops spin db_force_whitespace forever with all CPUs stopped
Summary
set $tabstops=N stores with no validation; every DDB output line (including the whitespace sync at the top of each db_readline) evaluates NEXT_TAB=rounddown(i+w,w) with no zero guard: tabstops=0 -> #DE inside the stopped-machine debugger, and because the trap handler prints through the same path it re-divides - 31 cascading fatal traps (~0x370 stack each) until silent brick; tabstops=-8 -> non-advancing inner loop -> silent infinite spin, all CPUs stopped. Both reproduced live on the stock kernel with a one-line console input; the very next 'db> ' prompt is the trigger. Validating setter + total NEXT_TAB fix validated on a rebuilt kernel (both variants print 'tabstops must be >= 1', classic trigger works, session resumes). Root-console/DF-2246-reachability class. db_input.c clean beyond DF-2238/2239 (history-recall OOB candidate REFUTED at byte level - nm shows db_line+120 = db_indent, upper bytes structurally zero, and DF-2238's victim object identified as db_indent via x/bx dump); db_examine.c clean beyond the DF-5720 family (format walk NUL-bounded, tbuf ksnprintf-bounded, s-format walk by-design under db_nofault).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5724 Β· 19 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.5 KB | β raw | |
| VERDICT.md | β | 3.7 KB | β raw | |
| build.log | β | 1.1 KB | view raw | |
| build.sh | β | 653 B | view raw | |
| df-5724-drain-a.log | β | 13.8 KB | view raw | |
| drive.py | β | 3.1 KB | view raw | |
| env.txt | β | 380 B | view raw | |
| fix.diff | β | 2.6 KB | view raw | |
| run.A.fixed.log | β | 214 B | view raw | |
| run.A.log | β | 13.9 KB | view raw | |
| run.B.fixed.log | β | 261 B | view raw | |
| run.B.log | β | 171 B | view raw | |
| run.sh | β | 487 B | view raw | |
| serial.fixedkernel.log | β | 27.2 KB | view raw | |
| serial.variantA.log | β | 13.8 KB | view raw | |
| serial.variantB.drain.log | β | 59 B | view raw | |
| serial.variantB.log | β | 13.1 KB | view raw | |
| serial_vm.sh | β | 1.7 KB | view raw | |
| verdict.json | β | 4.0 KB | view raw |
DF-5724 β unvalidated $tabstops DDB variable: divide-by-zero trap cascade / infinite-loop hard hang
File: sys/ddb/db_output.c (db_force_whitespace), sys/ddb/db_variables.c
Severity: Medium (whole-host unrecoverable freeze from the DDB console;
DDB entry is reachable by jailed root via sysctl debug.enter_debugger per DF-2246)
Status: reproduced live (both variants), fix validated on a rebuilt kernel.
The bug
set $tabstops=N stores N into db_tab_stop_width with no validation
(sys/ddb/db_variables.c:52 registration, db_write_variable
sys/ddb/db_variables.c:120-128 does a raw store). The output layer then computes
/* sys/ddb/db_output.c:65 */
#define NEXT_TAB(i) rounddown((i) + db_tab_stop_width, db_tab_stop_width)
/* sys/sys/param.h:400 */
#define rounddown(x, y) (((x)/(y))*(y)) /* no zero guard */
inside db_force_whitespace() (sys/ddb/db_output.c:74-93), which runs on
every printing character of every DDB prompt/output line β and, critically,
at the top of every db_readline() (sys/ddb/db_input.c:306).
tabstops=0->rounddown(x,0)= integer division by zero -> #DE (x86 trap 18) inside the stopped-machine debugger. The trap handler's own output re-triggers the division: cascading fatal traps, each ~0x370 of kernel stack consumed, until the machine is a silent brick.tabstops=-8->next_tabcomputes <= 0,next_tab <= db_output_positionholds, the innerwhile (last_print < next_tab)never advances and the outerwhile (last_print < db_output_position)spins forever β hard hang with all CPUs stopped, no trap, no output, ssh dead. (The pending trailing space of the very next"db> "prompt is enough βdb_readline's synch call fires it before any command is even typed.)
Same class for huge tabstops (0x7fffffffffffffff): signed overflow in
(i) + db_tab_stop_width wraps negative -> same non-advancing loop.
Build / run
./serial_vm.sh up ssh -F ../../../dfbsd-qemu/config dfbsd 'sysctl debug.enter_debugger=ddb' # root vehicle python3 drive.py A # set $tabstops=0 -> fatal trap 18 cascade # reboot, then python3 drive.py B # set $tabstops=-8 -> silent hard hang
Expected output
Variant A (stock, serial.variantA.log, 31 traps):
db> set $tabstops=0 db> Fatal trap 18: integer divide fault while in kernel mode cpuid = 1; lapic id = 1 instruction pointer = 0x8:0xffffffff802cba26 <- db_force_whitespace+0x26 ... Fatal trap 18: ... (31x, stack pointer descending 0xfffff801183f7458 -> 0xfffff8004f0686d8) [then silence; ssh dead; only a hard QEMU reset recovers]
Variant B (stock, serial.variantB.log):
db> set $tabstops=-8 db> <- last output ever; no echo, no trap [ssh dead; CPU spinning in db_force_whitespace; hard reset required]
Fixed kernel (serial.fixedkernel.log, built from fix.diff):
db> set $tabstops=0
tabstops must be >= 1
db> x/x db_active
db_active: 1
db> set $tabstops=-8
tabstops must be >= 1
db> print/d 5
5
db> x/x db_active
db_active: 1
db> <- session fully alive
Fix
fix.diff β validating db_varfcn_t setters for $tabstops/$maxwidth
(reject < 1 via db_error, which longjmps before the store,
sys/ddb/db_command.c:481-488) plus a defensive db_tab_stop_width > 0
ternary in NEXT_TAB so no future writer can #DE/hang the debugger.
Validated: baseline (trap cascade + hang) vs patched (clean rejection,
debugger alive) on the same guest, same rig.
DF-5724 VERDICT β reproduced (panic-class trap cascade + hard hang), fix validated
Narrative
$tabstops is a raw-settable DDB variable (sys/ddb/db_variables.c:52,
:120-128). db_force_whitespace() (sys/ddb/db_output.c:74-93) evaluates
NEXT_TAB(i) = rounddown(i + db_tab_stop_width, db_tab_stop_width) and
DragonFly's rounddown (sys/sys/param.h:400) has no zero guard.
Variant A β set $tabstops=0 (stock kernel, live): accepted silently.
The next "db> " prompt leaves a pending trailing space (pos=4, last_non=3);
db_readline()'s output-position sync (sys/ddb/db_input.c:306) calls
db_force_whitespace(), which executes rounddown(3+0, 0) -> fatal trap
18 (integer divide fault) in kernel mode, RIP 0xffffffff802cba26 =
db_force_whitespace+0x26 (symbolized via nm, env.txt). Because the trap
handler itself prints through the same path, the machine enters a cascade
of 31 identical fatal traps (serial.variantA.log), the kernel stack
descending from 0xfffff801183f7458 to 0xfffff8004f0686d8 (~0x370/trap)
before going silent. ssh dead; only a hard reset recovers. Fatal trap 18
count: 31.
Variant B β set $tabstops=-8 (stock kernel, live): accepted silently;
the same prompt-sync enters the whitespace loop with next_tab = 0 <= pos
and an inner while (last_print < next_tab) that never advances -> silent
infinite loop with all CPUs stopped (serial.variantB.log: transcript ends
at the prompt; zero traps; ssh dead). The exact print/d 5 trigger
(db_printf("%11ld") leading spaces at column 0) was never even needed β the
prompt's own pending space suffices.
Controls in-session: before each set, x/x db_active prints normally
(tabstops still 8).
Fix validation (same guest, rebuilt kernel): fix.diff applied to the
guest's /usr/src (md5-verified source parity with the audited tree), kernel
rebuilt (nativekernel, BUILD_RC=0, installed #1: Sat Oct 10 17:53:17 UTC
2026), guest rebooted, identical driver rerun: both variants now print
tabstops must be >= 1 (db_error longjmp aborts the set before the store),
and the previously-fatal commands (x/x db_active, print/d 5) work; the
DDB session stays fully alive and c continues. Zero fatal traps
(serial.fixedkernel.log). Baseline vs patched captured in the same pack.
Impact
Whole-host unrecoverable freeze/trap-cascade from the DDB console.
DDB entry itself needs root β including jailed root via
sysctl debug.enter_debugger=ddb (reachability established by DF-2246), so
the realistic ceiling is "jailed root freezes the entire host, survives
nothing less than a hard reset". No memory corruption, no info leak: pure
DoS/panic class. Severity Medium (matches DF-2246's bar), confidence certain.
Exploit chain
(none β denial of service only)
jailed root -> sysctl debug.enter_debugger=ddb -> set $tabstops=0 ->
cascade of fatal trap 18 (db_force_whitespace+0x26) eating the kernel
stack -> brick; or set $tabstops=-8 -> infinite loop in
db_force_whitespace with all CPUs stopped -> silent brick.
Kernel references
- sys/ddb/db_output.c:65 (NEXT_TAB), :74-93 (db_force_whitespace loop/div)
- sys/ddb/db_variables.c:48-53 (raw variable table), :120-128 (unvalidated store)
- sys/ddb/db_input.c:306 (db_readline sync call that fires it)
- sys/sys/param.h:400 (rounddown without zero guard)
- sys/ddb/db_command.c:481-488 (db_error longjmp used by the fix)
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 stock kernel: variant A = 31-trap fatal cascade + brick; variant B = silent all-CPU hang. Patched kernel (fix.diff, rebuilt + installed + rebooted): both variants print 'tabstops must be >= 1', x/x db_active and print/d 5 (the exact B trigger) execute normally, DDB session continues, 'c' resumes β zero fatal traps. Bad behavior gone.
serial.fixedkernel.log (rejected + working output + alive session, 0 'Fatal trap' occurrences); build.log (BUILD_RC=0, install completed); fix.diff
Confirmed kernel references
Detail
Exploit chain
jailed root (DF-2246 reachability: sysctl debug.enter_debugger=ddb) -> 'set $tabstops=0' -> integer divide fault in DDB -> trap handler re-divides while printing -> 31x fatal trap 18 cascade eats the kernel stack -> whole host bricked (hard reset only). Variant B: 'set $tabstops=-8' -> non-advancing whitespace loop -> silent all-CPU hang. DoS only; no privilege or memory primitive.
Evidence (decisive lines)
serial.variantA.log (31x 'Fatal trap 18', RIP 0xffffffff802cba26 = db_force_whitespace+0x26 symbolized in env.txt); serial.variantB.log (transcript ends at 'db> ' after set $tabstops=-8, zero traps, ssh dead); serial.fixedkernel.log (both variants rejected, print/d 5 works, zero traps); fix.diff; build.log
PoC changes
Trigger reworked vs intuition: no explicit printing command is needed β the pending trailing space of the next 'db> ' prompt fires db_force_whitespace via db_readline's sync (db_input.c:306) before any echo; variant B likewise hangs before echoing anything. Driver paces <=8 bytes/150ms (guest UART RX FIFO 16) and uses one persistent socket with continuous drain.
Verified recommended fix
Validating db_varfcn_t setters for $tabstops/$maxwidth (reject <1 via db_error, which longjmps before the store) + defensive db_tab_stop_width>0 ternary in NEXT_TAB.
Verdict
set $tabstops=0 followed by any DDB output (the next 'db> ' prompt suffices, via db_readline's db_force_whitespace sync at db_input.c:306) executes rounddown(x,0) in db_force_whitespace (db_output.c:80, NEXT_TAB at :65, rounddown sys/sys/param.h:400) -> fatal trap 18 cascade (31 traps, RIP db_force_whitespace+0x26 = 0xffffffff802cba26, ~0x370 kernel stack consumed per trap) ending in an unrecoverable brick; set $tabstops=-8 makes the same loop spin forever with all CPUs stopped (silent hard hang, zero traps). Both reproduced live on the stock kernel via the socket-serial DDB rig; fix.diff (validating db_varfcn_t setters + defensive NEXT_TAB ternary) validated on a rebuilt kernel: both variants rejected with 'tabstops must be >= 1', previously-fatal commands work, session alive, zero traps.
No comments yet.