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
12 changes: 10 additions & 2 deletions os/StarryOS/kernel/src/syscall/mm/mmap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -450,8 +450,16 @@ pub fn sys_mprotect(addr: usize, length: usize, prot: u32) -> AxResult<isize> {
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()) {
Comment thread
ZR233 marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can_access_range + MappingFlags::empty() 是正确的选择:纯连续覆盖检查,contains(empty()) 恒为 true,等价于仅检查区间是否有空洞。逻辑简洁且与 POSIX/Linux 语义一致。

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can_access_range(start_addr, length, MappingFlags::empty()) 逻辑正确:纯连续覆盖检查,contains(empty()) 恒为 true,遇到空洞即返回 false。检查在 protect() 前完成,失败路径映射不变。

return Err(AxError::NoMemory);
}
if permission_flags.contains(MmapProt::WRITE) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <signal.h>
#include <setjmp.h>

#ifndef PROT_GROWSDOWN
#define PROT_GROWSDOWN 0x01000000
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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");
Comment thread
ZR233 marked this conversation as resolved.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

write_faults + SIGSEGV/sigsetjmp 探针方案正确且轻量:在 mprotect 失败后直接写一字节到左右页,若页面仍可写则不触发 SIGSEGV(g_faulted == 0),否则通过 siglongjmp 安全恢复。这精确锁住了 ZR233 要求的原子性 ABI。

注释也正确文档化了 StarryOS 的原子语义(先检查再改,全有或全无)与 Linux 的差异(先改前缀再返回 ENOMEM),避免后续维护者误以为这是 bug。

/* 原子性: 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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 测试结构正确:3 页映射 → munmap 中间页造洞 → mprotect 整段期望 ENOMEM。覆盖了旧实现 find_area(start) 无法检测的 [mapped][hole][mapped] 场景。

}
}

/* ===================== munmap ===================== */

/* happy path */
Expand Down