~james/scope

9c94494309f7f445681b73217edf6cdb93d70503 — james verified 16 days ago e557cd7
test(guest): compare predicted faults with executed qemu accesses
5 files changed, 505 insertions(+), 0 deletions(-)

A guest/Makefile
A guest/boot.S
A guest/guest.c
A guest/layout.ld
A guest/verify.py
A guest/Makefile => guest/Makefile +30 -0
@@ 0,0 1,30 @@
CC ?= cc
LD ?= ld
OBJCOPY ?= objcopy
CFLAGS = -std=c11 -O2 -Wall -Wextra -Werror -ffreestanding -fno-pie -fno-stack-protector -fno-builtin -mno-red-zone -mgeneral-regs-only

.PHONY: all check clean

all: build/disk

build:
	mkdir -p build

build/boot.o: boot.S | build
	$(CC) -c -m64 -fno-pie $< -o $@

build/guest.o: guest.c | build
	$(CC) $(CFLAGS) -c $< -o $@

build/guest: build/boot.o build/guest.o layout.ld
	$(LD) -T layout.ld build/boot.o build/guest.o -o $@

build/disk: build/guest
	$(OBJCOPY) -O binary $< $@
	python3 -c 'from pathlib import Path; p = Path("$@"); b = p.read_bytes(); assert len(b) <= 61440; p.write_bytes(b.ljust(61952, b"\0"))'

check: build/disk
	python3 verify.py ../_build/default/bin/main.exe build/disk

clean:
	rm -rf build

A guest/boot.S => guest/boot.S +173 -0
@@ 0,0 1,173 @@
.section .boot,"ax"
.code16
.global boot
boot:
  cli
  xor %ax, %ax
  mov %ax, %ds
  mov %ax, %es
  mov %ax, %ss
  mov $0x7c00, %sp
  mov $packet, %si
  mov $0x42, %ah
  int $0x13
  jc stopped
  in $0x92, %al
  or $2, %al
  out %al, $0x92
  lgdt descriptor
  mov %cr0, %eax
  or $1, %eax
  mov %eax, %cr0
  ljmpl $8, $protected
stopped:
  hlt
  jmp stopped
.balign 8
packet:
  .byte 16, 0
  .word 120
  .word 0, 0x1000
  .quad 1
.global gdt
gdt:
  .quad 0
  .quad 0x00cf9a000000ffff
  .quad 0x00cf92000000ffff
  .quad 0x00af9a000000ffff
  .quad 0x00affa000000ffff
  .quad 0x00cff2000000ffff
  .quad 0, 0
descriptor:
  .word 63
  .long gdt
.org 510
.word 0xaa55

.section .text
.code32
protected:
  mov $0x10, %ax
  mov %ax, %ds
  mov %ax, %es
  mov %ax, %ss
  mov $0x90000, %esp
  cld
  mov $0x70000, %edi
  xor %eax, %eax
  mov $3072, %ecx
  rep stosl
  movl $0x71003, 0x70000
  movl $0x72003, 0x71000
  mov $0x72000, %edi
  mov $0x83, %eax
  mov $512, %ecx
1:
  mov %eax, (%edi)
  add $0x200000, %eax
  add $8, %edi
  loop 1b
  mov $0x20, %eax
  mov %eax, %cr4
  mov $0x70000, %eax
  mov %eax, %cr3
  mov $0xc0000080, %ecx
  rdmsr
  or $0x900, %eax
  wrmsr
  mov %cr0, %eax
  or $0x80010000, %eax
  mov %eax, %cr0
  ljmp $0x18, $longmode

.code64
longmode:
  mov $0x10, %ax
  mov %ax, %ds
  mov %ax, %es
  mov %ax, %ss
  mov $0x90000, %rsp
  lea __bss_start(%rip), %rdi
  lea __bss_end(%rip), %rcx
  sub %rdi, %rcx
  xor %eax, %eax
  rep stosb
  call guest
