Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions os/StarryOS/kernel/src/mm/aspace/backend/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Comment thread
mai-team-app[bot] marked this conversation as resolved.
// 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)
Expand Down
16 changes: 15 additions & 1 deletion os/arceos/modules/axfs-ng/src/highlevel/file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<u64> {
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
Expand Down Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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 <sys/mman.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <signal.h>
#include <setjmp.h>

#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();
}
Original file line number Diff line number Diff line change
@@ -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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>

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