/* memctx.c — vmie sensor adapter: vends ONE coherent guest address-space context — * the permanent System DirectoryTableBase (`kcr3`) PAIRED with a RAM-region locator * and a pre-opened O_RDONLY fd. This is NOT perception and NOT semantics: signaling * multicasts the datum + RO-fd, while the holder (an S-lib / any control) opens ITS OWN * read-only vmie_mem from the fd and does gva_read/scan/pmap itself. * * Cold bring-up (host_bootstrap) is CPU-bound and blocking, so it runs on an off-loop * worker; the loop thread only assembles the locator on the completion-eventfd and emits * the MEMCTX trigger. The epoch is stamped by the CORE (retained-context); on an epoch * change the core calls reg.invalidate, the adapter re-bootstraps and re-emits MEMCTX. * * RO outward is physical: O_RDONLY fd => mmap(PROT_WRITE) -> EACCES, so a write into the * guest on the holder side is structurally impossible. stub mode (without VMSIG_WITH_VMIE * or ram_path==NULL) synthesizes a kcr3 and a genuinely RO-mappable fd (memfd + seal) — * the seam is provable without a VM. */ #define _GNU_SOURCE #include "vmsig_adapter.h" #include "memctx.h" #include "adapter_util.h" /* vmsig_worker (off-loop bootstrap) */ #include #include #include #include #include #include #include #include #include /* one-shot backoff timer for cold-bootstrap retry */ #include /* persist file mode bits (0600) */ #ifdef VMSIG_WITH_VMIE #include "win32.h" /* vmie_win32_open/host_bootstrap/proc_list/close */ #endif /* memfd_create / seal — ABI fallbacks for old glibc/kernel (stub RO-fd backing). */ #ifndef MFD_CLOEXEC #include #include static int memfd_create(const char* name, unsigned int flags) { return (int)syscall(SYS_memfd_create, name, flags); } #endif #ifndef MFD_ALLOW_SEALING #define MFD_ALLOW_SEALING 0x0002U #endif #ifndef F_ADD_SEALS #define F_ADD_SEALS (1024 + 9) #define F_SEAL_SHRINK 0x0002 #define F_SEAL_GROW 0x0004 #endif #ifndef F_SEAL_FUTURE_WRITE #define F_SEAL_FUTURE_WRITE 0x0010 /* kernel 5.1+: forbid future writable mappings */ #endif #define MC_STUB_SIZE 0x10000u /* 64 KB of synthetic RAM image (stub) */ #define MC_MAX_SEG 8 #define MC_WORKER_DEPTH 16 /* one off-loop thread: rare bootstrap + writes */ /* Cold-bootstrap retry backoff (guest may still be booting when discovery attaches us; * host_bootstrap then finds no System process). Mirror of the discovery backoff so the * adapter stays decoupled from the discovery layer (Rule-of-three not reached): 50ms base, * exponential with the shift capped at 6, ceiling 2s steady-state. One-shot timerfd: armed * on failure, disarmed on success — no it_interval, no busy-wait. */ #define MC_BOOT_BACKOFF_BASE 50000000ull /* 50 ms */ #define MC_BOOT_BACKOFF_CAP 2000000000ull /* 2 s */ /* Adapter readiness fds are demuxed by per-slot cookie: slot 0 is the worker completion * eventfd, slot 1 is the one-shot backoff timerfd that re-kicks the bootstrap. */ enum { MC_COOKIE_WORKER = 0, MC_COOKIE_RETRY = 1 }; /* MC_JOB_RESUME: fast-path boot-session re-validation. On a daemon restart the cold scan * (host_bootstrap) is slow AND unstable (it hunts the agent beacon across physical RAM); if * the guest did NOT reboot, its System DTB (kcr3) is unchanged and was cached at the last * live scan. RESUME re-opens an O_RDONLY context with that cached kcr3 (vmie_win32_open_ro_fd, * which bypasses the beacon scan) — the boot-session discriminator is the kcr3 ITSELF against * the live RAM: it resolves the kernel (ntoskrnl) only if the guest is the same boot. */ enum { MC_JOB_BOOTSTRAP = 0, MC_JOB_WRITE = 1, MC_JOB_RESUME = 2 }; /* ---- kcr3 context persist: a cache of the cold-bootstrap result, mirror of the .slots * idiom in src/discovery/slot.c (magic+version POD, native byte order, atomic tmp+rename, * fail-soft load). Deliberately NOT factored into a shared helper: discovery (vmid<->slot) * and this adapter (kcr3 cache) are different layers with different lifecycles — Rule-of-three * is not reached, and a shared helper would couple the two prematurely. * * We persist the MINIMUM: only {magic, version, kcr3}. NO RAM metadata (st_ino/size/mtime/ * btime): those do NOT prove the RAM holds the same boot session (the backing file outlives a * memory overwrite, the inode can be reused). The boot-session discriminator is the kcr3 * self-validating against the live RAM at load time (see MC_JOB_RESUME), not file metadata. * * MEMWRITE-target safety: a persisted kcr3 is a READ locator only. The write target (a->kcr3) * is set ONLY by the bootstrap worker after a fresh live scan — never from this file. */ #define MC_PERSIST_MAGIC 0x4B435258u /* "KCRX" — kcr3 context cache */ #define MC_PERSIST_VERSION 1u typedef struct { uint32_t magic; uint32_t version; uint64_t kcr3; /* System DTB obtained from a live RAM scan; validated by open_ro_fd */ } mc_persist_blob; /* Atomic save: write a temp sibling then rename over the target, so a reader (or a racing * second daemon) sees either the whole old file or the whole new one. Loop-thread-only. * Returns 0 on success, -1 otherwise (best-effort: the datum is already published). */ static int mc_persist_save(const char* path, uint64_t kcr3) { if (!path || !*path) return -1; mc_persist_blob b; memset(&b, 0, sizeof b); b.magic = MC_PERSIST_MAGIC; b.version = MC_PERSIST_VERSION; b.kcr3 = kcr3; char tmp[512]; int n = snprintf(tmp, sizeof tmp, "%s.tmp", path); if (n < 0 || (size_t)n >= sizeof tmp) return -1; int fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC, 0600); if (fd < 0) return -1; ssize_t w = write(fd, &b, sizeof b); int rc = (w == (ssize_t)sizeof b) ? 0 : -1; if (close(fd) != 0) rc = -1; if (rc == 0 && rename(tmp, path) != 0) rc = -1; if (rc != 0) unlink(tmp); return rc; } /* Load + validate the POD header. Loop-thread-only. Returns 1 if a well-formed blob was read * (out filled), 0 otherwise (no file / short / wrong magic or version => fail-soft, fall back * to a cold bootstrap). No migrations: an old version is ignored and overwritten by the next * live scan result. NOTE: this validates only the file SHAPE; the kcr3 itself is validated * against live RAM on the worker (MC_JOB_RESUME), which is the real boot-session discriminator. */ static int mc_persist_load(const char* path, mc_persist_blob* out) { if (!path || !*path) return 0; int fd = open(path, O_RDONLY | O_CLOEXEC); if (fd < 0) return 0; /* no file => cold bootstrap */ mc_persist_blob b; ssize_t r = read(fd, &b, sizeof b); close(fd); if (r != (ssize_t)sizeof b || b.magic != MC_PERSIST_MAGIC || b.version != MC_PERSIST_VERSION) return 0; /* corrupt/old => cold bootstrap */ *out = b; return 1; } /* Drop the cache on a destructive VM-lifecycle (the RAM may have changed). Best-effort. * Hygiene only: even without the drop a stale kcr3 would be rejected by the self-validation, * but we do not leave a known-dead file around. Loop-thread-only. */ static void mc_persist_drop(const char* path) { if (path && *path) unlink(path); } /* worker req/res (POD <= VMSIG_WORK_SLOT). One off-loop worker runs BOTH the cold * bootstrap and the atomic writes (FIFO serializes a write against the close-on-rebootstrap). * boot_count drives the stub kcr3 (changes per epoch); the real guest kcr3 does NOT depend * on it (armed reads the System DTB). MC_JOB_WRITE copies SRC off-loop into req.src plus the * target cr3 (0 => System DTB; resolved on the worker against a->kcr3). */ typedef struct { uint32_t op; /* MC_JOB_* */ uint32_t boot_count; /* MC_JOB_BOOTSTRAP: drives the stub kcr3 per epoch */ uint32_t attempt; /* MC_JOB_BOOTSTRAP: consecutive-failure index of THIS */ /* kick (copy of a->boot_attempts); stub fails while */ /* attempt < a->fail_boots. NOT the epoch counter. */ /* --- MC_JOB_WRITE / MC_JOB_RESUME --- */ uint64_t cr3; /* WRITE: target AS root (0 => a->kcr3); RESUME: persisted kcr3 to validate */ uint64_t low; /* MC_JOB_RESUME: below-4G split for vmie_win32_open_ro_fd (ignored by others) */ uint64_t gva; uint32_t len; uint32_t corr; uint32_t origin; uint8_t src[VMSIG_MEMWRITE_MAX]; /* SRC bytes copied off-loop (gva_write reads this) */ } mc_req; typedef struct { uint32_t op; /* echoes the job type so on_ready demuxes */ int ok; /* MC_JOB_WRITE result */ uint32_t corr; uint32_t origin; uint64_t kcr3; /* MC_JOB_BOOTSTRAP result */ } mc_res; struct vmsig_adapter { uint32_t endpoint; int stub; const char* ram_path; /* armed: RAM-backing path (NOT published outward) */ const char* persist_path; /* armed: kcr3 cache file path (cfg, loop-thread-only); NULL => persist off */ uint64_t low; int cfg_ro_fd; /* >=0 => infra-sealed RO-fd (owned by adapter, closed in mc_close); <0 => default */ vmsig_emit emit; int registered; /* register_memctx already called */ vmsig_worker* worker; /* off-loop bootstrap + atomic writes */ uint32_t boot_count; /* incremented on each (re-)bootstrap (epoch tag) */ /* cold-bootstrap retry — loop-thread-only (attach/on_ready/invalidate/close). */ int retry_fd; /* one-shot backoff timerfd (-1 when none) */ uint32_t boot_attempts; /* consecutive bootstrap failures this cycle (0 = none); reset on success/epoch */ uint32_t fail_boots; /* test-only: fail the first N stub bootstraps (cfg); set once in mc_open, then read-only (worker reads it) */ #ifdef VMSIG_WITH_VMIE vmie_win32* win; /* held RW handle across the epoch (kcr3 source + gva_write target) */ vmie_mem* mem; /* vmie_win32_mem(win); borrowed, valid until vmie_win32_close */ #endif uint64_t kcr3; /* current System DTB (also published in cur_pod.kcr3) */ /* persistent locator: owned by the loop thread; worker only yields kcr3 into scratch. */ int have_ctx; vmsig_memctx cur_pod; /* kcr3/low/nseg/flags (epoch stamped by the core) */ vmsig_memseg cur_segs[MC_MAX_SEG]; uint32_t cur_nseg; int stub_fd; /* stub: memfd of synth RAM (+seal); share_fd reopens it */ }; /* fwd: MEMWRITE completion ACK (defined below mc_submit; used in mc_on_ready demux). */ static void mc_memwrite_ack(struct vmsig_adapter* a, int ok, uint32_t corr, uint32_t origin); /* mirror of the discovery backoff; kept in this adapter to stay decoupled from the discovery * layer (Rule-of-three not reached). Exponential with a shift capped at 6, clamped to CAP. */ static uint64_t mc_boot_backoff(uint32_t attempts) { uint64_t b = MC_BOOT_BACKOFF_BASE << (attempts < 6 ? attempts : 6); return b > MC_BOOT_BACKOFF_CAP ? MC_BOOT_BACKOFF_CAP : b; } /* Arm the one-shot backoff timer (it_value only — no it_interval). Loop-thread-only. * Best-effort: a settime failure is logged, not fatal (matches discovery rearm). */ static void mc_arm_retry(struct vmsig_adapter* a) { if (a->retry_fd < 0) return; uint64_t dt = mc_boot_backoff(a->boot_attempts); struct itimerspec its; memset(&its, 0, sizeof its); its.it_value.tv_sec = (time_t)(dt / 1000000000ull); its.it_value.tv_nsec = (long)(dt % 1000000000ull); if (timerfd_settime(a->retry_fd, 0, &its, NULL) != 0) fprintf(stderr, "vmsig memctx: endpoint %u retry timer arm failed\n", a->endpoint); } /* Disarm the backoff timer (zero itimerspec). Loop-thread-only. Used on bootstrap success * and at epoch change so a stale arm from a prior failure cannot fire over a fresh cycle. */ static void mc_disarm_retry(struct vmsig_adapter* a) { if (a->retry_fd < 0) return; struct itimerspec its; memset(&its, 0, sizeof its); (void)timerfd_settime(a->retry_fd, 0, &its, NULL); } /* ---- stub RO-fd: memfd + deterministic contents + seal of future writes ---- */ static int mc_make_stub_fd(uint32_t size) { int fd = memfd_create("vmsig_memctx", MFD_CLOEXEC | MFD_ALLOW_SEALING); if (fd < 0) fd = memfd_create("vmsig_memctx", MFD_CLOEXEC); if (fd < 0) return -1; if (ftruncate(fd, (off_t)size) != 0) { close(fd); return -1; } /* deterministic contents via a temporary RW mapping BEFORE the seal */ uint8_t* p = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (p != MAP_FAILED) { for (uint32_t i = 0; i < size; i++) p[i] = (uint8_t)(i & 0xFFu); munmap(p, size); } /* FUTURE_WRITE: even if the holder reopens the fd as O_RDWR, it gets no writable mapping. * best-effort (kernel 5.1+); on older kernels only the O_RDONLY fd protects. */ if (fcntl(fd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW | F_SEAL_FUTURE_WRITE) != 0) (void)fcntl(fd, F_ADD_SEALS, F_SEAL_SHRINK | F_SEAL_GROW); return fd; } #ifdef VMSIG_WITH_VMIE /* armed bring-up: open RAM (RW is vmie's internal concern), host_bootstrap, extract the * permanent System DTB as the System process cr3 (kcr3 — the root of the guest AS). The RW * handle is HELD across the epoch (kcr3 source + gva_write target); ONLY the RO-fd (share_fd) * leaves outward — write goes through this command plane, never a writable mmap. Runs on the * off-loop worker; a stale handle from a prior epoch is dropped first (serialized FIFO with * in-flight writes). */ static int mc_bootstrap_armed(struct vmsig_adapter* a, uint64_t* out_kcr3) { if (a->win) { vmie_win32_close(a->win); a->win = NULL; a->mem = NULL; } /* drop stale epoch handle */ vmie_win32* v = vmie_win32_open(a->ram_path, a->low); if (!v) return -1; if (host_bootstrap(v) != 0) { vmie_win32_close(v); return -1; } process procs[16]; int n = proc_list(v, 0, procs, 16); uint64_t kcr3 = 0; for (int i = 0; i < n && i < 16; i++) if (!strcmp(procs[i].name, "System")) { kcr3 = procs[i].cr3; break; } if (!kcr3) { vmie_win32_close(v); return -1; } a->win = v; /* HOLD: RW handle lives across the epoch */ a->mem = vmie_win32_mem(v); /* borrowed; valid until vmie_win32_close(v) */ a->kcr3 = kcr3; *out_kcr3 = kcr3; return 0; } #endif /* ---- worker job: cold bring-up OR atomic write, off-loop ----------------- * * Demultiplexed by rq->op. BOTH run on the SAME single worker thread, so a write on the * held handle never races the close-on-rebootstrap (FIFO). The job MUST NOT touch core * structures — it only reads a->mem/a->kcr3 (stable between re-bootstraps on this thread). */ static int mc_job(void* user, const void* req, void* res) { struct vmsig_adapter* a = user; const mc_req* rq = req; mc_res* rs = res; memset(rs, 0, sizeof *rs); rs->op = rq->op; if (rq->op == MC_JOB_WRITE) { rs->corr = rq->corr; rs->origin = rq->origin; if (a->stub) { rs->ok = 1; return 0; } /* stub: ack without actuation */ #ifdef VMSIG_WITH_VMIE /* a->mem is NULL until a bootstrap has succeeded (or after one failed and cleared it): * the guard turns that into an ok=0 ACK (observable to the initiator), not a crash. * cr3 resolve is on the worker (sole owner of a->kcr3): 0 => kernel AS (System DTB). */ uint64_t target = rq->cr3 ? rq->cr3 : a->kcr3; rs->ok = (a->mem && gva_write(a->mem, (uintptr_t)target, (uintptr_t)rq->gva, rq->src, rq->len) == 0); return rs->ok ? 0 : -1; #else rs->ok = 0; return -1; /* armed without the build flag: write impossible */ #endif } if (rq->op == MC_JOB_RESUME) { /* Fast-path boot-session re-validation: open an O_RDONLY context with the PERSISTED * kcr3 and let the engine decide if it still resolves the kernel in the LIVE RAM. * This is purely a READ validation — it NEVER touches a->win/a->mem/a->kcr3 (the * RW write-hold, owned by the bootstrap worker after a fresh live scan). MEMWRITE- * target safety: a persisted kcr3 must never become the gva_write target. */ if (a->stub) { /* No VMIE here, so there is no real RAM to validate against: synthetically ACCEPT a * nonzero kcr3 so the stub can exercise the persist MECHANICS (save/load/fast-vs-slow * selection). This is NOT real boot-session validation — that is armed-only. */ if (rq->cr3 == 0) return -1; rs->kcr3 = rq->cr3; return 0; } #ifdef VMSIG_WITH_VMIE /* fresh O_RDONLY fd over the backing (same source as mc_reg_share_fd: dup the infra * RO-fd, else open ram_path O_RDONLY). The RO context borrows it (dup'd internally), * so we close our copy after open. */ int rfd; if (a->cfg_ro_fd >= 0) rfd = fcntl(a->cfg_ro_fd, F_DUPFD_CLOEXEC, 0); else if (a->ram_path) rfd = open(a->ram_path, O_RDONLY | O_CLOEXEC); else return -1; if (rfd < 0) return -1; vmie_win32* v = vmie_win32_open_ro_fd(rfd, rq->low, rq->cr3); close(rfd); /* borrowed by open_ro_fd (dup'd internally) */ if (!v) return -1; /* kcr3 no longer resolves the kernel => stale/guest-reboot */ /* Second, independent signal: the System process must be present AND its cr3 must equal * the persisted kcr3 (the System DTB by definition). Catches the pathology "kcr3 resolves * a DIFFERENT kernel". Cheap — the RO context is already built. Fail-closed on mismatch. */ process procs[16]; int n = proc_list(v, 0, procs, 16); int system_ok = 0; for (int i = 0; i < n && i < 16; i++) if (!strcmp(procs[i].name, "System")) { system_ok = (procs[i].cr3 == rq->cr3); break; } vmie_win32_close(v); /* validation-only: the read datum needs no held handle */ if (!system_ok) return -1; rs->kcr3 = rq->cr3; /* validated: publish the read datum (NOT a->kcr3) */ return 0; #else return -1; /* armed without the build flag: resume impossible -> cold bootstrap */ #endif } /* MC_JOB_BOOTSTRAP */ if (a->stub) { /* test-only: fail the first fail_boots attempts to exercise the retry path * deterministically (a->fail_boots is set once in open, read-only here). */ if (rq->attempt < a->fail_boots) return -1; rs->kcr3 = 0xC0DE0000ull + (uint64_t)rq->boot_count * 0x1000ull; /* changes per epoch */ return 0; } #ifdef VMSIG_WITH_VMIE uint64_t kcr3 = 0; if (mc_bootstrap_armed(a, &kcr3) != 0) return -1; rs->kcr3 = kcr3; return 0; #else return -1; /* armed without the build flag: bootstrap impossible -> ERROR */ #endif } static void mc_kick_bootstrap(struct vmsig_adapter* a) { a->boot_count++; mc_req rq; memset(&rq, 0, sizeof rq); rq.op = MC_JOB_BOOTSTRAP; rq.boot_count = a->boot_count; rq.attempt = a->boot_attempts; /* failure index of this kick (loop-thread snapshot) */ (void)vmsig_worker_submit(a->worker, &rq, sizeof rq); /* full => drop (rare) */ } /* Submit the fast-path RESUME (off-loop: open_ro_fd reads image pages, not on the loop thread). * Carries the persisted kcr3 + the cfg low for vmie_win32_open_ro_fd. On miss/validation-fail the * completion handler falls back to a cold bootstrap — the persist never replaces it. */ static void mc_kick_resume(struct vmsig_adapter* a, uint64_t kcr3) { mc_req rq; memset(&rq, 0, sizeof rq); rq.op = MC_JOB_RESUME; rq.cr3 = kcr3; rq.low = a->low; (void)vmsig_worker_submit(a->worker, &rq, sizeof rq); /* full => drop (rare) */ } /* Single publication path for BOTH RESUME and BOOTSTRAP (no two ways to publish a MEMCTX). * Assembles the single-low locator from `kcr3` + a->low, marks have_ctx, and emits the MEMCTX * trigger; the core authoritatively re-describes and stamps the epoch. Loop-thread-only. * * Ownership: this writes kcr3 ONLY into cur_pod.kcr3 (the delivery copy). It does NOT touch * a->kcr3 — that is the gva_write TARGET, owned solely by the bootstrap worker. The difference * between the two callers is only the SOURCE of kcr3 and whether an RW-hold / persist-save * follows; the locator assembly itself is shared here. */ static void mc_publish_ctx(struct vmsig_adapter* a, uint64_t kcr3) { memset(&a->cur_pod, 0, sizeof a->cur_pod); a->cur_pod.kcr3 = kcr3; a->cur_pod.low = a->low ? a->low : MC_STUB_SIZE; a->cur_pod.flags = VMSIG_MEMCTX_RDONLY; a->cur_nseg = 1; /* single-low identity (gpa 0 .. low) */ a->cur_segs[0].gpa = 0; a->cur_segs[0].len = a->cur_pod.low; a->cur_segs[0].file_off = 0; a->cur_pod.nseg = a->cur_nseg; a->have_ctx = 1; /* emit the MEMCTX trigger: the core authoritatively re-describes + stamps the epoch. */ vmsig_event up; memset(&up, 0, sizeof up); up.kind = VMSIG_EV_MEMCTX; up.source = VMSIG_SRC_MEMCTX; up.dir = VMSIG_DIR_UP; up.prio = VMSIG_PRIO_NORMAL; up.endpoint = a->endpoint; memcpy(up.inln, &a->cur_pod, sizeof a->cur_pod); a->emit.emit(a->emit.token, &up); } /* ---- reg hooks (vmsig_memctx_reg.ctx = a; called by the core on the loop thread) ---- */ static void mc_reg_describe(void* ctx, vmsig_memctx* out_pod, const vmsig_memseg** out_segs, uint32_t* out_nseg) { struct vmsig_adapter* a = ctx; *out_pod = a->cur_pod; /* kcr3/low/nseg/flags; the core overwrites the epoch */ *out_segs = a->cur_segs; *out_nseg = a->cur_nseg; } static int mc_reg_share_fd(void* ctx) { struct vmsig_adapter* a = ctx; if (a->cfg_ro_fd >= 0) return fcntl(a->cfg_ro_fd, F_DUPFD_CLOEXEC, 0); /* infra-sealed RO-fd: dup */ if (a->stub) { if (a->stub_fd < 0) return -1; char path[64]; snprintf(path, sizeof path, "/proc/self/fd/%d", a->stub_fd); return open(path, O_RDONLY | O_CLOEXEC); /* fresh O_RDONLY on the backing */ } if (!a->ram_path) return -1; return open(a->ram_path, O_RDONLY | O_CLOEXEC); /* armed default */ } static void mc_reg_invalidate(void* ctx, uint32_t epoch) { struct vmsig_adapter* a = ctx; (void)epoch; /* the core owns the epoch; the adapter must re-bootstrap */ a->have_ctx = 0; /* the previous context is invalid */ /* destructive VM-lifecycle => the RAM may have changed => drop the kcr3 cache so the next * restart cannot fast-path off a now-dead kcr3 (the self-validation would reject it anyway, * but we do not leave a known-stale file). Best-effort, loop-thread-only. */ mc_persist_drop(a->persist_path); /* new cycle: drop a stale arm from the previous cycle and restart the failure counter at * zero so this bootstrap's backoff starts fresh (and the first-failure diagnostic re-arms). */ a->boot_attempts = 0; mc_disarm_retry(a); mc_kick_bootstrap(a); /* off-loop; on_ready re-emits MEMCTX (new epoch) */ } /* ---- vtable ---- */ static vmsig_adapter* mc_open(const void* cfg, uint32_t endpoint) { const vmsig_memctx_cfg* c = cfg; struct vmsig_adapter* a = calloc(1, sizeof *a); if (!a) return NULL; a->endpoint = endpoint; a->stub = c ? c->stub : 1; a->ram_path = c ? c->ram_path : NULL; a->low = c ? c->low : 0; a->cfg_ro_fd = (c && c->ro_fd >= 0) ? c->ro_fd : -1; if (!a->ram_path && a->cfg_ro_fd < 0) a->stub = 1; /* no path/fd => stub */ a->stub_fd = -1; a->retry_fd = -1; a->fail_boots = c ? c->fail_boots : 0; /* set once; read-only afterwards (worker reads) */ a->persist_path = c ? c->persist_path : NULL; /* NULL => persist disabled (cold bootstrap only) */ return a; } static int mc_attach(vmsig_adapter* a, const vmsig_emit* emit, vmsig_fd_reg* reg, int cap) { if (cap < 2) return -1; /* worker eventfd + one-shot backoff timerfd */ a->emit = *emit; a->worker = vmsig_worker_new(mc_job, a, 1, MC_WORKER_DEPTH); if (!a->worker) return -1; if (a->stub && a->cfg_ro_fd < 0) { a->stub_fd = mc_make_stub_fd(MC_STUB_SIZE); if (a->stub_fd < 0) { vmsig_worker_free(a->worker); a->worker = NULL; return -1; } } /* one-shot backoff timerfd: re-kicks the cold bootstrap when the guest is still booting. * Created here (loop-thread-only fd); armed on failure, disarmed on success. Rollback the * worker + stub_fd on failure, symmetric to mc_make_stub_fd above. */ a->retry_fd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC); if (a->retry_fd < 0) { if (a->stub_fd >= 0) { close(a->stub_fd); a->stub_fd = -1; } vmsig_worker_free(a->worker); a->worker = NULL; return -1; } /* worker completion-eventfd as the readiness source (cookie=worker). */ reg[0].fd = vmsig_worker_evfd(a->worker); reg[0].epoll_events = EPOLLIN; reg[0].shape = VMSIG_RDY_EVENTFD; reg[0].cookie = MC_COOKIE_WORKER; /* backoff timerfd as the second readiness source (cookie=retry). */ reg[1].fd = a->retry_fd; reg[1].epoll_events = EPOLLIN; reg[1].shape = VMSIG_RDY_TIMERFD; reg[1].cookie = MC_COOKIE_RETRY; /* register the reg BEFORE the first bootstrap: the core slot gets the hooks. describe * is not called until the slot is valid (which only happens after the first MEMCTX). */ if (a->emit.register_memctx) { vmsig_memctx_reg r; memset(&r, 0, sizeof r); r.endpoint = a->endpoint; r.source = VMSIG_SRC_MEMCTX; r.ctx = a; r.describe = mc_reg_describe; r.share_fd = mc_reg_share_fd; r.invalidate = mc_reg_invalidate; if (a->emit.register_memctx(a->emit.token, &r) == 0) a->registered = 1; } vmsig_event up; memset(&up, 0, sizeof up); up.kind = VMSIG_EV_SEAM_UP; up.source = VMSIG_SRC_MEMCTX; up.dir = VMSIG_DIR_UP; up.prio = VMSIG_PRIO_NORMAL; up.endpoint = a->endpoint; a->emit.emit(a->emit.token, &up); /* Fast-path: if a kcr3 cache exists, try a RESUME (re-validate it against live RAM) BEFORE * the cold scan. On a daemon restart over an unchanged guest this publishes the read datum * in milliseconds instead of minutes of beacon-scan retry. On any miss (persist off / stub / * no file / corrupt) we fall straight into the existing cold bootstrap. The RW-hold for * MEMWRITE is still acquired by a cold bootstrap (kicked in parallel after a RESUME hit). */ mc_persist_blob b; if (a->persist_path && *a->persist_path && mc_persist_load(a->persist_path, &b)) mc_kick_resume(a, b.kcr3); /* validate the cached kcr3 off-loop; cold fallback on miss */ else mc_kick_bootstrap(a); /* first cold bootstrap off-loop; assemble locator on completion */ return 2; /* worker eventfd + backoff timerfd */ } static int mc_on_ready(vmsig_adapter* a, uint32_t cookie, uint32_t events) { (void)events; /* epoll flags carry nothing we need; the cookie selects the source */ /* retry timerfd fired: the guest was still booting; drain and re-kick the bootstrap. * Re-kick is a fresh MC_JOB_BOOTSTRAP into the SAME FIFO worker queue, so it serializes * behind any in-flight write — nothing extra to synchronize. */ if (cookie == MC_COOKIE_RETRY) { uint64_t v; while (read(a->retry_fd, &v, sizeof v) == (ssize_t)sizeof v) { /* drain to EAGAIN */ } mc_kick_bootstrap(a); return 0; } /* cookie == MC_COOKIE_WORKER: worker completion. */ vmsig_worker_ack(a->worker); mc_res rs; int rc; while (vmsig_worker_poll(a->worker, &rs, sizeof rs, &rc) == 1) { if (rs.op == MC_JOB_WRITE) { /* atomic write completed: addressed ACT_ACK to the initiator. */ mc_memwrite_ack(a, rs.ok && rc == 0, rs.corr, rs.origin); continue; } if (rs.op == MC_JOB_RESUME) { /* Fast-path completion. The persisted kcr3 was validated against the LIVE RAM on the * worker (open_ro_fd != NULL [+ System-cr3 match]) — the read datum is safe to publish. * Note: the worker did NOT set a->kcr3/a->win/a->mem (the RW write-hold), so MEMWRITE * stays ok=0 until a cold bootstrap acquires it. */ if (rc == 0) { mc_publish_ctx(a, rs.kcr3); /* video lives instantly (read datum), epoch by core */ mc_kick_bootstrap(a); /* in parallel: acquire the RW-hold (a->kcr3) for MEMWRITE */ /* Do NOT save the persist (the kcr3 came FROM the file) and do NOT arm a retry * (the read datum is up; the parallel bootstrap arms its own retry on failure). */ } else { /* validation miss: the persisted kcr3 no longer resolves the kernel (guest rebooted * or corrupt). Fall back to an honest cold scan; on success it rewrites the persist * with a fresh kcr3. Do NOT retry the RESUME — the cache is under suspicion. */ mc_kick_bootstrap(a); } continue; } /* MC_JOB_BOOTSTRAP */ if (rc != 0) { /* bootstrap failed: the guest is likely still booting (host_bootstrap found no * System process). This is NOT a control-level error — do NOT emit VMSIG_EV_ERROR * (it would spam URGENT during a normal multi-second guest boot). Instead schedule a * backoff retry; the context simply stays unpublished until a kick succeeds. One * diagnostic line on the FIRST failure of the cycle (symmetric to the discovery * "never came up" note), not on every attempt. */ if (a->boot_attempts == 0) fprintf(stderr, "vmsig memctx: endpoint %u bootstrap not ready yet, retrying\n", a->endpoint); a->boot_attempts++; mc_arm_retry(a); /* one-shot timer at mc_boot_backoff(boot_attempts) */ continue; } /* bootstrap succeeded: a->kcr3/a->mem (the gva_write TARGET / RW-hold) were set on the * worker (mc_bootstrap_armed); the loop must NOT also write a->kcr3 (it would race an * in-flight write — same FIFO thread owns it). MEMWRITE is now possible. cur_pod.kcr3 is * loop-only (delivery) and is set inside mc_publish_ctx. * * Cancel any pending retry and reset the failure counter BEFORE publishing, so a stale * timer armed by a prior failure cannot fire over a live context. */ a->boot_attempts = 0; mc_disarm_retry(a); /* Publish only if a RESUME has not already published this same context (same kcr3): a * parallel cold bootstrap after a RESUME hit must acquire the RW-hold WITHOUT emitting a * redundant MEMCTX. First-time publication otherwise. */ if (!a->have_ctx) mc_publish_ctx(a, rs.kcr3); /* Cache the freshly-scanned kcr3 for the next daemon restart (best-effort; the datum is * already published). Only the cold scan writes the persist — never the RESUME path (its * kcr3 came from the file). Gated on persist_path presence: production stub paths get a * NULL persist_path from discovery, so they never write; a test may supply one to exercise * the persist mechanics (the stub bootstrap yields a synthetic-but-stable kcr3). */ if (a->persist_path && *a->persist_path) (void)mc_persist_save(a->persist_path, rs.kcr3); } return 0; } /* Emit an addressed ACT_ACK for a MEMWRITE (source MEMCTX, to the initiator). inln carries * {ok,corr,origin} (same shape as the input adapter's ACK), so control reads ok at offset 0. * ok=0 covers extent-deny / no-SRC / queue-full / write failure (default-deny, observable). */ static void mc_memwrite_ack(struct vmsig_adapter* a, int ok, uint32_t corr, uint32_t origin) { struct { int ok; uint32_t corr; uint32_t origin; } body = { ok, corr, origin }; vmsig_event up; memset(&up, 0, sizeof up); up.kind = VMSIG_EV_ACT_ACK; up.source = VMSIG_SRC_MEMCTX; up.dir = VMSIG_DIR_UP; up.prio = VMSIG_PRIO_NORMAL; up.endpoint = a->endpoint; up.corr = corr; up.origin = origin; up.payload.flags = VMSIG_PL_INLINE; memcpy(up.inln, &body, sizeof body); a->emit.emit(a->emit.token, &up); } /* DOWN MEMWRITE handler: validate extent, copy SRC off-loop, submit the atomic gva_write to * the worker. Default-deny: any invalid path (no SRC flag, len out of bounds, short payload, * queue full) ACKs ok=0 and does NOT actuate. The completion ACK for a queued write arrives * via mc_on_ready. Returns 0 when the event is consumed by this seam, 1 when it is not ours. */ static int mc_submit(vmsig_adapter* a, const vmsig_event* ev) { if (ev->kind != VMSIG_EV_CMD_MEMWRITE) return 1; /* not for this seam */ const vmsig_memwrite* mw = (const vmsig_memwrite*)ev->inln; uint32_t len = mw->len; if (len == 0 || len > VMSIG_MEMWRITE_MAX) { /* extent: bounded */ mc_memwrite_ack(a, 0, ev->corr, ev->origin); return 0; } mc_req rq; memset(&rq, 0, sizeof rq); rq.op = MC_JOB_WRITE; rq.cr3 = mw->cr3; rq.gva = mw->gva; rq.len = len; rq.corr = ev->corr; rq.origin = ev->origin; /* copy SRC into the worker req (off-loop gva_write reads from rq.src). */ if (mw->flags & VMSIG_MW_SRC_INLINE) { if (len > VMSIG_MEMWRITE_INLINE) { mc_memwrite_ack(a, 0, ev->corr, ev->origin); return 0; } memcpy(rq.src, ev->inln + sizeof *mw, len); /* inln tail after the 24-byte header */ } else if (mw->flags & VMSIG_MW_SRC_PAYLOAD) { if (!ev->payload.data || ev->payload.len < len) { mc_memwrite_ack(a, 0, ev->corr, ev->origin); return 0; } memcpy(rq.src, ev->payload.data, len); /* in-proc borrowed payload */ } else { mc_memwrite_ack(a, 0, ev->corr, ev->origin); /* no SRC flag */ return 0; } if (vmsig_worker_submit(a->worker, &rq, sizeof rq) != 0) { mc_memwrite_ack(a, 0, ev->corr, ev->origin); /* queue full -> ACK err */ return -1; } return 0; /* completion ACK arrives via mc_on_ready */ } static void mc_close(vmsig_adapter* a) { if (!a) return; if (a->registered && a->emit.unregister_memctx) a->emit.unregister_memctx(a->emit.token, a->endpoint); if (a->worker) vmsig_worker_free(a->worker); /* join: bootstrap + write jobs finished */ #ifdef VMSIG_WITH_VMIE if (a->win) vmie_win32_close(a->win); /* AFTER worker join: no in-flight gva_write */ #endif if (a->stub_fd >= 0) close(a->stub_fd); /* one-shot backoff timerfd: never spawns a worker job, so its close is independent of the * worker join — same contract as stub_fd. The core already epoll_ctl(DEL)'d the slot. */ if (a->retry_fd >= 0) close(a->retry_fd); /* ro_fd ownership transferred to the adapter at open(): close it here so a re-grant * (detach + re-attach with a fresh infra ro_fd) does not leak the prior one. Infra * that must keep its own copy dups before handing it in — symmetric to the holder * side, which dups the borrowed RO-fd it receives. */ if (a->cfg_ro_fd >= 0) close(a->cfg_ro_fd); free(a); } static const vmsig_adapter_ops MC_OPS = { .name = "memctx", .source = VMSIG_SRC_MEMCTX, .codec = VMSIG_CODEC_MEMCTX, .open = mc_open, .attach = mc_attach, .on_readiness = mc_on_ready, .submit = mc_submit, .close = mc_close }; const vmsig_adapter_ops* vmsig_memctx_ops(void) { return &MC_OPS; }