Repository navigation
[Bug] [Smart] terminal tty: ioctl passes raw user pointer into kernel dereference — arbitrary kernel read/write from unprivileged user process #11937
Description
RT-Thread Version
master (v5.4.0-dev, verified unpatched 2026-10-28) and all releases since v5.1.0 (introduced by PR #8672, commit 83e95bd, 2024-03-28). v5.0.x is unaffected (the terminal subsystem did not exist yet).
Affected area
RT-Thread Smart / terminal (tty) subsystem — components/lwp/terminal/ + components/drivers/serial/serial_tty.c + the sys_ioctl path.
Hardware-BSP vendor
Others (QEMU vexpress-a9; applies to every RT_USING_SMART board)
Architecture
ARM 32-bit Cortex-A verified (qemu-vexpress-a9); the defect is architecture-independent and also affects AArch64 RT-Smart builds.
Board and hardware details
QEMU vexpress-a9 (Cortex-A9) with the stock qemu-vexpress-a9 .config + RT-Thread CI rtsmart fragment (RT_USING_SMART=y → LWP_USING_TERMINAL default y). No extra hardware or mounts are needed: on any RT-Smart build every serial port is automatically registered as a tty device (/dev/ttyS<n>, dev_serial.c rt_hw_serial_register() → rt_hw_serial_register_tty()); the boot banner confirms Using /dev/ttyS0 as default console.
Develop Toolchain
arm-linux-musleabi gcc 7.3 (RT-Thread CI toolchain) for the kernel; freestanding user-space PoC using official syscalls only.
Describe the bug
An unprivileged RT-Smart user process can read and write arbitrary kernel memory (and kill the kernel with a single NULL pointer) using only official APIs: open("/dev/ttyS0") + ioctl(TIOCSWINSZ/TIOCGWINSZ).
Where this code comes from, and why it is safe in FreeBSD but not here
The terminal subsystem was ported from FreeBSD's tty framework into master by PR #8672 (commit 83e95bd, 2024-03-28); the files still carry the original BSD-2-Clause headers ("Copyright (c) 2008 Ed Schouten ed@FreeBSD.org"). Comparing against FreeBSD sys/kern/tty.c (main branch), the relevant ioctl cases are verbatim identical, e.g.:
/* FreeBSD sys/kern/tty.c */
case TIOCGWINSZ:
/* Obtain window size. */
*(struct winsize*)data = tp->t_winsize;
return (0);
case TIOCSWINSZ:
/* Set window size. */
tty_set_winsize(tp, data);
return (0);This code has no problem in FreeBSD: FreeBSD's sys_ioctl() (sys/kern/sys_generic.c) copyin()s the user argument into a kernel stack buffer before dispatching to the cdev driver, so data always points to kernel memory there — the dereference pattern is safe by construction upstream.
The port dropped that premise, and RT-Smart adds a boundary FreeBSD never had to defend inside this code:
sys_ioctl()(components/lwp/lwp_syscall.c:817) forwards the raw user pointer for all non-flock commands — there is no copyin layer like FreeBSD's;- the ported-in adapter
lwp_tty_ioctl_adapter()(components/lwp/terminal/freebsd/tty_compat.c:508-521) — an RT-Thread-specific function that does not exist in FreeBSD — only copies the TCGETS/TCSETS family in/out (tty_compat.c:348-408) and forwards the raw pointer forTIOCSCTTY / TIOCNOTTY / TIOCGPGRP / TIOCSPGRP / TIOCGSID / TIOCGWINSZ / TIOCSWINSZ / FIONREAD(error = IOCTL(cmd, (rt_caddr_t)args, fflags, td);). The ptmx master side does the same in its default branch (tty_ptmx.c:200 → tty_pts.cptsdev_ioctl, e.g. TIOCGETA writessizeof(struct termios)bytes); - in RT-Smart the kernel and the user process live in separate address spaces — so when a user process passes a kernel address as the ioctl argument, the FreeBSD-inherited dereference now runs in kernel mode on an address the caller was never allowed to touch:
/* components/lwp/terminal/freebsd/tty.c:1341 */
case TIOCGWINSZ:
*(struct winsize *)data = tp->t_winsize; /* 8-byte write to ANY kernel address */
return 0;
/* tty.c:1343 → tty_set_winsize, tty.c:1095 */
case TIOCSWINSZ:
tty_set_winsize(tp, data); /* memcmp + 8-byte read from ANY kernel address */Notably, TIOCSPGRP in the same passthrough set does validate (lwp_in_user_space() + copy, tty.c:1291-1303 — an RT-Thread addition; FreeBSD has no such branch), which shows this exact hazard was recognized for one command, but the rest of the set was missed.
Exploitation primitives (user process passes a kernel address as arg):
- Arbitrary kernel 8-byte READ:
ioctl(fd, TIOCSWINSZ, KADDR)thenioctl(fd, TIOCGWINSZ, &buf) - Arbitrary kernel 8-byte WRITE with fully controlled value:
ioctl(fd, TIOCSWINSZ, &pattern)thenioctl(fd, TIOCGWINSZ, KADDR) - ~60-byte partially controlled write on a ptmx master fd:
ioctl(mfd, TIOCGETA, KADDR)(tty_pts.c:303) - Deterministic kernel panic:
ioctl(fd, TIOCSWINSZ, NULL)(tty.c:1095 dereferences NULL)
Reproduction (full-chain verified on QEMU RT-Smart, kernel master@5d1fb66d, unprivileged user process as /sbin/init using only official syscalls):
-
Control: with user-space buffers,
TIOCSWINSZ(&w)/TIOCGWINSZ(&r)round-trips exactly (6f 00 de 00 33 33 44 44); the shell still answersversion. Kernel alive. -
Arbitrary read:
TIOCSWINSZ(0xc00b0388)(address ofrt_tick_getin kernel .text) leaks00 48 2d e9 04 b0 8d e2— byte-identical to the ELF contents at that address;TIOCSWINSZ(0xc00f40fc)(_uart_device) leaks kernel pointers0xc0114010/0xc00d93d8. Kernel stays alive. -
Arbitrary write:
TIOCSWINSZwith pattern42 41 44 43 46 45 48 47, thenTIOCGWINSZ(0xc0102f58)(kernel .bsstty_nin). A GDB hardware watchpoint on0xc0102f58catches the store at tty.c:1341 (Old (tty_nin=0, tty_nout=0xB0) → New (tty_nin=0x43444142, …)); reading the address back through the read primitive returns the exact 8-byte pattern. Kernel stays alive.Backtrace for the write (user-supplied kernel pointer flowing unvalidated through every layer):
sys_ioctl (lwp_syscall.c:817, data=0xc0102f58 <tty_nin>) → ioctl (dfs_posix.c:737) → fcntl → dfs_file_ioctl (dfs_file.c:1658) → dfs_devfs_ioctl (devfs.c:234, args=0xc0102f58) → tty_fops_ioctl (tty_device.c:114, arg=0xc0102f58) → lwp_tty_ioctl_adapter (tty_compat.c:520, args=0xc0102f58) → ttydev_ioctl (tty.c:650) → tty_ioctl (tty.c:1395) → tty_generic_ioctl (tty.c:1341) ← *(struct winsize *)0xc0102f58 = tp->t_winsize -
Availability:
TIOCGWINSZ(0xc00b0388)overwrites the first instructions ofrt_tick_get(kernel .text is writable in this configuration) → repeated data aborts → kernel dies. AlsoTIOCSWINSZ(NULL)→ data abort at 0x0 → kernel dies.
Expected behavior
The tty layer must never dereference a user-supplied raw pointer: FreeBSD guarantees this by copying ioctl args in/out at the syscall boundary. In RT-Thread, a kernel address or NULL passed as arg must be rejected with EFAULT, and user addresses must go through lwp_get_from_user()/lwp_put_to_user().
Actual behavior
Arbitrary kernel read/write plus trivial kernel panic, reachable by any unprivileged RT-Smart user process through official POSIX APIs on a default image (no mounts, no privileges, no races).
Suggested fix
In lwp_tty_ioctl_adapter() (and the ptm_fops_ioctl() default branch), copy in/out the payloads of every forwarded command into kernel stack buffers, mirroring FreeBSD sys_ioctl — one change closes all sibling sinks at once (TIOCSWINSZ/TIOCGWINSZ/TIOCGPGRP/TIOCGSID/FIONREAD on the slave side; TIOCGETA/TIOCGPTN/TIOCGPGRP/TIOCGSID/FIONREAD/TIOCSIG/TIOCPKT on the master side). Alternatively, reject lwp_in_user_space(data) inside tty_generic_ioctl()/ptsdev_ioctl().
Other additional context
- CWE-787 / CWE-125 (out-of-bounds write/read via unchecked pointer trust); CWE-476 for the NULL variant.
- CVSS 3.1: AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H = 8.8 (scope-change reading: the attacked authority is the unprivileged user process, the impacted resource is the kernel; the S:U reading gives 7.8). All three impact axes are demonstrated with serial logs and a GDB watchpoint, not inferred.
- Attack preconditions, stated explicitly: requires an RT-Smart build (
RT_USING_SMART=y;LWP_USING_TERMINALis default-y and serial ttys are auto-registered, so default Smart images ship the vulnerable devices); the attacker is any user-mode process; the/dev/ttyS<n>entry needs no mount (a/dev/pts/*pty entry also exists when ptyfs is mounted). - Not a duplicate: no existing issue/PR touches the terminal subsystem (searched winsize / TIOCGWINSZ / terminal ioctl / lwp tty / ptmx); none of the known CVEs cover it; verified still unpatched on origin/master as of 2026-10-28 (last touch was [smart] add FIONREAD ioctl support for tty #8996, 2024-05-28).
- The bug is in the RT-Thread porting/adaptation layer (the missing copyin premise + the RT-Thread-specific passthrough adapter), not in the FreeBSD-derived logic itself — the FreeBSD code only becomes unsafe because its precondition disappeared. Full repro artifacts (4-mode PoC, QEMU session harness, GDB watchpoint script, complete serial logs) available on request.
Activity
github-actions commentedon Oct 28, 2026
如需认领并处理此 Issue,请评论
/claim。To claim and work on this issue, comment
/claim.