/* Unified device manager - kernel events, device nodes, symlinks, conditions * * TODO / known limitations * - firmware_load() copies synchronously in the main event loop; a * multi-MB blob (e.g. iwlwifi) blocks all other event handling for * the duration. Worker pool / fork-per-event would unblock this * (see .notes/keventd-eudev-gap-analysis.md, gap #8). * - firmware_load() does not handle compressed firmware (.xz, .zst). * Modern linux-firmware ships .xz blobs; kernels with * CONFIG_FW_LOADER_COMPRESS decompress in-kernel via direct * loading, but the legacy /sys/.../loading path used here passes * raw bytes only. Adding liblzma/libzstd would close this gap for * drivers using the userhelper fallback on systems without * CONFIG_FW_LOADER_COMPRESS. * * Copyright (c) 2021-2025 Joachim Wiberg * * 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" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef _LIBITE_LITE # include #else # include #endif #include #include "keventd.h" #include "rules.h" #include "sysfs.h" #include "udevdb.h" #include "cond.h" #include "pid.h" #include "util.h" #define KEVENTD_VERSION "5.0" #define _PATH_SYSFS_PWR "/sys/class/power_supply" /* Default netlink group for uevent rebroadcast (libudev-zero convention) */ #define REBC_DEFAULT_NLGROUP 4 /* Used by settle and coldplug at startup */ struct coldplug_gate { unsigned long long last_seq; uint64_t stamp; /* kev_now_ms() of last change */ int primed; }; /* * 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; } uint64_t kev_now_ms(void) { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (uint64_t)ts.tv_sec * 1000 + ts.tv_nsec / 1000000; } /* * Track the kernel seqnum: returns 1 once it has been unchanged for * stable_ms, 0 while still moving (or before the first sample). */ static int seqnum_stable(struct coldplug_gate *cg, int stable_ms) { unsigned long long cur = 0; uint64_t now; if (read_uevent_seqnum(&cur)) return 0; now = kev_now_ms(); if (!cg->primed || cur != cg->last_seq) { cg->last_seq = cur; cg->stamp = now; cg->primed = 1; return 0; } return now - cg->stamp >= (uint64_t)stable_ms; } static int num_ac_online; static int num_ac; static int level; static int logon; static int passive; /* power-supply only, no device management */ static struct rule_list rules = TAILQ_HEAD_INITIALIZER(rules); static char *rules_dir; /* extra rules dir from -r option */ int debug; /* debug in other modules as well */ void logit(int prio, const char *fmt, ...) { va_list ap; va_start(ap, fmt); if (logon) vsyslog(prio, fmt, ap); else if (prio <= level) { vfprintf(stderr, fmt, ap); fprintf(stderr, "\n"); } va_end(ap); } #define panic(fmt, args...) { logit(LOG_CRIT, fmt ":%s", ##args, strerror(errno)); exit(1); } #define warn(fmt, args...) { logit(LOG_WARNING, fmt ":%s", ##args, strerror(errno)); } /* * Netlink rebroadcast support. * * The Linux kernel sends uevents to netlink multicast group 1 (bit 0) * of NETLINK_KOBJECT_UEVENT. Only the device manager should listen on * this raw kernel group. Userspace consumers (e.g., applications using * libudev) expect to receive processed events on a separate group -- * conventionally group 4 (bit 2), established by systemd/udevd. * * libudev-zero (https://github.com/illiliti/libudev-zero), a daemonless * replacement for libudev, listens on group 0x4 for these rebroadcast * events. Without rebroadcast, graphical applications, Wayland/X11 * compositors, libinput, and anything else using libudev to monitor * device hotplug will never see any events. * * Rebroadcast is enabled by default to group 0x4. Use -g to override * the group mask, or -G to disable rebroadcast entirely. Bit 0 is * always masked out to prevent a feedback loop with the kernel group. */ static int rebc_fd = -1; static unsigned int rebc_nlgroups; static void rebc_init(unsigned int nlgroups) { /* Mask out bit 0 (kernel group) to prevent feedback loop */ if (nlgroups & 1) { logit(LOG_WARNING, "rebroadcast group mask 0x%x includes kernel group (bit 0), masking it out", nlgroups); nlgroups &= ~1U; } if (!nlgroups) { logit(LOG_WARNING, "no valid rebroadcast groups remaining, rebroadcast disabled"); return; } rebc_fd = socket(AF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC, NETLINK_KOBJECT_UEVENT); if (rebc_fd == -1) { warn("failed creating rebroadcast socket"); return; } rebc_nlgroups = nlgroups; logit(LOG_NOTICE, "rebroadcasting uevents to netlink group(s) 0x%x", nlgroups); } static void rebc_event(char *buf, size_t len) { struct sockaddr_nl sa = { 0 }; struct msghdr hdr = { 0 }; struct iovec iov; if (rebc_fd == -1) return; iov.iov_base = buf; iov.iov_len = len; sa.nl_family = AF_NETLINK; sa.nl_groups = rebc_nlgroups; hdr.msg_name = &sa; hdr.msg_namelen = sizeof(sa); hdr.msg_iov = &iov; hdr.msg_iovlen = 1; if (sendmsg(rebc_fd, &hdr, 0) == -1) logit(LOG_DEBUG, "rebroadcast failed: %s", strerror(errno)); } static int check_online(const char *online) { int val; if (!online) return 0; val = atoi(online); logit(LOG_INFO, "AC %s", val ? "connected" : "disconnected"); return val; } static int is_ac(const char *type) { static const char *types[] = { "Mains", "USB", "BrickID", "Wireless", NULL }; int i; for (i = 0; types[i]; i++) { if (!strncmp(type, types[i], strlen(types[i]))) return 1; } return 0; } /* * Handle power_supply change events (original keventd functionality). */ static void power_supply_change(const struct uevent *ev, char *buf, size_t len) { int ac = 0; size_t i, hdrlen; /* Skip past header to key=value pairs */ hdrlen = strlen(buf) + 1; if (ev->devpath) hdrlen += strlen(ev->devpath) + 1; for (i = hdrlen; i < len; ) { char *line = buf + i; if (!*line) break; if (!strncmp(line, "POWER_SUPPLY_TYPE=", 18)) { ac = is_ac(&line[18]); } else if (!strncmp(line, "POWER_SUPPLY_ONLINE=", 20) && ac) { if (check_online(&line[20])) { if (!num_ac_online) cond_emit(COND_SYS, "pwr/ac", 1); num_ac_online++; } else { if (num_ac_online > 0) num_ac_online--; if (!num_ac_online) cond_emit(COND_SYS, "pwr/ac", 0); } } i += strlen(line) + 1; } } /* * True if rules have queued the kmod builtin for this event. Used to * suppress the direct modprobe path when 80-drivers.rules will fork * modprobe anyway -- otherwise coldplug runs modprobe twice per event. */ static int applied_has_kmod(const struct uevent *ev) { int i; for (i = 0; i < ev->applied.nruncmds; i++) { const char *cmd = ev->applied.run_cmds[i]; if (ev->applied.run_types[i] != RUN_BUILTIN) continue; if (!strncmp(cmd, "kmod", 4) && (cmd[4] == '\0' || isspace((unsigned char)cmd[4]))) return 1; } 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. */ static void handle_uevent(char *buf, size_t len) { struct uevent ev; if (uevent_parse(buf, len, &ev)) return; logit(LOG_DEBUG, "uevent: %s@%s subsys=%s dev=%s major=%d minor=%d", uevent_action_str(ev.action), ev.devpath ?: "", ev.subsystem ?: "", ev.devname ?: "", ev.major, ev.minor); /* Apply udev rules before built-in device handling */ if (!passive) rules_apply(&rules, &ev); switch (ev.action) { case ACT_ADD: if (!passive) { /* Firmware loading takes priority */ if (ev.firmware) firmware_load(&ev); if (ev.modalias && !applied_has_kmod(&ev)) modprobe_load(ev.modalias); /* Create device node if we have the info */ if (ev.major >= 0 && ev.minor >= 0 && ev.devname) devnode_add(&ev); /* Rename network interface if NAME= rule was applied */ if (ev.subsystem && !strcmp(ev.subsystem, "net")) netdev_add(&ev); /* Create symlinks */ symlink_add(&ev); /* class// -- covers sysfs-only * devices that don't get a /dev node. */ class_cond(&ev, 1); } break; case ACT_REMOVE: if (!passive) { class_cond(&ev, 0); /* Remove symlinks first */ symlink_del(&ev); /* Remove device node, mirrors devnode_add(): net * interfaces carry a devname but have no node. */ if (ev.major >= 0 && ev.minor >= 0 && ev.devname) devnode_del(&ev); } break; case ACT_CHANGE: /* Handle power supply changes */ if (ev.subsystem && !strcmp(ev.subsystem, "power_supply")) power_supply_change(&ev, buf, len); break; case ACT_BIND: if (!passive) driver_cond(&ev, 1); break; case ACT_UNBIND: if (!passive) driver_cond(&ev, 0); break; default: break; } /* Execute RUN+= commands from matched rules */ if (!passive) rules_run_cmds(&ev); /* Persist device properties to /run/udev/data/ */ if (!passive) { if (ev.action == ACT_REMOVE) udevdb_delete(&ev); else 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); } static void init_power_supply(void) { struct dirent **d = NULL; char path[384]; int i, n; n = scandir(_PATH_SYSFS_PWR, &d, NULL, alphasort); for (i = 0; i < n; i++) { const char *nm = d[i]->d_name; char buf[10]; snprintf(path, sizeof(path), "%s/%s/type", _PATH_SYSFS_PWR, nm); if (!sysfs_read_file(path, buf, sizeof(buf)) && is_ac(buf)) { num_ac++; snprintf(path, sizeof(path), "%s/%s/online", _PATH_SYSFS_PWR, nm); if (!sysfs_read_file(path, buf, sizeof(buf))) { if (check_online(buf)) num_ac_online++; } } free(d[i]); } if (n > 0) free(d); /* if any power_supply is online, or none can be found */ if (num_ac == 0 || num_ac_online > 0) cond_emit(COND_SYS, "pwr/ac", 1); } static void disable_uevent_helper(void) { FILE *fp; fp = fopen("/proc/sys/kernel/hotplug", "w"); if (!fp) return; fputs("\n", fp); fclose(fp); } static void set_logging(int prio) { setlogmask(LOG_UPTO(prio)); level = prio; } static void sigusr1_cb(uev_t *w, void *arg, int events) { (void)w; (void)arg; (void)events; debug ^= 1; set_logging(debug ? LOG_DEBUG : LOG_NOTICE); } static void sigterm_cb(uev_t *w, void *arg, int events) { (void)arg; (void)events; uev_exit(w->ctx); } static void sighup_cb(uev_t *w, void *arg, int events) { (void)w; (void)arg; (void)events; kev_rules_reload(); } static void sigchld_cb(uev_t *w, void *arg, int events) { (void)w; (void)arg; (void)events; /* Reap all the children. */ while (waitpid(-1, NULL, WNOHANG) > 0) ; } static void uevent_cb(uev_t *w, void *arg, int events) { char *uevent_buf = arg; char rebc_buf[UEVENT_BUFFER_SIZE]; int len; if (events & UEV_ERROR) { warn("netlink watcher error, re-arming"); uev_io_start(w); return; } len = recv(w->fd, uevent_buf, UEVENT_BUFFER_SIZE - 1, MSG_DONTWAIT); if (len == -1) { switch (errno) { case EINTR: case EAGAIN: return; case ENOBUFS: warn("lost events, buffer overflow"); return; default: panic("recv failed"); } } uevent_buf[len] = 0; /* Skip libudev events (start with "libudev") */ if (!strncmp(uevent_buf, "libudev", 7)) return; /* * Save raw buffer before handle_uevent() -- uevent_parse() * modifies the buffer in-place (splits @ and = separators). * Rebroadcast needs the original kernel format intact. */ if (rebc_fd != -1) memcpy(rebc_buf, uevent_buf, len); handle_uevent(uevent_buf, len); /* Rebroadcast after processing so consumers see ready state. */ if (rebc_fd != -1) rebc_event(rebc_buf, len); } /* * Pidfile gate used with -c: defer pidfile() until the kernel's * uevent_seqnum has been stable for 200ms, so that * means "/dev is populated, persistent symlinks are live" rather * than just "listening on netlink". */ unsigned long long kev_seq_processed(void) { return seq_processed; } static 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; (void)events; if (!seqnum_stable(cg, 200)) return; pidfile(NULL); logit(LOG_NOTICE, "keventd ready, coldplug queue drained"); uev_timer_stop(w); } /* * Stop-gap "settle" mode: poll /sys/kernel/uevent_seqnum until it has * been stable for `stable_ms` (default 200ms), or until `timeout_s` * elapses. Imperfect -- a slow probe firing after we return still * races -- but matches what typical `udevadm settle` users want. * * The proper fix lives in the IPC/queue work (see audit doc). */ static int cmd_settle(int timeout_s, int stable_ms) { struct coldplug_gate cg = { 0 }; uint64_t deadline = kev_now_ms() + (uint64_t)timeout_s * 1000; while (1) { if (seqnum_stable(&cg, stable_ms)) return 0; if (!cg.primed) { fprintf(stderr, "keventd: cannot read uevent_seqnum: %s\n", strerror(errno)); return 1; } if (kev_now_ms() >= deadline) return 1; usleep(50000); } } static int usage(int rc) { fprintf(stderr, "Usage: keventd [-dGhnpSv] [-c] [-g GROUP] [-r DIR] [-t SECONDS]\n" "\n" "Options:\n" " -c Run coldplug at startup\n" " -d Enable debug mode (foreground, verbose)\n" " -g GROUP Override netlink rebroadcast group (default: %d)\n" " -G Disable netlink rebroadcast entirely\n" " -h Show this help text\n" " -n Run in foreground (no daemon)\n" " -p Passive mode: power supply events only (no device management)\n" " -r DIR Extra rules directory, overrides the standard udev paths\n" " -S Settle: wait until kernel uevent queue is quiet, then exit\n" " -t SEC Settle timeout in seconds (default: 30, with -S)\n" " -v Show version\n" "\n", REBC_DEFAULT_NLGROUP); return rc; } /* * Unified device manager daemon. * * Started by Finit as soon as possible when base filesystem is up, * modules have been probed. Handles: * - Device node creation/removal in /dev * - Persistent symlinks in /dev/disk/by-*, /dev/input/by-* * - Module loading via MODALIAS * - Firmware loading via FIRMWARE * - Power supply conditions (sys/pwr/ac) * - Device conditions (dev/) */ int main(int argc, char *argv[]) { uev_t netlink_watcher, sigusr1_watcher, sigterm_watcher, sighup_watcher, sigchld_watcher; unsigned int nlgroups = REBC_DEFAULT_NLGROUP; static char uevent_buf[UEVENT_BUFFER_SIZE]; static struct coldplug_gate cg; struct sockaddr_nl nls = { 0 }; static uev_t coldplug_timer; int settle_timeout = 30; int do_coldplug = 0; int foreground = 0; int do_settle = 0; uev_ctx_t ctx; int nlfd; int c; /* Device nodes are created with explicit MODE= from rules or the * built-in devrules table; clear umask so mknod() honors the * requested bits verbatim instead of masking them. */ umask(0); while ((c = getopt(argc, argv, "cdg:Ghnpr:St:v")) != -1) { switch (c) { case 'c': do_coldplug = 1; break; case 'd': debug = 1; foreground = 1; break; case 'g': nlgroups = (unsigned int)atoi(optarg); break; case 'G': nlgroups = 0; break; case 'h': return usage(0); case 'n': foreground = 1; break; case 'p': passive = 1; break; case 'r': rules_dir = optarg; break; case 'S': do_settle = 1; break; case 't': settle_timeout = atoi(optarg); break; case 'v': printf("keventd v%s\n", KEVENTD_VERSION); return 0; default: return usage(1); } } 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); set_logging(LOG_NOTICE); logon = 1; } else { set_logging(debug ? LOG_DEBUG : LOG_NOTICE); } uev_init(&ctx); uev_signal_init(&ctx, &sigusr1_watcher, sigusr1_cb, NULL, SIGUSR1); uev_signal_init(&ctx, &sigterm_watcher, sigterm_cb, NULL, SIGTERM); uev_signal_init(&ctx, &sighup_watcher, sighup_cb, NULL, SIGHUP); uev_signal_init(&ctx, &sigchld_watcher, sigchld_cb, NULL, SIGCHLD); /* Initialize condition directories */ init_power_supply(); /* Disable legacy kernel uevent helper; we own events via netlink. * Skip in passive mode -- the hotplug daemon handles this. */ if (!passive) disable_uevent_helper(); /* Set up netlink socket for kernel uevents */ nlfd = socket(PF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC, NETLINK_KOBJECT_UEVENT); if (nlfd == -1) panic("failed creating netlink socket"); nls.nl_family = AF_NETLINK; nls.nl_pid = 0; nls.nl_groups = 1; /* Kernel uevents are on group 1 only */ if (bind(nlfd, (void *)&nls, sizeof(struct sockaddr_nl))) panic("bind failed"); /* Increase receive buffer to reduce event loss */ { int rcvbuf = 1024 * 1024; setsockopt(nlfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf)); } uev_io_init(&ctx, &netlink_watcher, uevent_cb, uevent_buf, nlfd, UEV_READ); /* Initialize rebroadcast socket (default on, -G to disable). * Skip in passive mode -- the hotplug daemon rebroadcasts. */ 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(); /* Load udev rules from standard directories (and -r extra_dir) */ if (!passive) rules_load_all(&rules, rules_dir); /* * With -c, defer pidfile until coldplug events have actually been * drained, so means "/dev populated", not just * "listening on netlink". Without -c, nothing is queued, drop * the pidfile immediately as before. */ if (do_coldplug) { uev_timer_init(&ctx, &coldplug_timer, coldplug_pidfile_cb, &cg, 100, 100); logit(LOG_NOTICE, "keventd v%s started, draining coldplug queue...", KEVENTD_VERSION); } else { pidfile(NULL); logit(LOG_NOTICE, "keventd v%s started, waiting for events...", KEVENTD_VERSION); } uev_run(&ctx, 0); #ifdef HAVE_DBUS devbus_exit(); #endif if (rebc_fd != -1) close(rebc_fd); close(nlfd); rules_free(&rules); logit(LOG_NOTICE, "keventd shutting down"); return 0; } /** * Local Variables: * indent-tabs-mode: t * c-file-style: "linux" * End: */