~james/scope

feat(xen): capture hvm table pages with owned pause cleanup

commit
31c614f530d6ec4b06ee9738d06289431e571fab browse tree
parent
e8f5f1b8ca985632f06e3dd63ed410c950dee329
author
james <j@cpl0.zip>
signature
Verified ~james signed with a key registered by ~james
date
2026-10-22 20:18 UTC

4 files changed, +319 −1

lib/dune modified

@@ -1,4 +1,9 @@
11 (library
22 (name scope)
33 (public_name scope)
4- (libraries unix))
4+ (libraries unix)
5+ (foreign_stubs
6+ (language c)
7+ (names native)
8+ (flags (:standard -std=gnu11 -Wall -Wextra -Werror)))
9+ (c_library_flags -ldl))

lib/native.c added

@@ -0,0 +1,273 @@
1+#define _GNU_SOURCE
2+#include <caml/mlvalues.h>
3+#include <caml/memory.h>
4+#include <caml/alloc.h>
5+#include <caml/custom.h>
6+#include <caml/fail.h>
7+#include <stdbool.h>
8+#include <stdint.h>
9+#include <string.h>
10+#include <stdio.h>
11+
12+#if defined(__has_include)
13+#if __has_include(<xenctrl.h>) && __has_include(<xenforeignmemory.h>)
14+#define SCOPE_XEN 1
15+#endif
16+#endif
17+
18+#ifdef SCOPE_XEN
19+#include <dlfcn.h>
20+#include <errno.h>
21+#include <sys/mman.h>
22+#include <xenctrl.h>
23+#include <xenforeignmemory.h>
24+#include <xentoollog.h>
25+#include <xen/hvm/save.h>
26+
27+static void quiet_message(xentoollog_logger *logger, xentoollog_level level,
28+ int error, const char *context, const char *format, va_list arguments) {
29+ (void)logger; (void)level; (void)error; (void)context; (void)format; (void)arguments;
30+}
31+
32+static void quiet_destroy(xentoollog_logger *logger) {
33+ (void)logger;
34+}
35+
36+static xentoollog_logger quiet_logger = {
37+ .vmessage = quiet_message,
38+ .progress = NULL,
39+ .destroy = quiet_destroy
40+};
41+
42+struct api {
43+ void *control;
44+ void *foreign;
45+ __typeof__(&xc_interface_open) control_open;
46+ __typeof__(&xc_interface_close) control_close;
47+ __typeof__(&xc_domain_getinfolist) info;
48+ __typeof__(&xc_domain_pause) pause;
49+ __typeof__(&xc_domain_unpause) unpause;
50+ __typeof__(&xc_domain_hvm_getcontext_partial) context;
51+ __typeof__(&xenforeignmemory_open) foreign_open;
52+ __typeof__(&xenforeignmemory_close) foreign_close;
53+ __typeof__(&xenforeignmemory_map) map;
54+ __typeof__(&xenforeignmemory_unmap) unmap;
55+};
56+
57+static struct api api;
58+
59+static bool load_api(void) {
60+ if (api.control && api.foreign) return true;
61+ api.control = dlopen("libxenctrl.so", RTLD_NOW | RTLD_LOCAL);
62+ api.foreign = dlopen("libxenforeignmemory.so", RTLD_NOW | RTLD_LOCAL);
63+ if (!api.control || !api.foreign) goto failed;
64+#define LOAD(member, library, symbol) \
65+ do { *(void **)(&api.member) = dlsym(api.library, symbol); if (!api.member) goto failed; } while (0)
66+ LOAD(control_open, control, "xc_interface_open");
67+ LOAD(control_close, control, "xc_interface_close");
68+ LOAD(info, control, "xc_domain_getinfolist");
69+ LOAD(pause, control, "xc_domain_pause");
70+ LOAD(unpause, control, "xc_domain_unpause");
71+ LOAD(context, control, "xc_domain_hvm_getcontext_partial");
72+ LOAD(foreign_open, foreign, "xenforeignmemory_open");
73+ LOAD(foreign_close, foreign, "xenforeignmemory_close");
74+ LOAD(map, foreign, "xenforeignmemory_map");
75+ LOAD(unmap, foreign, "xenforeignmemory_unmap");
76+#undef LOAD
77+ return true;
78+failed:
79+ if (api.control) dlclose(api.control);
80+ if (api.foreign) dlclose(api.foreign);
81+ memset(&api, 0, sizeof(api));
82+ return false;
83+}
84+
85+struct session {
86+ xc_interface *control;
87+ xenforeignmemory_handle *foreign;
88+ uint32_t domid;
89+ uint16_t vcpu;
90+ bool owned_pause;
91+ void *mapping;
92+ xen_domain_handle_t identity;
93+};
94+
95+static bool inspect(struct session *s, xc_domaininfo_t *info) {
96+ return api.info(s->control, s->domid, 1, info) == 1 && info->domain == s->domid &&
97+ memcmp(info->handle, s->identity, sizeof(s->identity)) == 0;
98+}
99+
100+static int release(struct session *s) {
101+ int failed = 0;
102+ if (s->mapping) {
103+ if (api.unmap(s->foreign, s->mapping, 1) != 0) failed = 1;
104+ s->mapping = NULL;
105+ }
106+ if (s->owned_pause) {
107+ xc_domaininfo_t info;
108+ if (!inspect(s, &info) || api.unpause(s->control, s->domid) != 0) failed = 1;
109+ s->owned_pause = false;
110+ }
111+ if (s->foreign && api.foreign_close(s->foreign) != 0) failed = 1;
112+ if (s->control && api.control_close(s->control) != 0) failed = 1;
113+ s->foreign = NULL;
114+ s->control = NULL;
115+ return failed;
116+}
117+
118+static void finalise(value block) {
119+ struct session *s = Data_custom_val(block);
120+ if (release(s)) fprintf(stderr, "scope: xen cleanup failed; inspect domain pause state\n");
121+}
122+
123+static struct custom_operations operations = {
124+ .identifier = "scope.xen.session.v1",
125+ .finalize = finalise,
126+ .compare = custom_compare_default,
127+ .hash = custom_hash_default,
128+ .serialize = custom_serialize_default,
129+ .deserialize = custom_deserialize_default,
130+ .compare_ext = custom_compare_ext_default,
131+ .fixed_length = custom_fixed_length_default
132+};
133+
134+static void check(struct session *s) {
135+ xc_domaininfo_t info;
136+ if (!s->control) caml_failwith("xen session is closed");
137+ if (!inspect(s, &info)) caml_failwith("domain identity changed or domain disappeared");
138+ if (!(info.flags & XEN_DOMINF_paused)) caml_failwith("domain is no longer paused");
139+}
140+
141+CAMLprim value scope_xen_available(value unit) {
142+ CAMLparam1(unit);
143+ CAMLreturn(Val_bool(load_api()));
144+}
145+
146+CAMLprim value scope_xen_open(value domain, value vcpu, value pause) {
147+ CAMLparam3(domain, vcpu, pause);
148+ CAMLlocal1(block);
149+ if (!load_api()) caml_failwith("cannot load xen control and foreign memory libraries");
150+ block = caml_alloc_custom(&operations, sizeof(struct session), 0, 1);
151+ struct session *s = Data_custom_val(block);
152+ memset(s, 0, sizeof(*s));
153+ s->domid = Int_val(domain);
154+ s->vcpu = Int_val(vcpu);
155+ s->control = api.control_open(&quiet_logger, &quiet_logger, 0);
156+ if (!s->control) caml_failwith("cannot open xen control interface");
157+ xc_domaininfo_t info;
158+ if (api.info(s->control, s->domid, 1, &info) != 1 || info.domain != s->domid ||
159+ !(info.flags & XEN_DOMINF_hvm_guest) || s->vcpu > info.max_vcpu_id) {
160+ release(s);
161+ caml_failwith("domain absent, not hvm, or vcpu outside domain range");
162+ }
163+ memcpy(s->identity, info.handle, sizeof(s->identity));
164+ if (Bool_val(pause)) {
165+ if (api.pause(s->control, s->domid) != 0) {
166+ release(s);
167+ caml_failwith("cannot acquire xen controller pause");
168+ }
169+ /* pause -> capture -> unpause, release only the reference acquired here */
170+ s->owned_pause = true;
171+ } else if (!(info.flags & XEN_DOMINF_paused)) {
172+ release(s);
173+ caml_failwith("domain must already be paused or capture must use --pause");
174+ }
175+ s->foreign = api.foreign_open(&quiet_logger, 0);
176+ if (!s->foreign) {
177+ if (release(s)) caml_failwith("foreign memory open and pause cleanup failed; inspect domain");
178+ caml_failwith("cannot open xen foreign memory interface");
179+ }
180+ CAMLreturn(block);
181+}
182+
183+CAMLprim value scope_xen_registers(value block) {
184+ CAMLparam1(block);
185+ CAMLlocal4(array, number, identity, pair);
186+ struct session *s = Data_custom_val(block);
187+ check(s);
188+ struct hvm_hw_cpu cpu;
189+ if (api.context(s->control, s->domid, HVM_SAVE_CODE(CPU), s->vcpu, &cpu, sizeof(cpu)) != 0)
190+ caml_failwith("cannot read hvm vcpu context");
191+ uint64_t values[] = {cpu.cr0, cpu.cr2, cpu.cr3, cpu.cr4, cpu.msr_efer, cpu.rflags, cpu.rip};
192+ char encoded[sizeof(s->identity) * 2 + 1];
193+ for (size_t i = 0; i < sizeof(s->identity); i++) snprintf(encoded + i * 2, 3, "%02x", s->identity[i]);
194+ array = caml_alloc(7, 0);
195+ for (int i = 0; i < 7; i++) Store_field(array, i, Val_unit);
196+ for (int i = 0; i < 7; i++) {
197+ number = caml_copy_int64(values[i]);
198+ Store_field(array, i, number);
199+ }
200+ identity = caml_copy_string(encoded);
201+ pair = caml_alloc_tuple(2);
202+ Store_field(pair, 0, array);
203+ Store_field(pair, 1, identity);
204+ CAMLreturn(pair);
205+}
206+
207+CAMLprim value scope_xen_read(value block, value physical) {
208+ CAMLparam2(block, physical);
209+ CAMLlocal1(bytes);
210+ struct session *s = Data_custom_val(block);
211+ check(s);
212+ if (s->mapping) caml_failwith("previous foreign unmap failed");
213+ uint64_t address = Int64_val(physical);
214+ if (address & 4095) caml_invalid_argument("unaligned guest physical frame");
215+ xen_pfn_t frame = address >> 12;
216+ int error = 0;
217+ void *mapping = api.map(s->foreign, s->domid, PROT_READ, 1, &frame, &error);
218+ if (mapping && mapping != MAP_FAILED) s->mapping = mapping;
219+ if (!mapping || mapping == MAP_FAILED || error) {
220+ int saved_error = error ? error : errno;
221+ if (s->mapping && api.unmap(s->foreign, mapping, 1) == 0) s->mapping = NULL;
222+ char reason[160];
223+ snprintf(reason, sizeof(reason), "foreign map failed at 0x%016llx: errno=%d",
224+ (unsigned long long)address, saved_error);
225+ caml_failwith(reason);
226+ }
227+ char copy[4096];
228+ memcpy(copy, mapping, sizeof(copy));
229+ if (api.unmap(s->foreign, mapping, 1) != 0) caml_failwith("foreign unmap failed");
230+ s->mapping = NULL;
231+ bytes = caml_alloc_initialized_string(sizeof(copy), copy);
232+ CAMLreturn(bytes);
233+}
234+
235+CAMLprim value scope_xen_close(value block) {
236+ CAMLparam1(block);
237+ if (release(Data_custom_val(block))) caml_failwith("xen cleanup failed; inspect domain pause state");
238+ CAMLreturn(Val_unit);
239+}
240+
241+#else
242+CAMLprim value scope_xen_available(value unit) {
243+ (void)unit;
244+ return Val_false;
245+}
246+
247+static void unavailable(void) {
248+ caml_failwith("xen capture unavailable; install xen development headers and rebuild scope");
249+}
250+
251+CAMLprim value scope_xen_open(value domain, value vcpu, value pause) {
252+ (void)domain; (void)vcpu; (void)pause;
253+ unavailable();
254+ return Val_unit;
255+}
256+
257+CAMLprim value scope_xen_registers(value block) {
258+ (void)block;
259+ unavailable();
260+ return Val_unit;
261+}
262+
263+CAMLprim value scope_xen_read(value block, value address) {
264+ (void)block; (void)address;
265+ unavailable();
266+ return Val_unit;
267+}
268+
269+CAMLprim value scope_xen_close(value block) {
270+ (void)block;
271+ return Val_unit;
272+}
273+#endif

