kbdsw dispatch functions dereference kbdsw[kb_index] without bounds/NULL checks unlike kbd_intr
Summary
kbdsw.c dispatch shim indexes global keyboard_switch_t *kbdsw[KBD_MAXKEYBOARDS=16] by kbd->kb_index and calls through vtable. Only kbd_intr (kbdsw.c:103) validates index+NULL ("if (i>=0 && i<KBD_MAXKEYBOARDS && kbdsw[i])"); other 16 dispatch functions (kbd_term/test_if/enable/disable/read/check/read_char/check_char/ioctl/lock/clear_state/get_state/set_state/get_fkeystr/poll/diag) deref kbdsw[kbd->kb_index] unconditionally. kbd_unregister NULLs kbdsw[i] at kbd.c:282; stale kbd pointer after unregister -> NULL-deref. OOB index reads adjacent kernel BSS as vtable pointer -> wild call. kb_index sole valid writer kbd.c:203 [0,16) so OOB needs separate corruption primitive; NULL form needs stale-pointer caller (syscons sc->kbd, kbdmux k->kbd, vkbd). Framework genkbd cdev re-validates kbd_get_keyboard every call safe; caching callers do not. Independent exploitation DoS-only panic; no code-exec/info-leak demonstrated. AV:L/AC:H/PR:L, A:L. Fix: kbdsw_valid() helper mirroring kbd_intr guard across all 16 sites.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2071 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| VERDICT.md | verdict | Full source-trace narrative (16-site table) + fix validation | 4.9 KB | β raw |
| README.md | readme | Original latent-primitive description | 1.0 KB | β raw |
| fix.diff | suggested-fix | git-apply-able: kbdsw_index_valid() helper + checks at all 16 sites | 4.4 KB | view raw |
| build.sh | reproduce | Combined kernel build script | 755 B | view raw |
| run.sh | reproduce | Source-only confirmation; no runtime PoC | 543 B | view raw |
| fix_build.log | build-log | Full untrimmed combined kernel build (rc=0, 0 errors) | 5.6 MB | β download |
| env.txt | environment | uname, kern.version, cc version | 501 B | view raw |
DF-2071 PoC β kbdsw dispatch shims skip the kb_index bounds/NULL check
Status: VERIFIED (source-only) + FIX VALIDATED
Only kbd_intr (kbdsw.c:103) validates kbd->kb_index before
indexing kbdsw[]. The other 16 dispatch shims deref
kbdsw[kbd->kb_index] raw β unsafe if a caller hands in a stale
keyboard_t * whose slot was unregistered (kbdsw[i] = NULL in
kbd.c:282) or whose kb_index is corrupt.
Latent: kb_index is not attacker-controllable through any in-tree
unprivileged path (the cdev entry points re-validate under kbd_token).
Needs a separate primitive to manifest (NULL-deref panic).
See VERDICT.md for the full 16-site table and fix validation.
Reproduce
./build.sh # rebuilds the patched kernel (rc=0 with -Werror) ./run.sh # source-only confirmation; no runtime PoC (latent)
Fix
fix.diff adds a kbdsw_index_valid(int i) helper mirroring the
kbd_intr guard and inserts a check at all 16 dispatch sites (return
value matches each function's return type).
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:103c 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:
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)0error:lines in the full 35,696-line build logkbdsw.cis part of in-kernelatkbd/kbdmux/ukbd; the patched TU compiled clean with the kernel's default-Werrorflags.nmonkernel.debugconfirms the 16 patched dispatch symbols are present (kbd_term,kbd_test_if,kbd_enable,kbd_ioctl, ...). Thekbdsw_index_validhelper isstatic inlineso 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)
Fix verification
fixedVALIDATED: combined kernel build rc=0 -Werror
VALIDATED: combined kernel build rc=0 -Werror
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
Source-confirmed: 16 of 17 kbdsw dispatch shims lack kbdsw[] bounds+NULL check (only kbd_intr has it). Latent defense-in-depth.
Verified recommended fix
Source-confirmed: 16 of 17 kbdsw dispatch shims lack kbdsw[] bounds+NULL check (only kbd_intr has it). Latent defense-in-depth.
Verdict
Source-confirmed: 16 of 17 kbdsw dispatch shims lack kbdsw[] bounds+NULL check (only kbd_intr has it). Latent defense-in-depth.
No comments yet.