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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5475 Β· 17 files
FileTypeDescriptionSize
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:277 while ((getDebugChar () & 0x7f) != '+');
  • getpacket() start-char wait β€” x86_64-gdbstub.c:189
  • getpacket() 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:

  1. the stub's very first putpacket() (the T stop-reply, x86_64-gdbstub.c:531) faults: in the shipped gcc 8.3 kernel the .constprop clone of putpacket deleted the accessor NULL guards, so the first character emit executes gdb_tab->cn_putc(gdb_tab->cn_gdbprivate, …) with gdb_tab == NULL β†’ page fault at putpacket+0x2c reading 0x68 (offsetof(struct consdev, cn_gdbprivate), sys/sys/cons.h:72), and
  2. the fault re-enters kdb_trap() (trap.c:1096, db_active != 0) which re-routes to the stub, which faults identically β€” an unterminating Fatal trap 12 storm (~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).

  1. ./run.sh β€” triggers sysctl debug.enter_debugger=gdb over ssh (production path: sys/ddb/db_sysctl.c:57-74 sets boothowto |= RB_GDB then calls Debugger()).
  2. 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: / stopped followed by eternal silence (no ddb banner, no $ packet β€” putDebugChar produced no bytes).
  3. ./run_control.sh β€” same entry via debug.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 the db> prompt β€” the guest's serial console is -serial file: and accepts no input β€” recover with dfbsd-qemu/vm.sh reset with-src.)
  4. Fix validation: fix.diff adds an early if (gdb_tab == NULL) return; to gdb_handle_exception. With the patched kernel the same trigger returns cleanly: the sysctl exits 0, echo STILL_ALIVE works, serial shows stopping/stopped/restarted. See run.patched.log.

Files

  • run.sh / run_control.sh / run_patched.sh β€” exact runnable commands
  • run.log / serial.baseline.txt β€” baseline hang (RC 124, wedged guest)
  • run.control.log / serial.control.txt β€” ddb differential control
  • run.patched.log / serial.patched.txt β€” fix validation
  • fix.diff β€” one-guard fix (never applied to the read-only sys/ tree)
  • env.txt β€” guest identity, symbols, CPU count
  • VERDICT.md, manifest.json, verdict.json
VERDICT.md
↓ download raw

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_OK prints, the machine keeps running; serial shows the complete clean cycle CPU0 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 β†’ restarted cycle, zero Fatal 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's stop_cpus never completed β€” the other CPUs had stopped answering IPIs. This aftermath artifact is outside this file: it lives in the SMP Xcpustop stop/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 against apic_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

fixed
baseline reproduced→ patch + rebuild →patched clean

fixed

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

Confirmed kernel references

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).