# DF-4297 VERDICT — genkbd_softc lifetime bug in kbd_detach (sys/dev/misc/kbd/kbd.c)

## Bottom line

**REPRODUCED** on the stock guest (kernel `6.5-DEVELOPMENT #0 Thu Jul 2 2026`,
snapshot clean-source): the bug gives (1) a permanently-blocked reader after
driver detach, (2) a byte-exact **write-after-free** into a re-allocated heap
object, and (3) a **fatal kernel trap (panic)** through the stale
`kb_callback.kc_arg` → `genkbd_event` → `KNOTE` path. The full chain is
triggered by nothing more than the sequence a real driver detach performs
(`ukbd_detach` → `kbd_detach`, ukbd.c:1408), exercised by a root-loaded helper
module that makes exactly that call, plus an ordinary open/read/close of
`/dev/kbdN` and a vkbd console keyboard.

## Reproduction narrative (baseline, unpatched)

Serial-console evidence in `run.log` / `panic.txt`:

1. `kbd_uaf` opens the freshly-minted `/dev/kbd2` and blocks in `read(2)` —
   `ps` shows `STAT S2 WCHAN kbdrea` (genkbdread tsleep, kbd.c:750). At this
   point `gkb_flags == 0x00000001` (KB_ASLEEP set by the sleeping reader).
2. `hw.kbdkill.run=1` → `kbd_detach(kbd)` (the identical call ukbd_detach
   makes): `si_drv1=0xfffff8008bb98180` freed, cdev minor removed — **the
   reader is still asleep on the freed address** (`ps` again: still `kbdrea`).
   The helper immediately re-allocates the 536-byte chunk and fills it with
   `0x55`; the first groom chunk lands **at the same address**
   (`first @ 0xfffff8008bb98180`).
3. `hw.kbdkill.snap=1`: freed sc `gkb_flags == 0x55555555`.
4. `kill -USR1` the reader: userland sees `read(2) returned -1 errno=4`
   (EINTR) — proof it executed the kbd.c:756-759 error path.
   `hw.kbdkill.check2=1`: freed sc `gkb_flags == 0x55555554` — **bit 0
   (KB_ASLEEP) cleared inside a live, re-owned kernel object at the freed
   address**: write-after-free, exactly the `sc->gkb_flags &= ~KB_ASLEEP`
   statement at kbd.c:757 operating on freed memory.
5. Holder close → vkbd termination → `kbd_unregister` → `KBDIO_UNLOADING`
   delivered to `genkbd_event(kbd, event, arg=sc)` with the stale `arg`
   registered by `kbd_allocate` (kbd.c:402-403) → `KNOTE(&sc->gkb_rkq.ki_note,
   0)` (kbd.c:905) walks a knote list embedded in the freed-and-rewritten
   chunk → **`Fatal trap 9: general protection fault`, `Stopped at
   knote+0x2c: movq 0x30(%r15),%rbx`** (r15 = 0x5555555555555555-derived
   garbage kn pointer). Guest dies at `db>`.

Root cause line map:

- kbd.c:597-603 — `kbd_detach` frees `dev->si_drv1` with no `KBD_IS_BUSY`
  check, no `wakeup`, no knote purge, no callback release.
- kbd.c:744-761 — `genkbdread` sleeps on `sc` and only revalidates `kbd`
  (still valid: `kbd_detach` doesn't unregister), never `si_drv1`.
- kbd.c:756-759 — EINTR path performs the stale write.
- kbd.c:267-278 (`kbd_unregister`) → `kb_callback.kc_func(KBDIO_UNLOADING)`
  with `kc_arg` = freed sc → kbd.c:898-907 (`genkbd_event`) → KNOTE UAF read
  → panic.

## Why the guest-panic is honest

The helper module only performs `kbd_detach()` — a public function every
keyboard driver calls on detach — under the same conditions (cdev open,
keyboard registered) a USB keyboard unplug produces. The groom/check sysctls
are read/write probes on the freed chunk; the panic trigger (step 5) is the
kernel's own `kbd_unregister` → `genkbd_event` path with no module
involvement.

## Impact classification

`panic` (kernel memory corruption, privileged/physical trigger). Not an
unprivileged escalation: opening `/dev/kbdN` requires `SYSCAP_RESTRICTEDROOT`
(kbd.c:672) and the detach requires driver detach (module unload = root, or
physically unplugging a USB keyboard on a console with `/dev/kbdN` open).

## Fix validation

`fix.diff` (this pack): `kbd_detach` releases the busy callback first
(`kbd_release`), `KNOTE`s and `wakeup()`s the sleepers, then frees;
`genkbdread` re-fetches `dev->si_drv1` after every wakeup and returns `ENXIO`
once it is NULL. Applied inside the guest's `/usr/src`, kernel rebuilt
(`make nativekernel KERNCONF=X86_64_GENERIC`, kernel #1 Sat Sep 19 06:32:43),
rebooted, identical PoC re-run (`run.fix.log`):

| observation | baseline (kernel #0) | patched (kernel #1) |
|---|---|---|
| reader after `kbd_detach` | still `S2 kbdrea` (never woken) | wakes immediately, `read returned -1 errno=6` |
| freed chunk after signal | `0x55555555 -> 0x55555554` (write-after-free) | `0x55555555` unchanged |
| holder close (kbd_unregister) | `Fatal trap 9` at `knote+0x2c`, guest dead | "still alive (no panic)", guest up |

Fix status: **fixed**. Guest reset to the clean-source snapshot afterwards.

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