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

sccngetch(): 8-byte size_t store through (size_t *)&fkeycp pun writes 4 bytes past a kernel static; unclamped bcopy into the 17-byte static fkey trusts the DF-1503-corruptible fkeytab length

Summary

The kernel-console input path keeps two function-scope statics, struct fkeytab fkey (17 bytes) and int fkeycp (4 bytes), and passes (size_t *)&fkeycp to kbd_get_fkeystr(). genkbd_get_fkeystr performs *len = kb_fkeytab[fkey].len - an 8-byte size_t store into the 4-byte int: 4 bytes are written past the object on every console function-key read. The shipped kernel binary proves the store survives codegen (cross-TU): genkbd_get_fkeystr+0x4c is 'mov %rax,(%r12)'. The same code then does fkey.len = fkeycp (int->u_char truncation) and bcopy(p, fkey.str, fkey.len) with NO clamp to sizeof(fkey.str)==16; since kb_fkeytab[].len is corruptible to arbitrary u_char values via SETFKEY (DF-1503), this sink becomes a static-buffer overflow of up to 238 bytes past fkey into adjacent kernel .bss. On the audited build the pun's stray bytes land in the 12-byte alignment gap before the next symbol, so impact there is latent; the victim depends purely on link layout. VERIFIED: objdump of the shipped kernel (both code sites) + canary-model harness on the guest (4 OOB bytes zeroed; 239 bytes past the 17-byte model with len=255; gcc warns). Trigger: Ctrl-Alt-Esc enters DDB, function key at the db> prompt; or a console user corrupts a fkeytab len via SETFKEY on an inherited fd. Panic-on-next-use ceiling; DDB/boot context makes weaponization marginal.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3747 Β· 10 files
FileTypeDescriptionSize
pun_harness.c β€” 4.2 KB view raw
build.sh β€” 65 B view raw
run.sh β€” 89 B view raw
build.log β€” 368 B view raw
run.log β€” 1.2 KB view raw
disasm.txt β€” 1.3 KB view raw
env.txt β€” 506 B view raw
fix.diff β€” 835 B view raw
VERDICT.md β€” 5.4 KB ↓ raw
verdict.json β€” 4.0 KB view raw
VERDICT.md
↓ download raw

DF-3747 β€” VERDICT

Status: inconclusive at runtime (kernel path untriggerable on this guest); defect, codegen and layout proven certain from the shipped kernel binary + executed userland replica.

The defect (source)

sys/dev/misc/syscons/syscons.c:2004-2005 declares two function-scope statics:

static struct fkeytab fkey;   /* { u_char str[16]; u_char len; } == 17 bytes */
static int fkeycp;            /* 4 bytes */

At syscons.c:2054 the console input path passes a type-punned pointer:

p = kbd_get_fkeystr(scp->sc->kbd, KEYCHAR(c), (size_t *)&fkeycp);

kbd_get_fkeystr dispatches to genkbd_get_fkeystr() (sys/dev/misc/kbd/kbd.c:1110-1127), which executes

*len = kbd->kb_fkeytab[fkey].len;   /* kbd.c:1123 β€” size_t store */

an 8-byte size_t store into a 4-byte object β€” 4 bytes written past fkeycp (upper half always 0, since the value is a zero-extended u_char). Then syscons.c:2055-2057:

fkey.len = fkeycp;              /* int -> u_char truncation */
if ((p != NULL) && (fkey.len > 0))
    bcopy(p, fkey.str, fkey.len);   /* length never clamped to 16 */

The copy length is kb_fkeytab[].len β€” an untrusted u_char. DF-1503 (pass-1, kbd.c) proved SETFKEY can leave len set to arbitrary values up to 255 (imin(-1, MAXFK) wraps into a u_char). Through this sink that corrupted length drives bcopy up to 255 bytes into the 17-byte static fkey.13184 β†’ up to 238 bytes of attacker-influenced overflow into adjacent kernel .bss statics of syscons.ko-adjacent objects. Even without DF-1503, the pun alone is a genuine OOB write whose victim depends purely on link layout.

Binary proof (guest /boot/kernel/kernel, objdump/nm)

  1. Layout: fkeycp.13185 @ 0xffffffff817a6da0; the next symbol is fkey.13184 @ 0xffffffff817a6db0 (+0x10). The pun's stray 4 bytes ([0x…6da4, 0x…6da8)) therefore land in the 12-byte alignment gap on this build β€” no functional corruption on this exact link, but the write is outside any C object.
  2. genkbd_get_fkeystr+0x4c (0xffffffff80b7c10c): mov %rax,(%r12) β€” the 8-byte store through the pun is really emitted (the pun crosses translation units, so GCC cannot shrink it; an in-TU replica IS shrunk to 32 bits, which the harness initially reproduced and then defeated with volatile).
  3. sccngetch+0x228 (0xffffffff80b8e2a8): mov fkeycp.13185,%ecx ; movzbl %cl,%edx ; mov %cl,fkey.13184+0x10 ; … callq memmove(&fkey.str, p, len) β€” the unclamped copy into the 17-byte static is a real memmove call with the untrusted length.

Harness (executed on the guest, build.log / run.log)

