Verified Commit 31c614f5 authored by james's avatar james
Browse files

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

parent e8f5f1b8
Loading
Loading
Loading
Loading
+6 −1
Changes for lib/dune: 6 added lines, 1 removed line.
Original line number Diff line number Diff line
(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))

lib/native.c

0 → 100644
+273 −0
Changes for lib/native.c: 273 added lines, 0 removed lines.
Original line number Diff line number Diff line
#define _GNU_SOURCE
#include <caml/mlvalues.h>
#include <caml/memory.h>
#include <caml/alloc.h>
#include <caml/custom.h>
#include <caml/fail.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#include <stdio.h>

#if defined(__has_include)
#if __has_include(<xenctrl.h>) && __has_include(<xenforeignmemory.h>)
#define SCOPE_XEN 1
#endif
#endif

#ifdef SCOPE_XEN
#include <dlfcn.h>
#include <errno.h>
#include <sys/mman.h>
#include <xenctrl.h>
#include <xenforeignmemory.h>
#include <xentoollog.h>
#include <xen/hvm/save.h>

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

lib/xen.ml

0 → 100644
+37 −0
Changes for lib/xen.ml: 37 added lines, 0 removed lines.
Original line number Diff line number Diff line
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)

lib/xen.mli

0 → 100644
+3 −0
Changes for lib/xen.mli: 3 added lines, 0 removed lines.
Original line number Diff line number Diff line
val available : unit -> bool
val capture : domid:int -> vcpu:int -> pause:bool -> features:Model.features ->
  root:int64 option -> max_pages:int -> Capture.t