From 31c614f530d6ec4b06ee9738d06289431e571fab Mon Sep 17 00:00:00 2001 From: james Date: Thu, 22 Oct 2026 21:18:46 +0100 Subject: [PATCH] feat(xen): capture hvm table pages with owned pause cleanup --- lib/dune | 7 +- lib/native.c | 273 +++++++++++++++++++++++++++++++++++++++++++++++++++ lib/xen.ml | 37 +++++++ lib/xen.mli | 3 + 4 files changed, 319 insertions(+), 1 deletion(-) create mode 100644 lib/native.c create mode 100644 lib/xen.ml create mode 100644 lib/xen.mli diff --git a/lib/dune b/lib/dune index 046a103..a07e7bc 100644 --- a/lib/dune +++ b/lib/dune @@ -1,4 +1,9 @@ (library (name scope) (public_name scope) - (libraries unix)) + (libraries unix) + (foreign_stubs + (language c) + (names native) + (flags (:standard -std=gnu11 -Wall -Wextra -Werror))) + (c_library_flags -ldl)) diff --git a/lib/native.c b/lib/native.c new file mode 100644 index 0000000..3f9b1e3 --- /dev/null +++ b/lib/native.c @@ -0,0 +1,273 @@ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(__has_include) +#if __has_include() && __has_include() +#define SCOPE_XEN 1 +#endif +#endif + +#ifdef SCOPE_XEN +#include +#include +#include +#include +#include +#include +#include + +static void quiet_message(xentoollog_logger *logger, xentoollog_level level, + int error, const char *context, const char *format, va_list arguments) { + (void)logger; (void)level; (void)error; (void)context; (void)format; (void)arguments; +} + +static void quiet_destroy(xentoollog_logger *logger) { + (void)logger; +} + +static xentoollog_logger quiet_logger = { + .vmessage = quiet_message, + .progress = NULL, + .destroy = quiet_destroy +}; + +struct api { + void *control; + void *foreign; + __typeof__(&xc_interface_open) control_open; + __typeof__(&xc_interface_close) control_close; + __typeof__(&xc_domain_getinfolist) info; + __typeof__(&xc_domain_pause) pause; + __typeof__(&xc_domain_unpause) unpause; + __typeof__(&xc_domain_hvm_getcontext_partial) context; + __typeof__(&xenforeignmemory_open) foreign_open; + __typeof__(&xenforeignmemory_close) foreign_close; + __typeof__(&xenforeignmemory_map) map; + __typeof__(&xenforeignmemory_unmap) unmap; +}; + +static struct api api; + +static bool load_api(void) { + if (api.control && api.foreign) return true; + api.control = dlopen("libxenctrl.so", RTLD_NOW | RTLD_LOCAL); + api.foreign = dlopen("libxenforeignmemory.so", RTLD_NOW | RTLD_LOCAL); + if (!api.control || !api.foreign) goto failed; +#define LOAD(member, library, symbol) \ + do { *(void **)(&api.member) = dlsym(api.library, symbol); if (!api.member) goto failed; } while (0) + LOAD(control_open, control, "xc_interface_open"); + LOAD(control_close, control, "xc_interface_close"); + LOAD(info, control, "xc_domain_getinfolist"); + LOAD(pause, control, "xc_domain_pause"); + LOAD(unpause, control, "xc_domain_unpause"); + LOAD(context, control, "xc_domain_hvm_getcontext_partial"); + LOAD(foreign_open, foreign, "xenforeignmemory_open"); + LOAD(foreign_close, foreign, "xenforeignmemory_close"); + LOAD(map, foreign, "xenforeignmemory_map"); + LOAD(unmap, foreign, "xenforeignmemory_unmap"); +#undef LOAD + return true; +failed: + if (api.control) dlclose(api.control); + if (api.foreign) dlclose(api.foreign); + memset(&api, 0, sizeof(api)); + return false; +} + +struct session { + xc_interface *control; + xenforeignmemory_handle *foreign; + uint32_t domid; + uint16_t vcpu; + bool owned_pause; + void *mapping; + xen_domain_handle_t identity; +}; + +static bool inspect(struct session *s, xc_domaininfo_t *info) { + return api.info(s->control, s->domid, 1, info) == 1 && info->domain == s->domid && + memcmp(info->handle, s->identity, sizeof(s->identity)) == 0; +} + +static int release(struct session *s) { + int failed = 0; + if (s->mapping) { + if (api.unmap(s->foreign, s->mapping, 1) != 0) failed = 1; + s->mapping = NULL; + } + if (s->owned_pause) { + xc_domaininfo_t info; + if (!inspect(s, &info) || api.unpause(s->control, s->domid) != 0) failed = 1; + s->owned_pause = false; + } + if (s->foreign && api.foreign_close(s->foreign) != 0) failed = 1; + if (s->control && api.control_close(s->control) != 0) failed = 1; + s->foreign = NULL; + s->control = NULL; + return failed; +} + +static void finalise(value block) { + struct session *s = Data_custom_val(block); + if (release(s)) fprintf(stderr, "scope: xen cleanup failed; inspect domain pause state\n"); +} + +static struct custom_operations operations = { + .identifier = "scope.xen.session.v1", + .finalize = finalise, + .compare = custom_compare_default, + .hash = custom_hash_default, + .serialize = custom_serialize_default, + .deserialize = custom_deserialize_default, + .compare_ext = custom_compare_ext_default, + .fixed_length = custom_fixed_length_default +}; + +static void check(struct session *s) { + xc_domaininfo_t info; + if (!s->control) caml_failwith("xen session is closed"); + if (!inspect(s, &info)) caml_failwith("domain identity changed or domain disappeared"); + if (!(info.flags & XEN_DOMINF_paused)) caml_failwith("domain is no longer paused"); +} + +CAMLprim value scope_xen_available(value unit) { + CAMLparam1(unit); + CAMLreturn(Val_bool(load_api())); +} + +CAMLprim value scope_xen_open(value domain, value vcpu, value pause) { + CAMLparam3(domain, vcpu, pause); + CAMLlocal1(block); + if (!load_api()) caml_failwith("cannot load xen control and foreign memory libraries"); + block = caml_alloc_custom(&operations, sizeof(struct session), 0, 1); + struct session *s = Data_custom_val(block); + memset(s, 0, sizeof(*s)); + s->domid = Int_val(domain); + s->vcpu = Int_val(vcpu); + s->control = api.control_open(&quiet_logger, &quiet_logger, 0); + if (!s->control) caml_failwith("cannot open xen control interface"); + xc_domaininfo_t info; + if (api.info(s->control, s->domid, 1, &info) != 1 || info.domain != s->domid || + !(info.flags & XEN_DOMINF_hvm_guest) || s->vcpu > info.max_vcpu_id) { + release(s); + caml_failwith("domain absent, not hvm, or vcpu outside domain range"); + } + memcpy(s->identity, info.handle, sizeof(s->identity)); + if (Bool_val(pause)) { + if (api.pause(s->control, s->domid) != 0) { + release(s); + caml_failwith("cannot acquire xen controller pause"); + } + /* pause -> capture -> unpause, release only the reference acquired here */ + s->owned_pause = true; + } else if (!(info.flags & XEN_DOMINF_paused)) { + release(s); + caml_failwith("domain must already be paused or capture must use --pause"); + } + s->foreign = api.foreign_open(&quiet_logger, 0); + if (!s->foreign) { + if (release(s)) caml_failwith("foreign memory open and pause cleanup failed; inspect domain"); + caml_failwith("cannot open xen foreign memory interface"); + } + CAMLreturn(block); +} + +CAMLprim value scope_xen_registers(value block) { + CAMLparam1(block); + CAMLlocal4(array, number, identity, pair); + struct session *s = Data_custom_val(block); + check(s); + struct hvm_hw_cpu cpu; + if (api.context(s->control, s->domid, HVM_SAVE_CODE(CPU), s->vcpu, &cpu, sizeof(cpu)) != 0) + caml_failwith("cannot read hvm vcpu context"); + uint64_t values[] = {cpu.cr0, cpu.cr2, cpu.cr3, cpu.cr4, cpu.msr_efer, cpu.rflags, cpu.rip}; + char encoded[sizeof(s->identity) * 2 + 1]; + for (size_t i = 0; i < sizeof(s->identity); i++) snprintf(encoded + i * 2, 3, "%02x", s->identity[i]); + array = caml_alloc(7, 0); + for (int i = 0; i < 7; i++) Store_field(array, i, Val_unit); + for (int i = 0; i < 7; i++) { + number = caml_copy_int64(values[i]); + Store_field(array, i, number); + } + identity = caml_copy_string(encoded); + pair = caml_alloc_tuple(2); + Store_field(pair, 0, array); + Store_field(pair, 1, identity); + CAMLreturn(pair); +} + +CAMLprim value scope_xen_read(value block, value physical) { + CAMLparam2(block, physical); + CAMLlocal1(bytes); + struct session *s = Data_custom_val(block); + check(s); + if (s->mapping) caml_failwith("previous foreign unmap failed"); + uint64_t address = Int64_val(physical); + if (address & 4095) caml_invalid_argument("unaligned guest physical frame"); + xen_pfn_t frame = address >> 12; + int error = 0; + void *mapping = api.map(s->foreign, s->domid, PROT_READ, 1, &frame, &error); + if (mapping && mapping != MAP_FAILED) s->mapping = mapping; + if (!mapping || mapping == MAP_FAILED || error) { + int saved_error = error ? error : errno; + if (s->mapping && api.unmap(s->foreign, mapping, 1) == 0) s->mapping = NULL; + char reason[160]; + snprintf(reason, sizeof(reason), "foreign map failed at 0x%016llx: errno=%d", + (unsigned long long)address, saved_error); + caml_failwith(reason); + } + char copy[4096]; + memcpy(copy, mapping, sizeof(copy)); + if (api.unmap(s->foreign, mapping, 1) != 0) caml_failwith("foreign unmap failed"); + s->mapping = NULL; + bytes = caml_alloc_initialized_string(sizeof(copy), copy); + CAMLreturn(bytes); +} + +CAMLprim value scope_xen_close(value block) { + CAMLparam1(block); + if (release(Data_custom_val(block))) caml_failwith("xen cleanup failed; inspect domain pause state"); + CAMLreturn(Val_unit); +} + +#else +CAMLprim value scope_xen_available(value unit) { + (void)unit; + return Val_false; +} + +static void unavailable(void) { + caml_failwith("xen capture unavailable; install xen development headers and rebuild scope"); +} + +CAMLprim value scope_xen_open(value domain, value vcpu, value pause) { + (void)domain; (void)vcpu; (void)pause; + unavailable(); + return Val_unit; +} + +CAMLprim value scope_xen_registers(value block) { + (void)block; + unavailable(); + return Val_unit; +} + +CAMLprim value scope_xen_read(value block, value address) { + (void)block; (void)address; + unavailable(); + return Val_unit; +} + +CAMLprim value scope_xen_close(value block) { + (void)block; + return Val_unit; +} +#endif diff --git a/lib/xen.ml b/lib/xen.ml new file mode 100644 index 0000000..8099562 --- /dev/null +++ b/lib/xen.ml @@ -0,0 +1,37 @@ +type session + +external available : unit -> bool = "scope_xen_available" +external open_domain : int -> int -> bool -> session = "scope_xen_open" +external registers : session -> int64 array * string = "scope_xen_registers" +external read_page : session -> int64 -> string = "scope_xen_read" +external close : session -> unit = "scope_xen_close" + +let capture ~domid ~vcpu ~pause ~features ~root:override_root ~max_pages = + if domid < 1 || domid >= 0x7ff0 then invalid_arg "capture requires an ordinary nonzero domain id"; + if vcpu < 0 || vcpu > 65535 then invalid_arg "vcpu outside xen context range"; + let signals = [Sys.sigint; Sys.sigterm] in + let original_mask = Unix.sigprocmask Unix.SIG_BLOCK signals in + let session = try open_domain domid vcpu pause with exn -> + ignore (Unix.sigprocmask Unix.SIG_SETMASK original_mask); + raise exn + in + let release () = + let mask = Unix.sigprocmask Unix.SIG_BLOCK signals in + Fun.protect ~finally:(fun () -> ignore (Unix.sigprocmask Unix.SIG_SETMASK mask)) (fun () -> close session) + in + Fun.protect ~finally:release (fun () -> + ignore (Unix.sigprocmask Unix.SIG_SETMASK original_mask); + let values, identity = registers session in + let open Model in + let registers = { cr0 = values.(0); cr2 = values.(1); + cr3 = values.(2); + cr4 = values.(3); efer = values.(4); rflags = values.(5); rip = values.(6) } in + let context = { registers; features; domid; vcpu; identity; root_override = override_root; + source = "xen hvm; operator-supplied guest cpuid profile"; + consistency = (if pause then "controller pause held during table capture; external writers not quiesced" + else "observed paused on reads; no owned pause; external writers not quiesced") } in + let reader address = + try Ok (read_page session (Address.Physical.to_int64 address)) + with Failure reason -> Error reason + in + Capture.collect ~max_pages context reader) diff --git a/lib/xen.mli b/lib/xen.mli new file mode 100644 index 0000000..1f48baf --- /dev/null +++ b/lib/xen.mli @@ -0,0 +1,3 @@ +val available : unit -> bool +val capture : domid:int -> vcpu:int -> pause:bool -> features:Model.features -> + root:int64 option -> max_pages:int -> Capture.t