~james/scope

test(xen): inject native failures and verify watch evidence retention

commit
3cec21766d164acbbd612dabbfa0ad045d6f4cf8 browse tree
parent
9c94494309f7f445681b73217edf6cdb93d70503
author
james <j@cpl0.zip>
signature
Verified ~james signed with a key registered by ~james
date
2026-10-27 18:26 UTC

2 files changed, +219 −0

test/native.c added

@@ -0,0 +1,138 @@
1+#include <xenctrl.h>
2+#include <xenforeignmemory.h>
3+#include <xentoollog.h>
4+#include <xen/hvm/save.h>
5+#include <errno.h>
6+#include <stdio.h>
7+#include <stdlib.h>
8+#include <string.h>
9+#include <signal.h>
10+
11+static int references;
12+static int calls;
13+static int samples;
14+
15+static int failing(const char *name) {
16+ const char *failure = getenv("SCOPE_TEST_FAILURE");
17+ return failure && strcmp(failure, name) == 0;
18+}
19+
20+static void record(const char *name) {
21+ const char *path = getenv("SCOPE_TEST_LOG");
22+ if (!path) abort();
23+ FILE *stream = fopen(path, "a");
24+ if (!stream) abort();
25+ fprintf(stream, "%s %d\n", name, references);
26+ if (fclose(stream)) abort();
27+}
28+
29+static void diagnostic(xentoollog_logger *logger, const char *format, ...) {
30+ va_list arguments;
31+ va_start(arguments, format);
32+ if (logger) logger->vmessage(logger, XTL_ERROR, EACCES, "mock", format, arguments);
33+ else vfprintf(stderr, format, arguments);
34+ va_end(arguments);
35+}
36+
37+xc_interface *xc_interface_open(xentoollog_logger *logger, xentoollog_logger *builder, unsigned flags) {
38+ (void)logger; (void)builder; (void)flags;
39+ references = getenv("SCOPE_TEST_PAUSED") ? 1 : 0;
40+ calls = 0;
41+ samples++;
42+ record("open");
43+ if (failing("logged")) { diagnostic(logger, "mock diagnostic\n"); return NULL; }
44+ return failing("open") ? NULL : (xc_interface *)1;
45+}
46+
47+int xc_interface_close(xc_interface *control) {
48+ (void)control;
49+ record("close");
50+ return 0;
51+}
52+
53+int xc_domain_getinfolist(xc_interface *control, uint32_t domain, unsigned count, xc_domaininfo_t *info) {
54+ (void)control; (void)count;
55+ calls++;
56+ if (failing("info")) return -1;
57+ memset(info, 0, sizeof(*info));
58+ info->domain = domain;
59+ info->max_vcpu_id = 3;
60+ info->flags = XEN_DOMINF_hvm_guest | (references ? XEN_DOMINF_paused : 0);
61+ if (failing("identity") && calls > 2) info->handle[0] = 1;
62+ return 1;
63+}
64+
65+int xc_domain_pause(xc_interface *control, uint32_t domain) {
66+ (void)control; (void)domain;
67+ if (failing("pause")) return -1;
68+ references++;
69+ record("pause");
70+ if (failing("opensignal")) raise(SIGTERM);
71+ return 0;
72+}
73+
74+int xc_domain_unpause(xc_interface *control, uint32_t domain) {
75+ (void)control; (void)domain;
76+ if (failing("unpause")) { record("unpausefailed"); return -1; }
77+ references--;
78+ record("unpause");
79+ return references < 0 ? -1 : 0;
80+}
81+
82+int xc_domain_hvm_getcontext_partial(xc_interface *control, uint32_t domain, uint16_t type,
83+ uint16_t instance, void *buffer, uint32_t size) {
84+ (void)control; (void)domain; (void)type; (void)instance;
85+ record("registers");
86+ if (failing("signal")) raise(SIGTERM);
87+ if (failing("registers")) return -1;
88+ if (size < sizeof(struct hvm_hw_cpu)) abort();
89+ struct hvm_hw_cpu *cpu = buffer;
90+ memset(cpu, 0, sizeof(*cpu));
91+ cpu->cr0 = 0x80010001;
92+ cpu->cr3 = 0x1000;
93+ if (getenv("SCOPE_TEST_WATCH") && samples >= 4) cpu->cr3 = 0x6000;
94+ cpu->cr4 = 0x20;
95+ cpu->msr_efer = 0xd00;
96+ cpu->rflags = 2;
97+ return 0;
98+}
99+
100+xenforeignmemory_handle *xenforeignmemory_open(struct xentoollog_logger *logger, unsigned flags) {
101+ (void)logger; (void)flags;
102+ record("foreignopen");
103+ return failing("foreignopen") ? NULL : (xenforeignmemory_handle *)2;
104+}
105+
106+int xenforeignmemory_close(xenforeignmemory_handle *memory) {
107+ (void)memory;
108+ record("foreignclose");
109+ return 0;
110+}
111+
112+void *xenforeignmemory_map(xenforeignmemory_handle *memory, uint32_t domain, int protection,
113+ size_t count, const xen_pfn_t *frames, int *errors) {
114+ (void)memory; (void)domain; (void)protection;
115+ record("map");
116+ if (count != 1) abort();
117+ if (failing("map")) { errno = EACCES; return NULL; }
118+ uint64_t *page = calloc(512, sizeof(*page));
119+ if (!page) abort();
120+ *errors = failing("page") ? EACCES : 0;
121+ switch (frames[0]) {
122+ case 1: case 6: page[0] = 0x2007; break;
123+ case 2: page[0] = 0x3007; break;
124+ case 3: page[2] = 0x4001; page[4] = 0x5003; break;
125+ case 4: page[0] = 0x9003; break;
126+ case 5: page[0] = getenv("SCOPE_TEST_WATCH") && samples >= 2 ? 0x800000000000a003ull : 0x8000000000009003ull; break;
127+ default: free(page); errno = ENOENT; return NULL;
128+ }
129+ return page;
130+}
131+
132+int xenforeignmemory_unmap(xenforeignmemory_handle *memory, void *address, size_t count) {
133+ (void)memory; (void)count;
134+ record("unmap");
135+ if (failing("unmap")) return -1;
136+ free(address);
137+ return 0;
138+}

