keventd: serve org.finit.Device1 on /run/keventd/bus

The udev parity gaps that were blocked on IPC -- settle, trigger,
info, queue introspection, runtime rule reload -- become bus methods.
keventd serves its own socket the way Finit serves /run/finit/bus:
brokerless, libink, one socket per daemon, no forwarding between the
two.

  Settle(u) -> b      parked until the queue drains or the timeout
                      passes; true when settled
  Trigger(s, s)       replay events, action + subsystem glob
  Info(s) -> a{ss}    /run/udev/data properties for a devpath
  RulesReload() -> u  re-read rules dirs, returns rule count
  QueueEmpty (b), SeqnumProcessed (t) properties
  DeviceProcessed (ss) signal after each fully handled event

The queue state is the highest kernel seqnum keventd has handled,
baselined at startup, against /sys/kernel/uevent_seqnum.  keventd -S
now asks the running daemon first and falls back to seqnum polling.
In passive mode Trigger and RulesReload refuse.  Adds
link_call_connection() for the park bookkeeping.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg committed 2026-08-16 22:03:43 +02:00
1 parent 6e60b8183a
commit 292e87c0d1
10 files changed
+811 -23

No files matched your search

+9
View File
@@ -9,6 +9,9 @@ keventd_SOURCES = keventd.c keventd.h uevent.c \
rules.c rules.h \
builtin.c builtin.h \
$(top_srcdir)/src/util.c $(top_srcdir)/src/util.h
# Per-target flags exist even when their conditional is false, so
# assign the base unconditionally and append under DBUS
keventd_CPPFLAGS = $(AM_CPPFLAGS)
keventd_CFLAGS = -W -Wall -Wextra -std=gnu99
# Silence noisy false positives from PATH_MAX-into-PATH_MAX sysfs path
# concatenations; real concerns (e.g. USB strings into small buffers) are
@@ -21,6 +24,12 @@ keventd_LDADD = $(lite_LIBS)
keventd_LDADD += $(uev_LIBS)
keventd_LDADD += $(blkid_LIBS)
if DBUS
keventd_SOURCES += dbus.c
keventd_CPPFLAGS += -I$(top_srcdir)/libink
keventd_LDADD += $(top_builddir)/libink/libink.la
endif
# Curated udev rules derived from eudev 3.2.14 (GPL-2.0-or-later).
# Installed to /lib/udev/rules.d/ so existing third-party rules and
# local overrides under /etc/udev/rules.d/ coexist with standard udev
+587
View File
@@ -0,0 +1,587 @@
/* keventd D-Bus interface: org.finit.Device1 on its own local bus
*
* keventd serves /run/keventd/bus the same way Finit serves
* /run/finit/bus: brokerless, libink, one socket per daemon. No
* message forwarding exists between the two, by design -- clients
* that want both connect to both.
*
* Copyright (c) 2026 Joachim Wiberg <troglobit@gmail.com>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
#include "config.h"
#ifdef HAVE_DBUS
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <time.h>
#include <unistd.h>
#include <uev/uev.h>
#ifdef _LIBITE_LITE
# include <libite/lite.h>
#else
# include <lite/lite.h>
#endif
#include "link.h"
#include "keventd.h"
#include "udevdb.h"
#include "util.h"
void logit(int prio, const char *fmt, ...);
#define DEVBUS_DIR "/run/keventd"
#define DEVBUS_SOCKET DEVBUS_DIR "/bus"
#define DEVBUS_PATH_OBJECT "/org/finit/device"
#define DEVBUS_INTERFACE "org.finit.Device1"
#define DEVBUS_MAX_PEERS 16
#define DEVBUS_MAX_SETTLES 8
struct peer {
uev_t watcher;
link_connection_t *conn;
int dead;
TAILQ_ENTRY(peer) link;
};
static TAILQ_HEAD(, peer) peers = TAILQ_HEAD_INITIALIZER(peers);
static TAILQ_HEAD(, peer) reapq = TAILQ_HEAD_INITIALIZER(reapq);
static link_server_t *server;
static uev_t accept_watcher;
static uev_t reap_timer;
static uev_ctx_t *devbus_ctx;
static size_t peer_count;
static int bus_passive;
/* Parked Settle() calls with their own deadlines */
struct settle {
link_connection_t *conn;
link_authz_t tok;
uint64_t deadline; /* CLOCK_MONOTONIC ms */
};
static struct settle settles[DEVBUS_MAX_SETTLES];
static uev_t settle_timer;
static int settle_armed;
static uint64_t now_ms(void)
{
struct timespec ts;
clock_gettime(CLOCK_MONOTONIC, &ts);
return (uint64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
}
/*
* Same policy as /run/finit/bus: root and members of the --with-group
* group. The group set comes from the kernel (SO_PEERGROUPS), the
* owning gid is resolved once at init so no call ever hits NSS.
*/
static gid_t privileged_gid = (gid_t)-1; /* DEFGROUP, resolved at init */
static int caller_is_privileged(uid_t uid, const gid_t *groups, int ngroups,
void *userdata)
{
(void)userdata;
if (uid == 0)
return 1;
if (privileged_gid == (gid_t)-1)
return 0; /* no owning group to match against */
for (int i = 0; i < ngroups; i++) {
if (groups[i] == privileged_gid)
return 1;
}
return 0;
}
static void link_log_cb(void *userdata, const char *func, const char *msg)
{
(void)userdata;
logit(LOG_DEBUG, "%s():%s", func, msg);
}
/* ---------- peer lifecycle, same shape as src/dbus.c ---------- */
static void peer_reap(uev_t *w, void *arg, int events)
{
struct peer *p;
(void)w; (void)arg; (void)events;
while ((p = TAILQ_FIRST(&reapq))) {
TAILQ_REMOVE(&reapq, p, link);
link_connection_close(p->conn);
free(p);
}
}
static void peer_drop(struct peer *p)
{
int i;
if (p->dead)
return;
p->dead = 1;
uev_io_stop(&p->watcher);
TAILQ_REMOVE(&peers, p, link);
peer_count--;
/* Settles parked on this connection die with it */
for (i = 0; i < DEVBUS_MAX_SETTLES; i++) {
if (settles[i].conn == p->conn)
settles[i].conn = NULL;
}
TAILQ_INSERT_TAIL(&reapq, p, link);
uev_timer_set(&reap_timer, 10, 0);
}
static void peer_cb(uev_t *w, void *arg, int events)
{
struct peer *p = arg;
(void)w;
if (UEV_ERROR == events || link_connection_process(p->conn) < 0)
peer_drop(p);
}
static void accept_cb(uev_t *w, void *arg, int events)
{
(void)arg;
if (UEV_ERROR == events) {
logit(LOG_ERR, "D-Bus accept watcher error");
return;
}
for (;;) {
link_connection_t *conn = NULL;
struct peer *p;
if (link_server_accept(server, &conn) < 0) {
if (errno != EAGAIN && errno != EWOULDBLOCK)
logit(LOG_ERR, "Failed accepting D-Bus client: %s",
strerror(errno));
break;
}
if (peer_count >= DEVBUS_MAX_PEERS) {
logit(LOG_WARNING, "D-Bus peer cap reached (%zu), dropping",
peer_count);
link_connection_close(conn);
continue;
}
p = calloc(1, sizeof(*p));
if (!p) {
link_connection_close(conn);
continue;
}
p->conn = conn;
TAILQ_INSERT_TAIL(&peers, p, link);
peer_count++;
if (uev_io_init(w->ctx, &p->watcher, peer_cb, p,
link_connection_get_fd(conn), UEV_READ))
peer_drop(p);
}
}
/* ---------- parked Settle() bookkeeping ---------- */
static void settle_sweep(uev_t *w, void *arg, int events);
/*
* The queue can only drain when an event is processed, and
* devbus_notify() sweeps on exactly that; the timer exists solely to
* enforce deadlines, so arm it one-shot for the nearest one.
*/
static void settle_arm(uint64_t deadline)
{
uint64_t now = now_ms();
int ms = deadline > now ? (int)(deadline - now) : 1;
if (!settle_armed) {
uev_timer_init(devbus_ctx, &settle_timer, settle_sweep,
NULL, ms, 0);
settle_armed = 1;
} else {
uev_timer_set(&settle_timer, ms, 0);
}
}
/*
* Resume parked settles: all of them when the queue drained (the
* handler re-runs, sees the empty queue, replies true), expired ones
* otherwise (the handler re-runs, sees link_call_resumed(), replies
* false).
*/
static void settle_sweep(uev_t *w, void *arg, int events)
{
uint64_t next = 0;
uint64_t now = now_ms();
int empty = kev_queue_empty();
int i;
(void)w; (void)arg; (void)events;
for (i = 0; i < DEVBUS_MAX_SETTLES; i++) {
struct settle *s = &settles[i];
if (!s->conn)
continue;
if (empty || now >= s->deadline) {
link_connection_t *conn = s->conn;
link_authz_t tok = s->tok;
s->conn = NULL;
link_call_resume(conn, tok);
continue;
}
if (!next || s->deadline < next)
next = s->deadline;
}
if (next) {
settle_arm(next);
} else if (settle_armed) {
uev_timer_stop(&settle_timer);
settle_armed = 0;
}
}
/* ---------- org.finit.Device1 ---------- */
static int device1_settle(link_call_t *call, void *u)
{
link_writer_t *w;
uint32_t timeout;
int i;
int empty;
(void)u;
if (link_call_read_u32(call, &timeout) < 0)
return link_call_reply_error(call,
"org.freedesktop.DBus.Error.InvalidArgs",
"expected (u)");
empty = kev_queue_empty();
if (!empty && !link_call_resumed(call)) {
for (i = 0; i < DEVBUS_MAX_SETTLES; i++) {
if (settles[i].conn)
continue;
if (link_call_park(call, &settles[i].tok))
break;
settles[i].conn = link_call_connection(call);
settles[i].deadline = now_ms() + (uint64_t)timeout * 1000;
settle_arm(settles[i].deadline);
return 0;
}
/* no room to wait: answer with the state as it stands */
}
w = link_call_reply(call);
link_w_bool(w, empty);
return 0;
}
static int device1_trigger(link_call_t *call, void *u)
{
const char *action, *glob;
(void)u;
if (link_call_read_string(call, &action) < 0 ||
link_call_read_string(call, &glob) < 0)
return link_call_reply_error(call,
"org.freedesktop.DBus.Error.InvalidArgs",
"expected (s, s)");
if (bus_passive)
return link_call_reply_error(call,
"org.finit.Error.Failed",
"keventd is passive, device events are not handled here");
if (*action && !uevent_action_valid(action))
return link_call_reply_error(call,
"org.freedesktop.DBus.Error.InvalidArgs",
"unknown uevent action");
if (coldplug_trigger(action, glob))
return link_call_reply_error(call,
"org.finit.Error.Failed", "trigger failed");
(void)link_call_reply(call);
return 0;
}
static int device1_info(link_call_t *call, void *u)
{
struct uevent ev;
const char *devpath;
link_writer_t *w;
int i;
(void)u;
if (link_call_read_string(call, &devpath) < 0 || *devpath != '/')
return link_call_reply_error(call,
"org.freedesktop.DBus.Error.InvalidArgs",
"expected (s), an absolute devpath");
memset(&ev, 0, sizeof(ev));
if (udevdb_read_devpath(devpath, &ev)) {
uevent_env_free(&ev);
return link_call_reply_error(call,
"org.finit.Error.NoSuchDevice",
"no database record for devpath");
}
w = link_call_reply(call);
link_w_array_begin(w, '{');
for (i = 0; i < ev.nenv; i++) {
link_w_struct_begin(w);
link_w_string(w, ev.env_key[i]);
link_w_string(w, ev.env_val[i]);
link_w_struct_end(w);
}
link_w_array_end(w);
uevent_env_free(&ev);
return 0;
}
static int device1_rules_reload(link_call_t *call, void *u)
{
link_writer_t *w;
(void)u;
if (bus_passive)
return link_call_reply_error(call,
"org.finit.Error.Failed",
"keventd is passive, rules are not applied here");
w = link_call_reply(call);
link_w_u32(w, (uint32_t)kev_rules_reload());
return 0;
}
static int prop_queue_empty(link_writer_t *w, void *u)
{
(void)u;
link_w_bool(w, kev_queue_empty());
return 0;
}
static int prop_seqnum(link_writer_t *w, void *u)
{
(void)u;
link_w_u64(w, kev_seq_processed());
return 0;
}
static const link_method_t device_methods[] = {
{ .name = "Settle", .in_sig = "u", .out_sig = "b",
.handler = device1_settle },
{ .name = "Trigger", .in_sig = "ss", .out_sig = "",
.flags = LINK_METHOD_PRIVILEGED, .handler = device1_trigger },
{ .name = "Info", .in_sig = "s", .out_sig = "a{ss}",
.handler = device1_info },
{ .name = "RulesReload", .in_sig = "", .out_sig = "u",
.flags = LINK_METHOD_PRIVILEGED, .handler = device1_rules_reload },
{ NULL, NULL, NULL, 0, NULL }
};
static const link_property_t device_properties[] = {
{ .name = "QueueEmpty", .sig = "b", .getter = prop_queue_empty },
{ .name = "SeqnumProcessed", .sig = "t", .getter = prop_seqnum },
{ NULL, NULL, NULL }
};
static const link_signal_t device_signals[] = {
{ .name = "DeviceProcessed", .sig = "ss" },
{ NULL, NULL }
};
static const link_vtable_t device_vtable = {
.interface = DEVBUS_INTERFACE,
.methods = device_methods,
.properties = device_properties,
.signals = device_signals,
};
/* ---------- signal emission + settle nudge ---------- */
static void devbus_emit_signal(const char *member, const char *sig,
const uint8_t *body, size_t len)
{
struct peer *p, *tmp;
TAILQ_FOREACH_SAFE(p, &peers, link, tmp) {
if (link_connection_emit_signal(p->conn, DEVBUS_PATH_OBJECT,
DEVBUS_INTERFACE, member,
sig, body, len) >= 0)
continue;
if (errno == EMSGSIZE || errno == EINVAL)
break; /* nothing on the wire, same for all */
peer_drop(p);
}
}
void devbus_notify(const char *devpath, const char *action)
{
uint8_t body[512];
link_writer_t w;
ssize_t blen;
if (!server)
return;
/* A processed event may have been the last one in flight */
if (settle_armed)
settle_sweep(NULL, NULL, 0);
if (TAILQ_EMPTY(&peers) || !devpath)
return;
link_writer_init(&w, body, sizeof(body));
link_w_string(&w, devpath);
link_w_string(&w, action ?: "");
blen = link_writer_finish(&w);
if (blen < 0)
return;
devbus_emit_signal("DeviceProcessed", "ss", body, (size_t)blen);
}
/* ---------- init / exit / settle client ---------- */
int devbus_init(uev_ctx_t *ctx, int passive)
{
devbus_ctx = ctx;
bus_passive = passive;
link_set_logger(link_log_cb, NULL);
if (mkpath(DEVBUS_DIR, 0755) && errno != EEXIST) {
logit(LOG_ERR, "Failed creating %s: %s", DEVBUS_DIR,
strerror(errno));
return -1;
}
if (link_server_new(&server, DEVBUS_SOCKET, 0660) < 0) {
logit(LOG_ERR, "Failed creating D-Bus socket %s: %s",
DEVBUS_SOCKET, strerror(errno));
return -1;
}
/* Resolve the owning group once, see caller_is_privileged() */
privileged_gid = getgroup(DEFGROUP);
if (chown(DEVBUS_SOCKET, geteuid(), privileged_gid))
logit(LOG_WARNING, "Failed setting group %s on %s: %s",
DEFGROUP, DEVBUS_SOCKET, strerror(errno));
link_server_set_authorizer(server, caller_is_privileged, NULL);
if (link_server_add_object(server, DEVBUS_PATH_OBJECT,
&device_vtable, NULL) < 0) {
logit(LOG_ERR, "Failed registering %s", DEVBUS_INTERFACE);
link_server_free(server);
server = NULL;
return -1;
}
uev_timer_init(ctx, &reap_timer, peer_reap, NULL, 0, 0);
if (uev_io_init(ctx, &accept_watcher, accept_cb, NULL,
link_server_get_fd(server), UEV_READ)) {
logit(LOG_ERR, "Failed watching D-Bus socket");
link_server_free(server);
server = NULL;
return -1;
}
logit(LOG_NOTICE, "Serving %s on %s", DEVBUS_INTERFACE, DEVBUS_SOCKET);
return 0;
}
void devbus_exit(void)
{
struct peer *p;
if (!server)
return;
while ((p = TAILQ_FIRST(&peers)))
peer_drop(p);
peer_reap(NULL, NULL, 0);
uev_io_stop(&accept_watcher);
link_server_free(server);
server = NULL;
}
/*
* Client side of Settle, for `keventd -S`: ask the running keventd.
* Returns 0 settled, 1 timeout, -1 when no bus answers (fall back to
* polling the kernel seqnum).
*/
int devbus_client_settle(int timeout_s)
{
link_client_t *c;
link_reader_t r;
const link_reply_t *reply;
int rc, settled = 0;
c = link_client_open(DEVBUS_SOCKET);
if (!c)
return -1;
rc = link_client_call_v(c, DEVBUS_PATH_OBJECT, DEVBUS_INTERFACE,
"Settle", "u", (uint32_t)timeout_s);
if (rc != LINK_CALL_OK) {
link_client_close(c);
return -1;
}
reply = link_client_reply(c);
if (reply && reply->body) {
link_reader_init(&r, reply->body, reply->body_len);
link_r_bool(&r, &settled);
}
link_client_close(c);
return settled ? 0 : 1;
}
#endif /* HAVE_DBUS */
+98 -17
View File
@@ -34,6 +34,8 @@
* THE SOFTWARE.
*/
#include "config.h"
#include <ctype.h>
#include <errno.h>
#include <dirent.h>
@@ -303,6 +305,26 @@ static int applied_has_kmod(const struct uevent *ev)
return 0;
}
/*
* Queue bookkeeping for the D-Bus Settle/QueueEmpty surface: the
* highest kernel seqnum keventd has fully handled. The baseline is
* taken at bus init, events from before keventd started are not ours
* to wait for.
*/
static unsigned long long seq_processed;
static void note_seqnum(const char *seqnum)
{
unsigned long long seq;
if (!seqnum)
return;
seq = strtoull(seqnum, NULL, 10);
if (seq > seq_processed)
seq_processed = seq;
}
/*
* Handle a single uevent from the kernel.
*/
@@ -395,6 +417,12 @@ static void handle_uevent(char *buf, size_t len)
udevdb_write(&ev);
}
/* Fully handled: only now may QueueEmpty/Settle count it */
note_seqnum(ev.seqnum);
#ifdef HAVE_DBUS
devbus_notify(ev.devpath, uevent_action_str(ev.action));
#endif
uevent_env_free(&ev);
}
@@ -485,8 +513,7 @@ static void sigterm_cb(uev_t *w, void *arg, int events)
static void sighup_cb(uev_t *w, void *arg, int events)
{
(void)w; (void)arg; (void)events;
rules_free(&rules);
rules_load_all(&rules, rules_dir);
kev_rules_reload();
}
static void sigchld_cb(uev_t *w, void *arg, int events)
@@ -550,24 +577,66 @@ static void uevent_cb(uev_t *w, void *arg, int events)
* means "/dev is populated, persistent symlinks are live" rather
* than just "listening on netlink".
*/
/*
* Read the kernel's uevent sequence counter. Shared by the coldplug
* gate, settle mode, and the D-Bus queue state.
*/
static int read_uevent_seqnum(unsigned long long *out)
{
FILE *fp;
int rc;
fp = fopen("/sys/kernel/uevent_seqnum", "r");
if (!fp)
return -1;
rc = fscanf(fp, "%llu", out) == 1 ? 0 : -1;
fclose(fp);
return rc;
}
unsigned long long kev_seq_processed(void)
{
return seq_processed;
}
void kev_seq_baseline(void)
{
unsigned long long seq;
if (!read_uevent_seqnum(&seq) && seq > seq_processed)
seq_processed = seq;
}
int kev_queue_empty(void)
{
unsigned long long seq;
if (read_uevent_seqnum(&seq))
return 1; /* no counter, nothing to wait for */
return seq <= seq_processed;
}
int kev_rules_reload(void)
{
rules_free(&rules);
return rules_load_all(&rules, rules_dir);
}
static void coldplug_pidfile_cb(uev_t *w, void *arg, int events)
{
struct coldplug_gate *cg = arg;
unsigned long long cur = 0;
struct timespec now;
FILE *fp;
long dt_ms;
(void)events;
fp = fopen("/sys/kernel/uevent_seqnum", "r");
if (!fp)
if (read_uevent_seqnum(&cur))
return;
if (fscanf(fp, "%llu", &cur) != 1)
cur = cg->last_seq;
fclose(fp);
clock_gettime(CLOCK_MONOTONIC, &now);
if (!cg->primed) {
@@ -610,17 +679,11 @@ static int cmd_settle(int timeout_s, int stable_ms)
last_change = start;
while (1) {
FILE *fp;
fp = fopen("/sys/kernel/uevent_seqnum", "r");
if (!fp) {
if (read_uevent_seqnum(&cur_seq)) {
fprintf(stderr, "keventd: cannot read uevent_seqnum: %s\n",
strerror(errno));
return 1;
}
if (fscanf(fp, "%llu", &cur_seq) != 1)
cur_seq = last_seq;
fclose(fp);
clock_gettime(CLOCK_MONOTONIC, &now);
@@ -736,8 +799,17 @@ int main(int argc, char *argv[])
}
}
if (do_settle)
if (do_settle) {
#ifdef HAVE_DBUS
/* Ask the running keventd, which tracks the queue where
* the events flow; fall back to polling the kernel's
* seqnum when no bus is up. */
int rc = devbus_client_settle(settle_timeout);
if (rc >= 0)
return rc;
#endif
return cmd_settle(settle_timeout, 200);
}
if (!foreground) {
openlog("keventd", LOG_PID, LOG_DAEMON);
@@ -787,6 +859,12 @@ int main(int argc, char *argv[])
if (nlgroups && !passive)
rebc_init(nlgroups);
#ifdef HAVE_DBUS
/* Bus up before coldplug so Settle covers the coldplug drain */
kev_seq_baseline();
devbus_init(&ctx, passive);
#endif
/* Run coldplug if requested */
if (do_coldplug)
coldplug();
@@ -811,6 +889,9 @@ int main(int argc, char *argv[])
uev_run(&ctx, 0);
#ifdef HAVE_DBUS
devbus_exit();
#endif
if (rebc_fd != -1)
close(rebc_fd);
close(nlfd);
+16
View File
@@ -163,6 +163,22 @@ int coldplug (void);
int coldplug_trigger (const char *action, const char *subsys_glob);
int uevent_action_valid(const char *action);
/* keventd.c: queue and rules state, for the D-Bus layer */
unsigned long long kev_seq_processed(void);
void kev_seq_baseline (void);
int kev_queue_empty (void);
int kev_rules_reload (void);
#ifdef HAVE_DBUS
#include <uev/uev.h>
/* dbus.c: org.finit.Device1 on /run/keventd/bus */
int devbus_init (uev_ctx_t *ctx, int passive);
void devbus_exit (void);
void devbus_notify (const char *devpath, const char *action);
int devbus_client_settle(int timeout_s);
#endif
#endif /* FINIT_KEVENTD_H_ */
/**