diff --git a/os/StarryOS/kernel/src/syscall/mm/mmap.rs b/os/StarryOS/kernel/src/syscall/mm/mmap.rs index 3c576258fe..242a09eaf1 100644 --- a/os/StarryOS/kernel/src/syscall/mm/mmap.rs +++ b/os/StarryOS/kernel/src/syscall/mm/mmap.rs @@ -450,8 +450,16 @@ pub fn sys_mprotect(addr: usize, length: usize, prot: u32) -> AxResult { let mut aspace = aspace_arc.lock(); let length = align_up_4k(length); let start_addr = VirtAddr::from(addr); - // man 2 mprotect: addresses without a mapping → ENOMEM. - if aspace.find_area(start_addr).is_none() { + // man 2 mprotect: if any page in [addr, addr+len) lacks a mapping → ENOMEM. + // Linux validates the whole range, not just the first page: an unmapped hole + // in the middle of `[mapped][hole][mapped]` must fail instead of silently + // protecting only the mapped fragments. `can_access_range` with an empty + // access mask is a pure contiguous-coverage check (every area's flags + // trivially contain the empty set), so it returns false on the first gap. + // We pre-check and leave the mapping untouched on failure, i.e. report + // ENOMEM atomically without any half-applied protection — the errno real + // programs test for. + if !aspace.can_access_range(start_addr, length, MappingFlags::empty()) { return Err(AxError::NoMemory); } if permission_flags.contains(MmapProt::WRITE) { diff --git a/test-suit/starryos/normal/qemu-smp1/syscall/test-mmap-family/c/src/main.c b/test-suit/starryos/normal/qemu-smp1/syscall/test-mmap-family/c/src/main.c index c65a8b2f4b..5f134166b5 100644 --- a/test-suit/starryos/normal/qemu-smp1/syscall/test-mmap-family/c/src/main.c +++ b/test-suit/starryos/normal/qemu-smp1/syscall/test-mmap-family/c/src/main.c @@ -15,6 +15,8 @@ #include #include #include +#include +#include #ifndef PROT_GROWSDOWN #define PROT_GROWSDOWN 0x01000000 @@ -42,6 +44,28 @@ } \ } while(0) +/* 写一字节到 *p, 返回是否触发 SIGSEGV (用于验证某页是否仍可写)。 */ +static sigjmp_buf g_jb; +static volatile int g_faulted; +static void on_segv(int sig) +{ + (void)sig; + g_faulted = 1; + siglongjmp(g_jb, 1); +} +static int write_faults(volatile char *p) +{ + struct sigaction sa = {0}, old; + sa.sa_handler = on_segv; + sigaction(SIGSEGV, &sa, &old); + g_faulted = 0; + if (sigsetjmp(g_jb, 1) == 0) { + *p = 0x55; + } + sigaction(SIGSEGV, &old, NULL); + return g_faulted; +} + int main(void) { TEST_START("mmap/munmap/mprotect"); @@ -128,6 +152,34 @@ int main(void) CHECK_ERR(mprotect(gone, ps, PROT_READ), ENOMEM, "mprotect 未映射区间 → ENOMEM"); + /* mprotect 跨 [mapped][hole][mapped] → ENOMEM (回归: 起始页有映射但区间 + * 中段是空洞)。man 2 mprotect: 区间内任一页未映射即 ENOMEM。旧 starry 仅 + * 检查起始页所在 VMA (find_area(start) 命中) 便放行, 静默成功跳过空洞; + * 修复后用 can_access_range 做整段连续覆盖校验。*/ + { + char *hole = mmap(NULL, 3 * ps, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + CHECK(hole != MAP_FAILED, "mmap 3 页用于 hole-mprotect 测试"); + if (hole != MAP_FAILED) { + /* 在中间页打一个洞, 留下 [mapped][hole][mapped] */ + CHECK_RET(munmap(hole + ps, ps), 0, "munmap 中间页造洞"); + CHECK_ERR(mprotect(hole, 3 * ps, PROT_READ), + ENOMEM, "mprotect 跨中段空洞 → ENOMEM"); + /* 原子性: StarryOS 用 can_access_range 在改任何权限前先校验整段, + * 命中空洞即原子拒绝、一页都不改, 故左右两页保持原 RW。 + * 注: 真 Linux 在此并非原子 —— 它把保护位应用到空洞前的前缀页(左页 + * 会变成 PROT_READ)再返回 ENOMEM; StarryOS 选择更安全的原子语义。 + * 本断言锁住该原子行为: 若日后把 pre-check 挪到逐页 protect 之后, + * 左页会被半改成只读, 这里立刻抓住。*/ + CHECK(write_faults(hole) == 0, + "失败 mprotect 后左页仍可写 (StarryOS 原子拒绝, 未半改)"); + CHECK(write_faults(hole + 2 * ps) == 0, + "失败 mprotect 后右页仍可写 (空洞之后, 任何实现都不应改)"); + munmap(hole, ps); + munmap(hole + 2 * ps, ps); + } + } + /* ===================== munmap ===================== */ /* happy path */