# DF-5724 — unvalidated `$tabstops` DDB variable: divide-by-zero trap cascade / infinite-loop hard hang

**File:** `sys/ddb/db_output.c` (db_force_whitespace), `sys/ddb/db_variables.c`
**Severity:** Medium (whole-host unrecoverable freeze from the DDB console;
DDB entry is reachable by jailed root via `sysctl debug.enter_debugger` per DF-2246)
**Status:** reproduced live (both variants), fix validated on a rebuilt kernel.

## The bug

`set $tabstops=N` stores N into `db_tab_stop_width` with **no validation**
(`sys/ddb/db_variables.c:52` registration, `db_write_variable`
`sys/ddb/db_variables.c:120-128` does a raw store). The output layer then computes

```c
/* sys/ddb/db_output.c:65 */
#define NEXT_TAB(i) rounddown((i) + db_tab_stop_width, db_tab_stop_width)
/* sys/sys/param.h:400 */
#define rounddown(x, y) (((x)/(y))*(y))     /* no zero guard */
```

inside `db_force_whitespace()` (`sys/ddb/db_output.c:74-93`), which runs on
every printing character of every DDB prompt/output line — and, critically,
at the top of every `db_readline()` (`sys/ddb/db_input.c:306`).

- **`tabstops=0`** -> `rounddown(x,0)` = integer division by zero -> #DE
  (x86 trap 18) **inside the stopped-machine debugger**. The trap handler's
  own output re-triggers the division: cascading fatal traps, each ~0x370 of
  kernel stack consumed, until the machine is a silent brick.
- **`tabstops=-8`** -> `next_tab` computes <= 0, `next_tab <= db_output_position`
  holds, the inner `while (last_print < next_tab)` never advances and the outer
  `while (last_print < db_output_position)` spins forever — hard hang with all
  CPUs stopped, no trap, no output, ssh dead. (The pending trailing space of
  the very next `"db> "` prompt is enough — `db_readline`'s synch call fires it
  before any command is even typed.)

Same class for huge tabstops (`0x7fffffffffffffff`): signed overflow in
`(i) + db_tab_stop_width` wraps negative -> same non-advancing loop.

## Build / run

```
./serial_vm.sh up
ssh -F ../../../dfbsd-qemu/config dfbsd 'sysctl debug.enter_debugger=ddb'   # root vehicle
python3 drive.py A    # set $tabstops=0  -> fatal trap 18 cascade
# reboot, then
python3 drive.py B    # set $tabstops=-8 -> silent hard hang
```

## Expected output

Variant A (stock, `serial.variantA.log`, 31 traps):

```
db> set $tabstops=0
db>
Fatal trap 18: integer divide fault while in kernel mode
cpuid = 1; lapic id = 1
instruction pointer     = 0x8:0xffffffff802cba26      <- db_force_whitespace+0x26
...
Fatal trap 18: ... (31x, stack pointer descending 0xfffff801183f7458 -> 0xfffff8004f0686d8)
[then silence; ssh dead; only a hard QEMU reset recovers]
```

Variant B (stock, `serial.variantB.log`):

```
db> set $tabstops=-8
db>                                      <- last output ever; no echo, no trap
[ssh dead; CPU spinning in db_force_whitespace; hard reset required]
```

Fixed kernel (`serial.fixedkernel.log`, built from `fix.diff`):

```
db> set $tabstops=0
tabstops must be >= 1
db> x/x db_active
db_active:      1
db> set $tabstops=-8
tabstops must be >= 1
db> print/d 5
          5
db> x/x db_active
db_active:      1
db>                                    <- session fully alive
```

## Fix

`fix.diff` — validating `db_varfcn_t` setters for `$tabstops`/`$maxwidth`
(reject < 1 via `db_error`, which longjmps before the store,
`sys/ddb/db_command.c:481-488`) plus a defensive `db_tab_stop_width > 0`
ternary in `NEXT_TAB` so no future writer can #DE/hang the debugger.
Validated: baseline (trap cascade + hang) vs patched (clean rejection,
debugger alive) on the same guest, same rig.
