Skip to content

syscall:建立 interpose 沙箱并观察内存隔离 ​

版本:syscall,提交 0e53502c9eddec54b592c1660dd3d3948de6009e。阅读系统调用和进程后,按三个阶段实现。2025 的核心是 sandbox,不能照用 trace/sysinfo 的旧实验答案。

先让入口可观察 ​

运行 make qemu-gdb,另一终端启动合适的 RISC-V GDB,断在 syscall。观察 p->trapframe->a7,根据 syscall.h 找出编号对应接口;查看调用栈;将打印出的 sepc 与 kernel/kernel.asm 对照。故意造成一次受控的内核空地址访问后,先解释 scause/sepc/stval 再修复改动。

这一练习的产物不是一组固定地址。地址取决于本次编译,应记录“哪个函数、哪条指令、访问了什么地址、为什么该页表不允许访问”。若只抄 handout 地址,换一个编译器就失去解释能力。

接通 interpose 的五层接口 ​

新增用户声明、usys.pl 桩、系统调用编号、分派表入口、sys_interpose()。将 mask 存到当前进程字段中,并保证 allocproc/freeproc 不会把旧进程限制留给复用槽位。kfork 复制父进程限制;exec 应保留同一进程已经建立的限制。

在 syscall() 中、执行具体实现之前检查 mask。测试 sandbox 32768 - cat README 应无法打开文件。这里 32768 来自该版本 SYS_open 的位,不应把它当跨年度固定常量。

设计不变量:某进程的策略由内核拥有;子进程初始化完成并变为 RUNNABLE 前已经继承策略;被拒绝的调用不产生业务副作用;拒绝时返回用户可理解的错误,而不是 panic 整个内核。

路径例外:不要保存用户指针 ​

扩展 sys_interpose 读取允许路径,复制到 struct proc 的 MAXPATH 缓冲区。masked open/exec 的路径与允许路径匹配时可放行。"-" 不是匹配所有路径的通配符;比较应是完整字符串比较,不能因为路径以减号开头就跳过它。官方 sandbox_minus 测试会实际创建名为 --- 的文件,再验证它能按允许路径访问。

参数读取和政策判断要拆开。首先安全复制本次请求路径;失败则返回错误;然后比较;最后再调用真正实现。读取路径时不要假设所有系统调用的第一个参数都是字符串,仅在 open/exec 这样的已知接口上使用相应位置。

构造测试矩阵:允许的文件、不同文件、过长字符串、无效地址、fork 后打开、exec 后打开、未被 mask 的调用。若只有父进程测试成功而子进程绕过,问题通常在继承;若新启动的无关进程也被拦截,通常是全局变量或复用字段初始化问题。

内存残留实验:隔离还包含初始化 ​

该分支刻意移除了分配页清零及分配器部分填充,让回收后再分配的物理页可能保留旧使用者数据。任务让 user/attack.c 在本地 xv6 教学环境中观察 secret 程序留下的内容。这揭示了权限正确仍可能泄漏:新的进程合法访问自己的新页面,却读到了旧进程尚未擦除的字节。

建议先阅读 secret.c 的数据布局,再在纸上画出“旧进程写入 → 退出 → 物理页回到 freelist → 新进程分配”的生命周期。只在分配范围内观察,使用标记识别内容,输出遵循测试协议。不要依赖某一次运行的绝对物理地址,因为分配顺序可能改变。

真正修复应在资源重新交给不相关使用者之前建立确定的初始内容,通常为清零。释放时填充垃圾主要用于暴露错误,不等价于保密清零。还应区分测试人为引入的漏洞与正常分支行为,不要把该实验现象描述成所有 xv6 版本都如此。

验收与排错 ​

先执行 ./grade-lab-syscall sandbox_mask,再运行全部 make grade。保留每个失败测试名,并对照 user/sandbox.c、测试脚本与 syscall 分派路径。若 unknown syscall,检查编号和表;若 fork 后失效,检查 kfork;若路径随机变化,检查是否复制字符串;若内核崩溃,检查是否直接解引用用户地址。

自测:为什么把限制放在 exec 加载器中仍不够?

已经运行的程序仍能直接发起系统调用,fork 子进程也不一定立刻 exec。策略必须在请求发生时的内核分派边界执行,exec 只是应保存策略的生命周期事件之一。

来源:2025 syscall 官方任务,源码重点为 syscall.c/sysproc.c/proc.c/proc.h 及本分支的 user/sandbox.c/secret.c/attack.c。