ddb '/'-modifier parsing copies stale db_tok_string for numeric modifier tokens β the typed modifier is silently substituted with the previously lexed identifier (deterministically the command's own name)
Summary
tNUMBER is accepted as a modifier but db_tok_string is only populated for tIDENT, so modif[] receives the previous identifier (the command's own name on a plain line): 'x/4 addr' silently behaves as 'x/x', 'examine/4 addr' becomes a per-character format walk of the literal string 'examine' (hex dump + symbolized pointer read + a DISASSEMBLY step), 'print/4' prints empty, 'write/4' refuses 'Unknown size'. Live-proven on the stock guest via the DF-5561 socket-serial rig with clean controls. Bounded at Info (lexer-bounded modif[120], single-char consumer switches, fail-safe write path, console/root surface). ksnprintf-from-db_tok_number fix. db_fncall fault cascade owned by DF-5561 (its pack cites the :515 trigger); breakpoint-recursive-session scenario disproven (db_stop_at_pc clears breakpoints+watchpoints on every entry).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5720 Β· 18 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.5 KB | β raw | |
| VERDICT.md | β | 6.1 KB | β raw | |
| drive_ddb.py | β | 2.7 KB | view raw | |
| experiment.py | β | 1.9 KB | view raw | |
| ab_cycle.sh | β | 1.4 KB | view raw | |
| run.sh | β | 179 B | view raw | |
| build.sh | β | 125 B | view raw | |
| build.log | β | 133 B | view raw | |
| run.log | β | 456 B | view raw | |
| run.wedge-session1.log | β | 456 B | view raw | |
| serial.full.txt | β | 13.5 KB | view raw | |
| serial.wedge-session1.txt | β | 13.5 KB | view raw | |
| exp.A-short-none.drain.log | β | 59 B | view raw | |
| env.txt | β | 214 B | view raw | |
| fix.diff | β | 587 B | view raw | |
| hashes.txt | β | 159 B | view raw | |
| verdict.json | β | 3.4 KB | view raw | |
| manifest.json | β | 1.3 KB | view raw |
DF-5720 β ddb /-modifier parsing copies a stale token string (numeric modifiers silently substituted with the previous identifier)
File: sys/ddb/db_command.c (lines 361β370)
Severity: Info (debugger parser correctness / stale-state injection; no memory-safety impact)
Status: reproduced live on the QEMU guest (deterministic).
The bug
db_command() accepts a numeric token after the command's / but copies
db_tok_string into modif[] β and the lexer only populates db_tok_string
for tIDENT tokens (numbers go into db_tok_number, sys/ddb/db_lex.c:190-216).
The string therefore copied is whatever identifier was lexed last β on a plain
line that is the command's own name (x/4 β modif "x", examine/4 β
modif "examine", write/4 β modif "write", print/4 β modif "print").
Consumers then switch on the letters of that stale string:
| input | modif used | observed behavior (stock kernel) |
|---|---|---|
x/4 addr |
"x" |
silently behaves as x/x (4 ignored) |
examine/4 addr |
"examine" |
per-char walk: x hex + a pointer/symbol read + i disassembly |
print/4 0x41 |
"print" |
'p' has no case in db_print_cmd β empty line |
write/4 a v |
"write" |
'w' β db_error("Unknown size") β write refused |
Controls in the same session prove letter modifiers work (x/x, print/x,
write/l).
Build / run
No compilation β this is a serial-console ddb session:
./serial_vm.sh upβ relaunch the guest with a writable socket serial console (adapted from the proven DF-5561 rig).ssh -F ../../../dfbsd-qemu/config dfbsd 'sysctl debug.enter_debugger=ddb'(root vehicle; kernel stops atdb>on the serial console).python3 drive_ddb.pyβ sends the battery above with proper UART pacing.- Transcript lands in
/tmp/opencode/df-5720-drain.log(copied torun.log).
./run.sh automates 1β4.
Expected output (see run.log)
db> x/x db_active
db_active: 1
db> x/4 db_active
db_active: 1 <-- "4" silently ignored (same as x/x)
db> examine/4 db_active
db_active: 1 0x100000001addb %al,(%rax)
db_global_jmpbuf_valid+0x6: <-- modif="examine": x + a + i walk (!)
db> print 0x41
41
db> print/4 0x41
<-- empty line: modif="print" -> 'p'
db> print/x 0x41
41
db> write/4 db_active 1
Unknown size <-- modif="write" -> 'w' -> error
db> write/l db_active 1
db_active 1 = 1 <-- control: letter modifier works
db> x/x db_active
db_active: 1
db> c
CPU0 restarting CPUs: 0x000000000000003e
restarted
Success criterion: the three markers (mixed a/i walk on examine/4,
empty output on print/4, Unknown size on write/4) with all controls
behaving normally.
Note β post-continue guest state (NOT part of this finding)
After c, this snapshot's guest does not resume userland (no serial echo, no
ssh; vCPUs ~5%). A controlled experiment (ab_cycle.sh 5 none, i.e. enter ddb,
send no commands, c) reproduces the same starvation β the behavior is a
property of the debug.enter_debugger/kdb_trap/restart_cpus resume path
(adjacent to DF-2246/DF-5561 territory), not of anything in
sys/ddb/db_command.c. Documented as a cross-file lead in VERDICT.md; the
modifier-bug evidence above was fully captured before the continue.
DF-5720 β VERDICT
Status: reproduced (deterministic, first clean attempt on the stock
X86_64_GENERIC snapshot guest, DragonFly dfbsd 6.5-DEVELOPMENT #0 Thu Jul 2
06:02:54 UTC 2026 x86_64). Impact: none (Info-class debugger parser
correctness bug β no memory unsafety, no privilege boundary crossed; DDB
requires console/root access). Confidence: certain β the wrong-modifier
behavior is directly visible in the serial transcript (run.log) with
in-line letter-modifier controls, and the root cause is line-verified in the
lexer.
Root cause (line-accurate)
sys/ddb/db_command.c:361-370, standard command syntax:
361: t = db_read_token();
362: if (t == tSLASH) {
363: t = db_read_token();
364: if (t != tIDENT && t != tNUMBER) { /* tNUMBER is accepted β¦ */
367: return;
368: }
369: db_strcpy(modif, db_tok_string); /* β¦ but the copied buffer is
only filled for tIDENT */
The lexer never writes db_tok_string for numbers β sys/ddb/db_lex.c:170-216
stores digits in db_tok_number only; db_tok_string retains the previous
identifier token, which on a plain command line is the command's own name
(lexed at db_command.c:295). modif therefore becomes, deterministically:
x/4β"x"(coincidentally a valid hex-format letter β the4is silently ignored)examine/4β"examine"(per-char format walk indb_examine,sys/ddb/db_examine.c:78-193)print/4β"print"(db_print_cmdswitches onmodif[0];'p'has no case β prints only\n,sys/ddb/db_examine.c:204-248)write/4β"write"('w'hitsdefault:βdb_error("Unknown size\n"),sys/ddb/db_write_cmd.c:56-67)
Why Info (impact bounded)
- No memory unsafety:
modif[TOK_STRING_SIZE=120](db_command.c:289) is filled fromdb_tok_string[120], which the lexer bounds at β€119 chars + NUL and NUL-terminates on everytIDENTsuccess path (sys/ddb/db_lex.c:225-256; the over-long path at:245-249longjmps out viadb_errorbefore any consumer runs). - Every consumer switches on single characters with bounded defaults
(
db_examine.c:58-59,208-209,db_write_cmd.c:56-67,db_run.c:310,db_watch.c:281-297). The worst observed effect is a format change (a read command silently disassembling via the'i'letter) and a refused write (write/4β "Unknown size", fail-safe: no size expansion is possible β the stale string always begins with the command's own first letter, and no write-relevant command name begins withb/h). - DDB itself is console/root-privileged (unprivileged writes to
debug.enter_debuggerare EPERM β DF-2246 verified that gate).
Live evidence (run.log, from /tmp/opencode/df-5720-drain.log)
db> x/x db_active <- control
db_active: 1
db> x/4 db_active <- numeric modifier silently ignored
db_active: 1
db> examine/4 db_active <- modif="examine": hex + 'a' pointer + 'i' disasm
db_active: 1 0x100000001addb %al,(%rax)
db_global_jmpbuf_valid+0x6:
db> print 0x41 <- control
41
db> print/4 0x41 <- modif="print" -> 'p': no case -> EMPTY LINE
db> print/x 0x41 <- control (letter modifier works)
41
db> write/4 db_active 1 <- modif="write" -> 'w' -> "Unknown size"
Unknown size
db> write/l db_active 1 <- control
db_active 1 = 1
All three bug markers present, all four controls normal. db_active value
verified unchanged (1) across the session; the write/l control wrote the
same value it read.
Deliberate non-goals / delimitation
- The db_fncall raw-jump β nested-fault β db_active leak cascade is
DF-5561 (filed against
sys/platform/pc64/x86_64/db_interface.c:152-195; its pack citesdb_command.c:515as the trigger). Not re-verified here. - Verified in-bounds: the
MAXARGS=11fence (db_command.c:537) checks before the write at:542, so the last write isargs[10];nargs==11hits thedefault:arm (:602-605) and never calls.retvalis assigned on every switch arm. - Verified no breakpoint-driven recursive session:
db_stop_at_pcclears software breakpoints and watchpoints on every debugger entry (sys/ddb/db_run.c:78-80), so abreak db_printf-style trap at thedb>prompt cannot re-enterkdb_traprecursively; in-command faults are caught by thedb_nofault/db_global_jmpbufnets (db_interface.c:141-153).
Cross-file lead observed during verification (NOT this finding)
After c, the guest does not resume userland: no serial getty echo, no sshd
banner (QEMU slirp fakes the host-side handshake, so that is not evidence of
a live stack), vCPU threads at ~5%. The controlled cycle
ab_cycle.sh 5 none (enter ddb via root sysctl, send zero commands,
c) reproduces the identical starvation β proving it is a property of the
debug.enter_debugger / kdb_trap / stop_cpus/restart_cpus resume path
(sys/ddb/db_sysctl.c:56-76, sys/platform/pc64/x86_64/db_interface.c:160-200),
not of any db_command.c behavior. DF-5561's baseline transcript shows
userland running immediately after c only because its leaked db_active
rerouted the first user-mode trap into a fresh kdb_trap; its patched run
(like this one) shows a silent " restarted" tail. Recommended follow-up for
the db_interface.c / mp owners; evidence in exp.A-short-none.drain.log.
Fix
fix.diff β populate modif from db_tok_number for numeric modifier
tokens (alternative: strictly reject tNUMBER as a modifier; no consumer
defines a numeric meaning). Not kernel-rebuild-validated: Info-class
correctness fix, behavior-verified at the parser level (the patched branch
is unreachable-before/ reachable-after reasoning is trivial and no panic or
corruption is involved).
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_testableInfo-class parser correctness fix; kernel-rebuild validation not warranted (no panic/corruption involved). fix.diff is a minimal, line-accurate unified diff against sys/ddb/db_command.c (never applied to sys/).
findings/poc/DF-5720/fix.diff
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log (aka /tmp/opencode/df-5720-drain.log snapshot): 'examine/4 db_active' -> 'db_active: 1 0x100000001addb %al,(%rax)' + second address header 'db_global_jmpbuf_valid+0x6:' (modif='examine' per-char walk); 'print/4 0x41' -> empty line; 'write/4 db_active 1' -> 'Unknown size'; controls 'x/x db_active' -> 'db_active: 1', 'print/x 0x41' -> '41', 'write/l db_active 1' -> 'db_active 1 = 1'. VERDICT.md has full delimitation vs DF-5561 and the post-continue cross-file lead with its battery=none control.
PoC changes
Authored directly (no seed pack existed): socket-serial rig adapted from the proven DF-5561 pattern with per-queue pacing; QEMU monitor 'server=nowait' modernized to 'server=on,wait=off'.
Verified recommended fix
In db_command's '/'-modifier parsing, format db_tok_number into modif for tNUMBER tokens (or reject tNUMBER outright) instead of copying the tIDENT-only db_tok_string buffer.
Verdict
Reproduced deterministically on the stock snapshot guest: db_command.c:364 accepts a tNUMBER token as a '/'-modifier but :369 copies db_tok_string, which the lexer fills only for tIDENT tokens (db_lex.c:170-216), so modif[] receives the previously lexed identifier β deterministically the command's own name. Live serial transcript shows examine/4 silently switching to a mixed hex/pointer/disassembly format walk, print/4 printing an empty line ('p' has no case in db_print_cmd), and write/4 refused with 'Unknown size', while letter-modifier controls (x/x, print/x, write/l) behave normally. Impact is none: modif is lexer-bounded (<=119+NUL into modif[120]) and every consumer switches on single characters with bounded defaults, so this is a debugger parser correctness/stale-state bug (console-privileged), not memory unsafety.
No comments yet.