pun_harness.c models the exact layout (int @+0, gap, 17-byte struct, canary guards) and performs both stores verbatim: * [1] the 8-byte store zeroes &fkeycp+4..+8 (the OOB bytes) β€” shown as [4]=00 [5]=00 [6]=00 [7]=00; * [2] with a corrupted len=255 (DF-1503 style), 239 bytes are written past the model of fkey into the guard region. GCC flags the replicated copy itself: warning: 'memcpy' forming offset [101, 303] is out-of-bounds.

Why not reproduced

The trigger is the kernel-console cngetc path: a function-key press read through the kernel console (DDB db> prompt via Ctrl-Alt-Esc, boot "press any key", panic/dump prompts). On the audit guest:

  • the active console driver is sio0 (serial); sc0 is attached but its cngetc/cncheckc are never dispatched (dmesg: "sio0: console"),
  • there is no physical keyboard (headless QEMU), and machdep.enable_panic_key=0.

So the kernel sink cannot be exercised at runtime on this guest β€” evidence is binary-level certainty + the executed replica (same convention as DF-3746). On a desktop DragonFly install where sc is the console, any physical-console user can enter DDB (Ctrl-Alt-Esc, no privilege required) and press F1 to fire the pun store; overflowing fkey additionally requires the DF-1503 corruption (console-logged-in user via SETFKEY on the inherited tty fd).

Exploitability assessment

  • Standalone pun: 4 zero bytes at a fixed link-layout-dependent offset, DDB/boot context only. On the audited build: padding. Low.
  • With DF-1503: up to 238 attacker-chosen bytes past a kernel .bss static (fkey.13184), DDB/boot context. Corrupting adjacent syscons statics (e.g. sc_console, cctl_dev, scrn_blank_time) yields at best panic-on-next-use on this no-KASLR guest; context (debugger prompt) makes weaponization marginal. Low overall β€” the fix is cheap defense-in-depth and DF-1503's own fix removes the length corruption.

Fix

fix.diff (never applied to sys/): declare static size_t fkeycp and clamp fkeycp to sizeof(fkey.str) before the copy. Compiles by inspection; not kernel-build-validated because the path cannot be exercised on this guest β€” fix_status: not_testable.

Cross-file note

The store lives in kbd.c (already covered by DF-1503/DF-1504 territory); this finding is filed against syscons.c for the pun creation and the unclamped sink, both of which are in this file.

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

not_testable
baseline no→ patch + rebuild →patched clean

fix.diff authored after binary-level verification (size_t fkeycp + clamp); cannot be kernel-build-validated on this guest because the sink (kernel-console cngetc with a function key) cannot be exercised at runtime (active console is sio0, no keyboard, enable_panic_key=0)

fix.diff (unified, against read-only sys/ tree; never applied)
↓ fix.diffper-fix-DF-3747

Confirmed kernel references

Detail

Exploit chain

physical-console user enters DDB via Ctrl-Alt-Esc (no privilege) and presses a function key -> genkbd_get_fkeystr 8-byte store at &fkeycp (4 OOB zero bytes, layout-dependent victim); combined with DF-1503 (SETFKEY-corrupted len=255 via inherited tty fd): bcopy of up to 255 attacker bytes into static fkey.str[16] -> up to 238-byte overflow of adjacent kernel .bss statics -> best-case panic-on-next-use; weaponization marginal given DDB/boot context

Evidence (decisive lines)

disasm.txt (objdump of genkbd_get_fkeystr+0x4c and sccngetch+0x228 from the shipped kernel), run.log (nm layout fkeycp.13185/fkey.13184 + harness sections [1] OOB-zero bytes and [2] 239-byte overflow), build.log (GCC -Warray-bounds on the replica), VERDICT.md

PoC changes

no seed PoC; harness written from scratch: canary-guarded static-layout model of sccngetch's statics performing the exact stores. Initial pun demo was optimized to 32 bits in-TU (matching no byte changes); fixed with a volatile pun to reproduce the kernel's cross-TU 8-byte codegen, and fixed the [1] probe to scan &fkeycp+4..+8

Verified recommended fix

make fkeycp a size_t and clamp the fkeytab length to sizeof(fkey.str) before bcopy (fix.diff)

Verdict

sccngetch() (sys/dev/misc/syscons/syscons.c:2004-2057) passes (size_t *)&fkeycp to kbd_get_fkeystr; genkbd_get_fkeystr (kbd.c:1123) executes an 8-byte size_t store into the 4-byte static int β€” a genuine 4-byte OOB write proven emitted in the shipped kernel binary (genkbd_get_fkeystr+0x4c 'mov %rax,(%r12)'; nm: fkeycp.13185 @0xffffffff817a6da0, next symbol fkey.13184 @+0x10, so the stray bytes land in alignment padding on this build). The same code then copies kb_fkeytab[].len bytes into the 17-byte static fkey without clamping (sccngetch+0x228 'callq memmove(&fkey.str,p,len)') β€” with DF-1503's corruptible len (up to 255) that is up to 238 bytes of overflow past a kernel .bss static. Runtime path is the kernel-console cngetc function-key read (DDB/boot prompt); the guest's active console is sio0 (serial) with no keyboard, so the kernel sink cannot be triggered here β€” defect/codegen/layout are instead proven from /boot/kernel/kernel plus an executed verbatim userland replica (build.log/run.log; GCC itself flags the replicated OOB copy).