DDB db_expression unbounded parenthesis recursion overflows the 16 KB kernel stack β deterministic double fault / panic from one console line
Summary
db_term's tLPAREN branch recurses with no depth limit at 272 bytes/level (measured from the guest kernel) while DDB runs on the interrupted context's 16KB guard-paged kstack - and the 120-char line admits ~118 levels = ~32KB, twice the stack: 'p ' + '('*60 deterministically hits the guard (DOUBLE FAULT -> panic -> 570MB dump, reproduced 2/2 boots; fresh-boot threshold varies with entry-context depth - run2 unwound into db_force_whitespace). Depth-cap-16 + longjmp-reset fix validated on a full kernel rebuild ('Expression too deep', controls pass). db_lex.c CLEAN beyond DF-5720 (unterminated-string-on-too-long longjmps before any consumer; $radix unvalidated but no memory-safety sink); db_ps.c clean beyond the filed race.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5728 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.4 KB | β raw | |
| VERDICT.md | β | 5.7 KB | β raw | |
| serial_vm.sh | β | 1.9 KB | view raw | |
| drive_ddb.py | β | 2.3 KB | view raw | |
| build.sh | β | 119 B | view raw | |
| run.sh | β | 108 B | view raw | |
| run1-fullserial.log | β | 13.9 KB | view raw | |
| run1-panic60.serial.raw | β | 915 B | β download | |
| run2-sweep.serial.log | β | 1.7 KB | view raw | |
| run3-controls.serial.log | β | 289 B | view raw | |
| postfix-run.log | β | 5.1 KB | view raw | |
| panic.txt | β | 1.8 KB | view raw | |
| build.log | β | 5.7 MB | β download | |
| fix.diff | β | 1.7 KB | view raw | |
| env.txt | β | 825 B | view raw | |
| code_hashes.txt | β | 158 B | view raw | |
| verdict.json | β | 4.9 KB | view raw |
DF-5728 β DDB db_expression unbounded '(' recursion overflows the 16 KB kernel stack
What
sys/ddb/db_expr.c has no recursion-depth limit. Each ( token drives six
nested frames (db_term β db_expression β db_shift_expr β
db_add_expr β db_mult_expr β db_unary, back into db_term).
Measured on the guest's X86_64_GENERIC kernel: 272 bytes of kernel
stack per nesting level (db_term 32 + db_expression 16 + db_shift_expr 64
+ db_add_expr 64 + db_mult_expr 64 + db_unary 32).
DDB runs on the interrupted context's kernel stack (UPAGES=4 β 16 KB,
with a guard page). The ddb input line buffer is 120 bytes
(sys/ddb/db_lex.c:42), so a single console line of (s requests up to
120 levels = ~32.6 KB of recursion β twice the stack. The 16 KB guard
page catches it: double fault β panic: double fault β crashdump.
Trigger line (typed at a db> prompt):
p (((((((((((((((((((((((((((((((((((((((((((1 <- 60 '(' = 16.3 KB: fatal
Empirical threshold varies with the interrupted context's remaining stack:
run 1 (sysctl/thread-context entry) survived 30 levels (8.2 KB) and died at
60; run 2 died at 40 β during the db_error("Syntax error") print at depth
(db_printf β db_force_whitespace), i.e. the error path at depth needs
headroom too. Two independent boots, 2/2 panics.
Files
serial_vm.shβ boots the guest with a writable socket serial console (same disk/kvm/pidfile asdfbsd-qemu/vm.sh)drive_ddb.pyβ paced serial driver (8-byte chunks, UART FIFO limits)run1-fullserial.logβ full chardev log, run 1: sanity + 30 survives + 60 β DOUBLE FAULTrun1-panic60.serial.rawβ driver-side capture of the same sessionrun2-sweep.serial.logβ run 2: first bomb (40) β DOUBLE FAULT (rip db_force_whitespace+0xc)run3-controls.serial.logβ controls on stock kernel:p (1+(2*3))=7, balanced-20 nesting=1, 117-frame unary chain=-1, cleancexitfix.diffβ DB_MAX_EXPR_DEPTH=16 cap (+ reset in db_flush_lex)build.logβ patched kernel build (nativekernel) + installpostfix-run.logβ patched kernel: same bombs, no panicpanic.txtβ the fatal double-fault frames from both runs
Reproduce (baseline panic)
./serial_vm.sh up(orvm.sh reset with-src, then this rig)dfbsd-qemu/vm.sh run_root 'sysctl debug.enter_debugger=ddb' &β enters DDB on the serial console (thread-context entry, the clean-exit class)DF5728_SOCK=/tmp/opencode/df-5728-ser.sock ./drive_ddb.py "$(python3 -c 'print("p "+"("*60+"1")')" 8- Expected:
DOUBLE FAULT - KERNEL STACK GUARD HIT!/panic: double fault, crashdump, host-side guest unresponsive (serial shows panic frames).
Success criterion
Kernel panic (panic: double fault, rsp page-aligned on the kstack guard
page) from a single DDB console line. With fix.diff: the same input yields
Expression too deep and the session continues β no panic.
Threat model
Anyone with write access to the (serial) console of a system where
debug.enter_debugger-style DDB entry or break-to-debugger is available:
serial-terminal servers, IPMI/SoC serial, hypervisor console pipes. Same
actor class as the other DDB findings (DF-2238/2239/2241): console access is
treated as privileged-adjacent (PR:H) β Low severity, availability-only.
DF-5728 VERDICT β REPRODUCED (panic/dos, 2/2 boots) + fix validated
Root cause (source-traced)
sys/ddb/db_expr.c:84-95βdb_term()'stLPARENbranch callsdb_expression()with no depth limit:c if (t == tLPAREN) { if (!db_expression(valuep)) { /* recursion */db_expressionβdb_shift_exprβdb_add_exprβdb_mult_exprβdb_unary(db_expr.c:225-229,193-223,165-191,127-163) β back intodb_termβ six frames per(.- Input length is capped only by
db_line[120](sys/ddb/db_lex.c:42), i.e. up to ~118 consecutive(after a command word (p). - DDB executes on the interrupted context's kernel stack: x86_64
UPAGES=4β 16 KB with a guard page (sys/cpu/x86_64/include/param.h). - Measured frames from the guest kernel (
objdump -d /boot/kernel/kernel): db_term 32 B (2 push+sub8+ret), db_expression 16 B, db_shift_expr 64 B (5 push+sub16+ret), db_add_expr 64 B, db_mult_expr 64 B, db_unary 32 B β 272 B per nesting level. 60 levels = 16.3 KB β₯ the whole stack; 120 levels = 32.6 KB.
Reproduction (live, socket-serial DDB rig)
Rig: serial_vm.sh (QEMU chardev socket serial, same disk as vm.sh) +
drive_ddb.py (paced 8-byte sends). DDB entered via root
sysctl debug.enter_debugger=ddb (sys/ddb/db_sysctl.c:75) β the
thread-context entry class exonerated for clean exit by DF-5722's controls.
- Run 1 (
run1-fullserial.log,run1-panic60.serial.raw): p (1+(2*3))β7(sanity, evaluator healthy)p+(Γ30 +1βSyntax error,db>returns (survives: 8.2 KB)p+(Γ60 +1βDOUBLE FAULT - KERNEL STACK GUARD HIT! Fatal double fault rip = 0xffffffff802ca73c <- db_unary+0xc (pure descent) rsp = 0xfffff801184cf000 <- page-aligned: kstack guard page panic: double faultβ 570 MB crashdump, guest dead.- Run 2 (
run2-sweep.serial.log, fresh boot): first bomb(Γ40 β descent survived (10.9 KB < free stack) but the unwind'sdb_error("Syntax error")βdb_printfβdb_force_whitespaceat depth crossed the guard:rip = 0xffffffff802cba0c <- db_force_whitespace+0xc rsp = 0xfffff80117e77000 <- page-aligned guard page panic: double faultβ second crashdump, 2/2 boots. - Run 3 (
run3-controls.serial.log, stock kernel, no bomb): balanced nesting depth 20 evaluates (1), a 117-frame unary chain (-----β¦-1) evaluates (ffffffffffffffff= -1, ~2.9 KB β unary chains alone are NOT fatal), andcexits DDB cleanly. (The guest then soft-wedged post-exit β the DF-5722 DDB-exit fragility family, unrelated to this finding; evidence was already captured and the box was reset.)
Threshold variance (30 ok/60 dead in run 1; 40 dead in run 2) is explained
by the interrupted thread's remaining stack at DDB entry β both bounds are
far below the 118-level maximum the lexer accepts, so the bug is
deterministically triggerable at p + (Γ118.
Note: crashdumps from runs 1/2 were written to the pre-revert disk state and
discarded by the with-src snapshot reverts; the serial logs above carry
the complete fatal frames (rip/rsp/trace), which is the evidence of record.
Impact classification
panic / DoS. The guard page converts the overflow into a clean double
fault + panic + dump (no silent adjacent-kernel-memory corruption was
observed; x86_64 DFly kstacks are guard-paged on this kernel). Reachable
from the DDB console only β same privileged-adjacent actor as DF-2238/
2239/2241 β Low severity, availability-only. Not an escalation path:
the console actor can already call/write/reset by DDB design.
Fix validation (mandatory for reproduced memory-corruption-class bugs)
fix.diff caps nesting at DB_MAX_EXPR_DEPTH 16 (β4.4 KB worst-case
recursion) in db_expression(), and resets db_expr_depth in
db_flush_lex() (sys/ddb/db_lex.c) because db_error() longjmps
(sys/ddb/db_command.c:486) past the decrement on every abort path β
db_flush_lex() is called by db_error() itself, so the counter cannot go
stale. The db_unary '-'/'*' self-recursion is NOT capped: bounded by the
120-char line to ~119 Γ 24-32 B β 3.8 KB worst case (control run 3 confirms
survival at 117 frames).
Built with make nativekernel KERNCONF=X86_64_GENERIC in the guest
(build.log), installed, rebooted, and re-driven with the identical rig:
- baseline (stock kernel):
p+(Γ40/Γ60 β double fault (runs 1-2) - patched kernel (
postfix-run.log):p+(Γ60 and(Γ118 βExpression too deep,db>returns, no panic; sane controls (p (1+(2*3))β 7; balanced-8 nesting evaluates; unary chain = -1). Usability tradeoff documented: balanced nesting deeper than 16 now errors instead of evaluating (legal-but-pathological input).
fix_status: fixed (baseline reproduced, patched not reproduced).
Not-a-bug notes from the same trace
db_term's other recursions (*/-unary,db_expr.c:106-121) are single-frame-per-char chains β bounded β€ ~3.8 KB by the line length.- Divide-by-zero is guarded (
db_expr.c:147-150); signed/-1#DE and the 32-bit shift truncation are DF-2241/DF-2242 (not re-reported). - Faults on bad
*addrderefs in DDB land intrap_fatalβkdb_trap(sys/platform/pc64/x86_64/trap.c:420,1096) β the known DF-2241 wedge family, not re-reported.
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
fixedfix.diff applied in guest /usr/src, kernel rebuilt (make nativekernel, build.log) and installed; identical rig and identical bomb lines (60 and 118 parens) produce 'Expression too deep' with the db> prompt returning and the session continuing through five further commands including a full ps walk - no panic, no double fault. Controls on the patched kernel: p (1+(2*3))=7, balanced-8 nesting evaluates, 117-frame unary chain evaluates. Known cost: balanced nesting deeper than 16 now errors on legal-but-pathological input.
findings/poc/DF-5728/fix.diff; findings/poc/DF-5728/postfix-run.log; findings/poc/DF-5728/build.log
Confirmed kernel references
Detail
Evidence (decisive lines)
run1-fullserial.log (sanity 7 -> 30-paren 'Syntax error' survives -> 60-paren DOUBLE FAULT with full frames); run2-sweep.serial.log (fresh boot: 40-paren DOUBLE FAULT, rip=db_force_whitespace+0xc, rsp page-aligned); panic.txt (both fatal frames + measured frame budget); run3-controls.serial.log (balanced-20=1, unary-117=-1, clean exit); postfix-run.log ('Expression too deep' at 60 and 118, no panic, healthy ps walk); build.log (patched kernel build); fix.diff (DB_MAX_EXPR_DEPTH=16 + db_flush_lex reset, git-apply-able, APPLIES-CLEAN)
PoC changes
No seed PoC. Built from scratch: socket-serial QEMU rig (adapted from DF-5561/DF-5721 pattern), paced serial DDB driver, sysctl DDB entry, threshold battery. Note: bare '(' lines never reach the parser (db_command rejects non-ident first tokens with '?') - the bomb must be 'p ' + parens; DF-2238's off-by-two (full 120-char line) was deliberately avoided by using 118-paren lines in the max-depth test.
Verified recommended fix
Cap db_expression() nesting at DB_MAX_EXPR_DEPTH=16 (~4.4 KB worst case) and reset the depth counter in db_flush_lex() so db_error() longjmps cannot leave it stale (fix.diff, validated: baseline panic gone, 'Expression too deep' returned, controls pass).
Verdict
REPRODUCED 2/2 boots on the stock with-src kernel: a single DDB console line 'p ' + 60 '(' (and 40 '(' on a second boot) drives db_term's unbounded tLPAREN recursion (db_expr.c:84-95; 272 bytes of kernel stack per level, measured from /boot/kernel/kernel) through the interrupted context's 16 KB kernel stack guard page -> 'DOUBLE FAULT - KERNEL STACK GUARD HIT!' / 'panic: double fault' -> crashdump, guest dead. Run 1 rip=db_unary+0xc (pure descent, 60 levels=16.3KB), run 2 rip=db_force_whitespace+0xc (the db_error->db_printf path at depth 40 after the descent itself survived) - both rsp page-aligned on the guard page. The lexer's 120-char line cap allows 118 levels = 32.1 KB, roughly twice the stack, so the trigger is deterministic. Controls: depth 30 survives (8.2 KB), balanced nesting and a 117-frame unary chain evaluate normally, clean session exit. Threat model is the DDB console actor (serial terminal server / IPMI / hypervisor console) - privileged-adjacent, availability-only, hence Low severity consistent with DF-2238/2239/2241. NOT uid0_privesc: the guard page converts the overflow into a clean panic; no silent adjacent-kernel-memory corruption observed, and the console actor already has call/write/reset by DDB design.
No comments yet.