From 9c94494309f7f445681b73217edf6cdb93d70503 Mon Sep 17 00:00:00 2001 From: james Date: Sun, 25 Oct 2026 15:52:08 +0000 Subject: [PATCH] test(guest): compare predicted faults with executed qemu accesses --- guest/Makefile | 30 +++++++ guest/boot.S | 173 +++++++++++++++++++++++++++++++++++++ guest/guest.c | 223 ++++++++++++++++++++++++++++++++++++++++++++++++ guest/layout.ld | 13 +++ guest/verify.py | 66 ++++++++++++++ 5 files changed, 505 insertions(+) create mode 100644 guest/Makefile create mode 100644 guest/boot.S create mode 100644 guest/guest.c create mode 100644 guest/layout.ld create mode 100644 guest/verify.py diff --git a/guest/Makefile b/guest/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..e6f4cf74a541be941cadf829e018bdec2e258bfb --- /dev/null +++ b/guest/Makefile @@ -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 diff --git a/guest/boot.S b/guest/boot.S new file mode 100644 index 0000000000000000000000000000000000000000..31369e9d0b51d1c71e78beeec395e6eb59771f7c --- /dev/null +++ b/guest/boot.S @@ -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 diff --git a/guest/guest.c b/guest/guest.c new file mode 100644 index 0000000000000000000000000000000000000000..eead06c1a55133dd76ccc461f40fe90c54c3d40a --- /dev/null +++ b/guest/guest.c @@ -0,0 +1,223 @@ +#include +#include + +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"); +} diff --git a/guest/layout.ld b/guest/layout.ld new file mode 100644 index 0000000000000000000000000000000000000000..14c5e2c607d97a206c1ef1189c087abc27934374 --- /dev/null +++ b/guest/layout.ld @@ -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*) } +} diff --git a/guest/verify.py b/guest/verify.py new file mode 100644 index 0000000000000000000000000000000000000000..7ac815b0aebe26d279033c1736a8e80bb778465e --- /dev/null +++ b/guest/verify.py @@ -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")