序言
第一个lab由于比较简单(即实现一些用户空间程序,如 find, exec等),就仅是通过,没有记录了,从lab2开始。
以下是前一个lab make grade结果:
== Test sleep, no arguments ==
$ make qemu-gdb
sleep, no arguments: OK (5.6s)
== Test sleep, returns ==
$ make qemu-gdb
sleep, returns: OK (1.1s)
== Test sleep, makes syscall ==
$ make qemu-gdb
sleep, makes syscall: OK (1.0s)
== Test sixfive_test ==
$ make qemu-gdb
sixfive_test: OK (0.9s)
== Test sixfive_readme ==
$ make qemu-gdb
sixfive_readme: OK (1.6s)
== Test sixfive_all ==
$ make qemu-gdb
sixfive_all: OK (1.5s)
== Test memdump, examples ==
$ make qemu-gdb
memdump, examples: OK (1.2s)
== Test memdump, format ii, S, p ==
$ make qemu-gdb
memdump, format ii, S, p: OK (2.8s)
== Test find, in current directory ==
$ make qemu-gdb
find, in current directory: OK (1.0s)
== Test find, in sub-directory ==
$ make qemu-gdb
find, in sub-directory: OK (1.1s)
== Test find, recursive ==
$ make qemu-gdb
find, recursive: OK (1.5s)
== Test exec ==
$ make qemu-gdb
exec: OK (1.0s)
== Test exec, multiple args ==
$ make qemu-gdb
exec, multiple args: OK (1.5s)
== Test exec, recursive find ==
$ make qemu-gdb
exec, recursive find: OK (1.5s)
Using gdb
第一个任务为探索性质的。
谁调用了syscall?
从backtrace可以看出是usertrap()

因为syscall本质上就是从用户态进入内核态的trap。当用户程序执行系统调用指令时(在此为ecall),CPU发生trap,切换上下文,特权级,跳到统一的trap入口,并进入usertrap()。
p->trapframe->a7的值是什么?这个值代表什么?

从调试信息来看,a7=15。这是因为在初始化init.c中在一开始进行了open系统调用。

open系统调用的编号即为15。

执行调用时会将编号load进入a7。

CPU 之前处于什么模式?
在进入监督态前处于机器态(machine mode),执行初始化后进行了权力移交。
The xv6 kernel code contains consistency checks whose failure causes the kernel to panic; you may find that your kernel modifications cause panics. For example, replace the statement num = p->trapframe->a7; with num = (int) 0; at the beginning of syscall, run make qemu, and you will see something similar to:
xv6 内核代码包含一致性检查,其失败会导致内核崩溃;你可能会发现你的内核修改会导致崩溃。例如,在 syscall 的开头将语句 num = p->trapframe->a7; 替换为 num = (int) 0; ,运行 make qemu ,你会看到类似以下内容:
xv6 kernel is booting hart 2 starting hart 1 starting scause=0xd sepc=0x80001bfe stval=0x0 panic: kerneltrap
记下内核崩溃时的汇编指令。哪个寄存器对应变量 num ?

可以直接debugger break到该行,查看寄存器为a3。
为什么内核崩溃?提示:查看文本中的图 3-3;地址 0 是否映射在内核地址空间中?上面的 scause 的值是否确认了这一点?(参见 RISC-V 特权指令中对 scause 的描述) 图3.3如下:

很明显,地址0被标记为unused,并没有映射在内核空间中。我们检查scause的描述:

发现scause由一位的interrupt位以及剩下的exception code组成。我们返回的scause为0xd,检查对应图表:

发现原因正是load page error。
当内核崩溃时,正在运行的过程的名称是什么?它的进程 ID( pid )是什么?