2:
  hlt
  jmp 2b

.global probe
probe:
  mov %rsp, recovery_stack(%rip)
  lea 5f(%rip), %rax
  mov %rax, recovery_address(%rip)
  test %edi, %edi
  jz 3f
  cmp $1, %edi
  je 4f
  call *%rsi
  jmp 5f
3:
  mov (%rsi), %rax
  jmp 5f
4:
  movq $0xc3, (%rsi)
5:
  ret

.global probe_user
probe_user:
  mov %rsp, recovery_stack(%rip)
  lea 9f(%rip), %rax
  mov %rax, recovery_address(%rip)
  pushq $0x2b
  pushq $0x86000
  pushq $2
  pushq $0x23
  lea 6f(%rip), %rax
  push %rax
  iretq
6:
  test %edi, %edi
  jz 7f
  cmp $1, %edi
  je 8f
  call *%rsi
  int $0x80
7:
  mov (%rsi), %rax
  int $0x80
8:
  movq $0xc3, (%rsi)
  int $0x80
9:
  ret

.global completed
completed:
  pushq $0
  jmp recover

.global pagefault
pagefault:
  movq $14, observed_vector(%rip)
  jmp recover
.global protection
protection:
  movq $13, observed_vector(%rip)
recover:
  push %rax
  mov 8(%rsp), %rax
  mov %rax, observed_error(%rip)
  mov recovery_address(%rip), %rax
  mov %rax, 16(%rsp)
  mov recovery_stack(%rip), %rax
  mov %rax, 40(%rsp)
  movq $0x18, 24(%rsp)
  movq $0x10, 48(%rsp)
  pop %rax
  add $8, %rsp
  iretq

.section .note.GNU-stack,"",@progbits

A guest/guest.c => guest/guest.c +223 -0
@@ 0,0 1,223 @@
#include <stdint.h>
#include <stddef.h>

static uint64_t root[512] __attribute__((aligned(4096)));
static uint64_t directory[512] __attribute__((aligned(4096)));
static uint64_t middle[512] __attribute__((aligned(4096)));
static uint64_t table[512] __attribute__((aligned(4096)));
static uint8_t payload[4096] __attribute__((aligned(4096)));

volatile uint64_t recovery_stack;
volatile uint64_t recovery_address;
volatile uint64_t observed_vector;
volatile uint64_t observed_error;

extern void probe(int access, uint64_t address);
extern void pagefault(void);
extern void protection(void);
extern void completed(void);
extern void probe_user(int access, uint64_t address);
extern uint64_t gdt[];

struct gate {
  uint16_t low;
  uint16_t selector;
  uint8_t ist;
  uint8_t flags;
  uint16_t middle;
  uint32_t high;
  uint32_t reserved;
} __attribute__((packed));

static struct gate idt[256];
static int phys_bits;
static int nx;
static int gb_pages;
static int user_mode;
static uint64_t root_bits;

static struct {
  uint32_t reserved;
  uint64_t rsp[3];
  uint64_t reserved1;
  uint64_t ist[7];
  uint64_t reserved2;
  uint16_t reserved3;
  uint16_t iomap;
} __attribute__((packed)) tss;

static void put(char c) {
  __asm__ volatile("outb %0, $0xe9" : : "a"(c));
}

static void text(const char *s) {
  while (*s) put(*s++);
}

static void hex(uint64_t n) {
  text("0x");
  for (int shift = 60; shift >= 0; shift -= 4) put("0123456789abcdef"[(n >> shift) & 15]);
}

static void decimal(int n) {
  if (n >= 10) decimal(n / 10);
  put('0' + n % 10);
}

static uint64_t cr0(void) {
  uint64_t value;
  __asm__ volatile("mov %%cr0, %0" : "=r"(value));
  return value;
}

static uint64_t cr4(void) {
  uint64_t value;
  __asm__ volatile("mov %%cr4, %0" : "=r"(value));
  return value;
}

