β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-2071

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2071 Β· 7 files
FileTypeDescriptionSize
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
README.md readme Original latent-primitive description
↓ download 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).

VERDICT.md verdict Full source-trace narrative (16-site table) + fix validation
↓ download raw

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:

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)

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED: combined kernel build rc=0 -Werror

VALIDATED: combined kernel build rc=0 -Werror
↓ fix.diffcombined build rc=0

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.