vmsig: management daemon, runtime endpoint lifecycle, roster, discovery, in-tree drivers, packaging

- core: runtime attach/detach of a per-endpoint adapter trio (runtime-safe add_adapter + vmsig_core_detach_endpoint, deferred reap)
- roster: VMSIG_EV_ROSTER + CAP_ROSTER, retained per-endpoint and replayed to late subscribers
- discovery: inotify trigger dir, vmid/endpoint slot allocator, host probe; vmsigd daemon with config + per-uid admission
- input driver and vgpu perception built in-tree; vgpu perception as a separate library
- memctx: own the supplied ro_fd (closed at detach)
- deb packaging: install rules, systemd unit, tmpfiles, default config
This commit is contained in:
2026-06-22 17:25:06 +03:00
parent 0d387a4249
commit 9bde398b6c
55 changed files with 4703 additions and 61 deletions
+37
View File
@@ -0,0 +1,37 @@
/* admission.c — vmsigd coarse admission policy (see vmsigd_admission.h). Translates a uid to
* a vmsig_grant, resolving entitled vmids to live endpoint bits via the discovery slot map. */
#define _GNU_SOURCE
#include "vmsigd_admission.h"
#include "discovery.h" /* vmsig_discovery_slot_of_vmid */
#include <string.h>
static const vmsigd_grant_rule* rule_for_uid(const vmsigd_config* cfg, uint32_t uid) {
for (int i = 0; i < cfg->ngrants; i++)
if (cfg->grants[i].uid == uid) return &cfg->grants[i];
return NULL;
}
vmsig_grant vmsigd_policy(uint32_t uid, uint32_t pid, void* ud) {
(void)pid;
vmsigd_admission* a = ud;
vmsig_grant g;
memset(&g, 0, sizeof g);
const vmsigd_grant_rule* r = (a && a->cfg) ? rule_for_uid(a->cfg, uid) : NULL;
if (!r) return g; /* no stanza => empty grant => REJECT */
g.principal = uid;
g.source_mask = 0xFFFFFFFFu; /* coarse: control enforces source finer behind us */
g.cap_mask = r->cap_mask;
g.arb_prio = r->arb_prio;
if (r->all_vms) {
g.endpoint_mask = ~0ull; /* covers all current + future endpoints */
} else {
for (int i = 0; i < r->nvmids; i++) {
int ep = a->disc ? vmsig_discovery_slot_of_vmid(a->disc, r->vmids[i]) : -1;
if (ep >= 0 && ep < 64) g.endpoint_mask |= (1ull << ep);
}
}
return g;
}
+126
View File
@@ -0,0 +1,126 @@
/* config.c — vmsigd config parser (see vmsigd.h). INI-ish: `key = value` globals + repeated
* `[grant uid=N]` stanzas. Pure libc; no core/vmie dependency (unit-testable in any build). */
#define _GNU_SOURCE
#include "vmsigd.h"
#include "vmsig_control.h" /* VMSIG_CAP_* */
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>
void vmsigd_config_defaults(vmsigd_config* c) {
memset(c, 0, sizeof *c);
snprintf(c->socket, sizeof c->socket, "%s", "/run/vmsig/vmsigd.sock");
snprintf(c->watch, sizeof c->watch, "%s", "/dev/shm/vmsig");
snprintf(c->pve_conf, sizeof c->pve_conf, "%s", "/etc/pve/qemu-server");
snprintf(c->qmp_dir, sizeof c->qmp_dir, "%s", "/var/run/qemu-server");
snprintf(c->slots, sizeof c->slots, "%s", "/dev/shm/vmsig/.slots");
}
uint32_t vmsigd_caps_from_str(const char* s) {
static const struct { const char* k; uint32_t bit; } map[] = {
{ "observe", VMSIG_CAP_OBSERVE },
{ "input", VMSIG_CAP_INPUT },
{ "lifecycle", VMSIG_CAP_LIFECYCLE },
{ "power", VMSIG_CAP_POWER },
{ "vm", VMSIG_CAP_VM },
{ "memctx", VMSIG_CAP_MEMCTX },
{ "memwrite", VMSIG_CAP_MEMWRITE },
{ "roster", VMSIG_CAP_ROSTER },
};
uint32_t mask = 0;
while (s && *s) {
while (*s == ',' || *s == ' ' || *s == '\t') s++;
const char* w = s;
while (*s && *s != ',' && *s != ' ' && *s != '\t') s++;
size_t len = (size_t)(s - w);
for (size_t i = 0; i < sizeof map / sizeof map[0]; i++)
if (len == strlen(map[i].k) && strncmp(w, map[i].k, len) == 0) { mask |= map[i].bit; break; }
}
return mask;
}
/* Trim leading/trailing whitespace in place; returns the trimmed start. */
static char* trim(char* s) {
while (*s == ' ' || *s == '\t' || *s == '\r') s++;
char* e = s + strlen(s);
while (e > s && (e[-1] == ' ' || e[-1] == '\t' || e[-1] == '\r' || e[-1] == '\n')) *--e = 0;
return s;
}
static void set_path(char* dst, size_t cap, const char* v) { snprintf(dst, cap, "%s", v); }
static void parse_vmids(vmsigd_grant_rule* g, const char* v) {
g->all_vms = 0; g->nvmids = 0;
if (strchr(v, '*')) { g->all_vms = 1; return; }
while (*v) {
while (*v == ',' || *v == ' ' || *v == '\t') v++;
if (*v < '0' || *v > '9') { if (*v) v++; continue; }
uint32_t id = (uint32_t)strtoul(v, NULL, 10);
while (*v >= '0' && *v <= '9') v++;
if (id && g->nvmids < VMSIGD_MAX_VMIDS) g->vmids[g->nvmids++] = id;
}
}
int vmsigd_config_parse_buf(vmsigd_config* c, const char* buf) {
if (!c || !buf) return -1;
char* copy = strdup(buf);
if (!copy) return -1;
vmsigd_grant_rule* cur = NULL; /* current [grant] stanza, or NULL for globals */
char* save = NULL;
for (char* line = strtok_r(copy, "\n", &save); line; line = strtok_r(NULL, "\n", &save)) {
char* p = trim(line);
if (!*p || *p == '#' || *p == ';') continue;
if (*p == '[') {
cur = NULL;
/* [grant uid=N] */
char* u = strstr(p, "uid=");
if (u && c->ngrants < VMSIGD_MAX_GRANTS) {
cur = &c->grants[c->ngrants++];
memset(cur, 0, sizeof *cur);
cur->uid = (uint32_t)strtoul(u + 4, NULL, 10);
}
continue;
}
char* eq = strchr(p, '=');
if (!eq) continue;
*eq = 0;
char* key = trim(p);
char* val = trim(eq + 1);
if (cur) {
if (!strcmp(key, "vmids")) parse_vmids(cur, val);
else if (!strcmp(key, "caps")) cur->cap_mask = vmsigd_caps_from_str(val);
else if (!strcmp(key, "arb_prio")) cur->arb_prio = (uint32_t)strtoul(val, NULL, 10);
} else {
if (!strcmp(key, "socket")) set_path(c->socket, sizeof c->socket, val);
else if (!strcmp(key, "watch")) set_path(c->watch, sizeof c->watch, val);
else if (!strcmp(key, "pve_conf")) set_path(c->pve_conf, sizeof c->pve_conf, val);
else if (!strcmp(key, "qmp_dir")) set_path(c->qmp_dir, sizeof c->qmp_dir, val);
else if (!strcmp(key, "slots")) set_path(c->slots, sizeof c->slots, val);
}
}
free(copy);
return 0;
}
int vmsigd_config_parse_file(vmsigd_config* c, const char* path) {
int fd = open(path, O_RDONLY | O_CLOEXEC);
if (fd < 0) return -1;
char buf[16 * 1024];
size_t got = 0;
for (;;) {
ssize_t n = read(fd, buf + got, sizeof buf - 1 - got);
if (n < 0) { close(fd); return -1; }
if (n == 0) break;
got += (size_t)n;
if (got >= sizeof buf - 1) break;
}
close(fd);
buf[got] = 0;
return vmsigd_config_parse_buf(c, buf);
}
+47
View File
@@ -0,0 +1,47 @@
#ifndef VMSIGD_H
#define VMSIGD_H
#include <stdint.h>
/* vmsigd.h — private config model of the vmsig daemon.
*
* The daemon owns the /dev/shm/vmsig discovery namespace and serves a unix-socket control
* plane over the signaling layer for the VMs discovered there. Its only policy is a COARSE
* admission grant per uid (SISC: signaling is not a fine-grained access broker — the control
* enforces per-user caps behind the grant). Entitlements are expressed in vmid terms and
* translated to an endpoint_mask at connect time against the live slot map. */
#define VMSIGD_MAX_GRANTS 64
#define VMSIGD_MAX_VMIDS 64
#define VMSIGD_PATH_MAX 256
typedef struct {
uint32_t uid;
int all_vms; /* `vmids = *` */
uint32_t vmids[VMSIGD_MAX_VMIDS];
int nvmids;
uint32_t cap_mask; /* VMSIG_CAP_* (from `caps =` keywords) */
uint32_t arb_prio;
} vmsigd_grant_rule;
typedef struct {
char socket[VMSIGD_PATH_MAX]; /* control listener ('@' => abstract) */
char watch[VMSIGD_PATH_MAX]; /* discovery dir (/dev/shm/vmsig) */
char pve_conf[VMSIGD_PATH_MAX]; /* /etc/pve/qemu-server */
char qmp_dir[VMSIGD_PATH_MAX]; /* /var/run/qemu-server */
char slots[VMSIGD_PATH_MAX]; /* slot persistence ("" => off) */
vmsigd_grant_rule grants[VMSIGD_MAX_GRANTS];
int ngrants;
} vmsigd_config;
/* Populate with built-in defaults. */
void vmsigd_config_defaults(vmsigd_config* c);
/* Parse the INI-ish config (globals + repeated [grant uid=N] stanzas) over the defaults
* already in `c`. Unknown keys are ignored. Returns 0, or -1 on open/usage error. */
int vmsigd_config_parse_file(vmsigd_config* c, const char* path);
int vmsigd_config_parse_buf (vmsigd_config* c, const char* buf); /* same, from memory (tests) */
/* Translate a comma/space-separated cap keyword list to a VMSIG_CAP_* mask. */
uint32_t vmsigd_caps_from_str(const char* s);
#endif /* VMSIGD_H */
+21
View File
@@ -0,0 +1,21 @@
#ifndef VMSIGD_ADMISSION_H
#define VMSIGD_ADMISSION_H
#include "vmsigd.h"
#include "vmsig_control.h" /* vmsig_grant */
struct vmsig_discovery;
/* Admission context handed to the socket listener as policy `ud`. The config is read-only at
* connect time; the live discovery resolves entitled vmids to their current endpoint bits. */
typedef struct {
const vmsigd_config* cfg;
struct vmsig_discovery* disc;
} vmsigd_admission;
/* vmsig_socket_policy: uid from SO_PEERCRED -> a coarse grant. No matching [grant uid=N]
* stanza => empty grant (the listener rejects). `vmids = *` => endpoint_mask covers all 64;
* a vmid list resolves each currently-attached vmid to its endpoint bit (an unbound entitled
* vmid contributes no bit yet — the peer learns liveness via the roster). */
vmsig_grant vmsigd_policy(uint32_t uid, uint32_t pid, void* ud);
#endif /* VMSIGD_ADMISSION_H */
+156
View File
@@ -0,0 +1,156 @@
/* vmsigd.c — the vmsig management daemon.
*
* Owns the /dev/shm/vmsig discovery namespace and serves a unix-socket control plane over the
* signaling layer for the VMs found there. It wires nothing VM-specific: discovery hot-plugs
* each VM's adapter trio and publishes the roster; the daemon only supplies the loop, the
* discovery roots, the control socket, and a coarse per-uid admission policy.
*
* Real input/memctx actuation needs an armed library build (memctx -> vmie). A stub build
* still runs (socket/admission/discovery machinery), but memctx will not bootstrap.
*
* Usage: vmsigd [--config PATH] [--socket S] [--watch DIR] [--pve-conf DIR] [--qmp-dir DIR]
* [--slots PATH] [--foreground]
* precedence: argv > environment (VMSIGD_*) > config file > built-in defaults. */
#define _GNU_SOURCE
#include "vmsig.h"
#include "vmsig_socket.h"
#include "discovery.h"
#include "core_internal.h" /* core_add_source (in-repo daemon, intimate with the core) */
#include "vmsigd.h"
#include "vmsigd_admission.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/signalfd.h>
static vmsig_core* g_core;
static vmsigd_config g_cfg;
static char g_cfg_path[VMSIGD_PATH_MAX];
/* Audit trace: admissions/denials, lease and memctx grants — on the loop thread, to stderr
* (systemd routes stderr to the journal). */
static void on_audit(void* ud, const vmsig_audit* a) {
(void)ud;
static const char* k[] = {
"ADMIT", "REJECT", "DOWN_DENIED", "LEASE_GRANTED", "LEASE_DENIED",
"LEASE_REVOKED", "LEASE_RECLAIMED", "MEMCTX_GRANTED"
};
const char* name = (a->kind <= VMSIG_AUDIT_MEMCTX_GRANTED) ? k[a->kind] : "?";
fprintf(stderr, "vmsigd: audit %-14s principal=%u ep=%u cmd=%u detail=%u\n",
name, a->principal, a->endpoint, a->cmd, a->detail);
}
/* Signals arrive as fd readiness (signalfd) on the loop thread — no async-handler hazards.
* TERM/INT => graceful stop; HUP => reload ONLY the admission table from the config file
* (paths/socket/adapters are untouched; already-connected grants are not retroactively
* changed — a peer reconnects to pick up a changed entitlement). */
static void on_signal(void* user, uint32_t events) {
(void)events;
int sfd = *(int*)user;
struct signalfd_siginfo si;
while (read(sfd, &si, sizeof si) == (ssize_t)sizeof si) {
if (si.ssi_signo == SIGINT || si.ssi_signo == SIGTERM) {
vmsig_core_stop(g_core);
} else if (si.ssi_signo == SIGHUP) {
vmsigd_config fresh;
vmsigd_config_defaults(&fresh);
if (g_cfg_path[0] && vmsigd_config_parse_file(&fresh, g_cfg_path) == 0) {
memcpy(g_cfg.grants, fresh.grants, sizeof g_cfg.grants);
g_cfg.ngrants = fresh.ngrants; /* swap admission table only */
fprintf(stderr, "vmsigd: reloaded %d grant rule(s)\n", g_cfg.ngrants);
}
}
}
}
static const char* arg_val(int argc, char** argv, int* i) {
char* a = argv[*i];
char* eq = strchr(a, '=');
if (eq) return eq + 1;
if (*i + 1 < argc) { (*i)++; return argv[*i]; }
return "";
}
static void apply_env(vmsigd_config* c) {
const char* v;
if ((v = getenv("VMSIGD_SOCKET"))) snprintf(c->socket, sizeof c->socket, "%s", v);
if ((v = getenv("VMSIGD_WATCH"))) snprintf(c->watch, sizeof c->watch, "%s", v);
if ((v = getenv("VMSIGD_PVE_CONF"))) snprintf(c->pve_conf, sizeof c->pve_conf, "%s", v);
if ((v = getenv("VMSIGD_QMP_DIR"))) snprintf(c->qmp_dir, sizeof c->qmp_dir, "%s", v);
if ((v = getenv("VMSIGD_SLOTS"))) snprintf(c->slots, sizeof c->slots, "%s", v);
}
int main(int argc, char** argv) {
/* config path: argv --config > env > default. */
const char* cfg_path = getenv("VMSIGD_CONFIG");
if (!cfg_path) cfg_path = "/etc/vmsig/vmsigd.conf";
for (int i = 1; i < argc; i++)
if (!strncmp(argv[i], "--config", 8)) { cfg_path = arg_val(argc, argv, &i); }
vmsigd_config_defaults(&g_cfg);
vmsigd_config_parse_file(&g_cfg, cfg_path); /* missing file => defaults (not fatal) */
snprintf(g_cfg_path, sizeof g_cfg_path, "%s", cfg_path);
apply_env(&g_cfg);
for (int i = 1; i < argc; i++) {
char* a = argv[i];
if (!strncmp(a, "--config", 8)) { (void)arg_val(argc, argv, &i); }
else if (!strncmp(a, "--socket", 8)) snprintf(g_cfg.socket, sizeof g_cfg.socket, "%s", arg_val(argc, argv, &i));
else if (!strncmp(a, "--watch", 7)) snprintf(g_cfg.watch, sizeof g_cfg.watch, "%s", arg_val(argc, argv, &i));
else if (!strncmp(a, "--pve-conf", 10)) snprintf(g_cfg.pve_conf, sizeof g_cfg.pve_conf, "%s", arg_val(argc, argv, &i));
else if (!strncmp(a, "--qmp-dir", 9)) snprintf(g_cfg.qmp_dir, sizeof g_cfg.qmp_dir, "%s", arg_val(argc, argv, &i));
else if (!strncmp(a, "--slots", 7)) snprintf(g_cfg.slots, sizeof g_cfg.slots, "%s", arg_val(argc, argv, &i));
else if (!strcmp(a, "--foreground")) { /* default; systemd Type=simple */ }
else if (!strcmp(a, "-h") || !strcmp(a, "--help")) {
fprintf(stderr, "usage: %s [--config P][--socket S][--watch D][--pve-conf D]"
"[--qmp-dir D][--slots P][--foreground]\n", argv[0]);
return 0;
}
}
/* Signals via signalfd, serviced on the loop thread. SIGPIPE ignored (dead-peer writes). */
signal(SIGPIPE, SIG_IGN);
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT); sigaddset(&mask, SIGTERM); sigaddset(&mask, SIGHUP);
sigprocmask(SIG_BLOCK, &mask, NULL);
int sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);
if (sfd < 0) { perror("vmsigd: signalfd"); return 1; }
vmsig_ctx* ctx = vmsig_ctx_new();
if (!ctx) { fprintf(stderr, "vmsigd: ctx_new failed\n"); close(sfd); return 1; }
g_core = vmsig_core_new(ctx);
if (!g_core) { fprintf(stderr, "vmsigd: core_new failed\n"); vmsig_ctx_free(ctx); close(sfd); return 1; }
vmsig_core_set_audit(g_core, on_audit, NULL);
if (core_add_source(g_core, sfd, on_signal, &sfd, NULL) != 0) {
fprintf(stderr, "vmsigd: signal source registration failed\n");
vmsig_core_free(g_core); vmsig_ctx_free(ctx); close(sfd); return 1;
}
vmsig_discovery* disc = vmsig_discovery_new(
g_core, g_cfg.watch, g_cfg.pve_conf, g_cfg.qmp_dir,
g_cfg.slots[0] ? g_cfg.slots : NULL, NULL, NULL);
if (!disc) {
fprintf(stderr, "vmsigd: discovery_new(%s) failed\n", g_cfg.watch);
vmsig_core_free(g_core); vmsig_ctx_free(ctx); close(sfd); return 1;
}
vmsigd_admission adm = { &g_cfg, disc };
if (vmsig_socket_attach(g_core, g_cfg.socket, vmsigd_policy, &adm) != 0) {
fprintf(stderr, "vmsigd: socket_attach(%s) failed\n", g_cfg.socket);
vmsig_core_free(g_core); vmsig_ctx_free(ctx); close(sfd); return 1;
}
fprintf(stderr, "vmsigd: serving %s (watch=%s pve=%s qmp=%s) %d grant rule(s)\n",
g_cfg.socket, g_cfg.watch, g_cfg.pve_conf, g_cfg.qmp_dir, g_cfg.ngrants);
int rc = vmsig_core_run(g_core);
fprintf(stderr, "vmsigd: loop exit rc=%d\n", rc);
vmsig_core_free(g_core); /* reaps discovery (source on_free) + closes the socket listener */
vmsig_ctx_free(ctx);
close(sfd);
return rc;
}