static void set_cr0(uint64_t value) {
  __asm__ volatile("mov %0, %%cr0" : : "r"(value) : "memory");
}

static void set_cr4(uint64_t value) {
  __asm__ volatile("mov %0, %%cr4" : : "r"(value) : "memory");
}

static uint64_t efer(void) {
  uint32_t low, high;
  __asm__ volatile("rdmsr" : "=a"(low), "=d"(high) : "c"(0xc0000080));
  return (uint64_t)high << 32 | low;
}

static void set_efer(uint64_t value) {
  __asm__ volatile("wrmsr" : : "a"((uint32_t)value), "d"((uint32_t)(value >> 32)), "c"(0xc0000080));
}

static void set_ac(int enabled) {
  uint64_t flags;
  __asm__ volatile("pushfq; pop %0" : "=r"(flags));
  flags = enabled ? flags | (1u << 18) : flags & ~(1u << 18);
  __asm__ volatile("push %0; popfq" : : "r"(flags) : "cc", "memory");
}

static void flush(void) {
  __asm__ volatile("mov %0, %%cr3" : : "r"((uint64_t)root | root_bits) : "memory");
}

static void gate(int vector, void (*handler)(void)) {
  uint64_t address = (uint64_t)handler;
  idt[vector] = (struct gate){address, 0x18, 0, 0x8e, address >> 16, address >> 32, 0};
}

static void reset(void) {
  __asm__ volatile("mov %0, %%cr3" : : "r"(0x70000ull) : "memory");
  set_cr4(0x20);
  set_cr0(cr0() | (1u << 16));
  set_efer(efer() | (1u << 11));
  set_ac(0);
  user_mode = 0;
  root_bits = 0;
  for (int i = 0; i < 512; i++) root[i] = directory[i] = middle[i] = table[i] = 0;
  root[0] = (uint64_t)directory | 7;
  directory[0] = (uint64_t)middle | 7;
  middle[0] = 0x83;
  middle[2] = (uint64_t)table | 7;
  table[0] = (uint64_t)payload | 3;
  payload[0] = 0xc3;
  flush();
}

static void page(uint64_t *frame) {
  text("page ");
  hex((uint64_t)frame);
  put(' ');
  const uint8_t *bytes = (const uint8_t *)frame;
  for (int i = 0; i < 4096; i++) {
    put("0123456789abcdef"[bytes[i] >> 4]);
    put("0123456789abcdef"[bytes[i] & 15]);
  }
  put('\n');
}

static void run(const char *name, int access, uint64_t address) {
  uint64_t flags, current_cr2, current_cr3;
  flush();
  __asm__ volatile("pushfq; pop %0" : "=r"(flags));
  __asm__ volatile("mov %%cr2, %0" : "=r"(current_cr2));
  __asm__ volatile("mov %%cr3, %0" : "=r"(current_cr3));
  text("begin "); text(name); put(' '); decimal(access); put(' '); hex(address);
  text(user_mode ? " user\n" : " supervisor\n");
  text("context "); hex(cr0()); put(' '); hex(current_cr2); put(' '); hex(current_cr3);
  put(' '); hex(cr4()); put(' '); hex(efer()); put(' '); hex(flags); text(" 0x0 ");
  decimal(phys_bits); put(' '); decimal(nx); put(' '); decimal(gb_pages); put('\n');
  page(root); page(directory); page(middle); page(table);
  observed_vector = 0;
  observed_error = 0;
  flush();
  if (user_mode) probe_user(access, address);
  else probe(access, address);
  text("observed "); decimal(observed_vector); put(' '); decimal(observed_error); put('\n');
  text("end\n");
}

static void run_user(const char *name, int access) {
  middle[0] |= 4;
  user_mode = 1;
  run(name, access, 0x400000);
}

