# DF-2071 — REPRODUCED (source-confirmed, latent) + FIX VALIDATED

## Verdict

**REPRODUCED via source-only trace.** Only `kbd_intr()` validates
`kbd->kb_index` before indexing the global `kbdsw[]` dispatch table;
the other 16 dispatch shims dereference `kbdsw[kbd->kb_index]` raw.
The bug pattern (missing defense-in-depth check) is confirmed; the
finding's latent classification is also confirmed (`kb_index` is not
attacker-controllable through any in-tree unprivileged path).

## Mechanism (path:line)

`kbdsw[]` is a global array of 16 (`KBD_MAXKEYBOARDS`) function-vector
pointers, populated at `kbd_register()` and **cleared to NULL** at
`kbd_unregister()` (`sys/dev/misc/kbd/kbd.c:282`). Each `kbd_*()`
dispatch shim in `kbdsw.c` is supposed to look up the right switch via
`kbd->kb_index` and call through it.

The bound + NULL check exists in exactly one shim:

* `sys/dev/misc/kbd/kbdsw.c:103`
  ```c
  if (i >= 0 && i < KBD_MAXKEYBOARDS && kbdsw[i]) {
      KBD_LOCK(kbd);
      error = (*kbdsw[i]->intr)(kbd, arg);
      KBD_UNLOCK(kbd);
  }
  ```

The comment at `kbdsw.c:90-92` even explains why `kbd_intr` is
suspicious: *"Be suspicious, just in case kbd_intr() is called from an
interrupt before the keyboard switch is completely installed."*

The other 16 dispatch sites do **not** have this guard:

| Function | Line | Defect |
|---|---|---|
| `kbd_term`         | `kbdsw.c:80`  | `(*kbdsw[kbd->kb_index]->term)(kbd)` |
| `kbd_test_if`      | `kbdsw.c:118` | `(*kbdsw[kbd->kb_index]->test_if)(kbd)` |
| `kbd_enable`       | `kbdsw.c:131` | `(*kbdsw[kbd->kb_index]->enable)(kbd)` |
| `kbd_disable`      | `kbdsw.c:144` | `(*kbdsw[kbd->kb_index]->disable)(kbd)` |
| `kbd_read`         | `kbdsw.c:157` | `(*kbdsw[kbd->kb_index]->read)(kbd, wait)` |
| `kbd_check`        | `kbdsw.c:170` | `(*kbdsw[kbd->kb_index]->check)(kbd)` |
| `kbd_read_char`    | `kbdsw.c:183` | `(*kbdsw[kbd->kb_index]->read_char)(kbd, wait)` |
| `kbd_check_char`   | `kbdsw.c:196` | `(*kbdsw[kbd->kb_index]->check_char)(kbd)` |
| `kbd_ioctl`        | `kbdsw.c:210` | checks `kbd != NULL` only; still no index/`kbdsw[i]` check |
| `kbd_lock`         | `kbdsw.c:225` | raw deref |
| `kbd_clear_state`  | `kbdsw.c:237` | raw deref |
| `kbd_get_state`    | `kbdsw.c:248` | raw deref |
| `kbd_set_state`    | `kbdsw.c:261` | raw deref |
| `kbd_get_fkeystr`  | `kbdsw.c:274` | raw deref |
| `kbd_poll`         | `kbdsw.c:290` | raw deref |
| `kbd_diag`         | `kbdsw.c:303` | raw deref |

If a caller hands any of these a stale `keyboard_t *` whose slot has
been unregistered (`kbdsw[i] == NULL`), the deref is a NULL-pointer
function-call → panic. If `kb_index` is corrupt (out of [0,16)), the
deref reads a wild `kbdsw[]` entry → arbitrary kernel function call.

## Reachability / impact ceiling

**Latent.** The cdev-facing `genkbd_*` entry points
(`kbd.c:genkbd_open/read/ioctl`) re-derive the live `keyboard_t *` from
`dev->si_drv1` under `kbd_token` and validate against
`keyboard[kbd->kb_index] != kbd` at every entry, so an unprivileged
`/dev/kbdN` user cannot normally reach the dispatchers with a stale
pointer. The primitive requires a separate bug that hands a stale
`keyboard_t *` to one of the 16 unchecked shims (e.g. a USB/vkbd
detach race that loses the `kbd_token` or skips `kbd_unregister`'s
invalidation). If such a path exists the result is **NULL-deref panic
/ local DoS**; no memory-corruption primitive is directly derivable.

## Fix

`fix.diff` adds a static inline helper mirroring the `kbd_intr` guard:

```c
static inline bool
kbdsw_index_valid(int i)
{
        return (i >= 0 && i < KBD_MAXKEYBOARDS && kbdsw[i] != NULL);
}
```

…and inserts `if (!kbdsw_index_valid(kbd->kb_index)) return <sentinel>;`
at the head of all 16 dispatchers (return value matches each function's
return type: `ENODEV` for `int`, `NOKEY` for `kbd_read_char`, `NULL`
for `kbd_get_fkeystr`, bare `return` for the two `void` shims). The
existing `kbd` NULL check in `kbd_ioctl` is preserved ahead of the
index check.

Matches the finding markdown's recommendation (`kbdsw_valid()` helper
mirroring the `kbd_intr` guard across all 16 dispatch sites).

## Phase-8 build validation

Combined kernel + modules build (DF-2068 / DF-2069 / DF-2070 / DF-2071 /
DF-2072) on DragonFly 6.5-DEVELOPMENT #0 baseline:

* `=== NK_DONE rc=0 ===` (2026-07-25 11:31:20 UTC)
* `0` `error:` lines in the full 35,696-line build log
* `kbdsw.c` is part of in-kernel `atkbd`/`kbdmux`/`ukbd`; the patched
  TU compiled clean with the kernel's default `-Werror` flags. `nm` on
  `kernel.debug` confirms the 16 patched dispatch symbols are present
  (`kbd_term`, `kbd_test_if`, `kbd_enable`, `kbd_ioctl`, ...). The
  `kbdsw_index_valid` helper is `static inline` so it has no standalone
  symbol (inlined into each dispatcher).

The default kernel build IS a `-Werror` build; see `fix_build.log` and
`env.txt`.

## Reproduce

```
./build.sh    # rebuilds the patched kernel (rc=0 with -Werror)
./run.sh      # source-only confirmation; no runtime PoC (latent)
```