打印发现,线程为init,pid=0。
Sandbox a command 对命令进行沙盒化
也就是实现interpose()系统调用。
In this assignment you will "sandbox" a process to restrict the system calls it can make. For example, one might sandbox a process to disallow opening files. You'll create a new interpose system call that will specify which system calls the kernel should reject from the calling process. interpose should take two arguments: an integer mask and a path. The mask's bits specify which system calls to reject. The second argument you will use in the next assignment and in this assignment it is always "-". For example, in order for a process to prevent itself from using the open system call, it should call interpose(1 << SYS_open, "-"), where SYS_open is a syscall number from kernel/syscall.h. Your implementation should cause the mask to be inherited by children of fork, so that children inherit the parent's restrictions.
在这个作业中,你需要"沙盒化"一个进程,以限制它能够进行的系统调用。例如,你可以沙盒化一个进程,禁止它打开文件。你将创建一个新的 interpose 系统调用,用于指定内核应该拒绝调用进程的哪些系统调用。 interpose 应该接受两个参数:一个整数掩码和一个路径。掩码的位指定要拒绝的系统调用。第二个参数你将在下一个作业中使用,在这个作业中它总是 "-" 。例如,为了防止一个进程使用 open 系统调用,它应该调用 interpose(1 << SYS_open, "-") ,其中 SYS_open 是一个来自 kernel/syscall.h 的调用号。你的实现应该使掩码能够被 fork 的子进程继承,这样子进程就能继承父进程的限制。
根据以下步骤我们可以实现一个syscall:
- 在Makefile中增加sandbox用户程序
- 在
user.h中增加函数签名 - 在
usys.pl中增加系统调用桩 - 在
syscall.h中增加系统调用号 - 在
sysproc.c中增加实现函数 - 将系统调用增加到
syscall.c的syscall数组中
对于interpose调用,我们还需要:
- 在
proc.c中,对一个进程增加scmask字段(代表SysCallMASK),类型为int - 修改
kfork()(fork具体实现),在fork时将该字段复制到子进程 - 修改
syscall(),在调用前检查是否被mask
这样,我们就实现了该系统调用
测试结果:

