~james/scope

test(guest): compare predicted faults with executed qemu accesses

commit
9c94494309f7f445681b73217edf6cdb93d70503 browse tree
parent
e557cd7d08fb8f5d6ddf4afedb5447c1e8ce23bf
author
james <j@cpl0.zip>
signature
Verified ~james signed with a key registered by ~james
date
2026-10-25 15:52 UTC

5 files changed, +505 −0

guest/Makefile added

@@ -0,0 +1,30 @@
1+CC ?= cc
2+LD ?= ld
3+OBJCOPY ?= objcopy
4+CFLAGS = -std=c11 -O2 -Wall -Wextra -Werror -ffreestanding -fno-pie -fno-stack-protector -fno-builtin -mno-red-zone -mgeneral-regs-only
5+
6+.PHONY: all check clean
7+
8+all: build/disk
9+
10+build:
11+ mkdir -p build
12+
13+build/boot.o: boot.S | build
14+ $(CC) -c -m64 -fno-pie $< -o $@
15+
16+build/guest.o: guest.c | build
17+ $(CC) $(CFLAGS) -c $< -o $@
18+
19+build/guest: build/boot.o build/guest.o layout.ld
20+ $(LD) -T layout.ld build/boot.o build/guest.o -o $@
21+
22+build/disk: build/guest
23+ $(OBJCOPY) -O binary $< $@
24+ python3 -c 'from pathlib import Path; p = Path("$@"); b = p.read_bytes(); assert len(b) <= 61440; p.write_bytes(b.ljust(61952, b"\0"))'
25+
26+check: build/disk
27+ python3 verify.py ../_build/default/bin/main.exe build/disk
28+
29+clean:
30+ rm -rf build

guest/boot.S added

@@ -0,0 +1,173 @@
1+.section .boot,"ax"
2+.code16
3+.global boot
4+boot:
5+ cli
6+ xor %ax, %ax
7+ mov %ax, %ds
8+ mov %ax, %es
9+ mov %ax, %ss
10+ mov $0x7c00, %sp
11+ mov $packet, %si
12+ mov $0x42, %ah
13+ int $0x13
14+ jc stopped
15+ in $0x92, %al
16+ or $2, %al
17+ out %al, $0x92
18+ lgdt descriptor
19+ mov %cr0, %eax
20+ or $1, %eax
21+ mov %eax, %cr0
22+ ljmpl $8, $protected
23+stopped:
24+ hlt
25+ jmp stopped
26+.balign 8
27+packet:
28+ .byte 16, 0
29+ .word 120
30+ .word 0, 0x1000
31+ .quad 1
32+.global gdt
33+gdt:
34+ .quad 0
35+ .quad 0x00cf9a000000ffff
36+ .quad 0x00cf92000000ffff
37+ .quad 0x00af9a000000ffff
38+ .quad 0x00affa000000ffff
39+ .quad 0x00cff2000000ffff
40+ .quad 0, 0
41+descriptor:
42+ .word 63
43+ .long gdt
44+.org 510
45+.word 0xaa55
46+
47+.section .text
48+.code32
49+protected:
50+ mov $0x10, %ax
51+ mov %ax, %ds
52+ mov %ax, %es
53+ mov %ax, %ss
54+ mov $0x90000, %esp
55+ cld
56+ mov $0x70000, %edi
57+ xor %eax, %eax
58+ mov $3072, %ecx
59+ rep stosl
60+ movl $0x71003, 0x70000
61+ movl $0x72003, 0x71000
62+ mov $0x72000, %edi
63+ mov $0x83, %eax
64+ mov $512, %ecx
65+1:
66+ mov %eax, (%edi)
67+ add $0x200000, %eax
68+ add $8, %edi
69+ loop 1b
70+ mov $0x20, %eax
71+ mov %eax, %cr4
72+ mov $0x70000, %eax
73+ mov %eax, %cr3
74+ mov $0xc0000080, %ecx
75+ rdmsr
76+ or $0x900, %eax
77+ wrmsr
78+ mov %cr0, %eax
79+ or $0x80010000, %eax
80+ mov %eax, %cr0
81+ ljmp $0x18, $longmode
82+
83+.code64
84+longmode:
85+ mov $0x10, %ax
86+ mov %ax, %ds
87+ mov %ax, %es
88+ mov %ax, %ss
89+ mov $0x90000, %rsp
90+ lea __bss_start(%rip), %rdi
91+ lea __bss_end(%rip), %rcx
92+ sub %rdi, %rcx
93+ xor %eax, %eax
94+ rep stosb
95+ call guest
96+2:
97+ hlt
98+ jmp 2b
99+
100+.global probe
101+probe:
102+ mov %rsp, recovery_stack(%rip)
103+ lea 5f(%rip), %rax
104+ mov %rax, recovery_address(%rip)
105+ test %edi, %edi
106+ jz 3f
107+ cmp $1, %edi
108+ je 4f
109+ call *%rsi
110+ jmp 5f
111+3:
112+ mov (%rsi), %rax
113+ jmp 5f
114+4:
115+ movq $0xc3, (%rsi)
116+5:
117+ ret
118+
119+.global probe_user
120+probe_user:
121+ mov %rsp, recovery_stack(%rip)
122+ lea 9f(%rip), %rax
123+ mov %rax, recovery_address(%rip)
124+ pushq $0x2b
125+ pushq $0x86000
126+ pushq $2
127+ pushq $0x23
128+ lea 6f(%rip), %rax
129+ push %rax
130+ iretq
131+6:
132+ test %edi, %edi
133+ jz 7f
134+ cmp $1, %edi
135+ je 8f
136+ call *%rsi
137+ int $0x80
138+7:
139+ mov (%rsi), %rax
140+ int $0x80
141+8:
142+ movq $0xc3, (%rsi)
143+ int $0x80
144+9:
145+ ret
146+
147+.global completed
148+completed:
149+ pushq $0
150+ jmp recover
151+
152+.global pagefault
153+pagefault:
154+ movq $14, observed_vector(%rip)
155+ jmp recover
156+.global protection
157+protection:
158+ movq $13, observed_vector(%rip)
159+recover:
160+ push %rax
161+ mov 8(%rsp), %rax
162+ mov %rax, observed_error(%rip)
163+ mov recovery_address(%rip), %rax
164+ mov %rax, 16(%rsp)
165+ mov recovery_stack(%rip), %rax
166+ mov %rax, 40(%rsp)
167+ movq $0x18, 24(%rsp)
168+ movq $0x10, 48(%rsp)
169+ pop %rax
170+ add $8, %rsp
171+ iretq
172+
173+.section .note.GNU-stack,"",@progbits

