Skip to content

[Bug] cromfs: directory name_size is not bounded, ls/getdents overflows the dirent buffer and hijacks the kernel PC #11905

Open

Description

@XueDugu

RT-Thread Version

master @ 1c801efabc36 (2026-10-19, v5.4.0-dev). Both copies still contain the unbounded copy, read via the contents API on 2026-10-20:

  • components/dfs/dfs_v1/filesystems/cromfs/dfs_cromfs.c:1121-1124
  • components/dfs/dfs_v2/filesystems/cromfs/dfs_cromfs.c:1181-1184

Introduced by commit 0bc01ad0d9 ([dfs] sync cromfs (#6739), 2022-12-14). Absent at v4.1.1; present from v5.0.0 (2023-03-31) through v5.3.0 and current master. The v2 copy was split out by #7200 (2023-04-08) and still has the same copy. #9197 and #10054 fixed other cromfs bugs and did not add a bound here. No later commit on either file touches dfs_cromfs_getdents.

Affected area

File systems (DFS)

Hardware/BSP vendor

Not applicable / Other

Architecture

ARM / AArch64

Board and hardware details

Reproduced on QEMU vexpress-a9 (Cortex-A9), stock bsp/qemu-vexpress-a9 plus RT_USING_DFS_CROMFS=y. The bug is portable C and is not specific to that board. In-tree BSPs that actually ship cromfs and mount it as the root filesystem are bsp/k230 (applications/mnt.c, dfs_mount(NULL, "/", "crom", 0, data)) and bsp/cvitek (drivers/port/mnt_cromfs.c, mount table entry "crom" on /).

Develop Toolchain

GCC (arm-none-eabi-gcc 10.3.1, scons)

Describe the bug

dfs_cromfs_getdents copies a directory name out of the cromfs image using the image's own uint16 name_size, with no comparison against struct dirent.d_name. d_name is DIRENT_NAME_MAX (256) bytes (components/libc/compilers/common/include/dirent.h). name_size can be 0..65535.

/* dfs_v1/.../cromfs/dfs_cromfs.c:1121-1124
 * dfs_v2/.../cromfs/dfs_cromfs.c:1181-1184 is the same four lines */
d->d_namlen = sub_dirent->dirent.name_size;
d->d_reclen = (rt_uint16_t)sizeof(struct dirent);
memcpy(d->d_name, (char *)name, sub_dirent->dirent.name_size);
d->d_name[sub_dirent->dirent.name_size] = '\0';

name_size >= 256 writes past d_name. The NUL is stored at index name_size, so it is also out of the array. Callers do not clip this. The msh ls command keeps one struct dirent on the shell thread's stack and passes sizeof(struct dirent):

/* dfs_v1/src/dfs_file.c:786-819 (dfs_v2/src/dfs_file.c:2708 and :2752, and copydir at :3024) */
void ls(const char *pathname)
{
    struct dirent dirent;
    ...
    length = dfs_file_getdents(&fd, &dirent, sizeof(struct dirent));

getdents turns that byte count into an entry count (count / sizeof(struct dirent) == 1) and then copies name_size bytes into that one object. A name longer than 255 bytes therefore overflows the stack object. The same pattern is used by readdir (DIR.buf[512]) and by sys_getdents (a kernel buffer of cnt * sizeof(struct dirent)).

Reproduction (stock mount + ls on RT-Thread, QEMU vexpress-a9, one boot, control then crash):

  1. Enable RT_USING_DFS_CROMFS and provide the image through a device mount can read (any byte-addressed device; cromfs uses cromfs_noblk_read_bytes for non-block devices). A minimal image is a 256-byte header (magic "CROMFSMG", root_dir_pos = 256, partition_size covering the blob, root_dir_size = the directory blob) followed by one cromfs_dirent: attr = 0, name_size, three zero uint32s, then the name bytes. No zlib stream is involved; directory entries are stored in the clear.
  2. Control image, name_size = 2, name = ok:
    mount crom_ok / crom
    ls /
    echo CONTROL_OK
    umount /
    
    Result: mount ... succeed!, ls prints ok, CONTROL_OK is printed, umount succeeds. The shell is still alive.
  3. Same steps with name_size = 512, name = 'A'*256 + 'B'*256:
    mount crom_evil / crom     # succeed
    ls /
    
    ls prints BAD file: and a run of As (the in-object part of the name; dfs_file.c:842), then the shell thread takes a prefetch abort and the kernel shuts down:
    prefetch abort:
    fp :0x42424242 ip :0xffffffff
    sp :0x60114088 lr :0x6007a590 pc :0x42424246
    cpsr:0x200f0013
    tshell  running
    shutdown...
    assertion failed at function:rt_hw_cpu_shutdown
    
    0x42 is 'B', i.e. the bytes past d_name[256]. fp is the saved frame pointer of ls. pc = 0x42424246 is that value plus 4, the faulting fetch. lr = 0x6007a590 resolves with addr2line to ls at dfs_file.c:848 (dfs_file_close / the epilogue). The backtrace printer itself is handed 0x42424242. r4–r9 are still 0xdeadbeef, so this is the overflowed frame, not a wiped register file. The command after ls (EVIL_SURVIVED) is never reached.

The only difference between the two images is name_size and the name bytes. Mount accepts both (magic and partition_size are consistent).

Expected behavior

dfs_cromfs_getdents must refuse a directory entry whose name_size >= DIRENT_NAME_MAX (the copy plus the terminating NUL have to fit in d_name). The same check belongs in both the v1 and the v2 copy. The entry should be skipped or the call should fail; it must not memcpy name_size bytes into the caller buffer. The name should also be required to lie inside the directory blob that was just loaded (fi->size), so a name_size that runs off the end of that buffer cannot be used as a copy length either.

Other additional context

  • Attack precondition: the attacker supplies a cromfs image. That image is the filesystem's input — a block device, or the data pointer passed to dfs_mount(..., "crom", ..., data) as k230 and cvitek do at boot. After the image is mounted, an ordinary directory listing (ls, readdir, or getdents) is enough. This is not a network bug, and RT_USING_DFS_CROMFS is not a global default (it is selected by the k230 and cvitek BSPs). On a non-Smart build the shell that runs mount/ls is already kernel context; the trust boundary being crossed is the filesystem image, not a user/kernel syscall. The corruption happens inside the parser before any file on that image is executed.
  • Impact: kernel stack buffer overflow in the thread that lists the directory, with the overflowing bytes chosen by the image. On the reproduced path that is the msh thread, and the saved frame pointer becomes attacker bytes, so the return from ls transfers control to an attacker-chosen address (here 0x42424246, which faults). A name that does not fault can instead return into chosen kernel code. CVSS 3.1: 7.8 High CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H. CWE-787 (the length comes from an unchecked image field, CWE-1284).
  • Not the same bug as [HUST CSE][dfs] fix stack buffer overflow on long path components in devtmpfs #11842 / [HUST CSE][dfs-v1]fix: validate tmpfs path components and ramfs entry names #11304 (devtmpfs / tmpfs / ramfs path-component limits), CVE-2024-24334 (dfs_file), or the GPT/MBR/mtd-cfi reports. No open issue or PR mentions cromfs together with name_size, getdents, or d_name.

Activity

  1. github-actions commented on Oct 20, 2026

    @github-actions

    如需认领并处理此 Issue,请评论 /claim。

    To claim and work on this issue, comment /claim.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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