mirror of
https://github.com/raspberrypi/linux.git
synced 2026-01-02 07:43:34 +00:00
This requirement was overeagerly loosened in commit2f83e38a09("tty: Permit some TIOCL_SETSEL modes without CAP_SYS_ADMIN"), but as it turns out, (1) the logic I implemented there was inconsistent (apologies!), (2) TIOCL_SELMOUSEREPORT might actually be a small security risk after all, and (3) TIOCL_SELMOUSEREPORT is only meant to be used by the mouse daemon (GPM or Consolation), which runs as CAP_SYS_ADMIN already. In more detail: 1. The previous patch has inconsistent logic: In commit2f83e38a09("tty: Permit some TIOCL_SETSEL modes without CAP_SYS_ADMIN"), we checked for sel_mode == TIOCL_SELMOUSEREPORT, but overlooked that the lower four bits of this "mode" parameter were actually used as an additional way to pass an argument. So the patch did actually still require CAP_SYS_ADMIN, if any of the mouse button bits are set, but did not require it if none of the mouse buttons bits are set. This logic is inconsistent and was not intentional. We should have the same policies for using TIOCL_SELMOUSEREPORT independent of the value of the "hidden" mouse button argument. I sent a separate documentation patch to the man page list with more details on TIOCL_SELMOUSEREPORT: https://lore.kernel.org/all/20250223091342.35523-2-gnoack3000@gmail.com/ 2. TIOCL_SELMOUSEREPORT is indeed a potential security risk which can let an attacker simulate "keyboard" input to command line applications on the same terminal, like TIOCSTI and some other TIOCLINUX "selection mode" IOCTLs. By enabling mouse reporting on a terminal and then injecting mouse reports through TIOCL_SELMOUSEREPORT, an attacker can simulate mouse movements on the same terminal, similar to the TIOCSTI keystroke injection attacks that were previously possible with TIOCSTI and other TIOCL_SETSEL selection modes. Many programs (including libreadline/bash) are then prone to misinterpret these mouse reports as normal keyboard input because they do not expect input in the X11 mouse protocol form. The attacker does not have complete control over the escape sequence, but they can at least control the values of two consecutive bytes in the binary mouse reporting escape sequence. I went into more detail on that in the discussion at https://lore.kernel.org/all/20250221.0a947528d8f3@gnoack.org/ It is not equally trivial to simulate arbitrary keystrokes as it was with TIOCSTI (commit83efeeeb3d("tty: Allow TIOCSTI to be disabled")), but the general mechanism is there, and together with the small number of existing legit use cases (see below), it would be better to revert back to requiring CAP_SYS_ADMIN for TIOCL_SELMOUSEREPORT, as it was already the case before commit2f83e38a09("tty: Permit some TIOCL_SETSEL modes without CAP_SYS_ADMIN"). 3. TIOCL_SELMOUSEREPORT is only used by the mouse daemons (GPM or Consolation), and they are the only legit use case: To quote console_codes(4): The mouse tracking facility is intended to return xterm(1)-compatible mouse status reports. Because the console driver has no way to know the device or type of the mouse, these reports are returned in the console input stream only when the virtual terminal driver receives a mouse update ioctl. These ioctls must be generated by a mouse-aware user-mode application such as the gpm(8) daemon. Jared Finder has also confirmed in https://lore.kernel.org/all/491f3df9de6593df8e70dbe77614b026@finder.org/ that Emacs does not call TIOCL_SELMOUSEREPORT directly, and it would be difficult to find good reasons for doing that, given that it would interfere with the reports that GPM is sending. More information on the interaction between GPM, terminals and the kernel with additional pointers is also available in this patch: https://lore.kernel.org/all/a773e48920aa104a65073671effbdee665c105fc.1603963593.git.tammo.block@gmail.com/ For background on who else uses TIOCL_SELMOUSEREPORT: Debian Code search finds one page of results, the only two known callers are the two mouse daemons GPM and Consolation. (GPM does not show up in the search results because it uses literal numbers to refer to TIOCLINUX-related enums. I looked through GPM by hand instead. TIOCL_SELMOUSEREPORT is also not used from libgpm.) https://codesearch.debian.net/search?q=TIOCL_SELMOUSEREPORT Cc: Jared Finder <jared@finder.org> Cc: Jann Horn <jannh@google.com> Cc: Hanno Böck <hanno@hboeck.de> Cc: Jiri Slaby <jirislaby@kernel.org> Cc: Kees Cook <kees@kernel.org> Cc: stable <stable@kernel.org> Fixes:2f83e38a09("tty: Permit some TIOCL_SETSEL modes without CAP_SYS_ADMIN") Signed-off-by: Günther Noack <gnoack3000@gmail.com> Link: https://lore.kernel.org/r/20250411070144.3959-2-gnoack3000@gmail.com Signed-off-by: Greg Kroah-Hartman <gregkh@linuxfoundation.org>
444 lines
11 KiB
C
444 lines
11 KiB
C
// SPDX-License-Identifier: GPL-2.0
|
|
/*
|
|
* This module exports the functions:
|
|
*
|
|
* 'int set_selection_user(struct tiocl_selection __user *,
|
|
* struct tty_struct *)'
|
|
* 'int set_selection_kernel(struct tiocl_selection *, struct tty_struct *)'
|
|
* 'void clear_selection(void)'
|
|
* 'int paste_selection(struct tty_struct *)'
|
|
* 'int sel_loadlut(u32 __user *)'
|
|
*
|
|
* Now that /dev/vcs exists, most of this can disappear again.
|
|
*/
|
|
|
|
#include <linux/module.h>
|
|
#include <linux/tty.h>
|
|
#include <linux/sched.h>
|
|
#include <linux/mm.h>
|
|
#include <linux/mutex.h>
|
|
#include <linux/slab.h>
|
|
#include <linux/types.h>
|
|
|
|
#include <linux/uaccess.h>
|
|
|
|
#include <linux/kbd_kern.h>
|
|
#include <linux/vt_kern.h>
|
|
#include <linux/consolemap.h>
|
|
#include <linux/selection.h>
|
|
#include <linux/tiocl.h>
|
|
#include <linux/console.h>
|
|
#include <linux/tty_flip.h>
|
|
|
|
#include <linux/sched/signal.h>
|
|
|
|
/* Don't take this from <ctype.h>: 011-015 on the screen aren't spaces */
|
|
#define is_space_on_vt(c) ((c) == ' ')
|
|
|
|
/* FIXME: all this needs locking */
|
|
static struct vc_selection {
|
|
struct mutex lock;
|
|
struct vc_data *cons; /* must not be deallocated */
|
|
char *buffer;
|
|
unsigned int buf_len;
|
|
volatile int start; /* cleared by clear_selection */
|
|
int end;
|
|
} vc_sel = {
|
|
.lock = __MUTEX_INITIALIZER(vc_sel.lock),
|
|
.start = -1,
|
|
};
|
|
|
|
/* clear_selection, highlight and highlight_pointer can be called
|
|
from interrupt (via scrollback/front) */
|
|
|
|
/* set reverse video on characters s-e of console with selection. */
|
|
static inline void highlight(const int s, const int e)
|
|
{
|
|
invert_screen(vc_sel.cons, s, e-s+2, true);
|
|
}
|
|
|
|
/* use complementary color to show the pointer */
|
|
static inline void highlight_pointer(const int where)
|
|
{
|
|
complement_pos(vc_sel.cons, where);
|
|
}
|
|
|
|
static u32
|
|
sel_pos(int n, bool unicode)
|
|
{
|
|
if (unicode)
|
|
return screen_glyph_unicode(vc_sel.cons, n / 2);
|
|
return inverse_translate(vc_sel.cons, screen_glyph(vc_sel.cons, n),
|
|
false);
|
|
}
|
|
|
|
/**
|
|
* clear_selection - remove current selection
|
|
*
|
|
* Remove the current selection highlight, if any from the console holding the
|
|
* selection.
|
|
*
|
|
* Locking: The caller must hold the console lock.
|
|
*/
|
|
void clear_selection(void)
|
|
{
|
|
highlight_pointer(-1); /* hide the pointer */
|
|
if (vc_sel.start != -1) {
|
|
highlight(vc_sel.start, vc_sel.end);
|
|
vc_sel.start = -1;
|
|
}
|
|
}
|
|
EXPORT_SYMBOL_GPL(clear_selection);
|
|
|
|
bool vc_is_sel(const struct vc_data *vc)
|
|
{
|
|
return vc == vc_sel.cons;
|
|
}
|
|
|
|
/*
|
|
* User settable table: what characters are to be considered alphabetic?
|
|
* 128 bits. Locked by the console lock.
|
|
*/
|
|
static u32 inwordLut[]={
|
|
0x00000000, /* control chars */
|
|
0x03FFE000, /* digits and "-./" */
|
|
0x87FFFFFE, /* uppercase and '_' */
|
|
0x07FFFFFE, /* lowercase */
|
|
};
|
|
|
|
static inline int inword(const u32 c)
|
|
{
|
|
return c > 0x7f || (( inwordLut[c>>5] >> (c & 0x1F) ) & 1);
|
|
}
|
|
|
|
/**
|
|
* sel_loadlut() - load the LUT table
|
|
* @lut: user table
|
|
*
|
|
* Load the LUT table from user space. Make a temporary copy so a partial
|
|
* update doesn't make a mess.
|
|
*
|
|
* Locking: The console lock is acquired.
|
|
*/
|
|
int sel_loadlut(u32 __user *lut)
|
|
{
|
|
u32 tmplut[ARRAY_SIZE(inwordLut)];
|
|
|
|
if (copy_from_user(tmplut, lut, sizeof(inwordLut)))
|
|
return -EFAULT;
|
|
|
|
console_lock();
|
|
memcpy(inwordLut, tmplut, sizeof(inwordLut));
|
|
console_unlock();
|
|
|
|
return 0;
|
|
}
|
|
|
|
/* does screen address p correspond to character at LH/RH edge of screen? */
|
|
static inline int atedge(const int p, int size_row)
|
|
{
|
|
return (!(p % size_row) || !((p + 2) % size_row));
|
|
}
|
|
|
|
/* stores the char in UTF8 and returns the number of bytes used (1-4) */
|
|
static int store_utf8(u32 c, char *p)
|
|
{
|
|
if (c < 0x80) {
|
|
/* 0******* */
|
|
p[0] = c;
|
|
return 1;
|
|
} else if (c < 0x800) {
|
|
/* 110***** 10****** */
|
|
p[0] = 0xc0 | (c >> 6);
|
|
p[1] = 0x80 | (c & 0x3f);
|
|
return 2;
|
|
} else if (c < 0x10000) {
|
|
/* 1110**** 10****** 10****** */
|
|
p[0] = 0xe0 | (c >> 12);
|
|
p[1] = 0x80 | ((c >> 6) & 0x3f);
|
|
p[2] = 0x80 | (c & 0x3f);
|
|
return 3;
|
|
} else if (c < 0x110000) {
|
|
/* 11110*** 10****** 10****** 10****** */
|
|
p[0] = 0xf0 | (c >> 18);
|
|
p[1] = 0x80 | ((c >> 12) & 0x3f);
|
|
p[2] = 0x80 | ((c >> 6) & 0x3f);
|
|
p[3] = 0x80 | (c & 0x3f);
|
|
return 4;
|
|
} else {
|
|
/* outside Unicode, replace with U+FFFD */
|
|
p[0] = 0xef;
|
|
p[1] = 0xbf;
|
|
p[2] = 0xbd;
|
|
return 3;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* set_selection_user - set the current selection.
|
|
* @sel: user selection info
|
|
* @tty: the console tty
|
|
*
|
|
* Invoked by the ioctl handle for the vt layer.
|
|
*
|
|
* Locking: The entire selection process is managed under the console_lock.
|
|
* It's a lot under the lock but its hardly a performance path.
|
|
*/
|
|
int set_selection_user(const struct tiocl_selection __user *sel,
|
|
struct tty_struct *tty)
|
|
{
|
|
struct tiocl_selection v;
|
|
|
|
if (copy_from_user(&v, sel, sizeof(*sel)))
|
|
return -EFAULT;
|
|
|
|
/*
|
|
* TIOCL_SELCLEAR and TIOCL_SELPOINTER are OK to use without
|
|
* CAP_SYS_ADMIN as they do not modify the selection.
|
|
*/
|
|
switch (v.sel_mode) {
|
|
case TIOCL_SELCLEAR:
|
|
case TIOCL_SELPOINTER:
|
|
break;
|
|
default:
|
|
if (!capable(CAP_SYS_ADMIN))
|
|
return -EPERM;
|
|
}
|
|
|
|
return set_selection_kernel(&v, tty);
|
|
}
|
|
|
|
static int vc_selection_store_chars(struct vc_data *vc, bool unicode)
|
|
{
|
|
char *bp, *obp;
|
|
unsigned int i;
|
|
|
|
/* Allocate a new buffer before freeing the old one ... */
|
|
/* chars can take up to 4 bytes with unicode */
|
|
bp = kmalloc_array((vc_sel.end - vc_sel.start) / 2 + 1, unicode ? 4 : 1,
|
|
GFP_KERNEL | __GFP_NOWARN);
|
|
if (!bp) {
|
|
printk(KERN_WARNING "selection: kmalloc() failed\n");
|
|
clear_selection();
|
|
return -ENOMEM;
|
|
}
|
|
kfree(vc_sel.buffer);
|
|
vc_sel.buffer = bp;
|
|
|
|
obp = bp;
|
|
for (i = vc_sel.start; i <= vc_sel.end; i += 2) {
|
|
u32 c = sel_pos(i, unicode);
|
|
if (unicode)
|
|
bp += store_utf8(c, bp);
|
|
else
|
|
*bp++ = c;
|
|
if (!is_space_on_vt(c))
|
|
obp = bp;
|
|
if (!((i + 2) % vc->vc_size_row)) {
|
|
/* strip trailing blanks from line and add newline,
|
|
unless non-space at end of line. */
|
|
if (obp != bp) {
|
|
bp = obp;
|
|
*bp++ = '\r';
|
|
}
|
|
obp = bp;
|
|
}
|
|
}
|
|
vc_sel.buf_len = bp - vc_sel.buffer;
|
|
|
|
return 0;
|
|
}
|
|
|
|
static int vc_do_selection(struct vc_data *vc, unsigned short mode, int ps,
|
|
int pe)
|
|
{
|
|
int new_sel_start, new_sel_end, spc;
|
|
bool unicode = vt_do_kdgkbmode(fg_console) == K_UNICODE;
|
|
|
|
switch (mode) {
|
|
case TIOCL_SELCHAR: /* character-by-character selection */
|
|
new_sel_start = ps;
|
|
new_sel_end = pe;
|
|
break;
|
|
case TIOCL_SELWORD: /* word-by-word selection */
|
|
spc = is_space_on_vt(sel_pos(ps, unicode));
|
|
for (new_sel_start = ps; ; ps -= 2) {
|
|
if ((spc && !is_space_on_vt(sel_pos(ps, unicode))) ||
|
|
(!spc && !inword(sel_pos(ps, unicode))))
|
|
break;
|
|
new_sel_start = ps;
|
|
if (!(ps % vc->vc_size_row))
|
|
break;
|
|
}
|
|
|
|
spc = is_space_on_vt(sel_pos(pe, unicode));
|
|
for (new_sel_end = pe; ; pe += 2) {
|
|
if ((spc && !is_space_on_vt(sel_pos(pe, unicode))) ||
|
|
(!spc && !inword(sel_pos(pe, unicode))))
|
|
break;
|
|
new_sel_end = pe;
|
|
if (!((pe + 2) % vc->vc_size_row))
|
|
break;
|
|
}
|
|
break;
|
|
case TIOCL_SELLINE: /* line-by-line selection */
|
|
new_sel_start = rounddown(ps, vc->vc_size_row);
|
|
new_sel_end = rounddown(pe, vc->vc_size_row) +
|
|
vc->vc_size_row - 2;
|
|
break;
|
|
case TIOCL_SELPOINTER:
|
|
highlight_pointer(pe);
|
|
return 0;
|
|
default:
|
|
return -EINVAL;
|
|
}
|
|
|
|
/* remove the pointer */
|
|
highlight_pointer(-1);
|
|
|
|
/* select to end of line if on trailing space */
|
|
if (new_sel_end > new_sel_start &&
|
|
!atedge(new_sel_end, vc->vc_size_row) &&
|
|
is_space_on_vt(sel_pos(new_sel_end, unicode))) {
|
|
for (pe = new_sel_end + 2; ; pe += 2)
|
|
if (!is_space_on_vt(sel_pos(pe, unicode)) ||
|
|
atedge(pe, vc->vc_size_row))
|
|
break;
|
|
if (is_space_on_vt(sel_pos(pe, unicode)))
|
|
new_sel_end = pe;
|
|
}
|
|
if (vc_sel.start == -1) /* no current selection */
|
|
highlight(new_sel_start, new_sel_end);
|
|
else if (new_sel_start == vc_sel.start)
|
|
{
|
|
if (new_sel_end == vc_sel.end) /* no action required */
|
|
return 0;
|
|
else if (new_sel_end > vc_sel.end) /* extend to right */
|
|
highlight(vc_sel.end + 2, new_sel_end);
|
|
else /* contract from right */
|
|
highlight(new_sel_end + 2, vc_sel.end);
|
|
}
|
|
else if (new_sel_end == vc_sel.end)
|
|
{
|
|
if (new_sel_start < vc_sel.start) /* extend to left */
|
|
highlight(new_sel_start, vc_sel.start - 2);
|
|
else /* contract from left */
|
|
highlight(vc_sel.start, new_sel_start - 2);
|
|
}
|
|
else /* some other case; start selection from scratch */
|
|
{
|
|
clear_selection();
|
|
highlight(new_sel_start, new_sel_end);
|
|
}
|
|
vc_sel.start = new_sel_start;
|
|
vc_sel.end = new_sel_end;
|
|
|
|
return vc_selection_store_chars(vc, unicode);
|
|
}
|
|
|
|
static int vc_selection(struct vc_data *vc, struct tiocl_selection *v,
|
|
struct tty_struct *tty)
|
|
{
|
|
int ps, pe;
|
|
|
|
poke_blanked_console();
|
|
|
|
if (v->sel_mode == TIOCL_SELCLEAR) {
|
|
/* useful for screendump without selection highlights */
|
|
clear_selection();
|
|
return 0;
|
|
}
|
|
|
|
v->xs = min_t(u16, v->xs - 1, vc->vc_cols - 1);
|
|
v->ys = min_t(u16, v->ys - 1, vc->vc_rows - 1);
|
|
v->xe = min_t(u16, v->xe - 1, vc->vc_cols - 1);
|
|
v->ye = min_t(u16, v->ye - 1, vc->vc_rows - 1);
|
|
|
|
if (mouse_reporting() && (v->sel_mode & TIOCL_SELMOUSEREPORT)) {
|
|
mouse_report(tty, v->sel_mode & TIOCL_SELBUTTONMASK, v->xs,
|
|
v->ys);
|
|
return 0;
|
|
}
|
|
|
|
ps = v->ys * vc->vc_size_row + (v->xs << 1);
|
|
pe = v->ye * vc->vc_size_row + (v->xe << 1);
|
|
if (ps > pe) /* make vc_sel.start <= vc_sel.end */
|
|
swap(ps, pe);
|
|
|
|
if (vc_sel.cons != vc) {
|
|
clear_selection();
|
|
vc_sel.cons = vc;
|
|
}
|
|
|
|
return vc_do_selection(vc, v->sel_mode, ps, pe);
|
|
}
|
|
|
|
int set_selection_kernel(struct tiocl_selection *v, struct tty_struct *tty)
|
|
{
|
|
int ret;
|
|
|
|
mutex_lock(&vc_sel.lock);
|
|
console_lock();
|
|
ret = vc_selection(vc_cons[fg_console].d, v, tty);
|
|
console_unlock();
|
|
mutex_unlock(&vc_sel.lock);
|
|
|
|
return ret;
|
|
}
|
|
EXPORT_SYMBOL_GPL(set_selection_kernel);
|
|
|
|
/* Insert the contents of the selection buffer into the
|
|
* queue of the tty associated with the current console.
|
|
* Invoked by ioctl().
|
|
*
|
|
* Locking: called without locks. Calls the ldisc wrongly with
|
|
* unsafe methods,
|
|
*/
|
|
int paste_selection(struct tty_struct *tty)
|
|
{
|
|
struct vc_data *vc = tty->driver_data;
|
|
int pasted = 0;
|
|
size_t count;
|
|
struct tty_ldisc *ld;
|
|
DECLARE_WAITQUEUE(wait, current);
|
|
int ret = 0;
|
|
|
|
console_lock();
|
|
poke_blanked_console();
|
|
console_unlock();
|
|
|
|
ld = tty_ldisc_ref_wait(tty);
|
|
if (!ld)
|
|
return -EIO; /* ldisc was hung up */
|
|
tty_buffer_lock_exclusive(&vc->port);
|
|
|
|
add_wait_queue(&vc->paste_wait, &wait);
|
|
mutex_lock(&vc_sel.lock);
|
|
while (vc_sel.buffer && vc_sel.buf_len > pasted) {
|
|
set_current_state(TASK_INTERRUPTIBLE);
|
|
if (signal_pending(current)) {
|
|
ret = -EINTR;
|
|
break;
|
|
}
|
|
if (tty_throttled(tty)) {
|
|
mutex_unlock(&vc_sel.lock);
|
|
schedule();
|
|
mutex_lock(&vc_sel.lock);
|
|
continue;
|
|
}
|
|
__set_current_state(TASK_RUNNING);
|
|
count = vc_sel.buf_len - pasted;
|
|
count = tty_ldisc_receive_buf(ld, vc_sel.buffer + pasted, NULL,
|
|
count);
|
|
pasted += count;
|
|
}
|
|
mutex_unlock(&vc_sel.lock);
|
|
remove_wait_queue(&vc->paste_wait, &wait);
|
|
__set_current_state(TASK_RUNNING);
|
|
|
|
tty_buffer_unlock_exclusive(&vc->port);
|
|
tty_ldisc_deref(ld);
|
|
return ret;
|
|
}
|
|
EXPORT_SYMBOL_GPL(paste_selection);
|