void guest(void) {
  uint32_t a, b, c, d;
  __asm__ volatile("cpuid" : "=a"(a), "=b"(b), "=c"(c), "=d"(d) : "a"(0x80000008), "c"(0));
  phys_bits = a & 255;
  __asm__ volatile("cpuid" : "=a"(a), "=b"(b), "=c"(c), "=d"(d) : "a"(0x80000001), "c"(0));
  nx = (d >> 20) & 1;
  gb_pages = (d >> 26) & 1;
  gate(13, protection);
  gate(14, pagefault);
  gate(0x80, completed);
  idt[0x80].flags = 0xee;
  tss.rsp[0] = 0x88000;
  tss.iomap = sizeof(tss);
  uint64_t base = (uint64_t)&tss;
  gdt[6] = (sizeof(tss) - 1) | ((base & 0xffffff) << 16) | (0x89ull << 40) | ((base >> 24) << 56);
  gdt[7] = base >> 32;
  __asm__ volatile("ltr %0" : : "r"((uint16_t)0x30));
  struct { uint16_t limit; uint64_t base; } __attribute__((packed)) idtr = {sizeof(idt) - 1, (uint64_t)idt};
  __asm__ volatile("lidt %0" : : "m"(idtr));
  reset(); run("read", 0, 0x400000);
  reset(); run("write", 1, 0x400000);
  reset(); run("execute", 2, 0x400000);
  reset(); middle[2] &= ~2ull; run("parent", 1, 0x400000);
  reset(); middle[2] &= ~2ull; set_cr0(cr0() & ~(1u << 16)); run("wp", 1, 0x400000);
  reset(); table[0] |= 1ull << 63; run("nx", 2, 0x400000);
  reset(); middle[2] |= 1ull << 63; run("ancestor", 2, 0x400000);
  reset(); table[0] = 0; run("absent", 0, 0x400000);
  reset(); table[0] = 0; run("absentwrite", 1, 0x400000);
  reset(); table[0] = 0; run("absentfetch", 2, 0x400000);
  reset(); table[0] |= 1ull << phys_bits; run("reserved", 0, 0x400000);
  reset(); table[0] |= 1ull << 63; set_efer(efer() & ~(1u << 11)); run("nxe", 0, 0x400000);
  reset(); middle[2] = 0x2083; run("alignment", 0, 0x400000);
  reset(); middle[2] = 0x1083; run("huge", 0, 0x401000);
  reset(); directory[1] = 0x83; run("giga", 0, 0x40001000);
  reset(); table[0] |= 0x80; run("pat", 0, 0x400000);
  reset(); root[1] = 0x83; run("pml", 0, 0x8000000000);
  reset(); run("canonical", 0, 0x800000000000);
  reset(); table[0] |= 4; set_cr4(cr4() | (1u << 20)); run("smep", 2, 0x400000);
  reset(); table[0] |= 4; set_cr4(cr4() | (1u << 21)); run("smap", 0, 0x400000);
  reset(); table[0] |= 4; set_cr4(cr4() | (1u << 21)); run("smapwrite", 1, 0x400000);
  reset(); table[0] |= 4; set_cr4(cr4() | (1u << 21)); set_ac(1); run("ac", 1, 0x400000);
  reset(); middle[2] &= ~2ull; table[0] = 0; run("precedence", 1, 0x400000);
  reset(); run_user("userdenied", 0);
  reset(); table[0] |= 4; run_user("userread", 0);
  reset(); table[0] |= 4; run_user("userwrite", 1);
  reset(); table[0] |= 4; run_user("userfetch", 2);
  reset(); table[0] |= 4; middle[2] &= ~2ull; set_cr0(cr0() & ~(1u << 16)); run_user("userwp", 1);
  reset(); table[0] |= (1ull << 63) | 4; run_user("usernx", 2);
  reset(); root_bits = 0xabc; run("rootbits", 0, 0x400000);
  reset(); set_cr4(cr4() | (1u << 17)); root_bits = 0xabc; run("pcid", 0, 0x400000);
  text("done\n");
  __asm__ volatile("outl %0, $0xf4" : : "a"(0x10));
  for (;;) __asm__ volatile("hlt");
}

