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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5728 Β· 17 files
FileTypeDescriptionSize
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 as dfbsd-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 FAULT
  • run1-panic60.serial.raw β€” driver-side capture of the same session
  • run2-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, clean c exit
  • fix.diff β€” DB_MAX_EXPR_DEPTH=16 cap (+ reset in db_flush_lex)
  • build.log β€” patched kernel build (nativekernel) + install
  • postfix-run.log β€” patched kernel: same bombs, no panic
  • panic.txt β€” the fatal double-fault frames from both runs

Reproduce (baseline panic)

  1. ./serial_vm.sh up (or vm.sh reset with-src, then this rig)
  2. dfbsd-qemu/vm.sh run_root 'sysctl debug.enter_debugger=ddb' & β€” enters DDB on the serial console (thread-context entry, the clean-exit class)
  3. DF5728_SOCK=/tmp/opencode/df-5728-ser.sock ./drive_ddb.py "$(python3 -c 'print("p "+"("*60+"1")')" 8
  4. 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.

VERDICT.md
↓ download raw

DF-5728 VERDICT β€” REPRODUCED (panic/dos, 2/2 boots) + fix validated

Root cause (source-traced)

  • sys/ddb/db_expr.c:84-95 β€” db_term()'s tLPAREN branch calls db_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 into db_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's db_error("Syntax error") β†’ db_printf β†’ db_force_whitespace at 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), and c exits 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 *addr derefs in DDB land in trap_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

fixed
baseline reproduced→ patch + rebuild →patched clean

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

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.