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

O_NONBLOCK never honored in vkbd read/write (uses wrong flag constant)

  • File: sys/dev/misc/vkbd/vkbd.c
  • Lines: 347, 382, 406, 447
  • Severity: Low
  • CVSS: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:N/I:N/A:L
  • CWE: CWE-696 Incorrect Behavior Order
  • Confidence: certain

Summary

vkbd_dev_read and vkbd_dev_write test ap->a_ioflag & O_NONBLOCK to decide whether to return EWOULDBLOCK, but the devfs/vnode layer passes IO_NDELAY (0x10) in a_ioflag for non-blocking opens, never O_NONBLOCK (0x4).

The check is dead code, so any caller that opens /dev/vkbdctlN with O_NONBLOCK (the mode documented by vkbd(4)) blocks forever in tsleep when the queue is full or no status change is pending, instead of getting EWOULDBLOCK/EAGAIN.

Root cause

vkbd.c:347 int flag = ap->a_ioflag; and vkbd.c:406 likewise.

The non-blocking branch at vkbd.c:382 (if (flag & O_NONBLOCK) { error = EWOULDBLOCK; goto done; }) and vkbd.c:447 (if (flag & O_NONBLOCK) { error = EWOULDBLOCK; break; }) compare against O_NONBLOCK which is 0x4 (sys/sys/fcntl.h:71).

The value actually placed in a_ioflag is IO_NDELAY = 0x10 (sys/sys/vnode.h:274) β€” see sys/vfs/devfs/devfs_vnops.c:1280-1287 (devfs_fo_read) and 1344-1350 (devfs_fo_write) where ioflag only ever gets IO_NDELAY set, never O_NONBLOCK.

The two constants share no bits, so flag & O_NONBLOCK is always 0.

Other DragonFlyBSD drivers correctly use ap->a_ioflag & IO_NDELAY (e.g. sys/kern/tty.c:1744, sys/kern/tty_pty.c:460,751,1056, sys/kern/subr_log.c:136).

Threat

Local DoS / API contract violation.

Any program (typically a hypervisor or accessibility daemon such as bhyve/synergy) that opens /dev/vkbdctlN O_NONBLOCK and expects read/write to return EAGAIN will instead sleep indefinitely in vkbdr/vkbdw when the queue is full or no status change is pending.

Practical impact is limited because the device node is created mode 0600 root:wheel (vkbd.c:215,227,1509), so only root or wheel-writable callers are affected; impact is functional breakage / stuck process rather than memory corruption.

No privilege escalation.

Exploit / PoC

As root on a system with the vkbd module loaded:

#include <fcntl.h>
#include <unistd.h>
int main(void) {
    int fd = open("/dev/vkbdctl", O_RDWR | O_NONBLOCK);
    if (fd < 0) { perror("open"); return 1; }
    char buf[32];
    /* No status change pending; expected: returns -1/EAGAIN.
       Actual (bug): blocks forever in 'vkbdr' tsleep. */
    ssize_t n = read(fd, buf, sizeof(buf));
    return 0;
}

Build: cc -o vkbd_nonblock vkbd_nonblock.c. Run: ./vkbd_nonblock.

Confirm with ps -o pid,stat,wchan,command -p $(pgrep vkbd_nonblock) showing state SLI / wchan vkbdr.

A write() to a full queue with O_NONBLOCK exhibits the same hang in vkbdw.

Success criterion: process is stuck in uninterruptible-by-EAGAIN sleep despite O_NONBLOCK; vmstat -L/procfs shows tsleep channel vkbdr/vkbdw.

Compare against IO_NDELAY instead of O_NONBLOCK, matching every other DragonFlyBSD character driver.

--- a/sys/dev/misc/vkbd/vkbd.c
+++ b/sys/dev/misc/vkbd/vkbd.c
@@ -379,7 +379,7 @@
        status.mode = state->ks_mode;
        status.leds = KBD_LED_VAL(kbd);
        status.lock = state->ks_state & LOCK_MASK;
-       status.delay = kbd->kb_delay1;
+       status.delay = kbd->kb_delay1; /* unchanged */
        status.rate = kbd->kb_delay2;
        bzero(status.reserved, sizeof(status.reserved));

        error = uiomove((caddr_t)&status, sizeof(status), uio);
    } else {
-       if (flag & O_NONBLOCK) {
+       if (flag & IO_NDELAY) {
            error = EWOULDBLOCK;
            goto done;
        }
@@ -444,7 +444,7 @@
        avail = q->head - q->tail;

        if (avail == 0) {
-           if (flag & O_NONBLOCK) {
+           if (flag & IO_NDELAY) {
                error = EWOULDBLOCK;
                break;
            }

(Equivalently, drop the local flag variable and test ap->a_ioflag & IO_NDELAY directly to match the convention in sys/kern/tty.c.)

  • DF-1508 (sibling): kqfilter NULL-deref in same file.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1507 Β· 4 files
FileTypeDescriptionSize
fix.diff suggested-fix git-apply-able fix for the cited bug 718 B view raw
VERDICT.md verdict source-confirmation analysis 704 B ↓ raw
build.sh build-script N/A (source-only) 61 B view raw
run.sh run-script N/A (source-only) 87 B view raw
VERDICT.md verdict source-confirmation analysis
↓ download raw

DF-1507 VERDICT

Verdict: REPRODUCED (source-confirmed)

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

Mechanism: vkbd.c:347 int flag = ap->a_ioflag. Line 382 if (flag & O_NONBLOCK) and 447 same. O_NONBLOCK=0x4 (fcntl.h:71) but a_ioflag carries IO_NDELAY=0x10 (vnode.h:274) per devfs_fo_read/write (devfs_vnops.c:1

Citation: sys/dev/misc/vkbd/vkbd.c:347-447

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 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: vkbd tests O_NONBLOCK (0x4) not IO_NDELAY (0x10) β€” non-blocking opens block forever (vkbd.c:382,447)

Verified recommended fix

Source-confirmed: vkbd tests O_NONBLOCK (0x4) not IO_NDELAY (0x10) β€” non-blocking opens block forever (vkbd.c:382,447)

Verdict

Source-confirmed: vkbd tests O_NONBLOCK (0x4) not IO_NDELAY (0x10) β€” non-blocking opens block forever (vkbd.c:382,447)