6.828-xv6-lab2-attack

107 阅读3分钟

由于涉及到system的回收和attack的创建,内存页表情况相对复杂,因此单独记录梳理。

TL;NR

因为是FILO的,只用关心发生在secret回收内存中,比heap更晚进入全局链表的内存页和fork,exec过程中的内存页即可。 同时fork过程中属于父进程的要归还也不用关心(除了trapframe,但可以视为exec阶段分配的,对本问题无影响)

因此:

  1. 晚于heap回收的,secret的三张页表页和attack的三张页表页抵消
  2. pipe需要一页,attack中的data,text,guard page,stack,trapframe 各1页,共6页
  3. 22-6 = 16,因此在第17页,跳过16页即可。

进程销毁

回收主要由父进程的wait触发,如果父进程父进程不在某个时刻调用wait/waitpid等,子进程就会一直停留在 ZOMBIE 状态(exit标记的),其残留的内存页表项和内核栈也不会被释放,甚至被init接管后才能回收。

int wait(uint64 addr) {
  //...
for(pp = proc; pp < &proc[NPROC]; pp++){
  if(pp->parent == p){
    // 遍历所有子进程
    //...
    if(pp->state == ZOMBIE){
      pid = pp->pid;
      if(addr != 0 && copyout(p->pagetable, addr, (char *)&pp->xstate,
                              sizeof(pp->xstate)) < 0) {
        release(&pp->lock);
        release(&wait_lock);
        return -1;
      }
      // 实际free
      freeproc(pp);
      
      release(&pp->lock);
      release(&wait_lock);
      return pid;
    }
    release(&pp->lock);
  }
   //...
}
static void
freeproc(struct proc *p)
{
  
  if(p->trapframe)// 每个进程在内核栈上预留的一块内存,用来在用户态 ↔ 内核态 切换时保存和恢复该进程的全部 CPU 寄存器状态。
    kfree((void*)p->trapframe);
  p->trapframe = 0;
  
  if(p->pagetable)
    proc_freepagetable(p->pagetable, p->sz);
  
  // ...
}
void
proc_freepagetable(pagetable_t pagetable, uint64 sz)
{
  //删除PTE项,不实际free, do_free = 0
  uvmunmap(pagetable, TRAMPOLINE, 1, 0);//只读的内核代码页,无需free
  uvmunmap(pagetable, TRAPFRAME, 1, 0);
  
  uvmfree(pagetable, sz); // 把从0到sz,该进程所持有的页都回收
}

void
uvmfree(pagetable_t pagetable, uint64 sz)
{
  if(sz > 0)
    // 虚拟空间地址从低到高回收页面
    uvmunmap(pagetable, 0, PGROUNDUP(sz)/PGSIZE, 1);
  freewalk(pagetable); //dfs 回收所有页表
}

进程创建

进程创建基本分为两个步骤,forkexec阶段,每个阶段都需要建立一个用户态页表。

int
fork(void)
{
  // 分配一个 proc 结构和内核栈、trapframe、pagetable
  if((np = allocproc()) == 0){
    return -1;
  }

  // 复制父进程的用户页表和所有用户物理页
  if(uvmcopy(p->pagetable, np->pagetable, p->sz) < 0){
    freeproc(np);
    release(&np->lock);
    return -1;
  }
  np->sz = p->sz;
  np->trace_mask = p->trace_mask;
  //...

  return pid;
}

static struct proc*
allocproc(void)
{
  //...
  
  // trapframe 页
  if((p->trapframe = (struct trapframe *)kalloc()) == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }

  // 根页表
  p->pagetable = proc_pagetable(p);
  if(p->pagetable == 0){
    freeproc(p);
    release(&p->lock);
    return 0;
  }

  //...
}

int
exec(char *path, char **argv)
{
  //...
  
  // 分配根页表
  if((pagetable = proc_pagetable(p)) == 0)
    goto bad;

  for(i=0, off=elf.phoff; i<elf.phnum; i++, off+=sizeof(ph)){
    if(readi(ip, 0, (uint64)&ph, off, sizeof(ph)) != sizeof(ph))
    // 分配程序段页表, data, text
    if((sz1 = uvmalloc(pagetable, sz, ph.vaddr + ph.memsz, flags2perm(ph.flags))) == 0)
      goto bad;
    sz = sz1;
    // ...
  }
  // ...

  p = myproc();
  uint64 oldsz = p->sz;

  // 分配guard page+stack size, 共2页
  sz = PGROUNDUP(sz);
  uint64 sz1;
  if((sz1 = uvmalloc(pagetable, sz, sz + (USERSTACK+1)*PGSIZE, PTE_W)) == 0)
  
  //...


  //归还内存页和页表页,沿用fork创建的内核页
  proc_freepagetable(oldpagetable, oldsz);
  //...
}

分析

  • secret 在heap之后归还的页仅有用户页表页 x3
  • fork 阶段不用考虑,因为拿到的所有page仍要返回,trapframe 看作是exec阶段创建的。
  • exec trapframe x1, 一级页表页 x1, 二级三级页表页 x2, data+text x2, guard+stack x2

注意因为管理内存页是FILO的,因此原本应该是0-31中的9,跳过9页,现在需要反过来跳过31-9=22页,再加上trapframe,data+text, guard+stack (页表页抵消了), 22-5=17.

此外父进程pipe还需要1页,综上应该是跳过16页。

#include "kernel/types.h"
#include "kernel/fcntl.h"
#include "user/user.h"
#include "kernel/riscv.h"


int
main(int argc, char *argv[])
{   
  int X=16;
  char *end = sbrk(PGSIZE*(X+1));
  write(2, end+X*PGSIZE+32, 8);
  exit(0);
}