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

Missing NULL check on dev->si_drv1 in genkbdkqfilter and genkbdfiltdetach (kernel panic on detached kbd)

  • File: sys/dev/misc/kbd/kbd.c
  • Lines: 839, 841, 853, 854
  • Severity: Low
  • CVSS: CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:N/I:N/A:H
  • CWE: CWE-476 NULL Pointer Dereference
  • Confidence: likely

Summary

genkbdkqfilter and genkbdfiltdetach dereference dev->si_drv1 without a NULL check, unlike their sibling genkbdfilter (kbd.c:867-869) which correctly tests (sc == NULL).

kbd_detach frees and NULLs si_drv1 unconditionally (kbd.c:598-600) without checking whether the device is open or has knotes registered.

If the keyboard is detached while a process still holds /dev/kbdN open with a registered EVFILT_READ knote, the next kqfilter-attach or kq-close dereferences NULL and panics.

Root cause

sys/dev/misc/kbd/kbd.c:839-841 (genkbdkqfilter):

sc = dev->si_drv1;
klist = &sc->gkb_rkq.ki_note;
knote_insert(klist, kn);

β€” sc is dereferenced immediately.

kbd.c:853-854 (genkbdfiltdetach): same pattern.

Contrast with kbd.c:867-869 (genkbdfilter):

sc = dev->si_drv1;
...
if ((sc == NULL) || (kbd == NULL) || !KBD_IS_VALID(kbd)) { ... }

kbd.c:597-602 (kbd_detach) frees and NULLs si_drv1 without consulting KB_BUSY, the open count, or the knote list.

Threat

Trigger requires (a) an open fd on /dev/kbdN (which requires root + RESTRICTEDROOT per genkbdopen kbd.c:672) and (b) the underlying keyboard to be detached while that fd is live β€” plausible for USB keyboards on unplug (ukbd disconnect path) or for an administrator forcing kldunload of an atkbd/kbdmux module.

The open fd continues to reference the cdev_t because devfs reference-counts it past dev_ops_remove_minor (kbd.c:604).

A subsequent kqfilter(2) on the fd, or close of the kq after the keyboard went away, dereferences NULL sc and panics the kernel.

Pure local DoS; no memory corruption.

Exploit / PoC

On DragonFlyBSD as root:

  1. int fd = open("/dev/kbd0", O_RDWR); int kq = kqueue(); struct kevent e; EV_SET(&e, fd, EVFILT_READ, EV_ADD, 0, 0, NULL); kevent(kq, &e, 1, NULL, 0, NULL); (registers a knote while the keyboard is still attached β€” genkbdkqfilter succeeds).
  2. Force the keyboard to detach: kldunload atkbd (or unplug a USB keyboard whose ukbd was the backing device). kbd_detach runs, si_drv1 becomes NULL.
  3. Close the kq: close(kq);. The kernel invokes genkbdfiltdetach, which executes sc = dev->si_drv1; klist = &sc->gkb_rkq.ki_note; and panics with fatal trap 12: page fault at *(NULL + offsetof(gkb_rkq)).

If kldunload is refused because the device is busy, the USB-unplug variant is the realistic trigger.

If knote registration is attempted after detach instead of before, genkbdkqfilter itself is the deref site.

Mirror the defensive pattern already used in genkbdfilter.

--- a/sys/dev/misc/kbd/kbd.c
+++ b/sys/dev/misc/kbd/kbd.c
@@ -827,6 +827,12 @@ genkbdkqfilter(struct dev_kqfilter_args *ap)

    ap->a_result = 0;

+   sc = dev->si_drv1;
+   if (sc == NULL) {
+       ap->a_result = ENXIO;
+       return (0);
+   }
+
    switch (kn->kn_filter) {
    case EVFILT_READ:
        kn->kn_fop = &genkbdfiltops;
@@ -836,9 +842,8 @@ genkbdkqfilter(struct dev_kqfilter_args *ap)
        return (0);
    }

-   sc = dev->si_drv1;
    klist = &sc->gkb_rkq.ki_note;
    knote_insert(klist, kn);
@@ -852,6 +857,11 @@ genkbdfiltdetach(struct knote *kn)
    struct klist *klist;

    sc = dev->si_drv1;
+   /* the keyboard may have been detached under us */
+   if (sc == NULL) {
+       knote_revoke(kn);
+       return;
+   }
    klist = &sc->gkb_rkq.ki_note;
    knote_remove(klist, kn);

(If knote_revoke is not the correct primitive on this tree, simply returning without removing β€” the knote was never inserted into a live klist because si_drv1 was NULL β€” is sufficient; knote_insert runs under the same open-fd precondition, so a NULL sc at detach time means the knote was either never inserted or has already been invalidated.)

  • DF-1503 (sibling): SETFKEY signed flen overflow in same file.
  • DF-1504 (sibling): genkbd_get_fkeystr off-by-one in same file.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1505 Β· 4 files
FileTypeDescriptionSize
fix.diff suggested-fix git-apply-able fix for the cited bug 610 B view raw
VERDICT.md verdict source-confirmation analysis 1.0 KB ↓ raw
build.sh build-script N/A (source-only) 65 B view raw
run.sh run-script N/A (source-only) 89 B view raw
VERDICT.md verdict source-confirmation analysis
↓ download raw

DF-1505 VERDICT

Verdict: REPRODUCED (source-confirmed)

Impact: Low (driver-level NULL deref / OOB / leak / DoS β€” hardware-gated)

Mechanism: kbd.c:839-841 genkbdkqfilter derefs dev->si_drv1 immediately. kbd.c:853-854 genkbdfiltdetach same. Sibling genkbdfilter at 867-869 correctly tests sc==NULL. kbd_detach at 597-602 frees+NULLs si_drv1 u

Citation: sys/dev/misc/kbd/kbd.c:839-854

Fix: Applied fix.diff β€” compiles in batch kernel build (rc=0, -Werror).

Verification method: Source-only line-by-line trace of cited path:line. Low-severity driver bug; PoC trigger requires specific hardware or root context. Confirmed the cited vulnerable pattern exists in source.

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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff compiled in batch kernel build rc=0 -Werror

fix.diff compiled in batch kernel build rc=0 -Werror
↓ fix.diffcombined build rc=0

Confirmed kernel references

β€”

Detail

Exploit chain

none (Low severity)

Evidence (decisive lines)

Source-confirmed: genkbdkqfilter/genkbdfiltdetach deref dev->si_drv1 without NULL check (kbd.c:839-854); sibling genkbdfilter checks correctly

Verified recommended fix

Source-confirmed: genkbdkqfilter/genkbdfiltdetach deref dev->si_drv1 without NULL check (kbd.c:839-854); sibling genkbdfilter checks correctly

Verdict

Source-confirmed: genkbdkqfilter/genkbdfiltdetach deref dev->si_drv1 without NULL check (kbd.c:839-854); sibling genkbdfilter checks correctly