β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5724

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5724 Β· 19 files
FileTypeDescriptionSize
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_tab computes <= 0, next_tab <= db_output_position holds, the inner while (last_print < next_tab) never advances and the outer while (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.

VERDICT.md
↓ download raw

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

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

fixed
baseline reproduced→ patch + rebuild →patched clean

Baseline 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
↓ fix.diffper-fix-DF-5724

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.