Published on

Lab 2: system calls

Authors
  • avatar
    Name
    Vegetog
    Twitter

Lab地址

Using gdb (easy)


Sandbox a command(moderate)

1. 任务全景:系统调用是如何被“沙盒化”的?

在 xv6 中,所有的系统调用最终都会流经 kernel/syscall.c 中的 syscall() 函数。我们要做的就是在执行真正的功能函数之前,插一个“安检口”。

执行流程图: 用户程序调用 interpose(mask, "-") -> 触发 ecall -> 进入内核 syscall() -> 执行 sys_interpose 保存掩码 -> 之后该进程再次执行其他系统调用 -> syscall() 检查掩码 -> 若命中则拒绝。

2. 详细实现步骤

第一阶段:打通用户态与内核态的“隧道”

你需要让内核识别 interpose 这个名字和对应的编号。

  1. 定义编号:在 kernel/syscall.h 中添加 #define SYS_interpose 22
  2. 用户态声明:在 user/user.h 中添加 int interpose(int, char*);,让 C 编译器知道这个函数的存在。
  3. 生成存根:在 user/usys.pl 末尾添加 entry("interpose");。这样 make 时会自动生成汇编代码,负责将 SYS_interpose 放入 a7 寄存器并执行 ecall
  4. Makefile 配置:在 UPROGS 中添加 $U/_sandbox\,确保 sandbox.c 会被编译进磁盘镜像。

第二阶段:在内核中建立“记忆”

内核需要为每个进程单独存储一份“禁止名单”。

  1. 修改 PCB:在 kernel/proc.hstruct proc 结构体中添加 int interpose_mask;
  2. 实现功能函数:在 kernel/sysproc.c 中编写 sys_interpose
  • 使用 argint(0, &mask) 获取第一个参数。
  • 使用 argstr(1, path, sizeof(path)) 获取第二个参数并进行错误检查。
  • mask 写入当前进程的 p->interpose_mask
  1. 内核映射:在 kernel/syscall.c 中,使用 extern 声明 sys_interpose,并在 syscalls[] 数组中建立映射。

第三阶段:权限拦截与继承

这是沙盒真正起作用的地方。

  1. 拦截逻辑:修改 kernel/syscall.csyscall() 函数。
  • num = p->trapframe->a7; 之后,检查 (p->interpose_mask & (1 << num))
  • 如果为真,说明该调用被禁止。将 p->trapframe->a0(返回值寄存器)设为 -1,然后直接 return,不再执行后续逻辑。
  1. 实现继承:修改 kernel/proc.c 中的 kfork()
  • 在父进程创建子进程 np 后,执行 np->interpose_mask = p->interpose_mask;
  • 面试点:为什么要拷贝?因为像 catls 这样的命令通常是由 sandbox 程序通过 fork 出来的子进程运行的,如果不拷贝,沙盒就失效了。

3. Q&A

  • 位掩码 (Bitmask) 的优势

  • 为什么用 int 就能存下所有禁止项?(因为目前系统调用号只有 20 多个,1 个 bit 代表 1 个调用,非常节省内核内存)。

  • 内核如何访问用户态数据

  • argstr 内部是如何工作的?(它会查找进程的页表 p->pagetable,将用户空间的虚拟地址翻译成内核可以访问的物理地址,然后拷贝数据)。

  • 安全性边界

  • 如果用户程序尝试自己调用 interpose(0, "-") 来解除禁令怎么办?(在这个实验中我们允许了,但在真正的沙盒如 Linux Seccomp 中,通常设置为“只增不减”的模式,一旦开启禁止就无法撤销)。


Sandbox with allowed pathnames(easy)

1. 核心区别:从“行为控制”到“资源控制”

  • 上一个任务(基础沙箱)

  • 逻辑:只要系统调用号在掩码中,就直接拒绝。

  • 效果:如果禁用了 open,进程就无法打开任何文件。这通常会导致程序无法运行(例如 grep 无法读取输入文件)。

  • 当前任务(路径名沙箱)

  • 逻辑:即使系统调用在掩码中被禁用,内核也会额外检查:该调用是否在尝试访问一个“特许路径”。

  • 效果:你可以禁用 open,但同时指定“允许访问 README”。此时,进程打开 README 能成功,但打开其他任何文件都会失败。

2. 开发要点(核心修改逻辑)

要在代码中实现这种区别,需要关注以下四个环节:

A. 状态存储的扩展 (Storage)

  • 要点:内核不仅要记住一个 int 掩码,还要记住一个路径名字符串
  • 实现:在 kernel/proc.hstruct proc 中增加 char allowed_path[MAXPATH]
  • 注意:必须使用 MAXPATH 来限制大小,防止内核内存被恶意的大字符串撑破。

B. 数据拷贝的安全性 (Data Copying)

  • 要点:在 sys_interpose 中,你需要使用 argstr 将用户空间的路径名拷贝到内核的 allowed_path 中。
  • 面试点:为什么要拷贝?因为用户空间的内存是不可信的。如果内核只存一个指针,用户程序可以在检查完路径后偷偷修改内存内容(即 TOCTOU 攻击)。

C. 子进程的完整继承 (Inheritance)

  • 要点:在 kernel/proc.ckfork() 中,必须确保 safestrcpy(np->allowed_path, p->allowed_path, MAXPATH)
  • 原因:因为 sandbox 命令通常是先设置权限,然后 exec 运行目标程序。如果子进程不继承这个字符串,沙盒在目标程序运行时就失效了。

D. 特殊系统调用的拦截处理 (Special Casing)

  • 要点:在 kernel/syscall.csyscall() 函数中,你需要识别 SYS_openSYS_exec
  • 步骤
  1. 检查掩码是否命中。
  2. 如果命中,且是 openexec,则提取该调用的第一个参数(路径名)。
  3. 使用 strncmp 比较提取出的路径与 p->allowed_path
  4. 如果匹配,放行(By-pass);如果不匹配,拒绝