guest/guest.c added

@@ -0,0 +1,223 @@
1+#include <stdint.h>
2+#include <stddef.h>
3+
4+static uint64_t root[512] __attribute__((aligned(4096)));
5+static uint64_t directory[512] __attribute__((aligned(4096)));
6+static uint64_t middle[512] __attribute__((aligned(4096)));
7+static uint64_t table[512] __attribute__((aligned(4096)));
8+static uint8_t payload[4096] __attribute__((aligned(4096)));
9+
10+volatile uint64_t recovery_stack;
11+volatile uint64_t recovery_address;
12+volatile uint64_t observed_vector;
13+volatile uint64_t observed_error;
14+
15+extern void probe(int access, uint64_t address);
16+extern void pagefault(void);
17+extern void protection(void);
18+extern void completed(void);
19+extern void probe_user(int access, uint64_t address);
20+extern uint64_t gdt[];
21+
22+struct gate {
23+ uint16_t low;
24+ uint16_t selector;
25+ uint8_t ist;
26+ uint8_t flags;
27+ uint16_t middle;
28+ uint32_t high;
29+ uint32_t reserved;
30+} __attribute__((packed));
31+
32+static struct gate idt[256];
33+static int phys_bits;
34+static int nx;
35+static int gb_pages;
36+static int user_mode;
37+static uint64_t root_bits;
38+
39+static struct {
40+ uint32_t reserved;
41+ uint64_t rsp[3];
42+ uint64_t reserved1;
43+ uint64_t ist[7];
44+ uint64_t reserved2;
45+ uint16_t reserved3;
46+ uint16_t iomap;
47+} __attribute__((packed)) tss;
48+
49+static void put(char c) {
50+ __asm__ volatile("outb %0, $0xe9" : : "a"(c));
51+}
52+
53+static void text(const char *s) {
54+ while (*s) put(*s++);
55+}
56+
57+static void hex(uint64_t n) {
58+ text("0x");
59+ for (int shift = 60; shift >= 0; shift -= 4) put("0123456789abcdef"[(n >> shift) & 15]);
60+}
61+
62+static void decimal(int n) {
63+ if (n >= 10) decimal(n / 10);
64+ put('0' + n % 10);
65+}
66+
67+static uint64_t cr0(void) {
68+ uint64_t value;
69+ __asm__ volatile("mov %%cr0, %0" : "=r"(value));
70+ return value;
71+}
72+
73+static uint64_t cr4(void) {
74+ uint64_t value;
75+ __asm__ volatile("mov %%cr4, %0" : "=r"(value));
76+ return value;
77+}
78+
79+static void set_cr0(uint64_t value) {
80+ __asm__ volatile("mov %0, %%cr0" : : "r"(value) : "memory");
81+}
82+
83+static void set_cr4(uint64_t value) {
84+ __asm__ volatile("mov %0, %%cr4" : : "r"(value) : "memory");
85+}
86+
87+static uint64_t efer(void) {
88+ uint32_t low, high;
89+ __asm__ volatile("rdmsr" : "=a"(low), "=d"(high) : "c"(0xc0000080));
90+ return (uint64_t)high << 32 | low;
91+}
92+
93+static void set_efer(uint64_t value) {
94+ __asm__ volatile("wrmsr" : : "a"((uint32_t)value), "d"((uint32_t)(value >> 32)), "c"(0xc0000080));
95+}
96+
97+static void set_ac(int enabled) {
98+ uint64_t flags;
99+ __asm__ volatile("pushfq; pop %0" : "=r"(flags));
100+ flags = enabled ? flags | (1u << 18) : flags & ~(1u << 18);
101+ __asm__ volatile("push %0; popfq" : : "r"(flags) : "cc", "memory");
102+}
103+
104+static void flush(void) {
105+ __asm__ volatile("mov %0, %%cr3" : : "r"((uint64_t)root | root_bits) : "memory");
106+}
107+
108+static void gate(int vector, void (*handler)(void)) {
109+ uint64_t address = (uint64_t)handler;
110+ idt[vector] = (struct gate){address, 0x18, 0, 0x8e, address >> 16, address >> 32, 0};
111+}
112+
113+static void reset(void) {
114+ __asm__ volatile("mov %0, %%cr3" : : "r"(0x70000ull) : "memory");
115+ set_cr4(0x20);
116+ set_cr0(cr0() | (1u << 16));
117+ set_efer(efer() | (1u << 11));
118+ set_ac(0);
119+ user_mode = 0;
120+ root_bits = 0;
121+ for (int i = 0; i < 512; i++) root[i] = directory[i] = middle[i] = table[i] = 0;
122+ root[0] = (uint64_t)directory | 7;
123+ directory[0] = (uint64_t)middle | 7;
124+ middle[0] = 0x83;
125+ middle[2] = (uint64_t)table | 7;
126+ table[0] = (uint64_t)payload | 3;
127+ payload[0] = 0xc3;
128+ flush();
129+}
130+
131+static void page(uint64_t *frame) {
132+ text("page ");
133+ hex((uint64_t)frame);
134+ put(' ');
135+ const uint8_t *bytes = (const uint8_t *)frame;
136+ for (int i = 0; i < 4096; i++) {
137+ put("0123456789abcdef"[bytes[i] >> 4]);
138+ put("0123456789abcdef"[bytes[i] & 15]);
139+ }
140+ put('\n');
141+}
142+
143+static void run(const char *name, int access, uint64_t address) {
144+ uint64_t flags, current_cr2, current_cr3;
145+ flush();
146+ __asm__ volatile("pushfq; pop %0" : "=r"(flags));
147+ __asm__ volatile("mov %%cr2, %0" : "=r"(current_cr2));
148+ __asm__ volatile("mov %%cr3, %0" : "=r"(current_cr3));
149+ text("begin "); text(name); put(' '); decimal(access); put(' '); hex(address);
150+ text(user_mode ? " user\n" : " supervisor\n");
151+ text("context "); hex(cr0()); put(' '); hex(current_cr2); put(' '); hex(current_cr3);
152+ put(' '); hex(cr4()); put(' '); hex(efer()); put(' '); hex(flags); text(" 0x0 ");
153+ decimal(phys_bits); put(' '); decimal(nx); put(' '); decimal(gb_pages); put('\n');
154+ page(root); page(directory); page(middle); page(table);
155+ observed_vector = 0;
156+ observed_error = 0;
157+ flush();
158+ if (user_mode) probe_user(access, address);
159+ else probe(access, address);
160+ text("observed "); decimal(observed_vector); put(' '); decimal(observed_error); put('\n');
161+ text("end\n");
162+}
163+
164+static void run_user(const char *name, int access) {
165+ middle[0] |= 4;
166+ user_mode = 1;
167+ run(name, access, 0x400000);
168+}
169+
170+void guest(void) {
171+ uint32_t a, b, c, d;
172+ __asm__ volatile("cpuid" : "=a"(a), "=b"(b), "=c"(c), "=d"(d) : "a"(0x80000008), "c"(0));
173+ phys_bits = a & 255;
174+ __asm__ volatile("cpuid" : "=a"(a), "=b"(b), "=c"(c), "=d"(d) : "a"(0x80000001), "c"(0));
175+ nx = (d >> 20) & 1;
176+ gb_pages = (d >> 26) & 1;
177+ gate(13, protection);
178+ gate(14, pagefault);
179+ gate(0x80, completed);
180+ idt[0x80].flags = 0xee;
181+ tss.rsp[0] = 0x88000;
182+ tss.iomap = sizeof(tss);
183+ uint64_t base = (uint64_t)&tss;
184+ gdt[6] = (sizeof(tss) - 1) | ((base & 0xffffff) << 16) | (0x89ull << 40) | ((base >> 24) << 56);
185+ gdt[7] = base >> 32;
186+ __asm__ volatile("ltr %0" : : "r"((uint16_t)0x30));
187+ struct { uint16_t limit; uint64_t base; } __attribute__((packed)) idtr = {sizeof(idt) - 1, (uint64_t)idt};
188+ __asm__ volatile("lidt %0" : : "m"(idtr));
189+ reset(); run("read", 0, 0x400000);
190+ reset(); run("write", 1, 0x400000);
191+ reset(); run("execute", 2, 0x400000);
192+ reset(); middle[2] &= ~2ull; run("parent", 1, 0x400000);
193+ reset(); middle[2] &= ~2ull; set_cr0(cr0() & ~(1u << 16)); run("wp", 1, 0x400000);
194+ reset(); table[0] |= 1ull << 63; run("nx", 2, 0x400000);
195+ reset(); middle[2] |= 1ull << 63; run("ancestor", 2, 0x400000);
196+ reset(); table[0] = 0; run("absent", 0, 0x400000);
197+ reset(); table[0] = 0; run("absentwrite", 1, 0x400000);
198+ reset(); table[0] = 0; run("absentfetch", 2, 0x400000);
199+ reset(); table[0] |= 1ull << phys_bits; run("reserved", 0, 0x400000);
200+ reset(); table[0] |= 1ull << 63; set_efer(efer() & ~(1u << 11)); run("nxe", 0, 0x400000);
201+ reset(); middle[2] = 0x2083; run("alignment", 0, 0x400000);
202+ reset(); middle[2] = 0x1083; run("huge", 0, 0x401000);
203+ reset(); directory[1] = 0x83; run("giga", 0, 0x40001000);
204+ reset(); table[0] |= 0x80; run("pat", 0, 0x400000);
205+ reset(); root[1] = 0x83; run("pml", 0, 0x8000000000);
206+ reset(); run("canonical", 0, 0x800000000000);
207+ reset(); table[0] |= 4; set_cr4(cr4() | (1u << 20)); run("smep", 2, 0x400000);
208+ reset(); table[0] |= 4; set_cr4(cr4() | (1u << 21)); run("smap", 0, 0x400000);
209+ reset(); table[0] |= 4; set_cr4(cr4() | (1u << 21)); run("smapwrite", 1, 0x400000);
210+ reset(); table[0] |= 4; set_cr4(cr4() | (1u << 21)); set_ac(1); run("ac", 1, 0x400000);
211+ reset(); middle[2] &= ~2ull; table[0] = 0; run("precedence", 1, 0x400000);
212+ reset(); run_user("userdenied", 0);
213+ reset(); table[0] |= 4; run_user("userread", 0);
214+ reset(); table[0] |= 4; run_user("userwrite", 1);
215+ reset(); table[0] |= 4; run_user("userfetch", 2);
216+ reset(); table[0] |= 4; middle[2] &= ~2ull; set_cr0(cr0() & ~(1u << 16)); run_user("userwp", 1);
217+ reset(); table[0] |= (1ull << 63) | 4; run_user("usernx", 2);
218+ reset(); root_bits = 0xabc; run("rootbits", 0, 0x400000);
219+ reset(); set_cr4(cr4() | (1u << 17)); root_bits = 0xabc; run("pcid", 0, 0x400000);
220+ text("done\n");
221+ __asm__ volatile("outl %0, $0xf4" : : "a"(0x10));
222+ for (;;) __asm__ volatile("hlt");
223+}

