test(xen): inject native failures and verify watch evidence retention
2 files changed, +219 −0
- test/native.c added +138 −0
- test/xen.py added +81 −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") | |