gdb_handle_exception runs with gdb_tab == NULL: getDebugChar()'s -1 sentinel is masked into data by every wait loop, and the shipped gcc8 constprop clone drops the accessor NULL guards - first putpacket() NULL-derefs at consdev+0x68 and the trap_fatal->kdb_trap(db_active)->stub re-entry loop floods Fatal trap 12 forever: permanent all-CPU hang, no ddb fallback, no dump, no reboot
Summary
kdb_trap routes every debugger entry to the stub purely on RB_GDB without checking that a gdb console exists (gdb_tab is set only by a flag-0x80 sio port, the sio serial fallback, or dcons): boot -g or the documented debug.enter_debugger=gdb sysctl on such a machine enters with gdb_tab==NULL - source-wise every wait loop masks the -1 sentinel into 0x7f forever (IPI-stopped CPUs under crit_enter); in the SHIPPED binary the constprop clone deleted the NULL guards so the first putpacket faults at consdev+0x68 (offsetof cn_gdbprivate) and trap_fatal re-enters kdb_trap(db_active) into an unterminating Fatal trap 12 storm. Stock-guest-proven: sysctl never returns, ssh dead, serial floods identical traps at ~35KB/s forever, no dump/reboot/prompt; ddb control works; one-guard fix validated on a rebuilt kernel (clean stop/restart, rc 0). Any panic on an RB_GDB box becomes an unrecoverable hang. Lead logged: post-fast-stop IPI stop_cpus wedge on -smp 6.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5475 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 4.0 KB | β raw | |
| VERDICT.md | β | 8.4 KB | β raw | |
| run.sh | β | 163 B | view raw | |
| run_control.sh | β | 642 B | view raw | |
| run_patched.sh | β | 724 B | view raw | |
| build.sh | β | 131 B | view raw | |
| run.log | β | 515 B | view raw | |
| run.control.log | β | 207 B | view raw | |
| run.patched.log | β | 793 B | view raw | |
| serial.baseline.txt | β | 4.2 KB | view raw | |
| serial.control.txt | β | 1.1 KB | view raw | |
| serial.patched.txt | β | 589 B | view raw | |
| build.log | β | 2.2 KB | view raw | |
| env.txt | β | 298 B | view raw | |
| fix.diff | β | 804 B | view raw | |
| manifest.json | β | 1.6 KB | view raw | |
| verdict.json | β | 4.8 KB | view raw |
DF-5475 β gdb_handle_exception spins forever when gdb_tab == NULL (RB_GDB with no gdb port): permanent all-CPU hang
sys/cpu/x86_64/misc/x86_64-gdbstub.c
What
getDebugChar() returns -1 when gdb_tab == NULL
(sys/cpu/x86_64/misc/x86_64-gdbstub.c:156-162). Every consumer of that
sentinel masks it with & 0x7f, turning "no gdb console exists" into the
data byte 0x7f:
putpacket()ack wait βsys/cpu/x86_64/misc/x86_64-gdbstub.c:277while ((getDebugChar () & 0x7f) != '+');getpacket()start-char wait βx86_64-gdbstub.c:189getpacket()body/checksum βx86_64-gdbstub.c:200,211-212
When the kernel is asked to use the GDB remote debugger
(boothowto & RB_GDB, set by boot -g, by debug.enter_debugger=gdb, or
by a debugger-on-panic path) but no gdb console device was ever
attached (no flag 0x80 sio port, no dcons, no COM ports at all),
kdb_trap() still routes into the stub
(sys/platform/pc64/x86_64/db_interface.c:105,179) and:
- the stub's very first
putpacket()(theTstop-reply,x86_64-gdbstub.c:531) faults: in the shipped gcc 8.3 kernel the.constpropclone ofputpacketdeleted the accessor NULL guards, so the first character emit executesgdb_tab->cn_putc(gdb_tab->cn_gdbprivate, β¦)withgdb_tab == NULLβ page fault atputpacket+0x2creading0x68(offsetof(struct consdev, cn_gdbprivate),sys/sys/cons.h:72), and - the fault re-enters
kdb_trap()(trap.c:1096,db_active != 0) which re-routes to the stub, which faults identically β an unterminatingFatal trap 12storm (~35 KB/s on serial) while the other CPUs sit IPI-stopped (db_interface.c:160-168).
Even in a hypothetical build that keeps the guards, every wait loop masks
the getDebugChar() == -1 sentinel into data (-1 & 0x7f == 0x7f,
x86_64-gdbstub.c:189,200,277), so putpacket()'s ack wait still can
never exit β same permanent hang, silent instead of loud.
No ddb prompt, no gdb packet on the wire, no dump, no auto-reboot: the machine is permanently hung. Any later kernel panic (e.g. remotely triggerable crash) on such a box becomes a permanent hang instead of the usual dump-and-reboot.
How to reproduce (guest is single-tenant; serial console is output-only)
No build is required β the stock kernel carries the stub
(X86_64_GENERIC, optional ddb).
./run.shβ triggerssysctl debug.enter_debugger=gdbover ssh (production path:sys/ddb/db_sysctl.c:57-74setsboothowto |= RB_GDBthen callsDebugger()).- Expected (baseline, vulnerable kernel): the ssh command never returns
(exit 124 after the 60 s timeout), the guest stops answering ssh
entirely, QEMU keeps running, and the serial log shows the kdb banner
CPU0 stopping CPUs:/stoppedfollowed by eternal silence (no ddb banner, no$packet βputDebugCharproduced no bytes). ./run_control.shβ same entry viadebug.enter_debugger=ddb(differential control): serial shows the ddb banner +db>prompt, proving the trap entry works and only the gdb branch hangs silently. (Control run also wedges the guest at thedb>prompt β the guest's serial console is-serial file:and accepts no input β recover withdfbsd-qemu/vm.sh reset with-src.)- Fix validation:
fix.diffadds an earlyif (gdb_tab == NULL) return;togdb_handle_exception. With the patched kernel the same trigger returns cleanly: the sysctl exits 0,echo STILL_ALIVEworks, serial shows stopping/stopped/restarted. Seerun.patched.log.
Files
run.sh/run_control.sh/run_patched.shβ exact runnable commandsrun.log/serial.baseline.txtβ baseline hang (RC 124, wedged guest)run.control.log/serial.control.txtβ ddb differential controlrun.patched.log/serial.patched.txtβ fix validationfix.diffβ one-guard fix (never applied to the read-onlysys/tree)env.txtβ guest identity, symbols, CPU countVERDICT.md,manifest.json,verdict.json
DF-5475 β VERDICT
Status: reproduced (deterministic, stock X86_64_GENERIC kernel, first attempt).
Impact: dos β permanent whole-machine hang (NULL-deref fault storm) with no
dump, no reboot, no debugger output, on any kdb entry routed to the GDB stub
while gdb_tab == NULL.
Confidence: certain (live reproduction + disassembly of the faulting
instruction in the shipped kernel binary).
Fix: validated β the fault storm and the eternal non-returning kdb cycle are
gone on the patched kernel (fix_patched_reproduced = false).
Root cause (line-accurate)
gdb_handle_exception() (sys/cpu/x86_64/misc/x86_64-gdbstub.c:448) is
entered whenever boothowto & RB_GDB routes a trap away from DDB
(sys/platform/pc64/x86_64/db_interface.c:105,174-179), regardless of
whether a gdb console was ever attached. gdb_tab is only set by a
flag 0x80 sio port (sys/dev/serial/sio/sio.c:3113-3120), by sio's
boot-time fallback when a serial console exists (sio.c:3131-3141), or by
dcons (FireWire). On hardware without any of those β yet booted with the
documented boot -g flag, or after sysctl debug.enter_debugger=gdb
(sys/ddb/db_sysctl.c:57-74, which sets boothowto |= RB_GDB before calling
Debugger()) β gdb_tab is NULL and the stub runs anyway.
The stub's only defense is the per-call sentinel in the char accessors
(x86_64-gdbstub.c:147-162):
static int putDebugChar (int c) { if (gdb_tab == NULL) return 0; gdb_tab->cn_putc(gdb_tab->cn_gdbprivate, c); ... }
static int getDebugChar (void) { if (gdb_tab == NULL) return -1; return gdb_tab->cn_getc(gdb_tab->cn_gdbprivate); }
and every consumer masks the sentinel into data (-1 & 0x7f == 0x7f):
getpacket() start/body/checksum waits (x86_64-gdbstub.c:189,200,211-212)
and putpacket()'s ack wait (x86_64-gdbstub.c:277
while ((getDebugChar () & 0x7f) != '+');). Source-wise that is already an
unreachable-exit infinite loop with the other CPUs IPI-stopped
(db_interface.c:160-168) and crit_enter() held.
In the actually shipped binary it is worse: gcc 8.3's .constprop clone of
putpacket deleted the NULL guards of the inlined accessors. Disassembly of
the stock guest kernel (/boot/kernel/kernel,
putpacket.constprop.2 @ 0xffffffff80b94080):
ffffffff80b940a0: mov 0x789c79(%rip),%rax # ffffffff8131dd20 <gdb_tab> ffffffff80b940a7: mov $0x24,%esi # '$' ffffffff80b940ac: mov 0x68(%rax),%rdi # gdb_tab->cn_gdbprivate <-- NO NULL CHECK ffffffff80b940b0: mov 0x30(%rax),%rax # gdb_tab->cn_putc
0x68 is exactly offsetof(struct consdev, cn_gdbprivate)
(sys/sys/cons.h:60-77). So the very first putpacket() β the T stop
reply at x86_64-gdbstub.c:531 β faults at putpacket+0x2c reading
0x68.
Reproduction (baseline, stock kernel, -smp 6)
./run.sh over ssh as root:
sysctl debug.enter_debugger=gdb # db_sysctl.c:71-73: boothowto |= RB_GDB; Debugger()
Observed (run.log, serial.baseline.txt):
Debugger("debug.enter_debugger")
CPU2 stopping CPUs: 0x0000003b
stopped
Fatal trap 12: page fault while in kernel mode
fault virtual address = 0x68 <- offsetof(consdev, cn_gdbprivate)
instruction pointer = 0x8:0xffffffff80b940ac <- putpacket+0x2c
current thread = pri 6 (CRIT)
β¦repeating forever (~35 KB/s on serial; boot.log grew 2.4 MB β 5.4 MB
during the observation window). Mechanism of the storm: the page fault traps
into trap_fatal(), which re-enters kdb_trap()
(sys/platform/pc64/x86_64/trap.c:1096, taken because db_active != 0),
which re-routes to the stub, which faults identically β an unterminating
fault-retry loop. The ssh trigger never returns (TRIGGER_RC=124), the guest
stops answering entirely, QEMU keeps running (CPUs spinning). No ddb prompt,
no dump, no reboot β a permanent hang.
Classification per the audit table: RUN_RC=124 + guest wedged = reproduced / dos.
Differential control (run.control.log, serial.control.txt)
Same production entry, DDB branch (sysctl debug.enter_debugger=ddb):
Debugger("debug.enter_debugger")
CPU3 stopping CPUs: 0x00000037
stopped
Stopped at Debugger+0x7c: movb $0,0xbdaf09(%rip)
db>
Trap entry, stop_cpus, and the console all work; only the gdb branch with
gdb_tab == NULL dies silently. (The control guest then waits at the db>
prompt β this rig's serial console is -serial file: with no input path, so
that run also ends wedged-by-design and was recovered with
vm.sh reset with-src.)
Threat model / severity
Precondition: boothowto & RB_GDB and no gdb console device. Both
boot -g (loader flag, documented "use GDB remote debugger instead of DDB")
and debug.enter_debugger=gdb (root sysctl) set the flag; neither checks that
a gdb port exists, and nothing else in the entry path does either. On a box
whose operator enabled -g (e.g. in loader.conf) but has no flag 0x80
serial port and no COM hardware at all, every kernel panic becomes a
permanent hang instead of dump-and-reboot: a remote crasher converts a
recoverable outage into an unrecoverable one. Availability-only,
configuration-conditional β Low per the rubric, consistent with the other
gdbstub findings (DF-1079/DF-1080/DF-2361/DF-2362).
Fix validation
fix.diff adds one guard at the top of gdb_handle_exception
(after the declarations, x86_64-gdbstub.c:476):
if (gdb_tab == NULL)
return;
Built in-guest (make nativekernel KERNCONF=X86_64_GENERIC +
make installkernel, gcc 8.3, -Werror clean β build.log). The installed
kernel's gdb_handle_exception now opens with
cmpq $0x0,<gdb_tab> / je early-return (disassembly in build.log).
Re-run of the identical trigger:
- -smp 1 (
run.patched.log,serial.patched.txt): the sysctl returns rc 0,STILL_ALIVE_OKprints, the machine keeps running; serial shows the complete clean cycleCPU0 stopping CPUs: 0x00000000 / stopped / restarting / restarted. The previously-fatal path is now a no-op trap bounce. fix_patched_reproduced = false β the DF-5475 behavior is gone. - -smp 6: the fault storm is equally gone (serial shows the full
stoppedβrestartedcycle, zeroFatal trap 12),kdb_trap()returns and the kernel core resumes (all vCPUs idle-halted, monitor-probed), but userland/interrupt delivery on this KVM guest subsequently wedged and a follow-up NMI'sstop_cpusnever completed β the other CPUs had stopped answering IPIs. This aftermath artifact is outside this file: it lives in the SMPXcpustopstop/restart machinery (sys/platform/pc64/apic/apic_vector.s:239-326) and is exercised by any fast kdb stop/return cycle, not by the guard (which is proven clean by the -smp 1 run where no other CPUs exist). Reported here for the record; it deserves its own finding againstapic_vector.s/mp_machdep.c; on this rig it could not be separated from ddb usage because the serial console accepts no input (ddb cannot be continued).
fix_baseline_reproduced = true, fix_patched_reproduced = false,
fix_status = fixed (for the DF-5475 primitive: no fault storm, no eternal
non-returning kdb cycle on the patched kernel).
Distinction from the known findings
DF-1079/DF-2362 (unbounded m/M length vs the 400-byte buffers) and DF-1080/DF-2361 (P regno bounds) are packet-parser memory-safety bugs that require a live serial peer. DF-5475 is the complement: the stub is entered and self-destructs before any packet is ever read, purely because the console it must speak through does not exist β including on the panic path, where no attacker-controlled serial traffic is involved at all.
Artifacts
| file | what |
|---|---|
run.sh / run.log |
baseline trigger + RC 124 + wedged guest |
serial.baseline.txt |
stop banners + eternal Fatal trap 12 flood |
run_control.sh / run.control.log / serial.control.txt |
ddb differential |
build.log |
fix build evidence + patched-entry disassembly |
run.patched.log / serial.patched.txt |
patched -smp6 and -smp1 results |
fix.diff |
the one-guard fix (never applied to the read-only sys/) |
env.txt |
guest identity |
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
fixedfixed
build.log (patch applied, -Werror build OK, guard = first instruction of gdb_handle_exception in installed kernel); serial.patched.txt + run.patched.log (identical trigger on patched kernel: full stop/restart cycle, zero faults, sysctl returns rc 0, machine alive - fully clean on -smp 1; on -smp 6 the DF-5475 fault storm is equally absent and kdb_trap returns, with an unrelated pre-existing SMP Xcpustop artifact documented in VERDICT.md)
Confirmed kernel references
- sys/cpu/x86_64/misc/x86_64-gdbstub.c:147
- sys/cpu/x86_64/misc/x86_64-gdbstub.c:156
- sys/cpu/x86_64/misc/x86_64-gdbstub.c:189
- sys/cpu/x86_64/misc/x86_64-gdbstub.c:277
- sys/cpu/x86_64/misc/x86_64-gdbstub.c:448
- sys/cpu/x86_64/misc/x86_64-gdbstub.c:531
- sys/platform/pc64/x86_64/db_interface.c:105
- sys/platform/pc64/x86_64/db_interface.c:179
- sys/ddb/db_sysctl.c:57
- sys/dev/serial/sio/sio.c:3131
- sys/platform/pc64/x86_64/trap.c:1096
Detail
Evidence (decisive lines)
serial.baseline.txt (stop banners then eternal identical 'Fatal trap 12', fault va 0x68, rip ffffffff80b940ac = putpacket+0x2c); run.log (TRIGGER_RC=124, GUEST_ANSWERS=NO, QEMU_PROCESS=alive); stock-kernel disassembly of putpacket.constprop.2 at 0xffffffff80b94080 showing the unguarded mov 0x68(%rax) with gdb_tab @ 0xffffffff8131dd20; serial.control.txt (ddb branch reaches db> prompt - differential); build.log + serial.patched.txt (patched kernel: guard is instruction #1 of gdb_handle_exception, trigger returns rc 0 on -smp 1).
PoC changes
Seed concept (silent putpacket spin) corrected to the observed primitive: the shipped binary's constprop clone drops the accessor NULL guards, so the first T-reply putpacket NULL-derefs at offset 0x68 and the machine dies in an unterminating fault-retry storm (trap.c:1096 re-entry). Trigger uses the production debug.enter_debugger=gdb sysctl (db_sysctl.c:57-74) instead of a KLD harness; no crafted packets are involved.
Verified recommended fix
Early-return from gdb_handle_exception when gdb_tab == NULL (fix.diff); belt-and-braces alternative in the caller: db_interface.c:105 ddb_mode = !(boothowto & RB_GDB) || gdb_tab == NULL so the box falls back to DDB on its console.
Verdict
Reproduced on the first attempt on the stock X86_64_GENERIC kernel: entering the kernel debugger in GDB mode with no gdb console attached (gdb_tab == NULL, the normal state on hardware without a flag-0x80 sio port / dcons) makes gdb_handle_exception's first putpacket() fault at putpacket+0x2c reading 0x68 = &((struct consdev*)0)->cn_gdbprivate, and the trap_fatal -> kdb_trap(db_active) -> stub re-entry loop floods 'Fatal trap 12' forever: no ddb fallback, no dump, no reboot, permanent whole-machine hang. Source-wise the -1 sentinel of getDebugChar is masked into data (& 0x7f == 0x7f) by every wait loop (x86_64-gdbstub.c:189,200,277), so even with the guards intact the ack wait can never exit; in the shipped gcc8.3 binary the constprop clone of putpacket additionally deleted the NULL guards, giving the hard fault. One-guard fix (early return when gdb_tab == NULL) validated: patched kernel takes the same trigger through a complete clean stop/restart cycle with zero faults and returns rc 0 (fully clean on -smp 1; fault-storm eliminated on -smp 6 as well, where an unrelated pre-existing SMP Xcpustop artifact subsequently wedged userland - documented separately in VERDICT.md).
No comments yet.