guest/layout.ld added

@@ -0,0 +1,13 @@
1+ENTRY(boot)
2+SECTIONS {
3+ .boot 0x7c00 : AT(0) { *(.boot) }
4+ .text 0x10000 : AT(512) { *(.text*) }
5+ .rodata : AT(LOADADDR(.text) + SIZEOF(.text)) { *(.rodata*) }
6+ .data : AT(LOADADDR(.rodata) + SIZEOF(.rodata)) { *(.data*) }
7+ .bss (NOLOAD) : ALIGN(4096) {
8+ __bss_start = .;
9+ *(.bss*) *(COMMON)
10+ __bss_end = .;
11+ }
12+ /DISCARD/ : { *(.eh_frame*) *(.note*) *(.comment*) }
13+}

guest/verify.py added

@@ -0,0 +1,66 @@
1+import json
2+from pathlib import Path
3+import subprocess
4+import sys
5+import tempfile
6+
7+
8+scope = Path(sys.argv[1]).resolve()
9+disk = Path(sys.argv[2]).resolve()
10+with tempfile.TemporaryDirectory(prefix="scopeguest") as directory:
11+ root = Path(directory)
12+ transcript = root / "transcript"
13+ command = [
14+ "qemu-system-x86_64", "-machine", "pc,accel=tcg", "-cpu", "max",
15+ "-m", "32M", "-display", "none", "-serial", "none", "-monitor", "none",
16+ "-no-reboot", "-drive", f"format=raw,file={disk}",
17+ "-debugcon", f"file:{transcript}", "-device", "isa-debug-exit,iobase=0xf4,iosize=0x04",
18+ ]
19+ try:
20+ result = subprocess.run(command, capture_output=True, text=True, timeout=45)
21+ except subprocess.TimeoutExpired:
22+ print(transcript.read_text()[-2000:] if transcript.exists() else "no guest output", file=sys.stderr)
23+ raise
24+ assert result.returncode == 33, (result.returncode, result.stderr, transcript.read_text()[-2000:])
25+ lines = transcript.read_text().splitlines()
26+ assert lines[-1] == "done", "guest did not finish"
27+ current = None
28+ checks = 0
29+ discrepancies = 0
30+ for line in lines:
31+ if line.startswith("begin "):
32+ _, name, access, address, privilege = line.split()
33+ current = []
34+ elif line.startswith("context ") or line.startswith("page "):
35+ assert current is not None
36+ current.append(line)
37+ elif line.startswith("observed "):
38+ _, vector, error = line.split()
39+ observed = int(vector), int(error)
40+ elif line == "end":
41+ assert current is not None
42+ source = root / "source"
43+ capture = root / "capture"
44+ source.write_text("\n".join(current) + "\n")
45+ subprocess.run([str(scope), "import", str(source), str(capture), "--source", f"qemu guest case {name}"],
46+ check=True, capture_output=True, text=True)
47+ result = subprocess.run([str(scope), "explain", str(capture), address, "--access",
48+ ["read", "write", "execute"][int(access)], "--privilege", privilege, "--json"],
49+ capture_output=True, text=True, timeout=10)
50+ assert result.returncode in (0, 1), (name, result.stderr)
51+ prediction = json.loads(result.stdout)["result"]
52+ if prediction["status"] == "mapped":
53+ predicted = 0, 0
54+ else:
55+ predicted = prediction["fault"]["vector"], prediction["fault"]["error_code"]
56+ if observed != predicted:
57+ known = name in {"reserved", "nxe", "alignment", "pml"} and observed == (14, predicted[1] & ~1) and predicted == (14, 9)
58+ assert known, (name, "observed", observed, "predicted", predicted, prediction)
59+ discrepancies += 1
60+ print(f"{name}: known tcg discrepancy, observed=0x{observed[1]:x} architectural=0x{predicted[1]:x}")
61+ else:
62+ print(f"{name}: vector={observed[0]} error=0x{observed[1]:x}")
63+ checks += 1
64+ current = None
65+ assert checks == 31, checks
66+ print(f"{checks - discrepancies} exact guest comparisons, {discrepancies} reserved-bit tcg discrepancies")