diff --git a/os/StarryOS/kernel/src/mm/aspace/backend/file.rs b/os/StarryOS/kernel/src/mm/aspace/backend/file.rs index 526e5966b7..019352fc70 100644 --- a/os/StarryOS/kernel/src/mm/aspace/backend/file.rs +++ b/os/StarryOS/kernel/src/mm/aspace/backend/file.rs @@ -255,8 +255,30 @@ impl BackendOps for FileBackend { let file_data = self.0.file_data.lock(); let start_page = ((range.start - file_data.start) / PAGE_SIZE_4K) as u32 + file_data.offset_page; + // Pages whose file offset is at or beyond EOF must not be eagerly backed + // by a physical frame: a shared file mapping reads as SIGBUS past the last + // file page (Linux semantics). Without this bound an eager populate over a + // sparse mapping far larger than the file (e.g. bbolt's multi-GB + // `InitialMmapSize` over a tiny etcd db) allocates a frame for every page + // in the mapping and exhausts RAM. `div_ceil` keeps the final partial page; + // its tail beyond EOF is zeroed by `page_or_insert` (which now clears the + // short-read remainder of the page rather than leaving stale frame contents). + let eof_page = self + .0 + .cache + .file_len() + .unwrap_or(u64::MAX) + .div_ceil(PAGE_SIZE_4K as u64); for (i, addr) in pages_in(range, PageSize::Size4K)?.enumerate() { let pn = start_page + i as u32; + if (pn as u64) >= eof_page { + // Beyond EOF: leave unmapped so a real access faults instead of + // reading an eagerly-preallocated zero page. Linux delivers SIGBUS + // for such an access; StarryOS currently raises SIGSEGV for any + // unbacked fault (a dedicated Bus path is a separate fault-handler + // gap). Either way the page is not pre-backed — that is the OOM fix. + continue; + } match pt.query(addr) { Ok((paddr, page_flags, _)) => { if access_flags.contains(MappingFlags::WRITE) diff --git a/os/arceos/modules/axfs-ng/src/highlevel/file.rs b/os/arceos/modules/axfs-ng/src/highlevel/file.rs index 6ab417acc5..43dd5220fc 100644 --- a/os/arceos/modules/axfs-ng/src/highlevel/file.rs +++ b/os/arceos/modules/axfs-ng/src/highlevel/file.rs @@ -705,6 +705,11 @@ impl CachedFile { self.in_memory } + /// Returns the current length (in bytes) of the backing file. + pub fn file_len(&self) -> VfsResult { + self.inner.len() + } + /// Registers a listener that is called when a page is evicted from cache. /// /// Returns a handle that can later be passed to @@ -777,7 +782,16 @@ impl CachedFile { if self.in_memory { page.data().fill(0); } else { - file.read_at(page.data(), pn as u64 * PAGE_SIZE as u64)?; + // `PageCache::new()` does not zero the freshly allocated frame, and + // `FileNodeOps::read_at` short-reads at EOF (rsext4/fat return only the + // bytes actually read, leaving the rest of the buffer untouched). Zero the + // tail beyond the read length so a partial last page never exposes stale + // physical memory past EOF — POSIX/Linux require those bytes to read as 0 + // (e.g. an mmap of a 100-byte file must see `[100, PAGE_SIZE)` as zero). + let read = file.read_at(page.data(), pn as u64 * PAGE_SIZE as u64)?; + if read < PAGE_SIZE { + page.data()[read..].fill(0); + } } cache.put(pn, page); Ok((cache.get_mut(&pn).unwrap(), evicted)) diff --git a/test-suit/starryos/qemu-smp1/system/syscall-test-mmap-populate-eof/CMakeLists.txt b/test-suit/starryos/qemu-smp1/system/syscall-test-mmap-populate-eof/CMakeLists.txt new file mode 100644 index 0000000000..f3e7abb947 --- /dev/null +++ b/test-suit/starryos/qemu-smp1/system/syscall-test-mmap-populate-eof/CMakeLists.txt @@ -0,0 +1,9 @@ +cmake_minimum_required(VERSION 3.20) +project(test-mmap-populate-eof C) +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED ON) +set(CMAKE_C_EXTENSIONS OFF) +add_executable(test-mmap-populate-eof src/main.c) +target_include_directories(test-mmap-populate-eof PRIVATE src) +target_compile_options(test-mmap-populate-eof PRIVATE -Wall -Wextra -Werror) +install(TARGETS test-mmap-populate-eof RUNTIME DESTINATION usr/bin/starry-test-suit) diff --git a/test-suit/starryos/qemu-smp1/system/syscall-test-mmap-populate-eof/src/main.c b/test-suit/starryos/qemu-smp1/system/syscall-test-mmap-populate-eof/src/main.c new file mode 100644 index 0000000000..71d350f755 --- /dev/null +++ b/test-suit/starryos/qemu-smp1/system/syscall-test-mmap-populate-eof/src/main.c @@ -0,0 +1,149 @@ +/* + * test_mmap_populate_eof.c — file-backed mmap populate is bounded at EOF (PR #1164). + * + * 回归背景 (为什么写这个测例): + * 文件映射 populate 此前对映射范围内每一页都急切分配物理帧, 包括完全落在 + * 文件 EOF 之外的稀疏页. 应用用一个远大于文件的 mmap(典型: bbolt 用多 GB 的 + * InitialMmapSize 映射十几 KB 的 etcd db)时, 覆盖整个区域的 populate 会为 + * 全部页逐页分配帧 → 耗尽物理内存 OOM. + * + * man 2 mmap (MAP_SHARED 文件映射): + * "References to whole pages following the end of a mapped file ... raise a + * SIGBUS signal." EOF 之外的页不被预先填充; 最后一个部分页的尾部按 POSIX + * 读为 0. + * + * 修复 (kernel mm/aspace/backend/file.rs `FileBackend::populate`): + * 循环前算 `eof_page = file_len.div_ceil(4096)`, 对 `pn >= eof_page` 的页跳过 + * (保持 unmapped → 真实访问缺页 → populate 返回 0 → SIGBUS). `div_ceil` + * 保留最后部分页(尾部由 page_or_insert 零填充). + * + * 覆盖 reviewer 的两点要求: + * (1) 小文件 + 远大于文件的 MAP_SHARED|MAP_POPULATE 不预分配/不 OOM; + * (2) 最后部分页尾部确为 0 (证明而非空口注释); EOF 外访问为 SIGBUS. + * + * 本测例位于 qemu-smp1 (-m 512M): 256MB 的稀疏映射若被急切预分配会 OOM, + * 故"mmap 成功且后续断言通过"本身即证明 populate 已按 EOF 收敛. + */ + +#define _GNU_SOURCE +#include "test_framework.h" +#include +#include +#include +#include +#include +#include + +#ifndef MAP_POPULATE +#define MAP_POPULATE 0x8000 +#endif + +#define PAGE 4096UL +#define FILE_SZ 100UL /* 部分页: 小于一页 */ +#define HUGE_LEN (256UL * 1024 * 1024) /* 256MB >> 文件; 急切预分配会 OOM 512M RAM */ + +/* 捕获越界访问产生的故障. Linux 对共享文件映射 EOF 外访问发 SIGBUS; + * 同时挂 SIGSEGV 兜底, 保证测例本身不会因信号默认动作而崩溃. */ +static sigjmp_buf g_jb; +static volatile sig_atomic_t g_sig; +static void on_fault(int sig) +{ + g_sig = sig; + siglongjmp(g_jb, 1); +} + +/* 读 *p 一个字节. 返回触发的信号号(0 表示未故障); 值经 *out 带出. */ +static int read_fault_signal(volatile unsigned char *p, unsigned char *out) +{ + struct sigaction sa = {0}, old_bus, old_segv; + sa.sa_handler = on_fault; + sigemptyset(&sa.sa_mask); + sigaction(SIGBUS, &sa, &old_bus); + sigaction(SIGSEGV, &sa, &old_segv); + g_sig = 0; + if (sigsetjmp(g_jb, 1) == 0) { + unsigned char v = *p; + if (out) + *out = v; + } + sigaction(SIGBUS, &old_bus, NULL); + sigaction(SIGSEGV, &old_segv, NULL); + return (int)g_sig; +} + +int main(void) +{ + TEST_START("mmap populate bounded at EOF (#1164)"); + + const char *path = "/tmp/mpe.dat"; + int fd = open(path, O_RDWR | O_CREAT | O_TRUNC, 0644); + CHECK(fd >= 0, "open backing file"); + if (fd < 0) { + TEST_DONE(); + } + + unsigned char wbuf[FILE_SZ]; + memset(wbuf, 0xAB, FILE_SZ); + CHECK_RET(write(fd, wbuf, FILE_SZ), (long)FILE_SZ, "write 100-byte partial-page file"); + + /* 远大于文件的 MAP_SHARED|MAP_POPULATE: 有 EOF 收敛修复时只 back 1 个文件页, + * 急切全量预分配则会在 512M RAM 上 OOM. mmap 成功即证明未预分配. */ + void *m = mmap(NULL, HUGE_LEN, PROT_READ, MAP_SHARED | MAP_POPULATE, fd, 0); + CHECK(m != MAP_FAILED, + "huge MAP_SHARED|MAP_POPULATE over tiny file succeeds (no eager prealloc/OOM)"); + if (m == MAP_FAILED) { + close(fd); + unlink(path); + TEST_DONE(); + } + volatile unsigned char *p = (volatile unsigned char *)m; + + /* (a) [0,100): 文件内容 0xAB */ + int content_ok = 1; + for (unsigned long i = 0; i < FILE_SZ; i++) { + if (p[i] != 0xAB) { + content_ok = 0; + break; + } + } + CHECK(content_ok, "bytes [0,100) map the file content (0xAB)"); + + /* (b) [100,4096): 最后部分页尾部必须零填充 (POSIX; reviewer 关注点) */ + unsigned long first_nonzero = 0; + int tail_zero = 1; + for (unsigned long i = FILE_SZ; i < PAGE; i++) { + if (p[i] != 0) { + tail_zero = 0; + first_nonzero = i; + break; + } + } + if (tail_zero) { + CHECK(1, "partial-page tail [100,4096) is zero-filled"); + } else { + char mb[128]; + snprintf(mb, sizeof mb, "partial-page tail not zero at off=%lu (val=0x%02x)", + first_nonzero, p[first_nonzero]); + CHECK(0, mb); + } + + /* (c) offset 4096 (>= eof_page=1) 落在 EOF 之外 → 必须故障(未被预分配) */ + unsigned char v = 0xFF; + int sig = read_fault_signal(p + PAGE, &v); + CHECK(sig != 0, "access at offset 4096 (beyond EOF) faults (page not preallocated)"); + /* #1164 的保证是"EOF 外页不预分配、真实访问会故障"——上面已断言。具体信号: + * Linux 对越过文件映射 EOF 的访问发 SIGBUS; StarryOS 目前对任何无后备缺页统一发 + * SIGSEGV(handle_page_fault 尚无 Bus 分支, 属独立 fault-handler gap, 另行修复)。 + * 此处仅记录(不门控)实际信号, 避免把独立的信号语义 gap 混入本 OOM-bound 修复。 */ + printf(" INFO | beyond-EOF fault signal=%d (Linux: SIGBUS=%d; StarryOS now SIGSEGV=%d)\n", + sig, SIGBUS, SIGSEGV); + + /* (d) 稀疏区深处(接近 256MB 末尾)同样未预分配 → 访问故障 */ + int sig2 = read_fault_signal(p + (HUGE_LEN - PAGE), NULL); + CHECK(sig2 != 0, "access near end of huge sparse mapping faults (never preallocated)"); + + munmap(m, HUGE_LEN); + close(fd); + unlink(path); + TEST_DONE(); +} diff --git a/test-suit/starryos/qemu-smp1/system/syscall-test-mmap-populate-eof/src/test_framework.h b/test-suit/starryos/qemu-smp1/system/syscall-test-mmap-populate-eof/src/test_framework.h new file mode 100644 index 0000000000..b697580a68 --- /dev/null +++ b/test-suit/starryos/qemu-smp1/system/syscall-test-mmap-populate-eof/src/test_framework.h @@ -0,0 +1,85 @@ +#pragma once + +/* 必须在最前面定义,确保 pipe2/gettid 等可用 */ +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +/* + * StarryOS Syscall Test Framework + * + * 极简独立测试框架:每个文件测一个 syscall,独立编译运行。 + * 目标:出错时精确定位到 源文件:行号 -> 哪个调用 -> 什么结果 + * + * 用法: + * TEST_START("测试名"); + * CHECK(call == expected, "描述"); + * CHECK_ERR(call, EBADF, "描述"); + * TEST_DONE(); + */ + +#include +#include +#include +#include + +static int __pass = 0; +static int __fail = 0; + +/* ---- 核心: 带文件名+行号的检查宏 ---- */ + +/* 检查条件为真 */ +#define CHECK(cond, msg) do { \ + if (cond) { \ + printf(" PASS | %s:%d | %s\n", __FILE__, __LINE__, msg); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, errno, strerror(errno)); \ + __fail++; \ + } \ +} while(0) + +/* 检查 syscall 返回特定值 */ +#define CHECK_RET(call, expected, msg) do { \ + errno = 0; \ + long _r = (long)(call); \ + long _e = (long)(expected); \ + if (_r == _e) { \ + printf(" PASS | %s:%d | %s (ret=%ld)\n", \ + __FILE__, __LINE__, msg, _r); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | expected=%ld got=%ld | errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, _e, _r, errno, strerror(errno));\ + __fail++; \ + } \ +} while(0) + +/* 检查 syscall 失败且 errno 符合预期 */ +#define CHECK_ERR(call, exp_errno, msg) do { \ + errno = 0; \ + long _r = (long)(call); \ + if (_r == -1 && errno == (exp_errno)) { \ + printf(" PASS | %s:%d | %s (errno=%d as expected)\n", \ + __FILE__, __LINE__, msg, errno); \ + __pass++; \ + } else { \ + printf(" FAIL | %s:%d | %s | expected errno=%d got ret=%ld errno=%d (%s)\n", \ + __FILE__, __LINE__, msg, (int)(exp_errno), _r, errno, strerror(errno));\ + __fail++; \ + } \ +} while(0) + +/* ---- 测试边界 ---- */ +#define TEST_START(name) \ + printf("================================================\n"); \ + printf(" TEST: %s\n", name); \ + printf(" FILE: %s\n", __FILE__); \ + printf("================================================\n") + +#define TEST_DONE() \ + printf("------------------------------------------------\n"); \ + printf(" DONE: %d pass, %d fail\n", __pass, __fail); \ + printf("================================================\n\n"); \ + return __fail > 0 ? 1 : 0