feat(memctx): persist kcr3 to fast-restart without a cold rescan

The cold host_bootstrap hunts the agent beacon across physical RAM and is
slow and unstable: after a restart the adapter re-scans from scratch, minutes
in which there is no address-space context to vend, though the guest is long
booted and its System DTB (kcr3) is unchanged.

Cache the kcr3 from a successful live scan in a watch-dir sibling of the slot
map (tmpfs: survives a restart, dies with the RAM file on host reboot). On
attach, re-validate the cached kcr3 against the live RAM via an O_RDONLY
context (open_ro_fd, which bypasses the beacon scan) plus a System-cr3 match,
and publish the read datum immediately when it still resolves the kernel. A
guest reboot changes the System DTB, so a stale kcr3 no longer resolves and
falls back to a cold scan: the boot-session discriminator is the kcr3 itself,
not file metadata.

The gva_write target is never taken from the cache: it is set only by a fresh
live scan, so a persisted kcr3 is a read locator only and MEMWRITE stays
fail-closed until a cold bootstrap acquires the write hold.

Persist is off unless the path is supplied (NULL keeps current behaviour).

Bump 0.3.12.
This commit is contained in:
2026-06-24 22:01:27 +03:00
parent 7ab6119b1f
commit bcf5d4f824
5 changed files with 370 additions and 30 deletions
+16 -1
View File
@@ -60,6 +60,10 @@ struct vmsig_discovery {
* writes these at attach; the vmhost seam borrows them to add input-linux objects. Same
* lifetime discipline as ep_facts (outlives the deferred adapter reap). */
struct { char evdev_a[64]; char evdev_b[64]; } ep_bridge[VMSIG_SLOT_COUNT];
/* Stable per-endpoint home for the memctx kcr3-cache path (sibling of .slots in the watch
* dir). The memctx adapter keeps the pointer across its lifetime; same lifetime discipline
* as ep_facts/ep_bridge (outlives the deferred adapter reap, overwritten on next attach). */
char ep_persist[VMSIG_SLOT_COUNT][DISC_PATH_MAX + 32];
};
static uint64_t now_ns(void) {
@@ -269,14 +273,25 @@ static void bootstrap_scan(vmsig_discovery* d) {
static int default_attach(void* ud, vmsig_core* core, uint32_t vmid, uint32_t endpoint,
const vmsig_host_facts* f) {
(void)vmid;
vmsig_discovery* d = ud; /* default sink carries the discovery handle (ep_bridge home) */
char* ev_a = d ? d->ep_bridge[endpoint].evdev_a : NULL;
char* ev_b = d ? d->ep_bridge[endpoint].evdev_b : NULL;
if (d) { ev_a[0] = '\0'; ev_b[0] = '\0'; } /* clear stale paths from a prior attach */
/* Form the kcr3-cache path (per-vmid, sibling of .slots/the RAM file in the watch dir).
* Gated on d->persist — one policy for all ephemeral watch-dir state. NULL => persist off. */
const char* persist_path = NULL;
if (d && d->persist) {
int pn = snprintf(d->ep_persist[endpoint], sizeof d->ep_persist[endpoint],
"%s/.kcr3-vm-%u", d->watch_dir, vmid);
/* only enable the cache if the path fit (a truncated path would point elsewhere). */
if (pn > 0 && (size_t)pn < sizeof d->ep_persist[endpoint])
persist_path = d->ep_persist[endpoint];
}
vmsig_memctx_cfg mc; memset(&mc, 0, sizeof mc);
mc.stub = 0; mc.ram_path = f->ram_path; mc.low = f->low; mc.ro_fd = -1;
mc.persist_path = persist_path;
vmsig_input_cfg in; memset(&in, 0, sizeof in);
/* input is uinput; power/lifecycle via the vmhost seam. The adapter publishes its uinput
* evdev paths into ep_bridge so the vmhost seam can forward them via input-linux. */