# DF-4297 — kbd.c genkbd_softc lifetime bug on kbd_detach (stuck reader + write-after-free + panic)

## What

`sys/dev/misc/kbd/kbd.c` `kbd_detach()` (lines 581-607) frees the cdev's
`si_drv1` (`genkbd_softc`, ~536 bytes, `M_DEVBUF`) while the `/dev/kbdN` file
descriptor that owns it may still be open:

* no `KBD_IS_BUSY()` check,
* no `wakeup()` of readers blocked in `genkbdread()` (`tsleep "kbdrea"`, kbd.c:750),
* no purge/invalidations for the kqueue knote list embedded in the sc
  (`sc->gkb_rkq.ki_note`, kbd.c:70),
* no release of the client callback registered by `kbd_allocate()`
  (`kb_callback.kc_arg == sc`, kbd.c:402-403).

Consequences (all demonstrated):

1. **Blocked reader never woken** — sleeps on a freed pointer forever until
   signalled.
2. **Write-after-free** — on signal, the EINTR path at kbd.c:756-759 executes
   `sc->gkb_flags &= ~KB_ASLEEP` on the freed (and, in our run, already
   re-allocated) chunk. Demonstrated byte-exact: groom pattern `0x55555555`
   became `0x55555554` at the freed address.
3. **Kernel panic (UAF read)** — when the keyboard is later unregistered
   (holder closes / driver terminates, exactly the ukbd unplug sequence:
   `ukbd_detach` → `kbd_detach`, ukbd.c:1408, then term → `kbd_unregister`),
   `kbd_unregister` delivers `KBDIO_UNLOADING` to `genkbd_event` whose
   registered `arg` is the freed sc; `genkbd_event` runs
   `KNOTE(&sc->gkb_rkq.ki_note, 0)` (kbd.c:905) walking a knote list inside
   freed memory → `Fatal trap 9: general protection fault at knote+0x2c`.

## Threat model

Reaching `genkbdread` requires opening `/dev/kbdN` (0600 root:wheel, plus
`caps_priv_check_self(SYSCAP_RESTRICTEDROOT)` in `genkbdopen`, kbd.c:672), and
`kbd_detach` requires driver detach — module unload, or a **physical USB
keyboard unplug** (`ukbd_detach` calls `kbd_detach` unconditionally while the
keyboard stays registered for replug). So the trigger is privileged or
physical-console; impact is kernel memory corruption / panic ⇒ **Low severity,
memory-corruption class**.

## Build (in guest, as root)

```
cd /root/kbduaf
cc -O -o kbd_uaf kbd_uaf.c
cc -O -o kbdrel kbdrel.c
cc -O -o kbdprobe kbdprobe.c
# module needs kbdreg.h next to the source (uses -I.)
cp /usr/src/sys/dev/misc/kbd/kbdreg.h .
printf 'KMOD=kbdkill\nSRCS=kbdkill.c\n\n.include <bsd.kmod.mk>\n' > Makefile
make
kldload vkbd            # mint fresh keyboards
kldload ./kbdkill.ko    # detach/groom/check helper module
```

`kbdkill.ko` does exactly what a driver detach does (`kbd_detach(kbd)`), plus
detection sysctls: `hw.kbdkill.index`, `.run` (kbd_detach + immediate
re-alloc of the freed chunk), `.snap` (gkb_flags at the freed address,
pre-signal), `.check2` (ggb_flags at the freed address, post-signal).

## Run (in guest, as root)

```
sh /root/kbduaf/run.sh
```

The script:
1. pre-cleans stale vkbd children from kbdmux (`KBRELKBD` via `/dev/ttyv0`),
2. mints a fresh vkbd keyboard (hold `/dev/vkbdctl` open) → new `/dev/kbdN`,
   releases it from kbdmux,
3. runs `kbd_uaf < /dev/kbdN` which blocks in `read(2)` (`kbdrea`),
4. `sysctl hw.kbdkill.run=1` → `kbd_detach()` frees the sc and immediately
   re-allocates the chunk filled with `0x55`,
5. `sysctl hw.kbdkill.snap=1` → freed sc `gkb_flags == 0x55555555`,
6. `kill -USR1` the reader → its EINTR path clears bit0 of the freed/re-owned
   object → `hw.kbdkill.check2=1` prints `0x55555554`,
7. closes the vkbd holder → kbd_unregister → KBDIO_UNLOADING → `KNOTE` on the
   freed sc → **kernel panic** (serial console: `Fatal trap 9 ... knote+0x2c`).

## Expected output

See `run.log` / `panic.txt` (serial console capture). Key lines:

```
kbdkill: run: before kbd_detach: kbd2 (vkbd0) si_drv1=0xfffff8008bb98180 gkb_flags=0x00000001
kbdkill: run: kbd_detach done - genkbd_softc freed at 0xfffff8008bb98180 ...
kbdkill: run: ... groomed 256 x 536 chunks (first @ 0xfffff8008bb98180)   <- same address
kbdkill: snap:  freed sc gkb_flags = 0x55555555 (pre-signal)
kbdkill: check2: freed sc gkb_flags now 0x55555554  <- WRITE-AFTER-FREE
Fatal trap 9: general protection fault while in kernel mode
Stopped at      knote+0x2c:     movq    0x30(%r15),%rbx
```

With `fix.diff` applied (patched kernel), the same script shows: the reader
waking at detach (`read returned -1 errno=6 No such device or address`), the
freed chunk staying `0x55555555`, and **no panic** at holder close.

## Fix

`fix.diff` — release the busy callback, `KNOTE`, `wakeup()`, then free;
readers re-fetch `dev->si_drv1` after every wakeup and bail `ENXIO`.
Validated by rebuild + re-run (see VERDICT.md).