lib/xen.ml added

@@ -0,0 +1,37 @@
1+type session
2+
3+external available : unit -> bool = "scope_xen_available"
4+external open_domain : int -> int -> bool -> session = "scope_xen_open"
5+external registers : session -> int64 array * string = "scope_xen_registers"
6+external read_page : session -> int64 -> string = "scope_xen_read"
7+external close : session -> unit = "scope_xen_close"
8+
9+let capture ~domid ~vcpu ~pause ~features ~root:override_root ~max_pages =
10+ if domid < 1 || domid >= 0x7ff0 then invalid_arg "capture requires an ordinary nonzero domain id";
11+ if vcpu < 0 || vcpu > 65535 then invalid_arg "vcpu outside xen context range";
12+ let signals = [Sys.sigint; Sys.sigterm] in
13+ let original_mask = Unix.sigprocmask Unix.SIG_BLOCK signals in
14+ let session = try open_domain domid vcpu pause with exn ->
15+ ignore (Unix.sigprocmask Unix.SIG_SETMASK original_mask);
16+ raise exn
17+ in
18+ let release () =
19+ let mask = Unix.sigprocmask Unix.SIG_BLOCK signals in
20+ Fun.protect ~finally:(fun () -> ignore (Unix.sigprocmask Unix.SIG_SETMASK mask)) (fun () -> close session)
21+ in
22+ Fun.protect ~finally:release (fun () ->
23+ ignore (Unix.sigprocmask Unix.SIG_SETMASK original_mask);
24+ let values, identity = registers session in
25+ let open Model in
26+ let registers = { cr0 = values.(0); cr2 = values.(1);
27+ cr3 = values.(2);
28+ cr4 = values.(3); efer = values.(4); rflags = values.(5); rip = values.(6) } in
29+ let context = { registers; features; domid; vcpu; identity; root_override = override_root;
30+ source = "xen hvm; operator-supplied guest cpuid profile";
31+ consistency = (if pause then "controller pause held during table capture; external writers not quiesced"
32+ else "observed paused on reads; no owned pause; external writers not quiesced") } in
33+ let reader address =
34+ try Ok (read_page session (Address.Physical.to_int64 address))
35+ with Failure reason -> Error reason
36+ in
37+ Capture.collect ~max_pages context reader)

lib/xen.mli added

@@ -0,0 +1,3 @@
1+val available : unit -> bool
2+val capture : domid:int -> vcpu:int -> pause:bool -> features:Model.features ->
3+ root:int64 option -> max_pages:int -> Capture.t