A guest/layout.ld => guest/layout.ld +13 -0
@@ 0,0 1,13 @@
ENTRY(boot)
SECTIONS {
  .boot 0x7c00 : AT(0) { *(.boot) }
  .text 0x10000 : AT(512) { *(.text*) }
  .rodata : AT(LOADADDR(.text) + SIZEOF(.text)) { *(.rodata*) }
  .data : AT(LOADADDR(.rodata) + SIZEOF(.rodata)) { *(.data*) }
  .bss (NOLOAD) : ALIGN(4096) {
    __bss_start = .;
    *(.bss*) *(COMMON)
    __bss_end = .;
  }
  /DISCARD/ : { *(.eh_frame*) *(.note*) *(.comment*) }
}

A guest/verify.py => guest/verify.py +66 -0
@@ 0,0 1,66 @@
import json
from pathlib import Path
import subprocess
import sys
import tempfile


scope = Path(sys.argv[1]).resolve()
disk = Path(sys.argv[2]).resolve()
with tempfile.TemporaryDirectory(prefix="scopeguest") as directory:
    root = Path(directory)
    transcript = root / "transcript"
    command = [
        "qemu-system-x86_64", "-machine", "pc,accel=tcg", "-cpu", "max",
        "-m", "32M", "-display", "none", "-serial", "none", "-monitor", "none",
        "-no-reboot", "-drive", f"format=raw,file={disk}",
        "-debugcon", f"file:{transcript}", "-device", "isa-debug-exit,iobase=0xf4,iosize=0x04",
    ]
    try:
        result = subprocess.run(command, capture_output=True, text=True, timeout=45)
    except subprocess.TimeoutExpired:
        print(transcript.read_text()[-2000:] if transcript.exists() else "no guest output", file=sys.stderr)
        raise
    assert result.returncode == 33, (result.returncode, result.stderr, transcript.read_text()[-2000:])
    lines = transcript.read_text().splitlines()
    assert lines[-1] == "done", "guest did not finish"
    current = None
    checks = 0
    discrepancies = 0
    for line in lines:
        if line.startswith("begin "):
            _, name, access, address, privilege = line.split()
            current = []
        elif line.startswith("context ") or line.startswith("page "):
            assert current is not None
            current.append(line)
        elif line.startswith("observed "):
            _, vector, error = line.split()
            observed = int(vector), int(error)
        elif line == "end":
            assert current is not None
            source = root / "source"
            capture = root / "capture"
            source.write_text("\n".join(current) + "\n")
            subprocess.run([str(scope), "import", str(source), str(capture), "--source", f"qemu guest case {name}"],
                           check=True, capture_output=True, text=True)
            result = subprocess.run([str(scope), "explain", str(capture), address, "--access",
                                     ["read", "write", "execute"][int(access)], "--privilege", privilege, "--json"],
                                    capture_output=True, text=True, timeout=10)
            assert result.returncode in (0, 1), (name, result.stderr)
            prediction = json.loads(result.stdout)["result"]
            if prediction["status"] == "mapped":
                predicted = 0, 0
            else:
                predicted = prediction["fault"]["vector"], prediction["fault"]["error_code"]
            if observed != predicted:
                known = name in {"reserved", "nxe", "alignment", "pml"} and observed == (14, predicted[1] & ~1) and predicted == (14, 9)
                assert known, (name, "observed", observed, "predicted", predicted, prediction)
                discrepancies += 1
                print(f"{name}: known tcg discrepancy, observed=0x{observed[1]:x} architectural=0x{predicted[1]:x}")
            else:
                print(f"{name}: vector={observed[0]} error=0x{observed[1]:x}")
            checks += 1
            current = None
    assert checks == 31, checks
    print(f"{checks - discrepancies} exact guest comparisons, {discrepancies} reserved-bit tcg discrepancies")