# 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`):

```c
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`):

```c
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.
