vmsig: a neutral signaling layer between sensors/input and controls

An epoll-driven, neutral transfer-event bus that connects sensors and input
actuators to one or more controls, bidirectionally. It owns the transfer context
and events — delivery order, priority, protocol-level timing, and an
interrupt-driven event model over fd sources (eventfd/timerfd/sockets) — and
stays agnostic to both the sensor/input drivers and the control.

What lives here:
- memctx: a coherent address-space context per endpoint — the guest address-space
  root paired with a pre-opened read-only RAM-region fd, with per-endpoint epoch
  invalidation and retained replay to late subscribers. Perception lives in
  out-of-tree sensor libraries that consume this datum read-only.
- exclusive-ownership leases for destructive resource classes (input, power,
  memory-write).
- write-signaled memory writes (MEMWRITE): an atomic write to guest memory routed
  through the seam under an exclusive lease, never a writable mapping.
- a host-management seam for VM lifecycle/status and a neutral input-injection
  command path.
- multi-VM endpoints; capability-gated, audited control authorization over an
  in-process or unix-socket transport.

Builds against headers only by default (a stub mode that exercises the seam
without a VM); armed builds link the real sensor/input libraries behind flags.
This commit is contained in:
2026-06-20 21:21:20 +03:00
commit e9aee057c7
36 changed files with 5820 additions and 0 deletions
+224
View File
@@ -0,0 +1,224 @@
/* core.c — core lifecycle and registration of adapters/controls.
* The loop and pumps live in loop.c. */
#include "core_internal.h"
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/epoll.h>
#include <sys/eventfd.h>
core_slot* core_register_fd(vmsig_core* c, int fd, uint32_t epoll_events, slot_role role) {
/* reuse a detached (SLOT_DEAD) slot so c->slots[] does not grow on every
* connection */
core_slot* s = NULL;
for (int i = 0; i < c->nslots; i++)
if (c->slots[i]->role == SLOT_DEAD) { s = c->slots[i]; break; }
if (!s) {
if (c->nslots == c->cap_slots) {
int ncap = c->cap_slots ? c->cap_slots * 2 : 16;
core_slot** ns = realloc(c->slots, (size_t)ncap * sizeof *ns);
if (!ns) return NULL;
c->slots = ns;
c->cap_slots = ncap;
}
s = calloc(1, sizeof *s);
if (!s) return NULL;
c->slots[c->nslots++] = s;
}
memset(s, 0, sizeof *s);
s->role = role;
s->fd = fd;
struct epoll_event ee;
memset(&ee, 0, sizeof ee);
ee.events = epoll_events;
ee.data.ptr = s;
if (epoll_ctl(c->epfd, EPOLL_CTL_ADD, fd, &ee) < 0) { s->role = SLOT_DEAD; return NULL; }
return s;
}
vmsig_core* vmsig_core_new(vmsig_ctx* ctx) {
if (!ctx) return NULL;
vmsig_core* c = calloc(1, sizeof *c);
if (!c) return NULL;
c->ctx = ctx;
c->epfd = -1;
c->wake_fd = -1;
c->epfd = epoll_create1(EPOLL_CLOEXEC);
if (c->epfd < 0) { free(c); return NULL; }
c->wake_fd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
if (c->wake_fd < 0) { close(c->epfd); free(c); return NULL; }
if (!core_register_fd(c, c->wake_fd, EPOLLIN, SLOT_WAKEUP)) {
close(c->wake_fd); close(c->epfd); free(c); return NULL;
}
/* context pacing timerfds (created in ctx_new) as loop sources */
for (int d = VMSIG_DIR_UP; d <= VMSIG_DIR_DOWN; d++) {
int tfd = vmsig_ctx_timing_fd(ctx, (vmsig_dir)d);
if (tfd >= 0) core_register_fd(c, tfd, EPOLLIN, SLOT_CTX_TIMING);
}
return c;
}
int vmsig_core_add_adapter(vmsig_core* c, const vmsig_adapter_ops* ops,
const void* cfg, uint32_t endpoint) {
if (!c || !ops || c->nadapters >= VMSIG_MAX_ADAPTERS) return -1;
vmsig_adapter* a = ops->open(cfg, endpoint);
if (!a) return -1;
vmsig_emit emit = { core_emit_up, core_register_memctx, core_unregister_memctx, c };
vmsig_fd_reg reg[VMSIG_ADAPTER_FDS];
memset(reg, 0, sizeof reg);
int n = ops->attach(a, &emit, reg, VMSIG_ADAPTER_FDS);
if (n < 0) { ops->close(a); return -1; }
for (int i = 0; i < n; i++) {
uint32_t events = reg[i].epoll_events ? reg[i].epoll_events : (uint32_t)EPOLLIN;
core_slot* s = core_register_fd(c, reg[i].fd, events, SLOT_ADAPTER);
if (!s) { ops->close(a); return -1; }
s->ops = ops;
s->adapter = a;
s->cookie = reg[i].cookie;
}
int id = c->nadapters;
c->adapters[c->nadapters].ops = ops;
c->adapters[c->nadapters].a = a;
c->adapters[c->nadapters].endpoint = endpoint;
c->nadapters++;
return id;
}
int vmsig_core_add_control(vmsig_core* c, const vmsig_control_ops* ops, void* ctl,
const vmsig_grant* grant) {
if (!c || !ops) return -1;
/* reuse a freed (reaped) slot; otherwise grow up to the ceiling */
int id = -1;
for (int i = 0; i < c->ncontrols; i++)
if (!c->controls[i].active) { id = i; break; }
if (id < 0) {
if (c->ncontrols >= VMSIG_MAX_CONTROLS) return -1;
id = c->ncontrols++;
}
core_control_ent* e = &c->controls[id];
uint16_t gen = e->gen; /* generation survives the slot memset */
memset(e, 0, sizeof *e);
e->gen = (uint16_t)(gen + 1); /* new generation for this (re)use */
e->ops = ops;
e->ctl = ctl;
e->active = 1;
if (grant) e->grant = *grant; /* otherwise stays zero => default-deny */
e->dctx.core = c;
e->dctx.ctl_id = id;
if (ops->subscribe) ops->subscribe(ctl, &e->sub);
/* emit_down token is our down_ctx, so emit_down can find this control's grant */
if (ops->set_emit_down) ops->set_emit_down(ctl, core_emit_down, &e->dctx);
int fd = ops->fd ? ops->fd(ctl) : -1;
if (fd >= 0) {
core_slot* s = core_register_fd(c, fd, EPOLLIN, SLOT_CONTROL);
if (!s) return -1;
s->cops = ops;
s->ctl = ctl;
e->slot = s;
}
/* Late subscriber: replay retained MEMCTX (if a context is already published and
* this control is qualified). For a control added BEFORE the first publication,
* the cell is not yet valid — it receives MEMCTX via the normal multicast in pump_up. */
core_memctx_replay(c, id);
return id; /* ncontrols already bumped when picking id (on growth); reuse does not grow it */
}
/* ===== MEMCTX registration: per-endpoint retain cell (called by the adapter on the loop thread) =====
* Registers the address-space context adapter's reg hooks. The core holds THIS and does
* NOT store a copy of the locator: on delivery/replay it calls reg.describe/share_fd.
* valid/epoch are maintained in route/epoch_bump (not here): register only records that
* "the adapter is connected". */
int core_register_memctx(void* token, const vmsig_memctx_reg* reg) {
vmsig_core* c = token;
if (!c || !reg || reg->endpoint >= 64) return -1;
core_memctx_cell* cell = &c->memctx[reg->endpoint];
cell->reg = *reg;
cell->registered = 1;
return 0;
}
void core_unregister_memctx(void* token, uint32_t endpoint) {
vmsig_core* c = token;
if (!c || endpoint >= 64) return;
core_memctx_cell* cell = &c->memctx[endpoint];
cell->registered = 0;
cell->valid = 0;
memset(&cell->reg, 0, sizeof cell->reg);
}
void vmsig_core_set_audit(vmsig_core* c, void (*cb)(void* ud, const vmsig_audit* a), void* ud) {
if (!c) return;
c->audit_cb = cb;
c->audit_ud = ud;
}
void core_audit(vmsig_core* c, const vmsig_audit* a) {
if (c && c->audit_cb) c->audit_cb(c->audit_ud, a);
}
void vmsig_core_set_arb_policy(vmsig_core* c, vmsig_arb_policy cb, void* ud) {
if (!c) return;
c->arb_cb = cb;
c->arb_ud = ud;
/* lease[][] is zeroed in vmsig_core_new (calloc) => all cells free. */
}
int core_add_source(vmsig_core* c, int fd, void (*cb)(void* user, uint32_t events),
void* user, void (*on_free)(void* user)) {
if (!c || fd < 0 || !cb) return -1;
core_slot* s = core_register_fd(c, fd, EPOLLIN, SLOT_SOURCE);
if (!s) return -1;
s->on_source = cb;
s->on_free = on_free;
s->source_user = user;
return 0;
}
void core_request_drop(vmsig_core* c, int ctl_id) {
if (!c || ctl_id < 0 || ctl_id >= c->ncontrols) return;
c->controls[ctl_id].reap = 1;
core_wake(c); /* wake the loop for a reap pass (without stop) */
}
void vmsig_core_free(vmsig_core* c) {
if (!c) return;
/* graceful: stop workers and close SI handles / sockets. Adapters are closed
* FIRST: their close stops off-loop workers and unregisters their seams (e.g.
* memctx) BEFORE destruction. */
for (int i = 0; i < c->nadapters; i++)
if (c->adapters[i].ops->close) c->adapters[i].ops->close(c->adapters[i].a);
for (int i = 0; i < c->ncontrols; i++)
if (c->controls[i].active && c->controls[i].ops->close)
c->controls[i].ops->close(c->controls[i].ctl);
/* cleanup of fd sources (e.g. unix listener: close listen/janitor fd + free) */
for (int i = 0; i < c->nslots; i++)
if (c->slots[i]->role == SLOT_SOURCE && c->slots[i]->on_free)
c->slots[i]->on_free(c->slots[i]->source_user);
for (int i = 0; i < c->nslots; i++) free(c->slots[i]);
free(c->slots);
if (c->wake_fd >= 0) close(c->wake_fd);
if (c->epfd >= 0) close(c->epfd);
/* ctx is not ours: its owner frees it */
free(c);
}