test/xen.py added

@@ -0,0 +1,81 @@
1+import json
2+import os
3+from pathlib import Path
4+import subprocess
5+import sys
6+import tempfile
7+
8+
9+project = Path(__file__).resolve().parent.parent
10+headers = Path(sys.argv[1] if len(sys.argv) > 1 else "/usr/include").resolve()
11+if not (headers / "xenctrl.h").exists():
12+ raise SystemExit("xen headers missing; pass their include directory")
13+
14+with tempfile.TemporaryDirectory(prefix="scopexen") as directory:
15+ root = Path(directory)
16+ environment = dict(os.environ)
17+ environment["C_INCLUDE_PATH"] = str(headers)
18+ build = root / "build"
19+ subprocess.run(["dune", "build", "--build-dir", str(build), "bin/main.exe"], cwd=project, env=environment, check=True)
20+ subprocess.run(["cc", "-std=gnu11", "-Wall", "-Wextra", "-Werror", "-shared", "-fPIC", "-I", str(headers),
21+ str(project / "test/native.c"), "-o", str(root / "libxenctrl.so")], check=True)
22+ (root / "libxenforeignmemory.so").symlink_to(root / "libxenctrl.so")
23+ environment["LD_LIBRARY_PATH"] = str(root)
24+ environment["SCOPE_TEST_LOG"] = str(root / "log")
25+ environment["OCAMLRUNPARAM"] = "s=4k"
26+ scope = build / "default/bin/main.exe"
27+ checks = 0
28+
29+ def run(failure=None, paused=False, pause=True, status=0):
30+ global checks
31+ env = dict(environment)
32+ if failure:
33+ env["SCOPE_TEST_FAILURE"] = failure
34+ if paused:
35+ env["SCOPE_TEST_PAUSED"] = "1"
36+ log = root / "log"
37+ log.unlink(missing_ok=True)
38+ command = [str(scope), "capture", "7", str(root / "capture"), "--vcpu", "0", "--phys-bits", "48",
39+ "--nx", "on", "--gb-pages", "on", "--json"]
40+ if pause:
41+ command.append("--pause")
42+ result = subprocess.run(command, env=env, text=True, capture_output=True, timeout=10)
43+ assert result.returncode == status, (failure, result.stdout, result.stderr)
44+ records = log.read_text().splitlines()
45+ if failure not in ("open", "logged"):
46+ assert records[-1].startswith("close ")
47+ if pause and failure not in ("open", "logged", "info", "pause", "identity", "unpause"):
48+ assert f"unpause {int(paused)}" in records, records
49+ assert f"close {int(paused)}" == records[-1], records
50+ if not pause:
51+ assert not any(line.startswith("unpause ") for line in records), records
52+ if failure is None:
53+ assert sum(line.startswith("map ") for line in records) == sum(line.startswith("unmap ") for line in records)
54+ if failure == "page":
55+ assert sum(line.startswith("map ") for line in records) == sum(line.startswith("unmap ") for line in records) == 1
56+ if failure == "unmap":
57+ assert sum(line.startswith("unmap ") for line in records) == 2
58+ checks += 1
59+ return json.loads(result.stdout or result.stderr), records
60+
61+ run()
62+ run(paused=True)
63+ run(paused=True, pause=False)
64+ run(pause=False, status=2)
65+ for failure in ("logged", "info", "pause", "foreignopen", "registers", "map", "page", "unmap", "unpause", "identity", "signal", "opensignal"):
66+ run(failure=failure, status=2)
67+ environment["SCOPE_TEST_WATCH"] = "1"
68+ log = root / "log"
69+ log.unlink()
70+ result = subprocess.run([str(scope), "watch", "7", str(root / "history"), "--vcpu", "0", "--phys-bits", "48",
71+ "--nx", "on", "--gb-pages", "on", "--pause", "--interval", "0.001", "--count", "4",
72+ "--retain", "2", "--aliases", "--json"], env=environment, text=True, capture_output=True, timeout=10)
73+ assert result.returncode == 1, (result.stdout, result.stderr)
74+ events = [json.loads(line) for line in result.stdout.splitlines()]
75+ assert [event["event"] for event in events] == ["baseline", "change", "sample", "address_space"]
76+ assert events[0]["summary"]["mappings"] == 2
77+ assert events[1]["changes"][0]["kinds"] == ["backing"]
78+ assert len(list((root / "history").iterdir())) == 2
79+ records = log.read_text().splitlines()
80+ assert records.count("pause 1") == records.count("unpause 0") == 4
81+ print(f"{checks} xen api fault-injection checks and a four-sample watch passed; no hypervisor was used")