From 4e7530eca354dc05a34d5ecdd6587c59df6244a3 Mon Sep 17 00:00:00 2001 From: Leng Mengqi <312309080229@home.hpu.edu.cn> Date: Wed, 21 Oct 2026 11:07:13 +0800 Subject: [PATCH 1/3] [dfs][tmpfs] fix use-after-free when truncating a file to zero length rt_realloc(ptr, 0) is implemented as free(ptr) + return RT_NULL on both heap backends (src/mem.c:429, src/memheap.c:830), so the NULL return cannot be told apart from an allocation failure. dfs_tmpfs_truncate() treated it as -ENOMEM and returned 0 without updating d_file->data or the file size, which left a dangling buffer behind a successful ftruncate(fd, 0). Handle offset == 0 explicitly before calling rt_realloc(): free the buffer, clear the pointer, and fall through so that d_file->size and file->vnode->size are reset to 0. Return -ENOMEM when the allocation really fails, so the error reaches ftruncate() through dfs_file_ftruncate(). Verified on bsp/qemu-vexpress-a9 (small_mem): read() after ftruncate(fd, 0) returns 0 instead of the stale length, and unlink() no longer trips the MEM_ISUSED assertion in rt_smem_free(). Fixes #11902 Signed-off-by: Leng Mengqi <312309080229@home.hpu.edu.cn> --- .../dfs/dfs_v2/filesystems/tmpfs/dfs_tmpfs.c | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/components/dfs/dfs_v2/filesystems/tmpfs/dfs_tmpfs.c b/components/dfs/dfs_v2/filesystems/tmpfs/dfs_tmpfs.c index 5f9299c11e43..dbed9fb7bc69 100644 --- a/components/dfs/dfs_v2/filesystems/tmpfs/dfs_tmpfs.c +++ b/components/dfs/dfs_v2/filesystems/tmpfs/dfs_tmpfs.c @@ -1022,11 +1022,28 @@ static int dfs_tmpfs_truncate(struct dfs_file *file, off_t offset) superblock = d_file->sb; RT_ASSERT(superblock != RT_NULL); - ptr = rt_realloc(d_file->data, offset); - if (ptr == RT_NULL) + if (offset == 0) { - rt_set_errno(-ENOMEM); - return 0; + /* rt_realloc(ptr, 0) frees the block and returns RT_NULL, which cannot + * be told apart from an allocation failure. Handle the empty case + * explicitly, otherwise d_file->data is left dangling. */ + if (d_file->data != RT_NULL) + { + rt_free(d_file->data); + d_file->data = RT_NULL; + } + ptr = RT_NULL; + } + else + { + ptr = rt_realloc(d_file->data, offset); + if (ptr == RT_NULL) + { + rt_set_errno(-ENOMEM); + return -ENOMEM; + } + + d_file->data = ptr; } rt_spin_lock(&superblock->lock); @@ -1034,7 +1051,6 @@ static int dfs_tmpfs_truncate(struct dfs_file *file, off_t offset) rt_spin_unlock(&superblock->lock); /* update d_file and file size */ - d_file->data = ptr; d_file->size = offset; file->vnode->size = d_file->size; LOG_D("tmpfile ptr:%x, size:%d", ptr, d_file->size); From 9a806f7b4d7b92458131528bc616ad926d1b3e39 Mon Sep 17 00:00:00 2001 From: Leng Mengqi <312309080229@home.hpu.edu.cn> Date: Wed, 21 Oct 2026 11:22:17 +0800 Subject: [PATCH 2/3] [dfs][tmpfs] add truncate regression tests Cover zero and repeated truncation, subsequent I/O, non-zero resize, and allocation failure without losing the original file contents. Signed-off-by: Leng Mengqi <312309080229@home.hpu.edu.cn> --- .../dfs_v2/filesystems/tmpfs/utest/Kconfig | 7 + .../dfs_v2/filesystems/tmpfs/utest/SConscript | 3 + .../filesystems/tmpfs/utest/tc_truncate.c | 153 ++++++++++++++++++ 3 files changed, 163 insertions(+) create mode 100644 components/dfs/dfs_v2/filesystems/tmpfs/utest/tc_truncate.c diff --git a/components/dfs/dfs_v2/filesystems/tmpfs/utest/Kconfig b/components/dfs/dfs_v2/filesystems/tmpfs/utest/Kconfig index 96f36468f5f7..ee7951bb22f4 100644 --- a/components/dfs/dfs_v2/filesystems/tmpfs/utest/Kconfig +++ b/components/dfs/dfs_v2/filesystems/tmpfs/utest/Kconfig @@ -1,5 +1,12 @@ menu "Tmpfs Testcase" +config RT_UTEST_TMPFS_TRUNCATE + bool "tmpfs truncate test" + default n + depends on RT_USING_POSIX_FS && RT_USING_DFS_TMPFS + help + Test zero-length truncation, subsequent I/O and allocation failures. + config RT_UTEST_TMPFS_CP bool "tmpfs cp test" default n diff --git a/components/dfs/dfs_v2/filesystems/tmpfs/utest/SConscript b/components/dfs/dfs_v2/filesystems/tmpfs/utest/SConscript index 8449323d7499..85275a37eda3 100644 --- a/components/dfs/dfs_v2/filesystems/tmpfs/utest/SConscript +++ b/components/dfs/dfs_v2/filesystems/tmpfs/utest/SConscript @@ -5,6 +5,9 @@ cwd = GetCurrentDir() src = [] CPPPATH = [cwd] +if GetDepend(['RT_UTEST_TMPFS_TRUNCATE']): + src += ['tc_truncate.c'] + if GetDepend(['RT_USING_SMART','RT_UTEST_TMPFS_CP']): src += ['tmpfs.c'] diff --git a/components/dfs/dfs_v2/filesystems/tmpfs/utest/tc_truncate.c b/components/dfs/dfs_v2/filesystems/tmpfs/utest/tc_truncate.c new file mode 100644 index 000000000000..1746c9d8c687 --- /dev/null +++ b/components/dfs/dfs_v2/filesystems/tmpfs/utest/tc_truncate.c @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2026, RT-Thread Development Team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include "utest.h" + +#define TEST_MOUNT "/tmpfs-truncate" +#define TEST_PATH TEST_MOUNT "/file" + +static const char payload[] = "0123456789abcdef"; + +static int create_file(void) +{ + int fd = open(TEST_PATH, O_CREAT | O_RDWR | O_TRUNC, 0600); + + uassert_true(fd >= 0); + if (fd < 0) + { + return -1; + } + + uassert_int_equal(write(fd, payload, sizeof(payload) - 1), sizeof(payload) - 1); + /* Closing flushes dirty pages before testing the backing buffer. */ + uassert_int_equal(close(fd), 0); + fd = open(TEST_PATH, O_RDWR, 0); + uassert_true(fd >= 0); + return fd; +} + +static void test_zero_length(void) +{ + struct stat st; + char buffer[sizeof(payload)]; + int fd = create_file(); + + if (fd < 0) + { + return; + } + + uassert_int_equal(ftruncate(fd, 0), 0); + uassert_int_equal(fstat(fd, &st), 0); + uassert_int_equal(st.st_size, 0); + uassert_int_equal(lseek(fd, 0, SEEK_SET), 0); + uassert_int_equal(read(fd, buffer, sizeof(buffer)), 0); + + /* Truncating an already empty file must also be safe. */ + uassert_int_equal(ftruncate(fd, 0), 0); + uassert_int_equal(write(fd, payload, sizeof(payload) - 1), sizeof(payload) - 1); + uassert_int_equal(lseek(fd, 0, SEEK_SET), 0); + uassert_int_equal(read(fd, buffer, sizeof(payload) - 1), sizeof(payload) - 1); + uassert_int_equal(rt_memcmp(buffer, payload, sizeof(payload) - 1), 0); + uassert_int_equal(close(fd), 0); + fd = open(TEST_PATH, O_RDONLY, 0); + uassert_true(fd >= 0); + if (fd >= 0) + { + uassert_int_equal(fstat(fd, &st), 0); + uassert_int_equal(st.st_size, sizeof(payload) - 1); + uassert_int_equal(close(fd), 0); + } + uassert_int_equal(unlink(TEST_PATH), 0); +} + +static void test_nonzero_length(void) +{ + struct stat st; + char buffer[4]; + int fd = create_file(); + + if (fd < 0) + { + return; + } + + uassert_int_equal(ftruncate(fd, sizeof(buffer)), 0); + uassert_int_equal(fstat(fd, &st), 0); + uassert_int_equal(st.st_size, sizeof(buffer)); + uassert_int_equal(lseek(fd, 0, SEEK_SET), 0); + uassert_int_equal(read(fd, buffer, sizeof(buffer)), sizeof(buffer)); + uassert_int_equal(rt_memcmp(buffer, payload, sizeof(buffer)), 0); + + uassert_int_equal(ftruncate(fd, 4096), 0); + uassert_int_equal(fstat(fd, &st), 0); + uassert_int_equal(st.st_size, 4096); + uassert_int_equal(lseek(fd, 0, SEEK_SET), 0); + uassert_int_equal(read(fd, buffer, sizeof(buffer)), sizeof(buffer)); + uassert_int_equal(rt_memcmp(buffer, payload, sizeof(buffer)), 0); + uassert_int_equal(close(fd), 0); + uassert_int_equal(unlink(TEST_PATH), 0); +} + +static void test_allocation_failure(void) +{ + rt_size_t total, used, max_used; + struct stat st; + char buffer[sizeof(payload)]; + int fd = create_file(); + + if (fd < 0) + { + return; + } + + /* An existing allocation cannot grow to the size of the entire heap. */ + rt_memory_info(&total, &used, &max_used); + uassert_true(total > 0); + uassert_true((off_t)total > 0); + errno = 0; + uassert_int_equal(ftruncate(fd, (off_t)total), -1); + uassert_int_equal(errno, -ENOMEM); + uassert_int_equal(fstat(fd, &st), 0); + uassert_int_equal(st.st_size, sizeof(payload) - 1); + uassert_int_equal(lseek(fd, 0, SEEK_SET), 0); + uassert_int_equal(read(fd, buffer, sizeof(payload) - 1), sizeof(payload) - 1); + uassert_int_equal(rt_memcmp(buffer, payload, sizeof(payload) - 1), 0); + uassert_int_equal(close(fd), 0); + uassert_int_equal(unlink(TEST_PATH), 0); +} + +static rt_err_t test_init(void) +{ + if (mkdir(TEST_MOUNT, 0700) != 0 && errno != -EEXIST) + { + return -RT_ERROR; + } + + return dfs_mount(RT_NULL, TEST_MOUNT, "tmp", 0, RT_NULL) == 0 ? RT_EOK : -RT_ERROR; +} + +static rt_err_t test_cleanup(void) +{ + int ret = dfs_unmount(TEST_MOUNT); + + rmdir(TEST_MOUNT); + return ret == 0 ? RT_EOK : -RT_ERROR; +} + +static void testcase(void) +{ + UTEST_UNIT_RUN(test_zero_length); + UTEST_UNIT_RUN(test_nonzero_length); + UTEST_UNIT_RUN(test_allocation_failure); +} +UTEST_TC_EXPORT(testcase, "components.dfs.tmpfs_truncate", test_init, test_cleanup, 30); From abbee538c39f483a5568b64683db84d0b21f6958 Mon Sep 17 00:00:00 2001 From: Leng Mengqi <312309080229@home.hpu.edu.cn> Date: Wed, 21 Oct 2026 11:23:37 +0800 Subject: [PATCH 3/3] [dfs][tmpfs] retain assertion failures across truncate checks Signed-off-by: Leng Mengqi <312309080229@home.hpu.edu.cn> --- .../dfs/dfs_v2/filesystems/tmpfs/utest/tc_truncate.c | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/components/dfs/dfs_v2/filesystems/tmpfs/utest/tc_truncate.c b/components/dfs/dfs_v2/filesystems/tmpfs/utest/tc_truncate.c index 1746c9d8c687..83ed37823d87 100644 --- a/components/dfs/dfs_v2/filesystems/tmpfs/utest/tc_truncate.c +++ b/components/dfs/dfs_v2/filesystems/tmpfs/utest/tc_truncate.c @@ -144,10 +144,15 @@ static rt_err_t test_cleanup(void) return ret == 0 ? RT_EOK : -RT_ERROR; } +static void test_truncate(void) +{ + test_zero_length(); + test_nonzero_length(); + test_allocation_failure(); +} + static void testcase(void) { - UTEST_UNIT_RUN(test_zero_length); - UTEST_UNIT_RUN(test_nonzero_length); - UTEST_UNIT_RUN(test_allocation_failure); + UTEST_UNIT_RUN(test_truncate); } UTEST_TC_EXPORT(testcase, "components.dfs.tmpfs_truncate", test_init, test_cleanup, 30);