我们再梳理一遍syscall过程:
- 调用者到达调用签名
- 到达调用桩(汇编),将调用号存储到
a7(以及存储一些参数)后触发ecall - 最终到达
syscall(),函数检查调用号和一些检查后触发对应函数 - 对应函数从
trapframe中读取入参后进行调用
Sandbox with allowed pathnames 允许路径名的沙盒
In this assignment you will extend the sandbox to allow masked open and exec system calls based on the pathname they use. The second argument of sys_interpose is the pathname allowed. If open or exec is masked but the pathname matches the allowed pathname, then these system calls should be allowed.
在这个作业中,你将扩展沙盒以允许基于它们使用的路径名掩码 open 和 exec 系统调用。 sys_interpose 的第二个参数是允许的路径名。如果 open 或 exec 被掩码,但路径名与允许的路径名匹配,则这些系统调用应该被允许。
这是基于上一个步骤的扩展。基于以下步骤实现:
- 向
proc中增加字段char scpath[MAXPATH],代表被允许的字段 - 修改上一步的
isscmask()为canpass(),逻辑修改为:- 如果没有被mask,则放行
- 如果系统调用号为
open或exec,则判断路径是否一致,一致则放行 - 其余则阻止
- 在
sys_interpose()(即interposesyscall的实现方法)中将路径参数通过argstr()获取,并safestrcpy()至proc字段中
结果如下:
== Test sandbox_mask ==
$ make qemu-gdb
sandbox_mask: OK (6.5s)
== Test sandbox_fork ==
$ make qemu-gdb
sandbox_fork: OK (0.7s)
== Test sandbox_path ==
$ make qemu-gdb
sandbox_path: OK (3.3s)
== Test sandbox_most ==
$ make qemu-gdb
sandbox_most: OK (0.7s)
== Test sandbox_minus ==
$ make qemu-gdb
sandbox_minus: OK (1.4s)
Attack xv6 攻击 xv6
The xv6 kernel isolates user programs from each other and isolates the kernel from user programs. As you saw in the above assignments, an application cannot directly call a function in the kernel or in another user program; instead, interactions occur only through system calls. However, if there is a bug in the kernel's implementation of a system call, an attacker may be able to exploit that bug to break the isolation boundaries. To get a sense for how bugs can be exploited, we have introduced a bug into xv6 and your goal is to exploit that bug to steal a secret from another process. xv6 内核将用户程序彼此隔离,并将内核与用户程序隔离。如你在之前的作业中所见,一个应用程序不能直接调用内核中的函数或另一个用户程序中的函数;相反,交互只能通过系统调用进行。然而,如果内核的系统调用实现中存在一个漏洞,攻击者可能会利用该漏洞打破隔离边界。为了了解漏洞如何被利用,我们在 xv6 中引入了一个漏洞,你的目标是通过利用该漏洞从另一个进程窃取一个秘密。
The bug is that the call to memset(mem, 0, sz) in uvmalloc() in kernel/vm.c to clear a newly-allocated page is omitted when compiling this lab. Similarly, when compiling kernel/kalloc.c for this lab the two lines that use memset to put garbage into free pages are omitted. The net effect of omitting these 3 lines (all marked by ifndef LAB_SYSCALL) is that newly allocated memory retains the contents from its previous use. Thus an application that calls sbrk() to allocate memory may receive pages that have data in them from previous uses. Despite the 3 deleted lines, xv6 mostly works correctly; it even passes most of usertests. 这个 bug 是编译这个实验时,在 uvmalloc() 中的 memset(mem, 0, sz) 调用中,用于清除新分配页面的调用被遗漏了。类似地,在编译 kernel/kalloc.c 这个实验时,使用 memset 将垃圾数据放入空闲页面的两行代码也被遗漏了。遗漏这三行(所有标记为 ifndef LAB_SYSCALL 的行)的净效果是,新分配的内存保留了其之前使用的内容。因此,调用 sbrk() 分配内存的应用程序可能会收到包含之前使用数据的页面。尽管删除了三行代码,xv6 大部分情况下仍然能正常工作;它甚至通过了 usertests 的大部分测试。
user/secret.c writes a secret string in its memory and then exits (which frees its memory). Your goal is to add a few lines of code to user/attack.c to find the secret that a previous execution of secret.c wrote to memory, and to print the secret on a line by itself. user/secret.c 在其内存中写入一个秘密字符串,然后退出(这将释放其内存)。你的目标是在 user/attack.c 中添加几行代码,以找到先前执行 secret.c 写入内存的秘密,并在单独的一行中打印该秘密。
Your attack.c must work with unmodified xv6 and unmodified secret.c. You can change anything to help you experiment and debug, but must revert those changes before final testing and submitting. 你的 attack.c 必须与未经修改的 xv6 和未经修改的 secret.c 兼容。你可以更改任何内容来帮助你进行实验和调试,但在最终测试和提交之前必须恢复这些更改。
简单来说,就是调用sbrk()分配内存时会分配到未清空的脏内存,这个任务相对比较简单,就是在attack程序中调用sbrk()分配一段内存,然后iterate一遍找到secret即可。
最终结果
== Test answers-syscall.txt ==
answers-syscall.txt: OK
== Test sandbox_mask ==
$ make qemu-gdb
sandbox_mask: OK (4.3s)
== Test sandbox_fork ==
$ make qemu-gdb
sandbox_fork: OK (0.7s)
== Test sandbox_path ==
$ make qemu-gdb
sandbox_path: OK (5.0s)
== Test sandbox_most ==
$ make qemu-gdb
sandbox_most: OK (0.5s)
== Test sandbox_minus ==
$ make qemu-gdb
sandbox_minus: OK (0.7s)
== Test attack ==
$ make qemu-gdb
attack: OK (0.8s)
== Test time ==
time: OK
Score: 45/45