From 30becc2c380428ca446656ebdcb547cf0cd76067 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 6 May 2026 23:50:12 +0200 Subject: [PATCH 01/24] runparts: improve usage text Signed-off-by: Joachim Wiberg --- src/runparts.c | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/runparts.c b/src/runparts.c index 84c52c98..2645ed05 100644 --- a/src/runparts.c +++ b/src/runparts.c @@ -193,7 +193,19 @@ int run_parts(char *dir, char *cmd, const char *env[], int progress, int sysv) #ifndef __FINIT__ static int usage(int rc) { - warnx("usage: runparts [-bdhps?] DIRECTORY"); + fprintf(stderr, + "Usage: runparts [OPTIONS] DIRECTORY\n" + "\n" + "Run all executable scripts in DIRECTORY in alphabetical order.\n" + "Scripts prefixed S are called with 'start', K with 'stop'.\n" + "\n" + "Options:\n" + " -b Batch mode, disable progress output and ANSI colors\n" + " -d Enable debug output\n" + " -h This help text\n" + " -p Show progress and exit status for each script\n" + " -s SysV compat, only run Sname and Kname scripts\n" + "\n"); return rc; } From bfc3a69ef604d3eaf8fbbe633ccd3af9458bc6a6 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 29 Jan 2026 12:13:03 +0100 Subject: [PATCH 02/24] keventd: expand scope to become a unified device manager Evolve keventd from a power_supply-only monitor into a full device manager capable of replacing mdev/mdevd on embedded systems. This is the first step towards Finit v5.0 where keventd absorbs devmon. New capabilities: - Parse all uevent actions (add, remove, change, bind, unbind) - Create and remove /dev nodes with subsystem-aware permissions - Create persistent symlinks in /dev/disk/by-{id,path} and /dev/input/by-{id,path}, tracked for cleanup on device removal - Load firmware from /lib/firmware/ via the sysfs loading protocol - Spawn modprobe for MODALIAS events (async, non-blocking) - Coldplug support via -c flag (walks /sys/devices to replay events) - Set dev/* conditions for Finit's service dependency system The original power_supply monitoring and sys/pwr/ac condition are preserved. New files: keventd.h (structures/API), uevent.c (all device logic). The receive buffer is increased to 8K with a 1MB socket buffer to reduce event loss during coldplug bursts. Signed-off-by: Joachim Wiberg --- configure.ac | 4 +- src/Makefile.am | 2 +- src/keventd.c | 277 +++++++++++---- src/keventd.h | 97 ++++++ src/uevent.c | 898 ++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 1215 insertions(+), 63 deletions(-) create mode 100644 src/keventd.h create mode 100644 src/uevent.c diff --git a/configure.ac b/configure.ac index 0b213b53..68668d12 100644 --- a/configure.ac +++ b/configure.ac @@ -118,7 +118,7 @@ AC_PLUGIN([resolvconf], [no], [Setup necessary files for resolvconf]) AC_PLUGIN([x11-common], [no], [Console setup (for X)]) AC_PLUGIN([netlink], [yes], [Basic netlink plugin for IFUP/IFDN and GW events. Can be replaced with externally built plugin that links with libnl or similar.]) AC_PLUGIN([hook-scripts], [no], [Trigger script execution from hook points]) -AC_PLUGIN([hotplug], [yes], [Start udevd or mdev kernel event datamon]) +AC_PLUGIN([hotplug], [no], [Start udevd or mdev kernel event datamon]) AC_PLUGIN([rtc], [yes], [Save and restore RTC using hwclock]) AC_PLUGIN([tty], [yes], [Automatically activate new TTYs, e.g. USB-to-serial]) AC_PLUGIN([urandom], [yes], [Setup and save random seed at boot/shutdown]) @@ -171,7 +171,7 @@ AC_ARG_WITH(random-seed, [random_seed=$withval], [random_seed=yes]) AC_ARG_WITH(keventd, - AS_HELP_STRING([--with-keventd], [Enable built-in keventd, default: no]),, [with_keventd=no]) + AS_HELP_STRING([--with-keventd], [Enable built-in keventd device manager, default: yes]),, [with_keventd=yes]) AC_ARG_WITH(sulogin, AS_HELP_STRING([--with-sulogin@<:@=USER@:>@], [Enable built-in sulogin, optional USER to request password for (default root), default: no.]),[sulogin=$withval],[with_sulogin=no]) diff --git a/src/Makefile.am b/src/Makefile.am index df8a8dfd..e685e2e8 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -30,7 +30,7 @@ getty_CFLAGS = -W -Wall -Wextra -std=gnu99 getty_CFLAGS += $(lite_CFLAGS) getty_LDADD = $(lite_LIBS) -keventd_SOURCES = keventd.c iwatch.c iwatch.h util.c util.h +keventd_SOURCES = keventd.c keventd.h uevent.c util.c util.h keventd_CFLAGS = -W -Wall -Wextra -std=gnu99 keventd_CFLAGS += $(lite_CFLAGS) keventd_LDADD = $(lite_LIBS) diff --git a/src/keventd.c b/src/keventd.c index ea2470e0..c4c5a3a6 100644 --- a/src/keventd.c +++ b/src/keventd.c @@ -1,4 +1,17 @@ -/* Listens to kernel events like AC power status and manages sys/ conditions +/* 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 * @@ -23,6 +36,7 @@ #include #include +#include #include #include #include @@ -33,6 +47,7 @@ #include #include +#include #include #include @@ -44,11 +59,13 @@ # include #endif +#include "keventd.h" #include "cond.h" #include "pid.h" #include "util.h" -#define _PATH_SYSFS_PWR "/sys/class/power_supply" +#define KEVENTD_VERSION "5.0" +#define _PATH_SYSFS_PWR "/sys/class/power_supply" static int num_ac_online; static int num_ac; @@ -75,7 +92,7 @@ void logit(int prio, const char *fmt, ...) #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)); } -static void sys_cond(char *cond, int set) +static void sys_cond(const char *cond, int set) { char oneshot[256]; @@ -108,7 +125,7 @@ static int fgetline(char *path, char *buf, size_t len) return 0; } -static int check_online(char *online) +static int check_online(const char *online) { int val; @@ -121,9 +138,9 @@ static int check_online(char *online) return val; } -static int is_ac(char *type) +static int is_ac(const char *type) { - char *types[] = { + static const char *types[] = { "Mains", "USB", "BrickID", @@ -140,7 +157,103 @@ static int is_ac(char *type) return 0; } -static void init(void) +/* + * Handle power_supply change events (original keventd functionality). + */ +static void power_supply_change(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) + sys_cond("pwr/ac", 1); + num_ac_online++; + } else { + if (num_ac_online > 0) + num_ac_online--; + if (!num_ac_online) + sys_cond("pwr/ac", 0); + } + } + + i += strlen(line) + 1; + } +} + +/* + * 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); + + switch (ev.action) { + case ACT_ADD: + /* Firmware loading takes priority */ + if (ev.firmware) + firmware_load(&ev); + + /* Module loading */ + if (ev.modalias) + modprobe_load(ev.modalias); + + /* Create device node if we have the info */ + if (ev.major >= 0 && ev.minor >= 0 && ev.devname) + devnode_add(&ev); + + /* Create symlinks */ + symlink_add(&ev); + break; + + case ACT_REMOVE: + /* Remove symlinks first */ + symlink_del(&ev); + + /* Remove device node */ + if (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: + case ACT_UNBIND: + /* Driver bind/unbind - could trigger conditions */ + break; + + default: + break; + } +} + +static void init_power_supply(void) { struct dirent **d = NULL; char *cond_dirs[] = { @@ -161,7 +274,7 @@ static void init(void) n = scandir(_PATH_SYSFS_PWR, &d, NULL, alphasort); for (i = 0; i < n; i++) { char *nm = d[i]->d_name; - char buf[10]; + char buf[10]; snprintf(path, sizeof(path), "%s/%s/type", _PATH_SYSFS_PWR, nm); if (!fgetline(path, buf, sizeof(buf)) && is_ac(buf)) { @@ -184,6 +297,16 @@ static void init(void) sys_cond("pwr/ac", 1); } +static void init_dev_condition_dir(void) +{ + char dir[256]; + + /* Create /run/finit/cond/dev/ directory for device conditions */ + snprintf(dir, sizeof(dir), "%s", _PATH_CONDDEV); + if (mkpath(dir, 0755) && errno != EEXIST) + warn("Failed creating dev condition directory %s", dir); +} + static void set_logging(int prio) { setlogmask(LOG_UPTO(prio)); @@ -204,109 +327,143 @@ static void shut_down(int signo) running = 0; } +static int usage(int rc) +{ + fprintf(stderr, + "Usage: keventd [-dhnv] [-c]\n" + "\n" + "Options:\n" + " -c Run coldplug at startup\n" + " -d Enable debug mode (foreground, verbose)\n" + " -h Show this help text\n" + " -n Run in foreground (no daemon)\n" + " -v Show version\n" + "\n"); + + return rc; +} + /* + * Unified device manager daemon. + * * Started by Finit as soon as possible when base filesystem is up, - * modules have been probed, or insmodded from /etc/finit.conf, so by - * now we should have /sys/class/power_supply/ available for probing. - * If none is found we assert /sys/pwr/ac condition anyway, this is what - * systemd does (ConditionACPower) and also makes most sense. + * 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[]) { struct sockaddr_nl nls = { 0 }; struct pollfd pfd; - char buf[1024]; - - if (argc > 1) { - if (!strcmp(argv[1], "-d")) + char buf[UEVENT_BUFFER_SIZE]; + int do_coldplug = 0; + int foreground = 0; + 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, "cdhnv")) != -1) { + switch (c) { + case 'c': + do_coldplug = 1; + break; + case 'd': debug = 1; + foreground = 1; + break; + case 'h': + return usage(0); + case 'n': + foreground = 1; + break; + case 'v': + printf("keventd v%s\n", KEVENTD_VERSION); + return 0; + default: + return usage(1); + } } - if (!debug) { + if (!foreground) { openlog("keventd", LOG_PID, LOG_DAEMON); set_logging(LOG_NOTICE); logon = 1; + } else { + set_logging(debug ? LOG_DEBUG : LOG_NOTICE); } signal(SIGUSR1, toggle_debug); signal(SIGTERM, shut_down); - init(); + signal(SIGCHLD, SIG_IGN); /* Don't wait for modprobe children */ + + /* Initialize condition directories */ + init_power_supply(); + init_dev_condition_dir(); + /* Set up netlink socket for kernel uevents */ pfd.events = POLLIN; - pfd.fd = socket(PF_NETLINK, SOCK_DGRAM, NETLINK_KOBJECT_UEVENT); + pfd.fd = socket(PF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC, NETLINK_KOBJECT_UEVENT); if (pfd.fd == -1) panic("failed creating netlink socket"); nls.nl_family = AF_NETLINK; nls.nl_pid = 0; - nls.nl_groups = -1; + nls.nl_groups = 1; /* Kernel uevents are on group 1 only */ if (bind(pfd.fd, (void *)&nls, sizeof(struct sockaddr_nl))) panic("bind failed"); - logit(LOG_DEBUG, "Waiting for events ..."); + /* Increase receive buffer to reduce event loss */ + { + int rcvbuf = 1024 * 1024; + setsockopt(pfd.fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf)); + } + + /* Run coldplug if requested */ + if (do_coldplug) + coldplug(); + + logit(LOG_NOTICE, "keventd v%s started, waiting for events...", KEVENTD_VERSION); + while (running) { - char *path = NULL; - int ac = 0; - int i, len; + int len; if (-1 == poll(&pfd, 1, -1)) { if (errno == EINTR) continue; - break; } - len = recv(pfd.fd, buf, sizeof(buf), MSG_DONTWAIT); + len = recv(pfd.fd, buf, sizeof(buf) - 1, MSG_DONTWAIT); if (len == -1) { switch (errno) { case EINTR: continue; case ENOBUFS: - warn("lost events"); + warn("lost events, buffer overflow"); continue; default: - panic("unhandled"); + panic("recv failed"); continue; } } buf[len] = 0; - logit(LOG_DEBUG, "%s", buf); - - /* skip libusb events, focus on kernel events/changes */ - if (strncmp(buf, "change@", 7)) - continue; - /* XXX: currently limited to monitoring this subsystem */ - if (!strstr(buf, "power_supply")) + /* Skip libudev events (start with "libudev") */ + if (!strncmp(buf, "libudev", 7)) continue; - i = 0; - while (i < len) { - char *line = buf + i; - - logit(LOG_DEBUG, "%s", line); - if (!strncmp(line, "DEVPATH=", 8)) { - path = &line[8]; - logit(LOG_DEBUG, "Got path %s", path); - } else 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) - sys_cond("pwr/ac", 1); - num_ac_online++; - } else { - if (num_ac_online > 0) - num_ac_online--; - if (!num_ac_online) - sys_cond("pwr/ac", 0); - } - } - - i += strlen(line) + 1; - } + handle_uevent(buf, len); } + close(pfd.fd); + logit(LOG_NOTICE, "keventd shutting down"); return 0; } diff --git a/src/keventd.h b/src/keventd.h new file mode 100644 index 00000000..f1156905 --- /dev/null +++ b/src/keventd.h @@ -0,0 +1,97 @@ +/* Unified device manager for Finit - uevent handling, device nodes, symlinks + * + * 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. + */ + +#ifndef FINIT_KEVENTD_H_ +#define FINIT_KEVENTD_H_ + +#ifdef _LIBITE_LITE +# include /* BSD sys/queue.h API */ +#else +# include /* BSD sys/queue.h API */ +#endif +#include + +/* Maximum uevent buffer size (kernel uses 8192 internally) */ +#define UEVENT_BUFFER_SIZE 8192 + +/* Uevent actions from kernel */ +typedef enum { + ACT_UNKNOWN = 0, + ACT_ADD, + ACT_REMOVE, + ACT_CHANGE, + ACT_MOVE, + ACT_ONLINE, + ACT_OFFLINE, + ACT_BIND, + ACT_UNBIND, +} uevent_action_t; + +/* + * Parsed uevent structure. + * Pointers are into the receive buffer, zero-copy. + */ +struct uevent { + uevent_action_t action; + char *devpath; /* /devices/pci0000:00/... */ + char *subsystem; /* block, input, net, power_supply */ + char *devname; /* sda, event0, ttyUSB0 */ + char *devtype; /* disk, partition */ + int major; + int minor; + char *modalias; /* module alias for auto-loading */ + char *firmware; /* firmware file name request */ + char *seqnum; /* kernel sequence number */ + char *driver; /* driver name */ +}; + +/* Tracked symlink for cleanup on device removal */ +struct dev_symlink { + TAILQ_ENTRY(dev_symlink) link; + char *devpath; /* sysfs devpath (key for removal) */ + char *linkpath; /* /dev/disk/by-id/... */ +}; + +/* Function prototypes - uevent.c */ +int uevent_parse (char *buf, size_t len, struct uevent *ev); +const char *uevent_action_str(uevent_action_t action); + +int devnode_add (struct uevent *ev); +int devnode_del (struct uevent *ev); + +int symlink_add (struct uevent *ev); +int symlink_del (struct uevent *ev); + +int firmware_load (struct uevent *ev); +int modprobe_load (const char *modalias); + +int coldplug (void); + +#endif /* FINIT_KEVENTD_H_ */ + +/** + * Local Variables: + * indent-tabs-mode: t + * c-file-style: "linux" + * End: + */ diff --git a/src/uevent.c b/src/uevent.c new file mode 100644 index 00000000..801ed07f --- /dev/null +++ b/src/uevent.c @@ -0,0 +1,898 @@ +/* Uevent parsing, device node management, symlinks, and firmware loading + * + * 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#ifdef _LIBITE_LITE +# include +#else +# include +#endif + +#include "keventd.h" +#include "cond.h" +#include "util.h" + +/* Forward declarations */ +void logit(int prio, const char *fmt, ...); + +/* + * Set/clear a device condition by creating/removing a symlink. + * keventd is a standalone daemon, so we manipulate the filesystem directly + * rather than using Finit's internal cond_set()/cond_clear() API. + */ +static void dev_cond(const char *devname, int set) +{ + char cond[PATH_MAX]; + char *dir; + + if (!devname) + return; + + snprintf(cond, sizeof(cond), "%s%s", _PATH_CONDDEV, devname); + + /* Create parent directory if needed (e.g., dev/input/) */ + dir = strdupa(cond); + dir = dirname(dir); + if (strcmp(dir, _PATH_CONDDEV)) { + if (mkpath(dir, 0755) && errno != EEXIST) + logit(LOG_WARNING, "Failed creating condition dir %s", dir); + } + + if (set) { + if (symlink(_PATH_RECONF, cond) && errno != EEXIST) + logit(LOG_WARNING, "Failed setting dev/%s condition", devname); + } else { + if (erase(cond) && errno != ENOENT) + logit(LOG_WARNING, "Failed clearing dev/%s condition", devname); + } +} + +/* Symlink tracking for cleanup on device removal */ +static TAILQ_HEAD(, dev_symlink) symlinks = TAILQ_HEAD_INITIALIZER(symlinks); + +/* + * Default device permissions based on subsystem/name. + * Simple built-in rules, no config file needed. + */ +struct devrule { + const char *subsystem; /* NULL matches any */ + const char *pattern; /* fnmatch pattern, NULL = default */ + mode_t mode; + uid_t uid; + gid_t gid; +}; + +static struct devrule devrules[] = { + /* Block devices */ + { "block", "sd[a-z]*", 0660, 0, 6 }, /* root:disk */ + { "block", "vd[a-z]*", 0660, 0, 6 }, + { "block", "nvme*", 0660, 0, 6 }, + { "block", "mmcblk*", 0660, 0, 6 }, + { "block", "loop*", 0660, 0, 6 }, + { "block", "dm-*", 0660, 0, 6 }, + { "block", "md*", 0660, 0, 6 }, + { "block", NULL, 0660, 0, 6 }, /* default block */ + + /* TTY devices */ + { "tty", "tty[0-9]*", 0620, 0, 5 }, /* root:tty */ + { "tty", "ttyS*", 0660, 0, 20 }, /* root:dialout */ + { "tty", "ttyUSB*", 0660, 0, 20 }, + { "tty", "ttyACM*", 0660, 0, 20 }, + { "tty", NULL, 0666, 0, 5 }, + + /* Input devices */ + { "input", "event*", 0660, 0, 0 }, /* root:input (13) */ + { "input", "mouse*", 0660, 0, 0 }, + { "input", "mice", 0660, 0, 0 }, + { "input", NULL, 0660, 0, 0 }, + + /* Sound devices */ + { "sound", NULL, 0660, 0, 29 }, /* root:audio */ + + /* Video devices */ + { "video4linux", NULL, 0660, 0, 44 }, /* root:video */ + + /* DRM (graphics) */ + { "drm", "card*", 0660, 0, 44 }, + { "drm", "render*", 0660, 0, 44 }, + + /* USB devices */ + { "usb", NULL, 0664, 0, 0 }, + + /* Network devices - no /dev node needed */ + + /* Common char devices - match by name regardless of subsystem */ + { NULL, "null", 0666, 0, 0 }, + { NULL, "zero", 0666, 0, 0 }, + { NULL, "full", 0666, 0, 0 }, + { NULL, "random", 0666, 0, 0 }, + { NULL, "urandom", 0666, 0, 0 }, + { NULL, "tty", 0666, 0, 5 }, + { NULL, "console", 0600, 0, 0 }, + { NULL, "ptmx", 0666, 0, 5 }, + { NULL, "kmsg", 0640, 0, 0 }, + { NULL, "mem", 0640, 0, 0 }, /* root:kmem */ + { NULL, "kmem", 0640, 0, 0 }, + { NULL, "port", 0640, 0, 0 }, + { NULL, "fuse", 0666, 0, 0 }, + { NULL, "kvm", 0660, 0, 0 }, + + /* Default fallback */ + { NULL, NULL, 0660, 0, 0 }, +}; + +static uevent_action_t parse_action(const char *str) +{ + if (!strcmp(str, "add")) + return ACT_ADD; + if (!strcmp(str, "remove")) + return ACT_REMOVE; + if (!strcmp(str, "change")) + return ACT_CHANGE; + if (!strcmp(str, "move")) + return ACT_MOVE; + if (!strcmp(str, "online")) + return ACT_ONLINE; + if (!strcmp(str, "offline")) + return ACT_OFFLINE; + if (!strcmp(str, "bind")) + return ACT_BIND; + if (!strcmp(str, "unbind")) + return ACT_UNBIND; + + return ACT_UNKNOWN; +} + +const char *uevent_action_str(uevent_action_t action) +{ + switch (action) { + case ACT_ADD: return "add"; + case ACT_REMOVE: return "remove"; + case ACT_CHANGE: return "change"; + case ACT_MOVE: return "move"; + case ACT_ONLINE: return "online"; + case ACT_OFFLINE: return "offline"; + case ACT_BIND: return "bind"; + case ACT_UNBIND: return "unbind"; + default: return "unknown"; + } +} + +/* + * Parse a uevent message from kernel netlink socket. + * + * Format: + * ACTION@DEVPATH\0 + * KEY=VALUE\0 + * KEY=VALUE\0 + * ... + * \0 + */ +int uevent_parse(char *buf, size_t len, struct uevent *ev) +{ + char *at, *line; + size_t i, hdrlen; + + memset(ev, 0, sizeof(*ev)); + ev->major = -1; + ev->minor = -1; + + /* Find ACTION@DEVPATH separator */ + at = strchr(buf, '@'); + if (!at) + return -1; + + /* Split action and devpath */ + *at = 0; + ev->action = parse_action(buf); + ev->devpath = at + 1; + + /* Skip past the header to the key=value pairs */ + hdrlen = strlen(buf) + 1 + strlen(ev->devpath) + 1; + i = hdrlen; + + /* Parse KEY=VALUE pairs */ + while (i < len) { + char *eq; + + line = buf + i; + if (!*line) + break; + + eq = strchr(line, '='); + if (eq) { + *eq = 0; + eq++; + + if (!strcmp(line, "SUBSYSTEM")) + ev->subsystem = eq; + else if (!strcmp(line, "DEVNAME")) + ev->devname = eq; + else if (!strcmp(line, "INTERFACE") && !ev->devname) + ev->devname = eq; /* net devices use INTERFACE=, not DEVNAME= */ + else if (!strcmp(line, "DEVTYPE")) + ev->devtype = eq; + else if (!strcmp(line, "MAJOR")) + ev->major = atoi(eq); + else if (!strcmp(line, "MINOR")) + ev->minor = atoi(eq); + else if (!strcmp(line, "MODALIAS")) + ev->modalias = eq; + else if (!strcmp(line, "FIRMWARE")) + ev->firmware = eq; + else if (!strcmp(line, "SEQNUM")) + ev->seqnum = eq; + else if (!strcmp(line, "DRIVER")) + ev->driver = eq; + } + + i += strlen(line) + (eq ? strlen(eq) + 1 : 1) + 1; + } + + return 0; +} + +static struct devrule *find_rule(struct uevent *ev) +{ + struct devrule *rule; + size_t i; + + for (i = 0; i < NELEMS(devrules); i++) { + rule = &devrules[i]; + + /* Check subsystem if rule specifies one */ + if (rule->subsystem && ev->subsystem) { + if (strcmp(rule->subsystem, ev->subsystem)) + continue; + } + + /* Check pattern if rule specifies one */ + if (rule->pattern && ev->devname) { + /* Just match basename, not full path */ + const char *name = strrchr(ev->devname, '/'); + name = name ? name + 1 : ev->devname; + + if (fnmatch(rule->pattern, name, 0)) + continue; + } + + return rule; + } + + /* Return last rule as default */ + return &devrules[NELEMS(devrules) - 1]; +} + +/* + * Create device node in /dev. + */ +int devnode_add(struct uevent *ev) +{ + struct devrule *rule; + char path[PATH_MAX]; + char *dir; + mode_t mode; + dev_t dev; + int rc; + + if (!ev->devname || ev->major < 0 || ev->minor < 0) + return -1; + + snprintf(path, sizeof(path), "/dev/%s", ev->devname); + + /* Create parent directories if needed (e.g., /dev/input/) */ + dir = strdupa(path); + dir = dirname(dir); + if (strcmp(dir, "/dev")) { + rc = mkpath(dir, 0755); + if (rc && errno != EEXIST) { + logit(LOG_ERR, "Failed creating %s: %s", dir, strerror(errno)); + return -1; + } + } + + /* Find matching rule for permissions */ + rule = find_rule(ev); + mode = rule->mode; + dev = makedev(ev->major, ev->minor); + + /* Remove existing node if present */ + unlink(path); + + /* Create device node */ + if (ev->subsystem && !strcmp(ev->subsystem, "block")) + mode |= S_IFBLK; + else + mode |= S_IFCHR; + + rc = mknod(path, mode, dev); + if (rc) { + logit(LOG_ERR, "Failed creating %s: %s", path, strerror(errno)); + return -1; + } + + /* mknod() applies umask; chmod() does not. Explicitly chmod to + * the requested mode so the result is independent of umask state. */ + if (chmod(path, mode & ~S_IFMT)) + logit(LOG_WARNING, "Failed chmod %s: %s", path, strerror(errno)); + + /* Set ownership */ + if (chown(path, rule->uid, rule->gid)) + logit(LOG_WARNING, "Failed chown %s: %s", path, strerror(errno)); + + logit(LOG_DEBUG, "Created %s (%d:%d) mode %04o", + path, ev->major, ev->minor, rule->mode); + + /* Set dev/ condition */ + dev_cond(ev->devname, 1); + + return 0; +} + +/* + * Remove device node from /dev. + */ +int devnode_del(struct uevent *ev) +{ + char path[PATH_MAX]; + + if (!ev->devname) + return -1; + + snprintf(path, sizeof(path), "/dev/%s", ev->devname); + + /* Clear dev/ condition first */ + dev_cond(ev->devname, 0); + + if (unlink(path) && errno != ENOENT) { + logit(LOG_WARNING, "Failed removing %s: %s", path, strerror(errno)); + return -1; + } + + logit(LOG_DEBUG, "Removed %s", path); + return 0; +} + +/* + * Track symlink for removal when device is unplugged. + */ +static void symlink_track(const char *devpath, const char *linkpath) +{ + struct dev_symlink *sl; + + sl = malloc(sizeof(*sl)); + if (!sl) + return; + + sl->devpath = strdup(devpath); + sl->linkpath = strdup(linkpath); + + if (!sl->devpath || !sl->linkpath) { + free(sl->devpath); + free(sl->linkpath); + free(sl); + return; + } + + TAILQ_INSERT_TAIL(&symlinks, sl, link); +} + +/* + * Create a symlink and track it. + */ +static int symlink_create(const char *target, const char *link, const char *devpath) +{ + char *dir; + int rc; + + /* Create parent directories */ + dir = strdupa(link); + dir = dirname(dir); + rc = mkpath(dir, 0755); + if (rc && errno != EEXIST) + return -1; + + /* Remove existing link */ + unlink(link); + + /* Create symlink */ + if (symlink(target, link)) { + if (errno != EEXIST) + return -1; + } + + /* Track for removal */ + symlink_track(devpath, link); + + logit(LOG_DEBUG, "Created symlink %s -> %s", link, target); + return 0; +} + +/* + * Read a single line from a sysfs attribute file. + */ +static int sysfs_read(const char *path, char *buf, size_t len) +{ + FILE *fp; + + fp = fopen(path, "r"); + if (!fp) + return -1; + + if (!fgets(buf, len, fp)) { + fclose(fp); + return -1; + } + + fclose(fp); + chomp(buf); + + return 0; +} + +/* + * Build disk ID string from sysfs attributes. + * Format: BUSTYPE-VENDOR_MODEL_SERIAL + */ +static int disk_serial_id(struct uevent *ev, char *id, size_t len) +{ + char path[PATH_MAX]; + char vendor[64] = "", model[64] = "", serial[64] = ""; + char *p; + + /* Try to read from device's sysfs attributes */ + snprintf(path, sizeof(path), "/sys%s/device/vendor", ev->devpath); + sysfs_read(path, vendor, sizeof(vendor)); + + snprintf(path, sizeof(path), "/sys%s/device/model", ev->devpath); + sysfs_read(path, model, sizeof(model)); + + snprintf(path, sizeof(path), "/sys%s/device/serial", ev->devpath); + if (sysfs_read(path, serial, sizeof(serial))) { + /* Try alternate location */ + snprintf(path, sizeof(path), "/sys%s/../serial", ev->devpath); + sysfs_read(path, serial, sizeof(serial)); + } + + /* Need at least model or serial */ + if (!model[0] && !serial[0]) + return -1; + + /* Clean up strings - replace spaces with underscores */ + for (p = vendor; *p; p++) + if (*p == ' ') *p = '_'; + for (p = model; *p; p++) + if (*p == ' ') *p = '_'; + for (p = serial; *p; p++) + if (*p == ' ') *p = '_'; + + /* Remove trailing underscores */ + for (p = vendor + strlen(vendor) - 1; p >= vendor && *p == '_'; p--) + *p = 0; + for (p = model + strlen(model) - 1; p >= model && *p == '_'; p--) + *p = 0; + + /* Build ID string */ + if (vendor[0] && model[0] && serial[0]) + snprintf(id, len, "%s_%s_%s", vendor, model, serial); + else if (model[0] && serial[0]) + snprintf(id, len, "%s_%s", model, serial); + else if (serial[0]) + snprintf(id, len, "%s", serial); + else + snprintf(id, len, "%s", model); + + return 0; +} + +/* + * Build disk path ID from devpath. + * Convert /devices/pci0000:00/.../host0/.../0:0:0:0/block/sda + * to pci-0000:00:1f.2-ata-1 + */ +static int disk_path_id(struct uevent *ev, char *id, size_t len) +{ + /* Simplified: just use the devpath hash for now */ + const char *p; + + /* Find last component before block/ */ + p = strstr(ev->devpath, "/block/"); + if (!p) + return -1; + + /* Use subsystem and devname */ + snprintf(id, len, "%s-%s", ev->subsystem ?: "disk", ev->devname); + + return 0; +} + + +/* + * Create symlinks for block devices in /dev/disk/by-*. + */ +static int symlink_add_disk(struct uevent *ev) +{ + char target[PATH_MAX], link[PATH_MAX], id[256]; + const char *name; + + if (!ev->devname) + return -1; + + /* Get basename for relative link */ + name = strrchr(ev->devname, '/'); + name = name ? name + 1 : ev->devname; + + /* by-id: serial-based identifier */ + if (!disk_serial_id(ev, id, sizeof(id))) { + snprintf(target, sizeof(target), "../../%s", name); + snprintf(link, sizeof(link), "/dev/disk/by-id/%s", id); + symlink_create(target, link, ev->devpath); + } + + /* by-path: topology-based identifier */ + if (!disk_path_id(ev, id, sizeof(id))) { + snprintf(target, sizeof(target), "../../%s", name); + snprintf(link, sizeof(link), "/dev/disk/by-path/%s", id); + symlink_create(target, link, ev->devpath); + } + + return 0; +} + +/* + * Create symlinks for input devices in /dev/input/by-*. + */ +static int symlink_add_input(struct uevent *ev) +{ + char target[PATH_MAX], link[PATH_MAX]; + char name[256], phys[256]; + char path[PATH_MAX]; + const char *devname; + char *p; + + if (!ev->devname) + return -1; + + devname = strrchr(ev->devname, '/'); + devname = devname ? devname + 1 : ev->devname; + + /* Read device name */ + snprintf(path, sizeof(path), "/sys%s/device/name", ev->devpath); + if (sysfs_read(path, name, sizeof(name))) + return -1; + + /* Clean up name */ + for (p = name; *p; p++) { + if (*p == ' ' || *p == '/') + *p = '_'; + } + + /* by-id: name-based */ + snprintf(target, sizeof(target), "../%s", devname); + snprintf(link, sizeof(link), "/dev/input/by-id/%s", name); + symlink_create(target, link, ev->devpath); + + /* by-path: physical path (if available) */ + snprintf(path, sizeof(path), "/sys%s/device/phys", ev->devpath); + if (!sysfs_read(path, phys, sizeof(phys))) { + for (p = phys; *p; p++) { + if (*p == ' ' || *p == '/') + *p = '_'; + } + snprintf(link, sizeof(link), "/dev/input/by-path/%s", phys); + symlink_create(target, link, ev->devpath); + } + + return 0; +} + +/* + * Create appropriate symlinks based on device subsystem. + */ +int symlink_add(struct uevent *ev) +{ + if (!ev->subsystem) + return 0; + + if (!strcmp(ev->subsystem, "block")) + return symlink_add_disk(ev); + + if (!strcmp(ev->subsystem, "input")) + return symlink_add_input(ev); + + return 0; +} + +/* + * Remove symlinks associated with a device. + */ +int symlink_del(struct uevent *ev) +{ + struct dev_symlink *sl, *tmp; + + if (!ev->devpath) + return -1; + + TAILQ_FOREACH_SAFE(sl, &symlinks, link, tmp) { + if (!strcmp(sl->devpath, ev->devpath)) { + unlink(sl->linkpath); + logit(LOG_DEBUG, "Removed symlink %s", sl->linkpath); + + TAILQ_REMOVE(&symlinks, sl, link); + free(sl->devpath); + free(sl->linkpath); + free(sl); + } + } + + return 0; +} + +/* + * Load firmware for a device. + * + * The kernel sends a uevent with FIRMWARE=filename when a driver + * requests firmware via request_firmware(). We need to: + * 1. Find the firmware file + * 2. Write "1" to /sys/.../loading + * 3. Write firmware data to /sys/.../data + * 4. Write "0" to /sys/.../loading (or "-1" on error) + */ +int firmware_load(struct uevent *ev) +{ + static const struct { + const char *fmt; + int kver; /* fmt has %s for uts.release before %s for firmware */ + } fw_paths[] = { + { "/lib/firmware/updates/%s/%s", 1 }, + { "/lib/firmware/updates/%s", 0 }, + { "/lib/firmware/%s/%s", 1 }, + { "/lib/firmware/%s", 0 }, + { "/usr/lib/firmware/updates/%s/%s", 1 }, + { "/usr/lib/firmware/updates/%s", 0 }, + { "/usr/lib/firmware/%s/%s", 1 }, + { "/usr/lib/firmware/%s", 0 }, + }; + char fwpath[PATH_MAX], loading[PATH_MAX], data[PATH_MAX]; + struct utsname uts; + char buf[4096]; + int fd_fw = -1, fd_data = -1; + int ok = 0, found = 0; + ssize_t n; + size_t i; + + if (!ev->firmware || !ev->devpath) + return -1; + + /* + * Build sysfs paths up front so the fail label can always write + * "-1" to abort the kernel firmware request, even if we never + * managed to write "1" to start it. + */ + snprintf(loading, sizeof(loading), "/sys%s/loading", ev->devpath); + snprintf(data, sizeof(data), "/sys%s/data", ev->devpath); + + uname(&uts); + + for (i = 0; i < NELEMS(fw_paths); i++) { + if (fw_paths[i].kver) + snprintf(fwpath, sizeof(fwpath), fw_paths[i].fmt, + uts.release, ev->firmware); + else + snprintf(fwpath, sizeof(fwpath), fw_paths[i].fmt, + ev->firmware); + + if (fexist(fwpath)) { + found = 1; + break; + } + } + + if (!found) { + logit(LOG_WARNING, "Firmware not found: %s", ev->firmware); + goto fail; + } + + logit(LOG_INFO, "Loading firmware %s from %s", ev->firmware, fwpath); + + fd_fw = open(fwpath, O_RDONLY | O_CLOEXEC); + if (fd_fw < 0) { + logit(LOG_ERR, "Failed to open firmware %s: %s", + fwpath, strerror(errno)); + goto fail; + } + + if (fnwrite("1", "%s", loading) < 0) { + logit(LOG_ERR, "Failed to signal firmware loading start: %s", + strerror(errno)); + goto fail; + } + + fd_data = open(data, O_WRONLY | O_CLOEXEC); + if (fd_data < 0) { + logit(LOG_ERR, "Failed to open %s: %s", data, strerror(errno)); + goto fail; + } + + for (;;) { + ssize_t total; + + n = read(fd_fw, buf, sizeof(buf)); + if (n == 0) + break; + if (n < 0) { + if (errno == EINTR) + continue; + logit(LOG_ERR, "Failed reading firmware %s: %s", + fwpath, strerror(errno)); + goto fail; + } + + for (total = 0; total < n; ) { + ssize_t w = write(fd_data, buf + total, n - total); + + if (w < 0) { + if (errno == EINTR) + continue; + logit(LOG_ERR, "Failed writing firmware data: %s", + strerror(errno)); + goto fail; + } + total += w; + } + } + + ok = 1; + +fail: + if (fd_fw >= 0) + close(fd_fw); + if (fd_data >= 0) + close(fd_data); + + if (fnwrite(ok ? "0" : "-1", "%s", loading) < 0) + logit(LOG_WARNING, "Failed to signal firmware load %s: %s", + ok ? "complete" : "abort", strerror(errno)); + else if (ok) + logit(LOG_INFO, "Firmware %s loaded successfully", ev->firmware); + + return ok ? 0 : -1; +} + +/* + * Load kernel module for a device based on modalias. + * + * Synchronous: waits for modprobe to complete before returning so + * that rules running after `kmod load` (e.g. IMPORT{builtin}=blkid + * once the filesystem driver is in) see the post-load state. + * keventd's main loop uses SIGCHLD=SIG_IGN to auto-reap stray + * children, so we temporarily restore the default handler here to + * make waitpid() observable. + */ +int modprobe_load(const char *modalias) +{ + struct sigaction sa_dfl, sa_old; + pid_t pid; + int status = 0; + + if (!modalias) + return -1; + + logit(LOG_DEBUG, "Loading module for %s", modalias); + + sigemptyset(&sa_dfl.sa_mask); + sa_dfl.sa_flags = 0; + sa_dfl.sa_handler = SIG_DFL; + sigaction(SIGCHLD, &sa_dfl, &sa_old); + + pid = fork(); + if (pid < 0) { + logit(LOG_ERR, "fork failed: %s", strerror(errno)); + sigaction(SIGCHLD, &sa_old, NULL); + return -1; + } + + if (pid == 0) { + execl("/sbin/modprobe", "modprobe", "-bq", modalias, NULL); + _exit(127); + } + + while (waitpid(pid, &status, 0) < 0 && errno == EINTR) + ; + sigaction(SIGCHLD, &sa_old, NULL); + + if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) + return -1; + return 0; +} + +/* + * Coldplug callback for nftw(). + * Writes "add" to each uevent file to trigger kernel to resend events. + */ +static int coldplug_cb(const char *path, const struct stat *st, + int type, struct FTW *ftw) +{ + size_t len; + + (void)st; + (void)ftw; + + if (type != FTW_F) + return 0; + + len = strlen(path); + if (len < 6) + return 0; + + /* Check if filename is "uevent" */ + if (strcmp(path + len - 6, "uevent")) + return 0; + + /* Trigger add event */ + fnwrite("add", "%s", path); + + return 0; +} + +/* + * Trigger coldplug - replay device events for devices already present. + */ +int coldplug(void) +{ + logit(LOG_INFO, "Starting coldplug..."); + + /* Walk /sys/devices and trigger uevents */ + if (nftw("/sys/devices", coldplug_cb, 64, FTW_PHYS) < 0) { + logit(LOG_ERR, "Coldplug failed: %s", strerror(errno)); + return -1; + } + + logit(LOG_INFO, "Coldplug complete"); + return 0; +} + +/** + * Local Variables: + * indent-tabs-mode: t + * c-file-style: "linux" + * End: + */ From 90cdf0546558639e11ed90b018be62e7927e54db Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 29 Jan 2026 12:16:33 +0100 Subject: [PATCH 03/24] doc: rewrite keventd documentation for unified device manager Rewrite doc/keventd.md from a 14-line stub into comprehensive documentation covering all features of the new unified keventd: device node creation, persistent symlinks, firmware loading, module loading, coldplug, conditions, and command-line usage. Update doc/conditions.md to list keventd as the primary provider of dev/* and sys/pwr/* conditions, with devmon as fallback when an external device manager is used instead. Signed-off-by: Joachim Wiberg --- doc/conditions.md | 16 +-- doc/config/runlevels.md | 6 +- doc/keventd.md | 218 ++++++++++++++++++++++++++++++++++++++-- 3 files changed, 221 insertions(+), 19 deletions(-) diff --git a/doc/conditions.md b/doc/conditions.md index bb425c4e..e61687bb 100644 --- a/doc/conditions.md +++ b/doc/conditions.md @@ -177,15 +177,19 @@ Built-in Conditions Finit comes with a set of plugins for conditions: - - `devmon` (built-in) - - `netlink` - - `pidfile` + - `keventd`: provides `` and `` + - `devmon` (built-in fallback for `` without keventd) + - `netlink`: provides `` + - `pidfile`: provides `` - `sys` - `usr` -The `devmon` (built-in) plugin monitors `/dev` and `/dev/dir` for device -nodes being created and removed. It is active only when a run, task, or -service has declared a `` or `` condition. +The `dev/` conditions are provided by `keventd`, the built-in device +manager. When keventd creates a device node in `/dev`, it also asserts +the corresponding `dev/` condition. When a device is removed, the +condition is cleared. If keventd is not in use (an external device +manager like udevd is used instead), the `devmon` built-in provides the +same conditions by monitoring `/dev` and `/dev/dir` with inotify. The `pidfile` plugin recursively watches `/run/` for PID files created by the monitored services, and sets a corresponding condition in the diff --git a/doc/config/runlevels.md b/doc/config/runlevels.md index 905270ca..fef72efb 100644 --- a/doc/config/runlevels.md +++ b/doc/config/runlevels.md @@ -125,8 +125,10 @@ Load kernel modules, each with optional arguments. Similar to the Deprecated, there is both a `modules-load.so` and a `modprobe.so` plugin that can handle module loading better. The former supports loading from `/etc/modules-load.d/`, the latter uses kernel modinfo to automatically -load (or coldplug) every required module. For hotplug we recommend the -BusyBox mdev tool, add to `/etc/mdev.conf`: +load (or coldplug) every required module. Hotplug module loading is +handled by [keventd](../keventd.md), the built-in device manager. On +systems using the hotplug plugin with BusyBox mdev instead, add to +`/etc/mdev.conf`: $MODALIAS=.* root:root 0660 @modprobe -b "$MODALIAS" diff --git a/doc/keventd.md b/doc/keventd.md index 7651b0d8..67bb5a1b 100644 --- a/doc/keventd.md +++ b/doc/keventd.md @@ -1,14 +1,210 @@ -keventd -======= +Bundled Device Manager +====================== -The kernel event daemon bundled with Finit is a simple uevent monitor -for `/sys/class/power_supply`. It provides the `sys/pwr/ac` condition, -which can be useful to prevent power hungry services like anacron to run -when a laptop is only running on battery, for instance. +The kernel event daemon `keventd` is a built-in device manager bundled +with Finit. It replaces the need for external device managers like +mdev, mdevd, or udevd on systems where a lighter-weight solution is +preferred, particularly on embedded systems. -Since keventd is not an integral part of Finit yet it is not enabled by -default. Enable it using `./configure --with-keventd`. The bundled -`contrib/` build scripts for Debian, Alpine, and Void have this enabled. +It is enabled by default since Finit v5. To disable it and use an +external device manager instead: `./configure --without-keventd` -This daemon is planned to be extended with monitoring of other uevents, -patches and ideas are welcome in the issue tracker. + +Features +-------- + +When started, keventd listens on a `NETLINK_KOBJECT_UEVENT` socket for +kernel events and handles: + +- **Device node creation**: creates and removes `/dev` nodes with + correct permissions on device add/remove events +- **Persistent symlinks**: creates `/dev/disk/by-id/`, `/dev/disk/by-path/`, + and `/dev/input/by-id/`, `/dev/input/by-path/` symlinks for stable + device naming +- **Firmware loading**: responds to kernel firmware requests by searching + `/lib/firmware/` and writing firmware data to sysfs +- **Module loading**: parses `MODALIAS` from uevents and spawns `modprobe` + to load the appropriate kernel module +- **Coldplug**: with the `-c` flag, walks `/sys/devices` and triggers + add events for all devices present at boot +- **Power supply monitoring**: tracks AC power status and provides the + `sys/pwr/ac` condition +- **Device conditions**: sets `dev/*` conditions in the Finit condition + system when device nodes appear or disappear + + +Device Nodes +------------ + +On receiving an `add` event with `MAJOR`, `MINOR`, and `DEVNAME` +fields, keventd creates the corresponding device node in `/dev` using +`mknod()`. Parent directories are created automatically (e.g., +`/dev/input/` for `/dev/input/event0`). + +On `remove` events, the device node and its associated symlinks and +conditions are cleaned up. + +### Default Permissions + +keventd applies permissions based on built-in rules that match on +device subsystem and name: + +| Subsystem | Pattern | Mode | Owner:Group | +|---------------|-------------|--------|----------------| +| block | sd*, vd*, nvme*, mmcblk*, loop*, dm-*, md* | 0660 | root:disk | +| tty | tty[0-9]* | 0620 | root:tty | +| tty | ttyS*, ttyUSB*, ttyACM* | 0660 | root:dialout | +| input | event*, mouse*, mice | 0660 | root:root | +| sound | * | 0660 | root:audio | +| video4linux | * | 0660 | root:video | +| drm | card*, render* | 0660 | root:video | +| (any) | null, zero, full, random, urandom | 0666 | root:root | +| (any) | console | 0600 | root:root | +| (default) | | 0660 | root:root | + + +Persistent Symlinks +------------------- + +For block devices, keventd creates symlinks under `/dev/disk/`: + +- **by-id**: based on the device serial number and model, read from + sysfs attributes (`/sys/.../device/vendor`, `model`, `serial`) +- **by-path**: based on the device topology path + +For input devices, symlinks are created under `/dev/input/`: + +- **by-id**: based on the device name from sysfs +- **by-path**: based on the physical device path + +These symlinks are tracked internally and automatically removed when the +corresponding device is unplugged. + + +Firmware Loading +---------------- + +When a kernel driver requests firmware (via `request_firmware()`), the +kernel sends a uevent with a `FIRMWARE=` field. keventd handles this +by: + +1. Searching for the firmware file in order: + - `/lib/firmware/updates//` + - `/lib/firmware/updates/` + - `/lib/firmware//` + - `/lib/firmware/` +2. Writing `1` to `/sys//loading` to signal start +3. Copying the firmware data to `/sys//data` +4. Writing `0` to `/sys//loading` on success (or `-1` on failure) + +This is particularly important early in boot when drivers for graphics +cards, network adapters, and other hardware need firmware before they +can operate. + + +Module Loading +-------------- + +When a device add event includes a `MODALIAS` field, keventd spawns +`modprobe -bq ` to load the matching kernel module. Module +loading is done asynchronously (keventd does not wait for modprobe to +complete) to avoid blocking other event processing. + + +Coldplug +-------- + +To handle devices that were present before keventd started, it supports +a coldplug mode activated with the `-c` flag. This walks the entire +`/sys/devices` tree and writes `add` to each `uevent` file, causing the +kernel to re-emit add events for all existing devices. + +This replaces the separate `coldplug` script previously used with mdev. + + +Conditions +---------- + +keventd provides conditions in two namespaces: + +### Device Conditions (`dev/`) + +When a device node is created, keventd asserts a corresponding condition +in `/run/finit/cond/dev/`. This allows services to wait for specific +devices: + + service mdadm { + description = "RAID monitor" + runlevel = "2345" + conditions = { "dev/sda" } + command = "/usr/sbin/mdadm --monitor /dev/md0" + } + + service gps-daemon { + description = "GPS daemon" + runlevel = "2345" + conditions = { "dev/ttyUSB0" } + command = "/usr/sbin/gps-daemon" + } + +When the device is removed, the condition is cleared and Finit stops +the dependent services. + +### Power Supply Conditions (`sys/pwr/`) + +keventd monitors the `power_supply` subsystem and provides: + +- `sys/pwr/ac` -- asserted when AC power is connected + +This is useful for preventing power-hungry services from running on +battery: + + service cron { + description = "Cron daemon" + runlevel = "2345" + conditions = { "sys/pwr/ac", "pid/syslogd" } + command = "cron -f" + } + + +Usage +----- + + keventd [-cdhnv] + + Options: + -c Run coldplug at startup + -d Enable debug mode (foreground, verbose) + -h Show help text + -n Run in foreground (no daemon) + -v Show version + +In normal operation, Finit starts keventd automatically via its system +configuration. The `-d` flag is useful for debugging device issues -- +it runs keventd in the foreground and logs all received uevents. + +Debug logging can also be toggled at runtime by sending `SIGUSR1`: + + kill -USR1 $(pidof keventd) + + +Integration with Finit +---------------------- + +keventd is a standalone daemon started by Finit as an internal service. +It communicates with Finit exclusively through the filesystem-based +condition system -- creating and removing symlinks in `/run/finit/cond/`. + +This means keventd can also be tested independently: + + # Run in debug mode to see all kernel events + keventd -d + + # Run with coldplug to populate /dev from scratch + keventd -c -n + +When keventd is enabled, it conflicts with external device managers. +Only one device manager should be active at a time. The system +configuration uses the `conflict:` directive to enforce this: + + service conflict:udevd,mdevd,mdev [...] keventd -c -- Finit device manager From ad563d9c578e7087836199c56b4dae6d19bedd33 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 29 Jan 2026 12:32:35 +0100 Subject: [PATCH 04/24] man: add fine manual for keventd(8) Signed-off-by: Joachim Wiberg --- man/Makefile.am | 4 + man/keventd.8 | 231 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 235 insertions(+) create mode 100644 man/keventd.8 diff --git a/man/Makefile.am b/man/Makefile.am index 0be59995..078e13cd 100644 --- a/man/Makefile.am +++ b/man/Makefile.am @@ -1,2 +1,6 @@ dist_man8_MANS = finit.8 initctl.8 dist_man5_MANS = finit.conf.5 + +if KEVENTD +dist_man8_MANS += keventd.8 +endif diff --git a/man/keventd.8 b/man/keventd.8 new file mode 100644 index 00000000..90d5b983 --- /dev/null +++ b/man/keventd.8 @@ -0,0 +1,231 @@ +.\" Hey, EMACS: -*- nroff -*- +.\" First parameter, NAME, should be all caps +.\" Second parameter, SECTION, should be 1-8, maybe w/ subsection +.\" other parameters are allowed: see man(7), man(1) +.Dd Jan 29, 2026 +.\" Please adjust this date whenever revising the manpage. +.Dt KEVENTD 8 SMM +.Os Linux +.Sh NAME +.Nm keventd +.Nd Finit device manager daemon +.Sh SYNOPSIS +.Nm +.Op Fl c +.Op Fl d +.Op Fl h +.Op Fl n +.Op Fl v +.Sh DESCRIPTION +.Nm +is a built-in device manager bundled with +.Xr finit 8 . +It listens for kernel uevents on a +.Dv NETLINK_KOBJECT_UEVENT +socket and handles device node creation, persistent symlinks, firmware +loading, kernel module loading, and condition management. +.Pp +It replaces the need for external device managers like +.Nm mdev , +.Nm mdevd , +or +.Nm udevd +on systems where a lighter-weight solution is preferred, particularly +on embedded systems. +.Sh OPTIONS +.Bl -tag -width Ds +.It Fl c +Run coldplug at startup. Walks the +.Pa /sys/devices +tree and writes +.Cm add +to each +.Pa uevent +file, causing the kernel to re-emit add events for all devices already +present. This populates +.Pa /dev +with nodes for hardware that existed before +.Nm +started. +.It Fl d +Enable debug mode. Implies +.Fl n . +All received uevents are logged to stderr. +.It Fl h +Show help text and exit. +.It Fl n +Run in foreground, do not daemonize. Log messages are written to +stderr instead of syslog. +.It Fl v +Show version and exit. +.El +.Sh DEVICE NODES +On receiving an +.Cm add +event with +.Cm MAJOR , +.Cm MINOR , +and +.Cm DEVNAME +fields, +.Nm +creates the device node in +.Pa /dev +using +.Xr mknod 2 . +Parent directories are created as needed, e.g.\& +.Pa /dev/input/ +for +.Pa /dev/input/event0 . +.Pp +On +.Cm remove +events the device node and its associated symlinks are cleaned up. +.Pp +Permissions are assigned based on built-in rules matching on device +subsystem and name. For example, block devices default to mode 0660 +owned by root:disk, TTY devices to root:tty, and common devices like +.Pa /dev/null +and +.Pa /dev/zero +are world-readable (0666). +.Sh PERSISTENT SYMLINKS +For block devices, +.Nm +creates symlinks in +.Pa /dev/disk/ : +.Bl -tag -width by-path -offset indent -compact +.It Pa by-id +Based on device serial number and model, read from sysfs. +.It Pa by-path +Based on the device topology path. +.El +.Pp +For input devices, symlinks are created in +.Pa /dev/input/ : +.Bl -tag -width by-path -offset indent -compact +.It Pa by-id +Based on the device name from sysfs. +.It Pa by-path +Based on the physical device path. +.El +.Pp +Symlinks are tracked internally and automatically removed when the +device is unplugged. +.Sh FIRMWARE LOADING +When a kernel driver requests firmware via +.Fn request_firmware , +the kernel emits a uevent with a +.Cm FIRMWARE +field. +.Nm +handles this by searching for the firmware file in the following order: +.Pp +.Bl -enum -compact -offset indent +.It +.Pa /lib/firmware/updates// +.It +.Pa /lib/firmware/updates/ +.It +.Pa /lib/firmware// +.It +.Pa /lib/firmware/ +.El +.Pp +The firmware is loaded by writing to the device's sysfs +.Pa loading +and +.Pa data +attributes. +.Sh MODULE LOADING +When a device add event includes a +.Cm MODALIAS +field, +.Nm +spawns +.Cm modprobe -bq +to load the matching kernel module. Module loading is asynchronous +to avoid blocking event processing. +.Sh CONDITIONS +.Nm +provides conditions for Finit's dependency system by creating and +removing symlinks in +.Pa /run/finit/cond/ . +.Ss Device Conditions +When a device node is created, +.Nm +asserts a corresponding +.Cm dev/ +condition. For example, creating +.Pa /dev/sda +asserts +.Cm dev/sda . +This allows services to depend on specific devices: +.Bd -literal -offset indent +service mdadm { + description = "RAID monitor" + runlevel = "2345" + conditions = { "dev/sda" } + command = "/usr/sbin/mdadm" +} + +service gpsd { + description = "GPS daemon" + runlevel = "2345" + conditions = { "dev/ttyUSB0" } + command = "/usr/sbin/gpsd" +} +.Ed +.Pp +When the device is removed, the condition is cleared and Finit stops +the dependent services. +.Ss Power Supply Conditions +.Nm +monitors the +.Cm power_supply +subsystem and provides: +.Bl -tag -width sys/pwr/ac -offset indent -compact +.It Cm sys/pwr/ac +Asserted when AC power is connected. +.El +.Pp +Useful for preventing power-hungry services from running on battery: +.Bd -literal -offset indent +service cron { + description = "Cron daemon" + runlevel = "2345" + conditions = { "sys/pwr/ac" } + command = "cron -f" +} +.Ed +.Sh SIGNALS +.Bl -tag -width SIGUSR1 +.It Dv SIGUSR1 +Toggle debug logging at runtime. +.It Dv SIGTERM +Graceful shutdown. +.El +.Sh FILES +.Bl -tag -width /run/finit/cond/dev/ -compact +.It Pa /dev/ +Device nodes managed by +.Nm . +.It Pa /dev/disk/by-id/ , Pa /dev/disk/by-path/ +Persistent block device symlinks. +.It Pa /dev/input/by-id/ , Pa /dev/input/by-path/ +Persistent input device symlinks. +.It Pa /lib/firmware/ +Firmware search path. +.It Pa /run/finit/cond/dev/ +Device condition symlinks. +.It Pa /run/finit/cond/sys/pwr/ +Power supply condition symlinks. +.El +.Sh SEE ALSO +.Xr finit 8 , +.Xr finit.conf 5 , +.Xr initctl 8 , +.Xr mknod 2 , +.Xr modprobe 8 +.Sh AUTHORS +.An Joachim Wiberg Aq Mt troglobit@gmail.com From 246970267caa130569af975da93af9858a9b59e4 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 29 Jan 2026 14:21:06 +0100 Subject: [PATCH 05/24] keventd: add netlink uevent rebroadcast for libudev-zero After keventd processes a uevent (creating device nodes, loading modules, etc.), rebroadcast the original event to netlink group 0x4 so that libudev-zero consumers -- graphical applications, Wayland/X11 compositors, libinput, and anything else using libudev to monitor device hotplug -- can receive device events. Rebroadcast is enabled by default. Use -g to override the target netlink group mask, or -G to disable rebroadcast entirely. Bit 0 (kernel group) is always masked out to prevent feedback loops. Ref: https://github.com/finit-project/finit/issues/451#issuecomment-3817233886 See: https://github.com/illiliti/libudev-zero Suggested-by: Aaron Andersen Signed-off-by: Joachim Wiberg --- src/keventd.c | 108 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 105 insertions(+), 3 deletions(-) diff --git a/src/keventd.c b/src/keventd.c index c4c5a3a6..5093f444 100644 --- a/src/keventd.c +++ b/src/keventd.c @@ -67,6 +67,9 @@ #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 + static int num_ac_online; static int num_ac; @@ -92,6 +95,74 @@ void logit(int prio, const char *fmt, ...) #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 void sys_cond(const char *cond, int set) { char oneshot[256]; @@ -330,15 +401,17 @@ static void shut_down(int signo) static int usage(int rc) { fprintf(stderr, - "Usage: keventd [-dhnv] [-c]\n" + "Usage: keventd [-dGhnv] [-c] [-g GROUP]\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" " -v Show version\n" - "\n"); + "\n", REBC_DEFAULT_NLGROUP); return rc; } @@ -360,6 +433,7 @@ int main(int argc, char *argv[]) struct sockaddr_nl nls = { 0 }; struct pollfd pfd; char buf[UEVENT_BUFFER_SIZE]; + unsigned int nlgroups = REBC_DEFAULT_NLGROUP; int do_coldplug = 0; int foreground = 0; int c; @@ -369,7 +443,7 @@ int main(int argc, char *argv[]) * requested bits verbatim instead of masking them. */ umask(0); - while ((c = getopt(argc, argv, "cdhnv")) != -1) { + while ((c = getopt(argc, argv, "cdg:Ghnv")) != -1) { switch (c) { case 'c': do_coldplug = 1; @@ -378,6 +452,12 @@ int main(int argc, char *argv[]) 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': @@ -425,6 +505,10 @@ int main(int argc, char *argv[]) setsockopt(pfd.fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf)); } + /* Initialize rebroadcast socket (default on, -G to disable) */ + if (nlgroups) + rebc_init(nlgroups); + /* Run coldplug if requested */ if (do_coldplug) coldplug(); @@ -432,6 +516,7 @@ int main(int argc, char *argv[]) logit(LOG_NOTICE, "keventd v%s started, waiting for events...", KEVENTD_VERSION); while (running) { + char rebc_buf[UEVENT_BUFFER_SIZE]; int len; if (-1 == poll(&pfd, 1, -1)) { @@ -459,9 +544,26 @@ int main(int argc, char *argv[]) if (!strncmp(buf, "libudev", 7)) continue; + /* + * 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, buf, len); + handle_uevent(buf, len); + + /* + * Rebroadcast after processing so that device nodes and + * symlinks exist by the time consumers receive the event. + */ + if (rebc_fd != -1) + rebc_event(rebc_buf, len); } + if (rebc_fd != -1) + close(rebc_fd); close(pfd.fd); logit(LOG_NOTICE, "keventd shutting down"); From 4a208cf0b3b94a312bed19ef41d966ee015ab040 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 29 Jan 2026 14:22:33 +0100 Subject: [PATCH 06/24] doc: update keventd docs with new rebrodcast feature Signed-off-by: Joachim Wiberg --- doc/keventd.md | 91 +++++++++++++++++++++++++++++++++++++++++--------- man/keventd.8 | 51 ++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 16 deletions(-) diff --git a/doc/keventd.md b/doc/keventd.md index 67bb5a1b..b6a934ff 100644 --- a/doc/keventd.md +++ b/doc/keventd.md @@ -7,7 +7,9 @@ mdev, mdevd, or udevd on systems where a lighter-weight solution is preferred, particularly on embedded systems. It is enabled by default since Finit v5. To disable it and use an -external device manager instead: `./configure --without-keventd` +external device manager instead: + + ./configure --without-keventd Features @@ -31,6 +33,8 @@ kernel events and handles: `sys/pwr/ac` condition - **Device conditions**: sets `dev/*` conditions in the Finit condition system when device nodes appear or disappear +- **Netlink rebroadcast**: rebroadcasts processed uevents to netlink + group 0x4 for [libudev-zero][] consumers (enabled by default) Device Nodes @@ -49,18 +53,18 @@ conditions are cleaned up. keventd applies permissions based on built-in rules that match on device subsystem and name: -| Subsystem | Pattern | Mode | Owner:Group | -|---------------|-------------|--------|----------------| -| block | sd*, vd*, nvme*, mmcblk*, loop*, dm-*, md* | 0660 | root:disk | -| tty | tty[0-9]* | 0620 | root:tty | -| tty | ttyS*, ttyUSB*, ttyACM* | 0660 | root:dialout | -| input | event*, mouse*, mice | 0660 | root:root | -| sound | * | 0660 | root:audio | -| video4linux | * | 0660 | root:video | -| drm | card*, render* | 0660 | root:video | -| (any) | null, zero, full, random, urandom | 0666 | root:root | -| (any) | console | 0600 | root:root | -| (default) | | 0660 | root:root | +| **Subsystem** | **Pattern** | **Mode** | **Owner:Group** | +|---------------|--------------------------------------------|----------|-----------------| +| block | sd*, vd*, nvme*, mmcblk*, loop*, dm-*, md* | 0660 | root:disk | +| tty | tty[0-9]* | 0620 | root:tty | +| tty | ttyS*, ttyUSB*, ttyACM* | 0660 | root:dialout | +| input | event*, mouse*, mice | 0660 | root:root | +| sound | * | 0660 | root:audio | +| video4linux | * | 0660 | root:video | +| drm | card*, render* | 0660 | root:video | +| (any) | null, zero, full, random, urandom | 0666 | root:root | +| (any) | console | 0600 | root:root | +| (default) | | 0660 | root:root | Persistent Symlinks @@ -122,6 +126,53 @@ kernel to re-emit add events for all existing devices. This replaces the separate `coldplug` script previously used with mdev. +Netlink Rebroadcast +------------------- + +The Linux kernel sends uevents to netlink multicast group 1 (bit 0) of +`NETLINK_KOBJECT_UEVENT`. Only the device manager listens on this raw +kernel group. Userspace consumers — applications using libudev — +expect to receive processed events on a separate netlink group. + +systemd/udevd established the convention of rebroadcasting processed +events to a separate group, and [libudev-zero][], a daemonless drop-in +replacement for libudev, listens on group `0x4` for these events. +Without a device manager rebroadcasting, graphical applications, +Wayland/X11 compositors, libinput, and anything else using libudev to +monitor device hotplug will never see any events. + +keventd rebroadcasts by default to netlink group 4 (`0x4`). A second +netlink socket is created at startup, and after each uevent has been +fully processed (device nodes created, symlinks set up, modules loaded), +the original raw event is sent to the configured group(s). +Rebroadcasting after processing ensures that device nodes and symlinks +already exist by the time consumers receive the notification. + +Use `-g GROUP` to override the default group mask, or `-G` to disable +rebroadcast entirely. Bit 0 of the group mask is always forced off to +prevent a feedback loop with the kernel's own multicast group. + +### Background + +The netlink uevent architecture uses separate multicast groups to +isolate the kernel-to-device-manager channel from the device-manager-to- +application channel: + +| **Group** | **Bit** | **Purpose** | +|-----------|---------|---------------------------------------------| +| 1 | 0 | Kernel events (device manager listens here) | +| 4 | 2 | Processed events (libudev consumers listen) | + +This two-group design was established by systemd/udevd and is the de +facto standard. [mdevd][] implements the same mechanism via its `-O` +flag, and keventd follows the same convention. + +For more details, see: + +- [libudev-zero][] — daemonless replacement for libudev +- [mdevd][] — mdev-compatible device manager with rebroadcast support + + Conditions ---------- @@ -154,7 +205,7 @@ the dependent services. keventd monitors the `power_supply` subsystem and provides: -- `sys/pwr/ac` -- asserted when AC power is connected +- `sys/pwr/ac` — asserted when AC power is connected This is useful for preventing power-hungry services from running on battery: @@ -170,11 +221,13 @@ battery: Usage ----- - keventd [-cdhnv] + keventd [-cdGhnv] [-g GROUP] Options: -c Run coldplug at startup -d Enable debug mode (foreground, verbose) + -g GROUP Override netlink rebroadcast group (default: 4) + -G Disable netlink rebroadcast entirely -h Show help text -n Run in foreground (no daemon) -v Show version @@ -193,7 +246,7 @@ Integration with Finit keventd is a standalone daemon started by Finit as an internal service. It communicates with Finit exclusively through the filesystem-based -condition system -- creating and removing symlinks in `/run/finit/cond/`. +condition system — creating and removing symlinks in `/run/finit/cond/`. This means keventd can also be tested independently: @@ -203,8 +256,14 @@ This means keventd can also be tested independently: # Run with coldplug to populate /dev from scratch keventd -c -n + # Run without rebroadcast (e.g., headless embedded system) + keventd -c -G + When keventd is enabled, it conflicts with external device managers. Only one device manager should be active at a time. The system configuration uses the `conflict:` directive to enforce this: service conflict:udevd,mdevd,mdev [...] keventd -c -- Finit device manager + +[libudev-zero]: https://github.com/illiliti/libudev-zero +[mdevd]: https://skarnet.org/software/mdevd/ diff --git a/man/keventd.8 b/man/keventd.8 index 90d5b983..1eac4ddb 100644 --- a/man/keventd.8 +++ b/man/keventd.8 @@ -13,6 +13,8 @@ .Nm .Op Fl c .Op Fl d +.Op Fl g Ar group +.Op Fl G .Op Fl h .Op Fl n .Op Fl v @@ -53,6 +55,21 @@ Enable debug mode. Implies All received uevents are logged to stderr. .It Fl h Show help text and exit. +.It Fl g Ar group +Override the netlink multicast group mask used for uevent rebroadcast. +The default is 4 (group 0x4), which is the +.Sy libudev-zero +convention. +Bit 0 is always masked out to prevent a feedback loop with the kernel +group. +See +.Sx NETLINK REBROADCAST +below. +.It Fl G +Disable netlink uevent rebroadcast entirely. +See +.Sx NETLINK REBROADCAST +below. .It Fl n Run in foreground, do not daemonize. Log messages are written to stderr instead of syslog. @@ -198,6 +215,40 @@ service cron { command = "cron -f" } .Ed +.Sh NETLINK REBROADCAST +The Linux kernel sends uevents to netlink multicast group 1 (bit 0) of +.Dv NETLINK_KOBJECT_UEVENT . +Only the device manager listens on this raw kernel group. Userspace +consumers such as applications using +.Sy libudev +expect to receive processed events on a separate netlink group. +.Pp +.Sy systemd/udevd +established the convention of rebroadcasting to a separate group, and +.Sy libudev-zero , +a daemonless replacement for libudev, listens on group 0x4 for these +events. Without rebroadcast, graphical applications, Wayland and X11 +compositors, libinput, and other libudev consumers will not receive +device hotplug events. +.Pp +.Nm +rebroadcasts by default to netlink group 4 (bit 2, i.e.\& +.Li 0x4 ) . +A second netlink socket is created at startup, and after each uevent +has been fully processed (device nodes created, modules loaded, +etc.\&), the original event is sent to the configured group. This +ensures that device nodes and symlinks already exist by the time +consumers receive the event. +.Pp +Use +.Fl g +to override the default group mask, or +.Fl G +to disable rebroadcast entirely. +.Pp +See also: +.Lk https://github.com/illiliti/libudev-zero "libudev-zero" , +.Lk https://skarnet.org/software/mdevd/ "mdevd" . .Sh SIGNALS .Bl -tag -width SIGUSR1 .It Dv SIGUSR1 From afb37b9724f1c47718c068379357043968ca36ba Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 29 Jan 2026 14:36:47 +0100 Subject: [PATCH 07/24] keventd: minor, constify + coding style Signed-off-by: Joachim Wiberg --- src/keventd.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/keventd.c b/src/keventd.c index 5093f444..466580cf 100644 --- a/src/keventd.c +++ b/src/keventd.c @@ -177,7 +177,7 @@ static void sys_cond(const char *cond, int set) } } -static int fgetline(char *path, char *buf, size_t len) +static int fgetline(const char *path, char *buf, size_t len) { FILE *fp; @@ -231,7 +231,7 @@ static int is_ac(const char *type) /* * Handle power_supply change events (original keventd functionality). */ -static void power_supply_change(struct uevent *ev, char *buf, size_t len) +static void power_supply_change(const struct uevent *ev, char *buf, size_t len) { int ac = 0; size_t i, hdrlen; @@ -344,7 +344,7 @@ static void init_power_supply(void) n = scandir(_PATH_SYSFS_PWR, &d, NULL, alphasort); for (i = 0; i < n; i++) { - char *nm = d[i]->d_name; + const char *nm = d[i]->d_name; char buf[10]; snprintf(path, sizeof(path), "%s/%s/type", _PATH_SYSFS_PWR, nm); @@ -430,12 +430,12 @@ static int usage(int rc) */ int main(int argc, char *argv[]) { + unsigned int nlgroups = REBC_DEFAULT_NLGROUP; struct sockaddr_nl nls = { 0 }; - struct pollfd pfd; char buf[UEVENT_BUFFER_SIZE]; - unsigned int nlgroups = REBC_DEFAULT_NLGROUP; int do_coldplug = 0; int foreground = 0; + struct pollfd pfd; int c; /* Device nodes are created with explicit MODE= from rules or the From c864531e65c540780d0a66528654e1db31d8c926 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 6 May 2026 22:09:29 +0200 Subject: [PATCH 08/24] keventd: disable legacy kernel uevent helper at startup Signed-off-by: Joachim Wiberg --- src/keventd.c | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/keventd.c b/src/keventd.c index 466580cf..058077dd 100644 --- a/src/keventd.c +++ b/src/keventd.c @@ -378,6 +378,17 @@ static void init_dev_condition_dir(void) warn("Failed creating dev condition directory %s", dir); } +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)); @@ -487,6 +498,9 @@ int main(int argc, char *argv[]) init_power_supply(); init_dev_condition_dir(); + /* Disable legacy kernel uevent helper; we own events via netlink */ + disable_uevent_helper(); + /* Set up netlink socket for kernel uevents */ pfd.events = POLLIN; pfd.fd = socket(PF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC, NETLINK_KOBJECT_UEVENT); From 803299293f550fe48462a5384f77bb58ce850cda Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 6 May 2026 22:32:23 +0200 Subject: [PATCH 09/24] keventd: add passive mode (-p) for coexistence with hotplug plugin This is a backwards compatible mode for users upgrading and not noticing that keventd is now build by default. Signed-off-by: Joachim Wiberg --- doc/keventd.md | 11 +++++----- src/conf.c | 16 +++++++++++---- src/keventd.c | 56 ++++++++++++++++++++++++++++++-------------------- 3 files changed, 52 insertions(+), 31 deletions(-) diff --git a/doc/keventd.md b/doc/keventd.md index b6a934ff..5722fff2 100644 --- a/doc/keventd.md +++ b/doc/keventd.md @@ -259,11 +259,12 @@ This means keventd can also be tested independently: # Run without rebroadcast (e.g., headless embedded system) keventd -c -G -When keventd is enabled, it conflicts with external device managers. -Only one device manager should be active at a time. The system -configuration uses the `conflict:` directive to enforce this: - - service conflict:udevd,mdevd,mdev [...] keventd -c -- Finit device manager +Only one device manager should be active at a time. This is settled +at build time: with the hotplug plugin enabled, Finit starts keventd +in passive mode (`-p`), leaving device management to the udevd, mdevd, +or mdev service from `system/10-hotplug.conf` and monitoring only +power supply events. Without the hotplug plugin, keventd runs as the +system device manager (`keventd -c`). [libudev-zero]: https://github.com/illiliti/libudev-zero [mdevd]: https://skarnet.org/software/mdevd/ diff --git a/src/conf.c b/src/conf.c index 6ffd6afc..f4329e46 100644 --- a/src/conf.c +++ b/src/conf.c @@ -2683,15 +2683,23 @@ int conf_init(uev_ctx_t *ctx) } #endif /* - * Start kernel event daemon as soon as possible, if enabled + * Start kernel event daemon as soon as possible, if enabled. + * In passive mode (-p) alongside the hotplug plugin, keventd + * only monitors power supply events (sys/pwr/ac condition). */ if (whichp(FINIT_EXECPATH_ "/keventd")) conf_save_service(SVC_TYPE_SERVICE, "keventd", "keventd.conf", - "\tdescription = \"Finit kernel event daemon\"\n" - "\trunlevel = \"S12345789\"\n" + "\tdescription = \"kernel event daemon\"\n" + "\trunlevel = \"S0123456789\"\n" "\tnotify = \"none\"\n" "\tcgroup init {}\n" - "\tcommand = \"" FINIT_EXECPATH_ "/keventd\"\n"); + "\tcommand = \"" FINIT_EXECPATH_ "/keventd" +#ifdef HAVE_HOTPLUG_PLUGIN + " -p" +#else + " -c" +#endif + "\"\n"); dbg("Allow plugins to register early runlevel 1 run/task/services ..."); plugin_run_hooks(HOOK_SVC_PLUGIN); diff --git a/src/keventd.c b/src/keventd.c index 058077dd..6552bb68 100644 --- a/src/keventd.c +++ b/src/keventd.c @@ -76,6 +76,7 @@ static int num_ac; static int running = 1; static int level; static int logon; +static int passive; /* power-supply only, no device management */ int debug; /* debug in other modules as well */ @@ -283,29 +284,33 @@ static void handle_uevent(char *buf, size_t len) switch (ev.action) { case ACT_ADD: - /* Firmware loading takes priority */ - if (ev.firmware) - firmware_load(&ev); + if (!passive) { + /* Firmware loading takes priority */ + if (ev.firmware) + firmware_load(&ev); - /* Module loading */ - if (ev.modalias) - modprobe_load(ev.modalias); + /* Module loading */ + if (ev.modalias) + modprobe_load(ev.modalias); - /* Create device node if we have the info */ - if (ev.major >= 0 && ev.minor >= 0 && ev.devname) - devnode_add(&ev); + /* Create device node if we have the info */ + if (ev.major >= 0 && ev.minor >= 0 && ev.devname) + devnode_add(&ev); - /* Create symlinks */ - symlink_add(&ev); + /* Create symlinks */ + symlink_add(&ev); + } break; case ACT_REMOVE: - /* Remove symlinks first */ - symlink_del(&ev); + if (!passive) { + /* Remove symlinks first */ + symlink_del(&ev); - /* Remove device node */ - if (ev.devname) - devnode_del(&ev); + /* Remove device node */ + if (ev.devname) + devnode_del(&ev); + } break; case ACT_CHANGE: @@ -412,7 +417,7 @@ static void shut_down(int signo) static int usage(int rc) { fprintf(stderr, - "Usage: keventd [-dGhnv] [-c] [-g GROUP]\n" + "Usage: keventd [-dGhnpv] [-c] [-g GROUP]\n" "\n" "Options:\n" " -c Run coldplug at startup\n" @@ -421,6 +426,7 @@ static int usage(int rc) " -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" " -v Show version\n" "\n", REBC_DEFAULT_NLGROUP); @@ -454,7 +460,7 @@ int main(int argc, char *argv[]) * requested bits verbatim instead of masking them. */ umask(0); - while ((c = getopt(argc, argv, "cdg:Ghnv")) != -1) { + while ((c = getopt(argc, argv, "cdg:Ghnpv")) != -1) { switch (c) { case 'c': do_coldplug = 1; @@ -474,6 +480,9 @@ int main(int argc, char *argv[]) case 'n': foreground = 1; break; + case 'p': + passive = 1; + break; case 'v': printf("keventd v%s\n", KEVENTD_VERSION); return 0; @@ -498,8 +507,10 @@ int main(int argc, char *argv[]) init_power_supply(); init_dev_condition_dir(); - /* Disable legacy kernel uevent helper; we own events via netlink */ - disable_uevent_helper(); + /* 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 */ pfd.events = POLLIN; @@ -519,8 +530,9 @@ int main(int argc, char *argv[]) setsockopt(pfd.fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf)); } - /* Initialize rebroadcast socket (default on, -G to disable) */ - if (nlgroups) + /* Initialize rebroadcast socket (default on, -G to disable). + * Skip in passive mode -- the hotplug daemon rebroadcasts. */ + if (nlgroups && !passive) rebc_init(nlgroups); /* Run coldplug if requested */ From acda0e1eb25feecde51b33b94b35dac49da1dddb Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 6 May 2026 23:24:10 +0200 Subject: [PATCH 10/24] keventd: signal readiness via pidfile after coldplug Signed-off-by: Joachim Wiberg --- src/conf.c | 2 +- src/keventd.c | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/conf.c b/src/conf.c index f4329e46..8127569d 100644 --- a/src/conf.c +++ b/src/conf.c @@ -2691,7 +2691,7 @@ int conf_init(uev_ctx_t *ctx) conf_save_service(SVC_TYPE_SERVICE, "keventd", "keventd.conf", "\tdescription = \"kernel event daemon\"\n" "\trunlevel = \"S0123456789\"\n" - "\tnotify = \"none\"\n" + "\tnotify = \"pid\"\n" "\tcgroup init {}\n" "\tcommand = \"" FINIT_EXECPATH_ "/keventd" #ifdef HAVE_HOTPLUG_PLUGIN diff --git a/src/keventd.c b/src/keventd.c index 6552bb68..b4fd5f24 100644 --- a/src/keventd.c +++ b/src/keventd.c @@ -539,6 +539,7 @@ int main(int argc, char *argv[]) if (do_coldplug) coldplug(); + pidfile(NULL); logit(LOG_NOTICE, "keventd v%s started, waiting for events...", KEVENTD_VERSION); while (running) { From b4df9930e7ee2f934beb2651a1938dc6d62a6df5 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 8 May 2026 11:03:31 +0200 Subject: [PATCH 11/24] Relocate keventd from src/ to keventd/ Signed-off-by: Joachim Wiberg --- Makefile.am | 4 ++++ configure.ac | 1 + keventd/.gitignore | 4 ++++ keventd/Makefile.am | 10 ++++++++++ {src => keventd}/keventd.c | 0 {src => keventd}/keventd.h | 0 {src => keventd}/uevent.c | 0 src/Makefile.am | 8 -------- 8 files changed, 19 insertions(+), 8 deletions(-) create mode 100644 keventd/.gitignore create mode 100644 keventd/Makefile.am rename {src => keventd}/keventd.c (100%) rename {src => keventd}/keventd.h (100%) rename {src => keventd}/uevent.c (100%) diff --git a/Makefile.am b/Makefile.am index a5333be6..73210dfc 100644 --- a/Makefile.am +++ b/Makefile.am @@ -1,5 +1,9 @@ ACLOCAL_AMFLAGS = -I m4 SUBDIRS = man plugins src system tmpfiles.d + +if KEVENTD +SUBDIRS += keventd +endif dist_doc_DATA = README.md LICENSE contrib/finit.conf if CONTRIB diff --git a/configure.ac b/configure.ac index 68668d12..d0e651cb 100644 --- a/configure.ac +++ b/configure.ac @@ -16,6 +16,7 @@ AC_CONFIG_FILES([Makefile man/Makefile plugins/Makefile src/Makefile + keventd/Makefile system/Makefile system/10-hotplug.conf test/test.env test/Makefile test/lib/Makefile test/src/Makefile diff --git a/keventd/.gitignore b/keventd/.gitignore new file mode 100644 index 00000000..138068ae --- /dev/null +++ b/keventd/.gitignore @@ -0,0 +1,4 @@ +.deps/ +keventd +Makefile +Makefile.in diff --git a/keventd/Makefile.am b/keventd/Makefile.am new file mode 100644 index 00000000..52d0cfba --- /dev/null +++ b/keventd/Makefile.am @@ -0,0 +1,10 @@ +AM_CPPFLAGS = -D_XOPEN_SOURCE=600 -D_BSD_SOURCE -D_GNU_SOURCE -D_DEFAULT_SOURCE +AM_CPPFLAGS += -I$(top_srcdir)/src + +pkglibexec_PROGRAMS = keventd + +keventd_SOURCES = keventd.c keventd.h uevent.c \ + $(top_srcdir)/src/util.c $(top_srcdir)/src/util.h +keventd_CFLAGS = -W -Wall -Wextra -std=gnu99 +keventd_CFLAGS += $(lite_CFLAGS) +keventd_LDADD = $(lite_LIBS) diff --git a/src/keventd.c b/keventd/keventd.c similarity index 100% rename from src/keventd.c rename to keventd/keventd.c diff --git a/src/keventd.h b/keventd/keventd.h similarity index 100% rename from src/keventd.h rename to keventd/keventd.h diff --git a/src/uevent.c b/keventd/uevent.c similarity index 100% rename from src/uevent.c rename to keventd/uevent.c diff --git a/src/Makefile.am b/src/Makefile.am index e685e2e8..14f11741 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -13,9 +13,6 @@ pkglibexec_PROGRAMS = getty logit runparts tmpfiles if SULOGIN pkglibexec_PROGRAMS += sulogin endif -if KEVENTD -pkglibexec_PROGRAMS += keventd -endif if WATCHDOGD pkglibexec_PROGRAMS += watchdogd endif @@ -30,11 +27,6 @@ getty_CFLAGS = -W -Wall -Wextra -std=gnu99 getty_CFLAGS += $(lite_CFLAGS) getty_LDADD = $(lite_LIBS) -keventd_SOURCES = keventd.c keventd.h uevent.c util.c util.h -keventd_CFLAGS = -W -Wall -Wextra -std=gnu99 -keventd_CFLAGS += $(lite_CFLAGS) -keventd_LDADD = $(lite_LIBS) - runparts_SOURCES = runparts.c runparts_CFLAGS = -W -Wall -Wextra -std=gnu99 runparts_CFLAGS += $(lite_CFLAGS) From 46316b2c6a76ee4460b653edecd561e4bf2c23de Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 8 May 2026 14:59:14 +0200 Subject: [PATCH 12/24] keventd: add udev rules engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Transform keventd from a power-supply monitor + basic hotplug handler into a full udev-compatible device manager. Rules engine (rules.c): - Full .rules file parser covering all udev key types: ACTION, KERNEL, SUBSYSTEM, DEVPATH, ENV, ATTR, SYSCTL, TAG, RESULT, PROGRAM, TEST, parent-chain KERNELS/SUBSYSTEMS/ATTRS/DRIVERS, and more - Pattern matching: plain string, fnmatch glob, and pipe-separated alternatives - Operators: ==, !=, =, +=, -=, := - Assignments: NAME=, MODE=, OWNER=, GROUP=, SYMLINK+=, ENV{k}=, TAG+=, RUN+= - IMPORT{program|file|builtin|parent|cmdline|db}= - PROGRAM= with stdout capture for subsequent RESULT== matching - GOTO=/LABEL= flow control - Loads *.rules from /lib/udev/rules.d, /run/udev/rules.d, /etc/udev/rules.d and an optional extra directory (-r DIR); reloads on SIGHUP Builtin framework (builtin.c): - kmod: load module by MODALIAS or explicit alias - hwdb: match device against *.hwdb text files in udev hwdb dirs; builds correct lookup key per subsystem — evdev:input:b*v*p*e* for input, usb:v*p* for USB, raw modalias for PCI/platform - path_id: build stable ID_PATH / ID_PATH_TAG from sysfs topology (PCI, USB, ATA, NVMe, platform, ACPI, virtio) - usb_id: read idVendor/idProduct/bcdDevice/serial from sysfs; look up ID_VENDOR_FROM_DATABASE and ID_MODEL_FROM_DATABASE from usb.ids (hwdata package) when available; silent fallback when absent - input_id: classify input devices (keyboard, mouse, joystick, touchscreen, touchpad) from evdev capability bitmasks in sysfs - net_id: generate predictable names — MAC-based enx and PCI-slot-based enps[f] - blkid: probe filesystem type, UUID, and label via libblkid; sets ID_FS_* and ID_PART_TABLE_* properties Network interface renaming (uevent.c): - netdev_add() renames interfaces via SIOCSIFNAME when a NAME= rule matched, then sets the Finit dev/ condition on the final name; and any setup using persistent interface naming via udev rules Device node and symlink improvements (uevent.c): - NAME=, MODE=, OWNER=, GROUP= overrides from matched rules applied at mknod/chown time, falling back to the built-in permission table - SYMLINK+= links from rules applied alongside built-in by-id/by-path links Device property database (udevdb.c): - Persist per-device E:/S:/I: records to /run/udev/data/ on ADD/CHANGE, delete on REMOVE; IMPORT{db}= restores saved properties into event env Build: - libblkid (util-linux) is now required for keventd Signed-off-by: Joachim Wiberg --- configure.ac | 8 +- doc/Makefile.am | 2 +- doc/udev-matching.md | 123 ++++ keventd/Makefile.am | 10 + keventd/builtin.c | 1530 +++++++++++++++++++++++++++++++++++++++ keventd/builtin.h | 46 ++ keventd/keventd.c | 80 ++- keventd/keventd.h | 89 ++- keventd/rules.c | 1621 ++++++++++++++++++++++++++++++++++++++++++ keventd/rules.h | 163 +++++ keventd/sysfs.c | 131 ++++ keventd/sysfs.h | 75 ++ keventd/udevdb.c | 243 +++++++ keventd/udevdb.h | 72 ++ keventd/uevent.c | 274 ++++++- mkdocs.yml | 1 + 16 files changed, 4433 insertions(+), 35 deletions(-) create mode 100644 doc/udev-matching.md create mode 100644 keventd/builtin.c create mode 100644 keventd/builtin.h create mode 100644 keventd/rules.c create mode 100644 keventd/rules.h create mode 100644 keventd/sysfs.c create mode 100644 keventd/sysfs.h create mode 100644 keventd/udevdb.c create mode 100644 keventd/udevdb.h diff --git a/configure.ac b/configure.ac index d0e651cb..9f00b36b 100644 --- a/configure.ac +++ b/configure.ac @@ -319,7 +319,11 @@ AS_IF([test "x$rtc_file" != "xno"], [ AC_DEFINE_UNQUOTED(RTC_FILE, "$rtcfile_path", [Save and restore system time from this file if /dev/rtc is missing.])],[ AC_DEFINE_UNQUOTED(RTC_FILE, NULL)]) -AS_IF([test "x$with_keventd" != "xno"], [with_keventd=yes]) +AS_IF([test "x$with_keventd" != "xno"], [ + with_keventd=yes + PKG_CHECK_MODULES([blkid], [blkid],, [ + AC_MSG_ERROR([libblkid (util-linux) is required to build keventd. + Install the dev package (e.g. libblkid-dev) or disable keventd with --without-keventd])])]) AS_IF([test "x$with_sulogin" != "xno"], [ AS_IF([test "x$sulogin" = "xyes"], [ @@ -435,7 +439,7 @@ Optional features: Install doc/..........: $enable_doc Install contrib/......: $enable_contrib Bash completion.......: $bash_completion_dir - Built-in keventd......: $with_keventd + Built-in keventd......: $with_keventd (blkid: $blkid_LIBS) Built-in sulogin......: $with_sulogin $sulogin Built-in watchdogd....: $with_watchdog $watchdog Built-in logrotate....: $enable_logrotate diff --git a/doc/Makefile.am b/doc/Makefile.am index a649398e..dc821c7d 100644 --- a/doc/Makefile.am +++ b/doc/Makefile.am @@ -6,6 +6,6 @@ dist_docs_DATA = build.md cmdline.md commands.md conditions.md \ distro.md example.md features.md index.md initctl.md \ keventd.md plugins.md requirements.md runlevels.md \ runparts.md service.md signals.md state-machine.md \ - watchdog.md + udev-matching.md watchdog.md dist_docs_DATA += img/logo.png img/svc-machine.png img/svc-machine.svg \ img/alpine-screenshot2.png diff --git a/doc/udev-matching.md b/doc/udev-matching.md new file mode 100644 index 00000000..3ec013f3 --- /dev/null +++ b/doc/udev-matching.md @@ -0,0 +1,123 @@ +# How udev rules work + +## Summary of the complete fix + +- hardware.c: Write authorized_default=1 on the bus path before nftw + runs (just 2 lines added). This exploits the fact that apath already + contains path/authorized_default at that point (set by the snprintf on + line 71). With authorized_default=1 set on the root hub, any device + that appears behind an intermediate hub during hub enumeration gets + auto-authorized by the kernel. +- 70-usb-authorize.rules: Belt-and-suspenders for the async case. If hub + port probing is slow enough that children appear after nftw has + already returned, udev sees the add event with authorized=0, walks up + the parent chain with ATTRS{authorized_default}=="1", and authorizes + the device. Locked buses (authorized_default=0) are correctly skipped. + +``` +ACTION=="add", SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", \ + ATTR{authorized}=="0", \ + ATTRS{authorized_default}=="1", \ + RUN+="/bin/sh -c 'echo 1 > /sys%p/authorized'" +``` + + This keeps authorized_default=2 (deny by default) fully intact — the + security window stays closed. The udev rule only fires when confd has + explicitly marked a bus as unlocked. + +## Comment from Undersigned + +Very interesting, I did not know you could do such complex operations +with udev rules. Could you explain a bit more in detail how the matching +works and how we can guarantee locked buses/ports are not accidentally unlocked? + +> Sure. Let's walk through it step by step. + +## How udev ATTRS{} matching works + +A udev rule matches against a device and its entire parent chain in the +sysfs device tree. When a device like 2-1.4 (the keyboard) is added, its +sysfs hierarchy looks like: + +``` +/sys/devices/.../0000:01:00.0/usb2/ ← root hub (usbN) + usb2/2-1/ ← VIA Labs hub + usb2/2-1/2-1.4/ ← keyboard ← event fires here +``` + +- ATTR{} (no trailing S) — matches attributes on the device itself only +- ATTRS{} (with S) — matches attributes on the device or any of its + parents, walking up the tree until it finds a match or runs out of + parents + +So ATTRS{authorized_default}=="1" on a keyboard event at 2-1.4 walks: + +1. 2-1.4/authorized_default — doesn't exist (not a hub/controller) +2. 2-1/authorized_default — doesn't exist (downstream hubs don't have + this attribute, only root hubs do) +3. usb2/authorized_default — exists, check its value + +This is exactly the right level: authorized_default only lives on root +hubs (usbN), which is precisely where confd writes. + +## Why locked buses can't be accidentally unlocked + +The invariant is: confd owns `authorized_default`. It sets it to 0 +(locked) or 1 (unlocked). The udev rule only reads it, never writes it. + +Walk through the locked case: + +``` +confd sets usb2/authorized_default = 0 ← locked +``` + +A device plugs into usb2. udev fires: + +1. ATTR{authorized}=="0" — matches (device is unauthorized) +2. ATTRS{authorized_default}=="1" — walks up to usb2, finds 0 → no match +3. Rule doesn't fire. Device stays denied. ✓ + +For a device behind an intermediate hub on a locked bus: + +``` +usb2/authorized_default = 0 ← bus locked +usb2/2-1/ ← hub (somehow authorized) +usb2/2-1/2-1.4/ ← keyboard +``` + +Even if the hub itself were authorized somehow, the keyboard's parent +chain still reaches `usb2/authorized_default = 0` before finding any +`authorized_default = 1`. The rule doesn't fire. ✓ + +The only remaining question: what if two buses exist and one is locked? + +Say usb1 is unlocked and usb2 is locked. A device on usb2 fires a udev +event. ATTRS walks up: `usb2/authorized_default = 0` — stops there (once +it finds the attribute it stops). It does not continue up to PCIe or +platform buses looking for usb1's attribute. Each USB device's parent +chain only goes through its own root hub. + +This is because authorized_default is found at usb2 — udev stops as soon +as it matches the attribute name, regardless of value. If the value +doesn't match the condition, the whole rule fails. It doesn't continue +looking at other buses. + +## Summary of the guarantee + +``` +┌───────────────────────┬─────────────────────────┬─────────────────────────┐ +│ Scenario │ usb2/authorized_default │ Rule fires? │ +├───────────────────────┼─────────────────────────┼─────────────────────────┤ +│ Bus unlocked by confd │ 1 │ Yes → device authorized │ +├───────────────────────┼─────────────────────────┼─────────────────────────┤ +│ Bus locked by confd │ 0 │ No → device denied │ +├───────────────────────┼─────────────────────────┼─────────────────────────┤ +│ Bus at kernel default │ 2 │ No → device denied │ +├───────────────────────┼─────────────────────────┼─────────────────────────┤ +│ Bus doesn't exist yet │ — │ No → no match │ +└───────────────────────┴─────────────────────────┴─────────────────────────┘ +``` + +The rule is purely reactive: it can only authorize devices on buses that +confd has already explicitly declared unlocked. It has no ability to +override a lock. diff --git a/keventd/Makefile.am b/keventd/Makefile.am index 52d0cfba..0b4f1166 100644 --- a/keventd/Makefile.am +++ b/keventd/Makefile.am @@ -4,7 +4,17 @@ AM_CPPFLAGS += -I$(top_srcdir)/src pkglibexec_PROGRAMS = keventd keventd_SOURCES = keventd.c keventd.h uevent.c \ + udevdb.c udevdb.h \ + sysfs.c sysfs.h \ + rules.c rules.h \ + builtin.c builtin.h \ $(top_srcdir)/src/util.c $(top_srcdir)/src/util.h 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 +# handled via explicit checks where present. Matches systemd-udev policy. +keventd_CFLAGS += -Wno-format-truncation keventd_CFLAGS += $(lite_CFLAGS) +keventd_CFLAGS += $(blkid_CFLAGS) keventd_LDADD = $(lite_LIBS) +keventd_LDADD += $(blkid_LIBS) diff --git a/keventd/builtin.c b/keventd/builtin.c new file mode 100644 index 00000000..faf50e2c --- /dev/null +++ b/keventd/builtin.c @@ -0,0 +1,1530 @@ +/* Builtin command implementations for keventd rules engine + * + * Copyright (c) 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include "builtin.h" +#include "keventd.h" +#include "sysfs.h" + +/* Forward declarations from keventd.c and uevent.c */ +void logit(int prio, const char *fmt, ...); +int modprobe_load(const char *modalias); + +/* Forward declarations — defined in the usb_id section below */ +static int find_usb_dev(const char *devpath, char *usbdir, size_t len); +static int is_scsi_sysname(const char *name); + +/* ---- kmod -------------------------------------------------------- */ + +static int builtin_kmod(struct uevent *ev, int argc, char **argv) +{ + int idx = 1; + int i; + + /* udev convention: kmod load [ ...] -- skip the subcommand */ + if (idx < argc && !strcmp(argv[idx], "load")) + idx++; + + if (idx >= argc) { + const char *alias = ev->modalias; + + if (!alias) + alias = uevent_getenv(ev, "MODALIAS"); + if (!alias) + return -1; + return modprobe_load(alias); + } + + { + int rc = 0; + + for (i = idx; i < argc; i++) { + if (modprobe_load(argv[i]) < 0) + rc = -1; + } + return rc; + } +} + +/* ---- hwdb -------------------------------------------------------- */ + +/* + * Match a single text hwdb file against the lookup key. + * Text format: match lines at column 0, property lines indented. + * Empty lines or lines starting with '#' reset the match state. + * + * Multiple consecutive match lines OR-merge: if any of the match lines + * matches the lookup key, the following property block applies. The + * group ends when we see a property line followed by another match + * line (start of next group), or a blank/comment line. + */ +static void hwdb_match_file(const char *path, const char *lookup, + struct uevent *ev) +{ + char line[1024]; + int matched = 0; + int in_props = 0; /* saw a property line for the current group */ + FILE *fp; + + fp = fopen(path, "r"); + if (!fp) + return; + + while (fgets(line, sizeof(line), fp)) { + line[strcspn(line, "\n")] = '\0'; + + if (!line[0] || line[0] == '#') { + matched = 0; + in_props = 0; + continue; + } + + if (isspace((unsigned char)line[0])) { + char *p, *eq; + + if (!matched) + continue; + p = line; + while (isspace((unsigned char)*p)) + p++; + eq = strchr(p, '='); + if (!eq) + continue; + *eq = '\0'; + uevent_setenv(ev, p, eq + 1); + in_props = 1; + } else { + /* Match line. If we just finished a property block, + * start a fresh match group; otherwise OR-merge with + * any previous match lines in this group. */ + if (in_props) { + matched = 0; + in_props = 0; + } + if (fnmatch(line, lookup, 0) == 0) + matched = 1; + } + } + + fclose(fp); +} + +/* + * Build the hwdb lookup key for input devices. + * + * udev hwdb files use the format: + * evdev:input:b{BUS}v{VENDOR}p{PRODUCT}e{VERSION}* + * where all fields are 4-digit uppercase hex. + * + * The id/ files live under the input device directory. For eventN nodes + * (devpath ends in .../inputN/eventN) we strip the last component first. + */ +static void hwdb_input_key(const struct uevent *ev, char *key, size_t len) +{ + char base[PATH_MAX]; + char path[PATH_MAX]; + char bus_s[8], vendor_s[8], product_s[8], version_s[8]; + + if (!ev->devpath) + return; + + snprintf(base, sizeof(base), "/sys%s", ev->devpath); + + /* Try parent dir first (for eventN nodes sitting under inputN/) */ + snprintf(path, sizeof(path), "%s", base); + { + char *sl = strrchr(path, '/'); + + if (sl) + *sl = '\0'; + } + snprintf(base, sizeof(base), "%s/id/bustype", path); + if (sysfs_read_file(base, bus_s, sizeof(bus_s)) < 0) { + /* Already at the input device dir */ + snprintf(base, sizeof(base), "/sys%s/id/bustype", ev->devpath); + if (sysfs_read_file(base, bus_s, sizeof(bus_s)) < 0) + return; + snprintf(path, sizeof(path), "/sys%s", ev->devpath); + } + + snprintf(base, sizeof(base), "%s/id/vendor", path); sysfs_read_file(base, vendor_s, sizeof(vendor_s)); + snprintf(base, sizeof(base), "%s/id/product", path); sysfs_read_file(base, product_s, sizeof(product_s)); + snprintf(base, sizeof(base), "%s/id/version", path); sysfs_read_file(base, version_s, sizeof(version_s)); + + snprintf(key, len, "evdev:input:b%04Xv%04Xp%04Xe%04X*", + (unsigned)strtoul(bus_s, NULL, 16), + (unsigned)strtoul(vendor_s, NULL, 16), + (unsigned)strtoul(product_s, NULL, 16), + (unsigned)strtoul(version_s, NULL, 16)); +} + +/* + * Build the hwdb lookup key for USB devices. + * + * udev hwdb files use the format: usb:v{VID}p{PID}* + * where VID and PID are 4-digit uppercase hex. + * + * Works for both usb_device events (devpath IS the USB device dir) and + * for child events where we need to navigate up to the USB device parent. + */ +static void hwdb_usb_key(const struct uevent *ev, char *key, size_t len) +{ + char path[PATH_MAX]; + char usbdir[PATH_MAX]; + char vendor_s[8] = "", product_s[8] = ""; + + if (!ev->devpath) + return; + + /* Try devpath directly first (DEVTYPE==usb_device rule context) */ + snprintf(path, sizeof(path), "/sys%s/idVendor", ev->devpath); + if (sysfs_read_file(path, vendor_s, sizeof(vendor_s)) < 0) { + /* Navigate up to the USB device ancestor */ + if (find_usb_dev(ev->devpath, usbdir, sizeof(usbdir)) < 0) + return; + snprintf(path, sizeof(path), "%s/idVendor", usbdir); + if (sysfs_read_file(path, vendor_s, sizeof(vendor_s)) < 0) + return; + snprintf(path, sizeof(path), "%s/idProduct", usbdir); + } else { + snprintf(path, sizeof(path), "/sys%s/idProduct", ev->devpath); + } + + if (sysfs_read_file(path, product_s, sizeof(product_s)) < 0) + return; + + snprintf(key, len, "usb:v%04Xp%04X*", + (unsigned)strtoul(vendor_s, NULL, 16), + (unsigned)strtoul(product_s, NULL, 16)); +} + +static void hwdb_scan(const char *key, struct uevent *ev) +{ + static const char *dirs[] = { + "/lib/udev/hwdb.d", + "/run/udev/hwdb.d", + "/etc/udev/hwdb.d", + NULL + }; + int i; + + for (i = 0; dirs[i]; i++) { + struct dirent **entries; + int n, j; + + n = scandir(dirs[i], &entries, NULL, alphasort); + if (n < 0) + continue; + + for (j = 0; j < n; j++) { + const char *name = entries[j]->d_name; + size_t nlen = strlen(name); + + if (nlen > 5 && !strcmp(name + nlen - 5, ".hwdb")) { + char path[PATH_MAX]; + + snprintf(path, sizeof(path), "%s/%s", dirs[i], name); + hwdb_match_file(path, key, ev); + } + free(entries[j]); + } + free(entries); + } +} + +static int builtin_hwdb(struct uevent *ev, int argc, char **argv) +{ + const char *subsys = NULL; + const char *prefix = NULL; + char key[256] = ""; + int i; + + /* Parse --subsystem=X and --lookup-prefix=Y */ + for (i = 1; i < argc; i++) { + if (!strncmp(argv[i], "--subsystem=", 12)) + subsys = argv[i] + 12; + else if (!strncmp(argv[i], "--lookup-prefix=", 16)) + prefix = argv[i] + 16; + } + if (!subsys) + subsys = ev->subsystem; + + if (subsys && !strcmp(subsys, "input")) { + hwdb_input_key(ev, key, sizeof(key)); + } else if (subsys && (!strcmp(subsys, "usb") || + !strcmp(subsys, "usb_device"))) { + hwdb_usb_key(ev, key, sizeof(key)); + } + + /* Fall back to MODALIAS / ev->modalias for pci, platform, etc. */ + if (!key[0]) { + const char *m = uevent_getenv(ev, "MODALIAS"); + + if (!m) + m = ev->modalias; + if (m) + snprintf(key, sizeof(key), "%s", m); + } + + if (!key[0]) + return 0; + + /* --lookup-prefix scopes the match by prepending to the key. */ + if (prefix && *prefix) { + char tmp[sizeof(key)]; + + snprintf(tmp, sizeof(tmp), "%s%s", prefix, key); + snprintf(key, sizeof(key), "%s", tmp); + } + + logit(LOG_DEBUG, "rules: hwdb lookup: %s", key); + hwdb_scan(key, ev); + return 0; +} + +/* ---- path_id ----------------------------------------------------- */ + +/* + * Build a stable device path ID from the sysfs topology. + * Walks devpath from root to leaf, emitting segments for recognised + * subsystems (pci, usb, ata, nvme, platform, acpi, virtio). + */ +static int builtin_path_id(struct uevent *ev, int argc, char **argv) +{ + char seg[PATH_MAX] = ""; + char tag[PATH_MAX]; + const char *dp; + const char *p; + + (void)argc; + (void)argv; + + dp = ev->devpath; + if (!dp) + return -1; + + p = dp; + while (*p) { + char comp[128]; + char cpath[PATH_MAX]; + char subsys[64] = ""; + char lnk[PATH_MAX]; + char new_seg[160] = ""; + ssize_t r; + const char *slash; + const char *sl; + size_t clen; + + while (*p == '/') + p++; + slash = strchr(p, '/'); + clen = slash ? (size_t)(slash - p) : strlen(p); + + if (!clen || clen >= sizeof(comp)) { + if (!slash) + break; + p = slash; + continue; + } + + memcpy(comp, p, clen); + comp[clen] = '\0'; + + /* sysfs path up to and including this component */ + snprintf(cpath, sizeof(cpath), "/sys%.*s", + (int)(p - dp + (ptrdiff_t)clen), dp); + + /* read subsystem symlink */ + { + char slnk[PATH_MAX]; + + snprintf(slnk, sizeof(slnk), "%s/subsystem", cpath); + r = readlink(slnk, lnk, sizeof(lnk) - 1); + } + if (r > 0) { + lnk[r] = '\0'; + sl = strrchr(lnk, '/'); + snprintf(subsys, sizeof(subsys), "%s", + sl ? sl + 1 : lnk); + } + + if (!strcmp(subsys, "pci")) { + snprintf(new_seg, sizeof(new_seg), "pci-%s", comp); + } else if (!strcmp(subsys, "usb")) { + /* + * Skip interfaces (':') and root hubs (usbN). + * Emit "usb-0:" to match eudev's path_id + * format so by-path symlinks are portable. + */ + if (!strchr(comp, ':') && isdigit((unsigned char)comp[0])) + snprintf(new_seg, sizeof(new_seg), "usb-0:%s", comp); + } else if (!strcmp(subsys, "scsi") && is_scsi_sysname(comp)) { + /* + * Generic SCSI default handler. scsi_device sysname + * is H:B:T:L; scsi_host/scsi_target also live under + * subsystem "scsi" but their sysnames don't match + * the 4-tuple pattern -- the is_scsi_sysname() guard + * skips those. FC, SAS, iSCSI transport-specific + * naming requires parent-walking we don't yet + * implement (audit gap #3). + */ + snprintf(new_seg, sizeof(new_seg), "scsi-%s", comp); + } else if (!strncmp(comp, "ata", 3) && + isdigit((unsigned char)comp[3])) { + snprintf(new_seg, sizeof(new_seg), "ata-%s", comp + 3); + } else if (!strncmp(comp, "nvme", 4) && + strchr(comp + 4, 'n')) { + /* nvme0n1 -> nvme-1 */ + const char *ns = strchr(comp + 4, 'n'); + + snprintf(new_seg, sizeof(new_seg), "nvme-%s", ns + 1); + } else if (!strcmp(subsys, "platform") || + !strcmp(subsys, "acpi") || + !strcmp(subsys, "virtio")) { + snprintf(new_seg, sizeof(new_seg), "%s-%s", subsys, comp); + } + + if (new_seg[0]) { + if (seg[0]) + strncat(seg, "-", sizeof(seg) - strlen(seg) - 1); + strncat(seg, new_seg, sizeof(seg) - strlen(seg) - 1); + } + + if (!slash) + break; + p = slash; + } + + if (!seg[0]) + return 0; + + uevent_setenv(ev, "ID_PATH", seg); + + /* + * ID_PATH_TAG: same as ID_PATH but with non-alnum/non-'-' chars + * replaced by '_', collapsing consecutive '_' and stripping any + * trailing one. Matches eudev's encoding so by-path symlinks + * use identical tag names across implementations. + */ + { + const char *s = seg; + char *t = tag; + int last_under = 0; + + while (*s && (size_t)(t - tag) < sizeof(tag) - 1) { + unsigned char c = *s++; + + if (isalnum(c) || c == '-') { + *t++ = c; + last_under = 0; + } else if (!last_under) { + *t++ = '_'; + last_under = 1; + } + } + while (t > tag && t[-1] == '_') + t--; + *t = '\0'; + } + uevent_setenv(ev, "ID_PATH_TAG", tag); + + return 0; +} + +/* ---- usb_id ------------------------------------------------------ */ + +/* + * Find the USB device directory (not the interface) in the sysfs devpath. + * A USB device component matches: digits + '-' + (digits '.')* + digits, + * with no ':' character (which would indicate an interface). + */ +static int find_usb_dev(const char *devpath, char *usbdir, size_t len) +{ + const char *p = devpath; + const char *last = NULL; + const char *lastend = NULL; + + while (*p) { + const char *slash; + size_t clen; + char seg[64]; + + while (*p == '/') + p++; + slash = strchr(p, '/'); + clen = slash ? (size_t)(slash - p) : strlen(p); + + if (clen > 0 && clen < sizeof(seg) && + isdigit((unsigned char)*p)) { + memcpy(seg, p, clen); + seg[clen] = '\0'; + if (strchr(seg, '-') && !strchr(seg, ':')) { + last = p; + lastend = slash; /* NULL if at end */ + } + } + + if (!slash) + break; + p = slash; + } + + if (!last) + return -1; + + ptrdiff_t prefix = lastend ? (lastend - devpath) + : (ptrdiff_t)strlen(devpath); + snprintf(usbdir, len, "/sys%.*s", (int)prefix, devpath); + return 0; +} + +/* + * Look up vendor and product names from a usb.ids file (hwdata package). + * + * Tries standard install paths; returns -1 if no file is available so the + * caller can silently skip setting the DATABASE keys on slim systems. + * + * usb.ids format: + * VVVV Vendor Name (vendor line, hex at column 0) + * PPPP Product Name (product line, one TAB indent) + * CC PP Interface name (two TABs — ignored) + * # comment / blank line + */ +static int lookup_usb_ids(unsigned int vendor_id, unsigned int product_id, + char *vendor_out, size_t vsz, + char *product_out, size_t psz) +{ + static const char *candidates[] = { + "/usr/share/hwdata/usb.ids", + "/usr/share/misc/usb.ids", + "/var/lib/usbutils/usb.ids", + NULL + }; + static const char *cached_path; + char line[512]; + FILE *fp = NULL; + int found_vendor = 0; + + if (cached_path) { + fp = fopen(cached_path, "r"); + } else { + int i; + + for (i = 0; candidates[i]; i++) { + fp = fopen(candidates[i], "r"); + if (fp) { + cached_path = candidates[i]; + break; + } + } + } + if (!fp) + return -1; + + vendor_out[0] = '\0'; + product_out[0] = '\0'; + + while (fgets(line, sizeof(line), fp)) { + unsigned int id; + char *name; + size_t nl; + + nl = strlen(line); + while (nl > 0 && (line[nl - 1] == '\n' || line[nl - 1] == '\r')) + line[--nl] = '\0'; + + if (!line[0] || line[0] == '#') + continue; + + if (line[0] == '\t' && line[1] == '\t') + continue; /* interface / protocol sub-entry */ + + if (line[0] == '\t') { + /* Product line: "\tPPPP Product Name" */ + if (!found_vendor) + continue; + if (sscanf(line + 1, "%4x", &id) != 1 || id != product_id) + continue; + name = line + 1 + 4; + while (*name == ' ') + name++; + snprintf(product_out, psz, "%s", name); + break; /* found both vendor and product */ + } else { + /* Vendor line: "VVVV Vendor Name" */ + if (sscanf(line, "%4x", &id) != 1) + continue; + if (found_vendor) + break; /* past our vendor's section, give up */ + if (id == vendor_id) { + name = line + 4; + while (*name == ' ') + name++; + snprintf(vendor_out, vsz, "%s", name); + found_vendor = 1; + } + } + } + + fclose(fp); + return (vendor_out[0] || product_out[0]) ? 0 : -1; +} + +/* + * Map a USB interface class to its short ID_TYPE string. + * Mirrors eudev's set_usb_iftype(). + */ +static const char *usb_iftype(int cls) +{ + switch (cls) { + case 0x01: return "audio"; + case 0x03: return "hid"; + case 0x06: return "media"; + case 0x07: return "printer"; + case 0x08: return "storage"; + case 0x09: return "hub"; + case 0x0e: return "video"; + default: return "generic"; + } +} + +/* Mass-storage subclass → ID_TYPE refinement. */ +static const char *usb_storage_subtype(int sub) +{ + switch (sub) { + case 1: return "rbc"; + case 2: return "atapi"; + case 3: return "tape"; + case 4: return "floppy"; + case 6: return "scsi"; + default: return "generic"; + } +} + +/* True if NAME has the SCSI scsi_device sysname shape "H:B:T:L". */ +static int is_scsi_sysname(const char *name) +{ + int h, b, t, l; + + return sscanf(name, "%d:%d:%d:%d", &h, &b, &t, &l) == 4; +} + +/* SCSI peripheral type code (from /sys/...//type) → ID_TYPE. */ +static const char *scsi_type(int n) +{ + switch (n) { + case 0: + case 0xe: return "disk"; + case 1: return "tape"; + case 4: + case 7: + case 0xf: return "optical"; + case 5: return "cd"; + default: return "generic"; + } +} + +/* + * Walk up from /sys{devpath} looking for a directory whose basename + * matches the SCSI sysname pattern "H:B:T:L" (4 ints colon-separated). + * That's the scsi_device dir, which has vendor/model/type/rev attrs. + */ +static int find_scsi_device(const char *devpath, char *scsidir, size_t len) +{ + char path[PATH_MAX]; + char *slash; + + snprintf(path, sizeof(path), "/sys%s", devpath); + + while ((slash = strrchr(path, '/')) != NULL && (slash - path) > 4) { + if (is_scsi_sysname(slash + 1)) { + snprintf(scsidir, len, "%s", path); + return 0; + } + *slash = '\0'; + } + return -1; +} + +/* + * Walk up from /sys{devpath} until a directory containing + * bInterfaceClass is found -- that's the USB interface dir. + * Returns -1 if called on the usb_device itself or anything + * without a USB interface ancestor. + */ +static int find_usb_interface(const char *devpath, char *ifdir, size_t len) +{ + char path[PATH_MAX], test[PATH_MAX]; + + snprintf(path, sizeof(path), "/sys%s", devpath); + + for (;;) { + char *slash; + + snprintf(test, sizeof(test), "%s/bInterfaceClass", path); + if (access(test, F_OK) == 0) { + snprintf(ifdir, len, "%s", path); + return 0; + } + slash = strrchr(path, '/'); + if (!slash || (slash - path) <= 4) /* stay below /sys */ + return -1; + *slash = '\0'; + } +} + +/* + * Collapse whitespace runs to '_', strip leading/trailing. Used to + * normalize human-readable sysfs strings before they end up in + * /dev/disk/by-id symlink names. Mirrors eudev's util_replace_whitespace. + */ +static void usb_normalize(const char *src, char *dst, size_t len) +{ + size_t i = 0; + int in_space = 1; /* skip leading whitespace */ + + while (*src && i + 1 < len) { + unsigned char c = *src++; + + if (isspace(c)) { + in_space = 1; + } else { + if (in_space && i > 0) + dst[i++] = '_'; + if (i + 1 < len) + dst[i++] = c; + in_space = 0; + } + } + dst[i] = '\0'; +} + +/* + * Parse the USB device's binary descriptors file and emit a string of + * `:CCSSPP` interface triplets (class/subclass/protocol). Matches + * eudev's dev_if_packed_info(). + */ +static int usb_read_interfaces(const char *usbdir, char *out, size_t len) +{ + char path[PATH_MAX]; + unsigned char buf[4096]; /* descriptors blobs are typically <1KB */ + ssize_t n; + size_t pos = 0, used = 0; + int fd; + + snprintf(path, sizeof(path), "%s/descriptors", usbdir); + fd = open(path, O_RDONLY | O_CLOEXEC); + if (fd < 0) + return -1; + n = read(fd, buf, sizeof(buf)); + close(fd); + if (n < 18) + return -1; + + out[0] = '\0'; + while (pos + 9 < (size_t)n && used + 8 < len) { + unsigned char blen = buf[pos]; + unsigned char btype = buf[pos + 1]; + + if (blen < 3) + break; + if (btype == 0x04) { /* USB_DT_INTERFACE */ + char triplet[8]; + + if (snprintf(triplet, sizeof(triplet), ":%02x%02x%02x", + buf[pos + 5], buf[pos + 6], buf[pos + 7]) == 7 && + !strstr(out, triplet)) { + memcpy(out + used, triplet, 7); + used += 7; + out[used] = '\0'; + } + } + pos += blen; + } + if (used && used + 1 < len) { + out[used++] = ':'; + out[used] = '\0'; + } + return used ? 0 : -1; +} + +static int builtin_usb_id(struct uevent *ev, int argc, char **argv) +{ + char usbdir[PATH_MAX]; + char ifdir[PATH_MAX]; + char path[PATH_MAX]; + char val[256]; + int ms_subclass = -1; /* mass-storage subclass (-1 = not mass-storage) */ + + (void)argc; + (void)argv; + + if (!ev->devpath) + return -1; + if (find_usb_dev(ev->devpath, usbdir, sizeof(usbdir)) < 0) + return -1; + + uevent_setenv(ev, "ID_BUS", "usb"); + + /* + * Pull per-interface info (class for ID_TYPE classification, + * bInterfaceNumber, driver name). Optional -- the device may be + * the usb_device itself, in which case we skip this and rely on + * the descriptors file below. + */ + if (find_usb_interface(ev->devpath, ifdir, sizeof(ifdir)) == 0) { + char cls_s[8] = "", sub_s[8] = ""; + + snprintf(path, sizeof(path), "%s/bInterfaceClass", ifdir); + sysfs_read_file(path, cls_s, sizeof(cls_s)); + snprintf(path, sizeof(path), "%s/bInterfaceSubClass", ifdir); + sysfs_read_file(path, sub_s, sizeof(sub_s)); + + if (cls_s[0]) { + int cls = (int)strtoul(cls_s, NULL, 16); + const char *type = usb_iftype(cls); + + if (cls == 8 && sub_s[0]) { + ms_subclass = (int)strtoul(sub_s, NULL, 16); + type = usb_storage_subtype(ms_subclass); + } + uevent_setenv(ev, "ID_TYPE", type); + } + + snprintf(path, sizeof(path), "%s/bInterfaceNumber", ifdir); + if (sysfs_read_file(path, val, sizeof(val)) == 0) + uevent_setenv(ev, "ID_USB_INTERFACE_NUM", val); + + if (sysfs_read_driver(ifdir, val, sizeof(val)) == 0) + uevent_setenv(ev, "ID_USB_DRIVER", val); + } + + /* Aggregated class/subclass/protocol per interface */ + if (usb_read_interfaces(usbdir, val, sizeof(val)) == 0) + uevent_setenv(ev, "ID_USB_INTERFACES", val); + + /* Plain hex IDs straight from sysfs */ +#define USB_ATTR_RAW(attr, key) do { \ + snprintf(path, sizeof(path), "%s/" attr, usbdir); \ + if (sysfs_read_file(path, val, sizeof(val)) == 0) \ + uevent_setenv(ev, key, val); \ +} while (0) + USB_ATTR_RAW("idVendor", "ID_VENDOR_ID"); + USB_ATTR_RAW("idProduct", "ID_MODEL_ID"); + USB_ATTR_RAW("bcdDevice", "ID_REVISION"); +#undef USB_ATTR_RAW + + /* + * Human-readable strings need both whitespace-normalized form + * (for /dev/disk/by-id paths) and udev-encoded form (_ENC, for + * symlinks containing the raw string). + */ +#define USB_ATTR_STR(attr, key) do { \ + char raw[256]; \ + snprintf(path, sizeof(path), "%s/" attr, usbdir); \ + if (sysfs_read_file(path, raw, sizeof(raw)) == 0) { \ + char enc[256]; \ + \ + usb_normalize(raw, val, sizeof(val)); \ + uevent_setenv(ev, key, val); \ + blkid_encode_string(raw, enc, sizeof(enc)); \ + snprintf(val, sizeof(val), "%s_ENC", key); \ + uevent_setenv(ev, val, enc); \ + } \ +} while (0) + USB_ATTR_STR("manufacturer", "ID_VENDOR"); + USB_ATTR_STR("product", "ID_MODEL"); + USB_ATTR_STR("serial", "ID_SERIAL_SHORT"); +#undef USB_ATTR_STR + + /* + * Mass storage (subclass 6 = SCSI transparent, 2 = ATAPI): + * SCSI inquiry data is more specific than the USB descriptor + * strings, so override ID_VENDOR / ID_MODEL / ID_TYPE / ID_REVISION + * with values from the scsi_device parent when one is present. + */ + if (ms_subclass == 6 || ms_subclass == 2) { + char scsidir[PATH_MAX]; + + if (find_scsi_device(ev->devpath, scsidir, sizeof(scsidir)) == 0) { + char raw[256]; + +#define SCSI_STR(attr, key) do { \ + snprintf(path, sizeof(path), "%s/" attr, scsidir); \ + if (sysfs_read_file(path, raw, sizeof(raw)) == 0) { \ + char enc[256]; \ + \ + usb_normalize(raw, val, sizeof(val)); \ + uevent_setenv(ev, key, val); \ + blkid_encode_string(raw, enc, sizeof(enc)); \ + snprintf(val, sizeof(val), "%s_ENC", key); \ + uevent_setenv(ev, val, enc); \ + } \ +} while (0) + SCSI_STR("vendor", "ID_VENDOR"); + SCSI_STR("model", "ID_MODEL"); +#undef SCSI_STR + + snprintf(path, sizeof(path), "%s/type", scsidir); + if (sysfs_read_file(path, val, sizeof(val)) == 0) + uevent_setenv(ev, "ID_TYPE", + scsi_type(atoi(val))); + + snprintf(path, sizeof(path), "%s/rev", scsidir); + if (sysfs_read_file(path, val, sizeof(val)) == 0) { + char norm[256]; + + usb_normalize(val, norm, sizeof(norm)); + uevent_setenv(ev, "ID_REVISION", norm); + } + } + } + + /* Build composite ID_SERIAL: vendor_model[_serial] */ + { + const char *vendor = uevent_getenv(ev, "ID_VENDOR"); + const char *model = uevent_getenv(ev, "ID_MODEL"); + const char *serial = uevent_getenv(ev, "ID_SERIAL_SHORT"); + + if (!vendor) + vendor = uevent_getenv(ev, "ID_VENDOR_ID"); + if (!model) + model = uevent_getenv(ev, "ID_MODEL_ID"); + + if (vendor && model) { + if (serial) + snprintf(val, sizeof(val), "%s_%s_%s", + vendor, model, serial); + else + snprintf(val, sizeof(val), "%s_%s", + vendor, model); + uevent_setenv(ev, "ID_SERIAL", val); + } + } + + /* Look up human-readable names from usb.ids (hwdata); silently skipped + * if the file is absent — sysfs manufacturer/product strings remain. */ + { + const char *vid_s = uevent_getenv(ev, "ID_VENDOR_ID"); + const char *pid_s = uevent_getenv(ev, "ID_MODEL_ID"); + + if (vid_s && pid_s) { + unsigned int vid = (unsigned int)strtoul(vid_s, NULL, 16); + unsigned int pid = (unsigned int)strtoul(pid_s, NULL, 16); + char vendor_db[128], model_db[128]; + + if (!lookup_usb_ids(vid, pid, + vendor_db, sizeof(vendor_db), + model_db, sizeof(model_db))) { + if (vendor_db[0]) + uevent_setenv(ev, "ID_VENDOR_FROM_DATABASE", + vendor_db); + if (model_db[0]) + uevent_setenv(ev, "ID_MODEL_FROM_DATABASE", + model_db); + } + } + } + + return 0; +} + +/* ---- input_id ---------------------------------------------------- */ + +/* + * Input device classification. Ports eudev's logic in + * udev-builtin-input_id.c -- the bit positions and the conditions + * matter, so we use the kernel-supplied constants from + * rather than redefining them. + */ +#define BITS_PER_LONG (sizeof(unsigned long) * 8) +#define NBITS(x) (((x) + BITS_PER_LONG - 1) / BITS_PER_LONG) +#define test_bit(bit, arr) \ + ((arr)[(bit) / BITS_PER_LONG] >> ((bit) % BITS_PER_LONG) & 1UL) + +/* High-keycode blocks scanned when no low keys are found (eudev mirror). */ +static const struct { unsigned start, end; } high_key_blocks[] = { + { KEY_OK, BTN_DPAD_UP }, + { KEY_ALS_TOGGLE, BTN_TRIGGER_HAPPY }, +}; + +/* + * Parse a kernel capability bitmap from sysfs. Kernel writes + * space-separated hex words, MSW first. We fill bitmask[] with LSW + * at index 0, matching the kernel's in-memory layout. text is + * mutated in place. + */ +static void parse_cap(char *text, unsigned long *bitmask, size_t nlongs) +{ + char *word; + size_t i = 0; + + memset(bitmask, 0, nlongs * sizeof(unsigned long)); + if (!text || !*text) + return; + + while ((word = strrchr(text, ' ')) != NULL) { + if (i < nlongs) + bitmask[i++] = strtoul(word + 1, NULL, 16); + *word = '\0'; + } + if (*text && i < nlongs) + bitmask[i] = strtoul(text, NULL, 16); +} + +static int read_cap(const char *base, const char *name, + unsigned long *bitmask, size_t nlongs) +{ + char path[PATH_MAX]; + char text[4096]; + + snprintf(path, sizeof(path), "%s/capabilities/%s", base, name); + if (sysfs_read_file(path, text, sizeof(text)) < 0) { + memset(bitmask, 0, nlongs * sizeof(unsigned long)); + return -1; + } + parse_cap(text, bitmask, nlongs); + return 0; +} + +/* Classify pointer-like devices (mouse / touchpad / touchscreen / tablet / + * joystick / pointing-stick / accelerometer). Ports eudev's test_pointers. */ +static void classify_pointer(struct uevent *ev, + const unsigned long *bm_ev, + const unsigned long *bm_abs, + const unsigned long *bm_key, + const unsigned long *bm_rel, + const unsigned long *bm_props) +{ + int has_keys = test_bit(EV_KEY, bm_ev); + int has_abs = test_bit(ABS_X, bm_abs) && test_bit(ABS_Y, bm_abs); + int has_mt = test_bit(ABS_MT_POSITION_X, bm_abs) && + test_bit(ABS_MT_POSITION_Y, bm_abs); + int is_direct = test_bit(INPUT_PROP_DIRECT, bm_props); + int has_touch = test_bit(BTN_TOUCH, bm_key); + int stylus = test_bit(BTN_STYLUS, bm_key) || + test_bit(BTN_TOOL_PEN, bm_key); + int finger_no_pen = test_bit(BTN_TOOL_FINGER, bm_key) && + !test_bit(BTN_TOOL_PEN, bm_key); + int has_rel = test_bit(EV_REL, bm_ev) && + test_bit(REL_X, bm_rel) && test_bit(REL_Y, bm_rel); + int has_mouse_button = 0; + int has_joy = 0; + int is_mouse = 0, is_touchpad = 0, is_touchscreen = 0; + int is_tablet = 0, is_joystick = 0; + + if (test_bit(INPUT_PROP_ACCELEROMETER, bm_props) || + (!has_keys && has_abs && test_bit(ABS_Z, bm_abs))) { + uevent_setenv(ev, "ID_INPUT_ACCELEROMETER", "1"); + return; + } + + if (test_bit(INPUT_PROP_POINTING_STICK, bm_props)) + uevent_setenv(ev, "ID_INPUT_POINTINGSTICK", "1"); + + /* mt overrides if device claims all abs axes */ + if (has_mt && test_bit(ABS_MT_SLOT, bm_abs) && + test_bit(ABS_MT_SLOT - 1, bm_abs)) + has_mt = 0; + + for (int b = BTN_MOUSE; b < BTN_JOYSTICK && !has_mouse_button; b++) + has_mouse_button = test_bit(b, bm_key); + + /* Joystick detection -- broad button/axis sweep (eudev mirror). */ + if (!test_bit(BTN_JOYSTICK - 1, bm_key)) { + for (int b = BTN_JOYSTICK; b < BTN_DIGI && !has_joy; b++) + has_joy = test_bit(b, bm_key); + for (int b = BTN_TRIGGER_HAPPY1; b <= BTN_TRIGGER_HAPPY40 && !has_joy; b++) + has_joy = test_bit(b, bm_key); + for (int b = BTN_DPAD_UP; b <= BTN_DPAD_RIGHT && !has_joy; b++) + has_joy = test_bit(b, bm_key); + } + for (int b = ABS_RX; b < ABS_PRESSURE && !has_joy; b++) + has_joy = test_bit(b, bm_abs); + + if (has_abs) { + if (stylus) is_tablet = 1; + else if (finger_no_pen && !is_direct) is_touchpad = 1; + else if (has_mouse_button) is_mouse = 1; /* VMware abs-axis mouse */ + else if (has_touch || is_direct) is_touchscreen = 1; + else if (has_joy) is_joystick = 1; + } else if (has_joy) { + is_joystick = 1; + } + + if (has_mt) { + if (stylus) is_tablet = 1; + else if (finger_no_pen && !is_direct) is_touchpad = 1; + else if (has_touch || is_direct) is_touchscreen = 1; + } + + if (!is_tablet && !is_touchpad && !is_joystick && has_mouse_button && + (has_rel || !has_abs)) + is_mouse = 1; + + if (is_mouse) uevent_setenv(ev, "ID_INPUT_MOUSE", "1"); + if (is_touchpad) uevent_setenv(ev, "ID_INPUT_TOUCHPAD", "1"); + if (is_touchscreen) uevent_setenv(ev, "ID_INPUT_TOUCHSCREEN", "1"); + if (is_tablet) uevent_setenv(ev, "ID_INPUT_TABLET", "1"); + if (is_joystick) uevent_setenv(ev, "ID_INPUT_JOYSTICK", "1"); +} + +/* Classify key-like devices. Ports eudev's test_key(). */ +static void classify_key(struct uevent *ev, + const unsigned long *bm_ev, + const unsigned long *bm_key) +{ + unsigned long found = 0; + unsigned int i; + + if (!test_bit(EV_KEY, bm_ev)) + return; + + /* Any KEY_* below BTN_MISC counts as "has keys" */ + for (i = 0; i < BTN_MISC / BITS_PER_LONG; i++) + found |= bm_key[i]; + + if (!found) { + unsigned int b; + + for (b = 0; b < sizeof(high_key_blocks) / sizeof(high_key_blocks[0]); b++) { + unsigned int k; + + for (k = high_key_blocks[b].start; + k < high_key_blocks[b].end; k++) { + if (test_bit(k, bm_key)) { + found = 1; + goto done; + } + } + } + } +done: + if (found) + uevent_setenv(ev, "ID_INPUT_KEY", "1"); + + /* Full keyboard: first 32 bits are ESC, 1..0, Q..D (mask 0xFFFFFFFE -- skip KEY_RESERVED). */ + if ((bm_key[0] & 0xFFFFFFFEUL) == 0xFFFFFFFEUL) + uevent_setenv(ev, "ID_INPUT_KEYBOARD", "1"); +} + +static int builtin_input_id(struct uevent *ev, int argc, char **argv) +{ + unsigned long bm_ev [NBITS(EV_MAX)]; + unsigned long bm_abs [NBITS(ABS_MAX)]; + unsigned long bm_key [NBITS(KEY_MAX)]; + unsigned long bm_rel [NBITS(REL_MAX)]; + unsigned long bm_props[NBITS(INPUT_PROP_MAX)]; + char base[PATH_MAX]; + + (void)argc; + (void)argv; + + if (!ev->devpath) + return -1; + + /* + * For input/eventN nodes, capabilities live one level up under + * inputN/. Try parent dir first, fall back to devpath itself + * (which is the input device dir for non-eventN cases). + */ + snprintf(base, sizeof(base), "/sys%s", ev->devpath); + { + char *sl = strrchr(base, '/'); + + if (sl) { + *sl = '\0'; + if (read_cap(base, "ev", bm_ev, NBITS(EV_MAX)) == 0) + goto have_base; + *sl = '/'; /* restore full path for retry */ + } + if (read_cap(base, "ev", bm_ev, NBITS(EV_MAX)) < 0) + return 0; + } +have_base: + + read_cap(base, "abs", bm_abs, NBITS(ABS_MAX)); + read_cap(base, "key", bm_key, NBITS(KEY_MAX)); + read_cap(base, "rel", bm_rel, NBITS(REL_MAX)); + read_cap(base, "properties", bm_props, NBITS(INPUT_PROP_MAX)); + + uevent_setenv(ev, "ID_INPUT", "1"); + classify_pointer(ev, bm_ev, bm_abs, bm_key, bm_rel, bm_props); + classify_key(ev, bm_ev, bm_key); + + /* Devices with only a scrollwheel get ID_INPUT_KEY too. */ + if (test_bit(EV_REL, bm_ev) && + (test_bit(REL_WHEEL, bm_rel) || test_bit(REL_HWHEEL, bm_rel)) && + !uevent_getenv(ev, "ID_INPUT_KEY")) + uevent_setenv(ev, "ID_INPUT_KEY", "1"); + + if (test_bit(EV_SW, bm_ev)) + uevent_setenv(ev, "ID_INPUT_SWITCH", "1"); + + return 0; +} + +/* ---- net_id ------------------------------------------------------ */ + +static int builtin_net_id(struct uevent *ev, int argc, char **argv) +{ + const char *devname; + char path[PATH_MAX]; + char val[64] = ""; + char mac[32] = ""; + + (void)argc; + (void)argv; + + devname = ev->devname; + if (!devname) + devname = uevent_getenv(ev, "INTERFACE"); + if (!devname) + return -1; + + /* + * Skip stacked devices (VLAN, bridge, bond, vlan, vrf, ...). + * They have ifindex != iflink because iflink points to the + * underlying physical device. Renaming them would corrupt the + * kernel-supplied name. + */ + { + char ifindex_s[16] = "", iflink_s[16] = ""; + + snprintf(path, sizeof(path), "/sys/class/net/%s/ifindex", devname); + sysfs_read_file(path, ifindex_s, sizeof(ifindex_s)); + snprintf(path, sizeof(path), "/sys/class/net/%s/iflink", devname); + sysfs_read_file(path, iflink_s, sizeof(iflink_s)); + if (ifindex_s[0] && iflink_s[0] && strcmp(ifindex_s, iflink_s)) + return 0; + } + + /* Onboard NIC name eno from BIOS/ACPI firmware. */ + snprintf(path, sizeof(path), "/sys/class/net/%s/device/firmware_node/acpi_index", + devname); + if (sysfs_read_file(path, val, sizeof(val)) != 0) { + val[0] = '\0'; + snprintf(path, sizeof(path), "/sys/class/net/%s/device/index", devname); + sysfs_read_file(path, val, sizeof(val)); + } + if (val[0]) { + int n = atoi(val); + + if (n > 0) { + char name[32]; + + snprintf(name, sizeof(name), "eno%d", n); + uevent_setenv(ev, "ID_NET_NAME_ONBOARD", name); + } + } + + /* BIOS-supplied label string (informational; surfaces in ip(8)). */ + val[0] = '\0'; + snprintf(path, sizeof(path), "/sys/class/net/%s/device/label", devname); + if (sysfs_read_file(path, val, sizeof(val)) == 0 && val[0]) + uevent_setenv(ev, "ID_NET_LABEL_ONBOARD", val); + + /* MAC-based predictable name: enx */ + snprintf(path, sizeof(path), "/sys/class/net/%s/address", devname); + if (sysfs_read_file(path, mac, sizeof(mac)) == 0 && mac[0]) { + char raw[32] = ""; + char name_mac[48]; + const char *s; + char *d; + + for (s = mac, d = raw; *s; s++) + if (*s != ':') + *d++ = *s; + *d = '\0'; + snprintf(name_mac, sizeof(name_mac), "enx%s", raw); + uevent_setenv(ev, "ID_NET_NAME_MAC", name_mac); + } + + /* + * PCI slot-based name: scan the full devpath for DDDD:BB:SS.F and take + * the LAST (deepest) match — that is the NIC endpoint, not a parent bridge. + */ + if (ev->devpath) { + const char *p = ev->devpath; + unsigned int last_bus = 0, last_dev = 0, last_func = 0; + int found = 0; + + while (*p) { + unsigned int dom, bus, dev, func; + + if (sscanf(p, "%4x:%2x:%2x.%1x", &dom, &bus, &dev, &func) == 4 && + p[12] == '/') { + (void)dom; + last_bus = bus; + last_dev = dev; + last_func = func; + found = 1; + } + p++; + } + + if (found) { + char name_slot[64]; + + if (last_func == 0) + snprintf(name_slot, sizeof(name_slot), + "enp%us%u", last_bus, last_dev); + else + snprintf(name_slot, sizeof(name_slot), + "enp%us%uf%u", last_bus, last_dev, last_func); + uevent_setenv(ev, "ID_NET_NAME_SLOT", name_slot); + } + } + + /* Driver name */ + if (ev->devpath) { + char drv[64]; + char syspath[PATH_MAX]; + + snprintf(syspath, sizeof(syspath), "/sys%s", ev->devpath); + if (sysfs_read_driver(syspath, drv, sizeof(drv)) == 0) + uevent_setenv(ev, "ID_NET_DRIVER", drv); + } + + return 0; +} + +/* ---- blkid ------------------------------------------------------- */ + +/* Publish base + base_ENC. Safe form (control chars → '_') for rule + * matching, encoded form (\xNN escapes) for use in symlink paths. */ +static void publish_dual(struct uevent *ev, const char *base, const char *val) +{ + char s[256], key[64]; + + blkid_safe_string(val, s, sizeof(s)); + uevent_setenv(ev, base, s); + blkid_encode_string(val, s, sizeof(s)); + snprintf(key, sizeof(key), "%s_ENC", base); + uevent_setenv(ev, key, s); +} + +/* Publish a single encoded value -- used where the property goes + * straight into a symlink path (PART_ENTRY_NAME, CD media IDs). */ +static void publish_encoded(struct uevent *ev, const char *key, const char *val) +{ + char s[256]; + + blkid_encode_string(val, s, sizeof(s)); + uevent_setenv(ev, key, s); +} + +static void publish_blkid(struct uevent *ev, const char *name, const char *val) +{ + if (!strcmp(name, "TYPE")) + uevent_setenv(ev, "ID_FS_TYPE", val); + else if (!strcmp(name, "USAGE")) + uevent_setenv(ev, "ID_FS_USAGE", val); + else if (!strcmp(name, "VERSION")) + uevent_setenv(ev, "ID_FS_VERSION", val); + else if (!strcmp(name, "UUID")) + publish_dual(ev, "ID_FS_UUID", val); + else if (!strcmp(name, "UUID_SUB")) + publish_dual(ev, "ID_FS_UUID_SUB", val); + else if (!strcmp(name, "LABEL")) + publish_dual(ev, "ID_FS_LABEL", val); + else if (!strcmp(name, "PTTYPE")) + uevent_setenv(ev, "ID_PART_TABLE_TYPE", val); + else if (!strcmp(name, "PTUUID")) + uevent_setenv(ev, "ID_PART_TABLE_UUID", val); + else if (!strcmp(name, "PART_ENTRY_NAME")) + publish_encoded(ev, "ID_PART_ENTRY_NAME", val); + else if (!strcmp(name, "PART_ENTRY_TYPE")) + publish_encoded(ev, "ID_PART_ENTRY_TYPE", val); + else if (!strncmp(name, "PART_ENTRY_", 11)) { + /* Generic fall-through: keep this AFTER the specific + * PART_ENTRY_NAME/TYPE branches above. */ + char key[64]; + + snprintf(key, sizeof(key), "ID_%s", name); + uevent_setenv(ev, key, val); + } + else if (!strcmp(name, "SYSTEM_ID")) + publish_encoded(ev, "ID_FS_SYSTEM_ID", val); + else if (!strcmp(name, "PUBLISHER_ID")) + publish_encoded(ev, "ID_FS_PUBLISHER_ID", val); + else if (!strcmp(name, "APPLICATION_ID")) + publish_encoded(ev, "ID_FS_APPLICATION_ID", val); + else if (!strcmp(name, "BOOT_SYSTEM_ID")) + publish_encoded(ev, "ID_FS_BOOT_SYSTEM_ID", val); +} + +static int builtin_blkid(struct uevent *ev, int argc, char **argv) +{ + char devpath[PATH_MAX]; + blkid_probe pr; + int nvals, i; + + (void)argc; + (void)argv; + + if (!ev->devname) + return -1; + + snprintf(devpath, sizeof(devpath), "/dev/%s", ev->devname); + + pr = blkid_new_probe_from_filename(devpath); + if (!pr) + return -1; + + blkid_probe_enable_superblocks(pr, 1); + blkid_probe_set_superblocks_flags(pr, + BLKID_SUBLKS_LABEL | BLKID_SUBLKS_UUID | + BLKID_SUBLKS_TYPE | BLKID_SUBLKS_SECTYPE | + BLKID_SUBLKS_USAGE | BLKID_SUBLKS_VERSION | + BLKID_SUBLKS_BADCSUM); + + blkid_probe_enable_partitions(pr, 1); + blkid_probe_set_partitions_flags(pr, BLKID_PARTS_ENTRY_DETAILS); + + if (blkid_do_safeprobe(pr) == 0) { + if (blkid_probe_has_value(pr, "SBBADCSUM")) + logit(LOG_WARNING, "blkid: %s has bad superblock checksum", + devpath); + + nvals = blkid_probe_numof_values(pr); + for (i = 0; i < nvals; i++) { + const char *name, *val; + + if (blkid_probe_get_value(pr, i, &name, &val, NULL) == 0) + publish_blkid(ev, name, val); + } + } + + blkid_free_probe(pr); + return 0; +} + +/* ---- dispatch ---------------------------------------------------- */ + +typedef int (*builtin_fn)(struct uevent *ev, int argc, char **argv); + +static const struct { + const char *name; + builtin_fn fn; +} builtin_table[] = { + { "kmod", builtin_kmod }, + { "hwdb", builtin_hwdb }, + { "path_id", builtin_path_id }, + { "usb_id", builtin_usb_id }, + { "input_id", builtin_input_id }, + { "net_id", builtin_net_id }, + { "blkid", builtin_blkid }, +}; + +int builtin_run(struct uevent *ev, const char *cmd) +{ + char buf[PATH_MAX]; + char *argv[16]; + int argc = 0; + char *p; + size_t i; + + strncpy(buf, cmd, sizeof(buf) - 1); + buf[sizeof(buf) - 1] = '\0'; + + p = buf; + while (argc < 15) { + char q = 0; + char *out; + + while (isspace((unsigned char)*p)) + p++; + if (!*p) + break; + + /* shell-style quoting: '...' or "..." consume the quotes */ + if (*p == '\'' || *p == '"') { + q = *p++; + } + + argv[argc++] = out = p; + while (*p) { + if (q) { + if (*p == q) { + p++; + break; + } + } else if (isspace((unsigned char)*p)) { + break; + } + *out++ = *p++; + } + if (*p) + p++; + *out = '\0'; + } + argv[argc] = NULL; + + if (!argc) + return -1; + + for (i = 0; i < sizeof(builtin_table) / sizeof(builtin_table[0]); i++) { + if (!strcmp(argv[0], builtin_table[i].name)) { + logit(LOG_DEBUG, "rules: builtin %s", argv[0]); + return builtin_table[i].fn(ev, argc, argv); + } + } + + logit(LOG_DEBUG, "rules: unknown builtin '%s'", argv[0]); + return -1; +} + +int builtin_has(const char *name) +{ + size_t i; + + if (!name) + return 0; + for (i = 0; i < sizeof(builtin_table) / sizeof(builtin_table[0]); i++) { + if (!strcmp(name, builtin_table[i].name)) + return 1; + } + return 0; +} + +/** + * Local Variables: + * indent-tabs-mode: t + * c-file-style: "linux" + * End: + */ diff --git a/keventd/builtin.h b/keventd/builtin.h new file mode 100644 index 00000000..b62eb0d3 --- /dev/null +++ b/keventd/builtin.h @@ -0,0 +1,46 @@ +/* Builtin command dispatch for keventd rules engine + * + * Copyright (c) 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. + */ + +#ifndef KEVENTD_BUILTIN_H_ +#define KEVENTD_BUILTIN_H_ + +#include "keventd.h" + +/* + * Execute a builtin command from a udev rule. + * cmd is the full command string, e.g. "hwdb --subsystem=input" or "path_id". + * Returns 0 on success, -1 if the builtin is unknown or fails. + */ +int builtin_run(struct uevent *ev, const char *cmd); + +/* True if NAME is a registered builtin (e.g. "path_id", "usb_id"). */ +int builtin_has(const char *name); + +#endif /* KEVENTD_BUILTIN_H_ */ + +/** + * Local Variables: + * indent-tabs-mode: t + * c-file-style: "linux" + * End: + */ diff --git a/keventd/keventd.c b/keventd/keventd.c index b4fd5f24..bfb3001f 100644 --- a/keventd/keventd.c +++ b/keventd/keventd.c @@ -34,6 +34,7 @@ * THE SOFTWARE. */ +#include #include #include #include @@ -60,6 +61,8 @@ #endif #include "keventd.h" +#include "rules.h" +#include "udevdb.h" #include "cond.h" #include "pid.h" #include "util.h" @@ -78,6 +81,10 @@ 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 */ +static volatile sig_atomic_t reload_rules; + int debug; /* debug in other modules as well */ void logit(int prio, const char *fmt, ...) @@ -267,6 +274,27 @@ static void power_supply_change(const struct uevent *ev, char *buf, size_t len) } } +/* + * 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; +} + /* * Handle a single uevent from the kernel. */ @@ -282,6 +310,10 @@ static void handle_uevent(char *buf, size_t len) 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) { @@ -289,14 +321,17 @@ static void handle_uevent(char *buf, size_t len) if (ev.firmware) firmware_load(&ev); - /* Module loading */ - if (ev.modalias) + 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); } @@ -327,6 +362,20 @@ static void handle_uevent(char *buf, size_t len) 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); + } + + uevent_env_free(&ev); } static void init_power_supply(void) @@ -414,10 +463,16 @@ static void shut_down(int signo) running = 0; } +static void sighup_handler(int signo) +{ + (void)signo; + reload_rules = 1; +} + static int usage(int rc) { fprintf(stderr, - "Usage: keventd [-dGhnpv] [-c] [-g GROUP]\n" + "Usage: keventd [-dGhnpv] [-c] [-g GROUP] [-r DIR]\n" "\n" "Options:\n" " -c Run coldplug at startup\n" @@ -427,6 +482,7 @@ static int usage(int rc) " -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 (in addition to standard udev paths)\n" " -v Show version\n" "\n", REBC_DEFAULT_NLGROUP); @@ -460,7 +516,7 @@ int main(int argc, char *argv[]) * requested bits verbatim instead of masking them. */ umask(0); - while ((c = getopt(argc, argv, "cdg:Ghnpv")) != -1) { + while ((c = getopt(argc, argv, "cdg:Ghnpr:v")) != -1) { switch (c) { case 'c': do_coldplug = 1; @@ -483,6 +539,9 @@ int main(int argc, char *argv[]) case 'p': passive = 1; break; + case 'r': + rules_dir = optarg; + break; case 'v': printf("keventd v%s\n", KEVENTD_VERSION); return 0; @@ -501,6 +560,7 @@ int main(int argc, char *argv[]) signal(SIGUSR1, toggle_debug); signal(SIGTERM, shut_down); + signal(SIGHUP, sighup_handler); signal(SIGCHLD, SIG_IGN); /* Don't wait for modprobe children */ /* Initialize condition directories */ @@ -539,6 +599,10 @@ int main(int argc, char *argv[]) if (do_coldplug) coldplug(); + /* Load udev rules from standard directories (and -r extra_dir) */ + if (!passive) + rules_load_all(&rules, rules_dir); + pidfile(NULL); logit(LOG_NOTICE, "keventd v%s started, waiting for events...", KEVENTD_VERSION); @@ -546,6 +610,13 @@ int main(int argc, char *argv[]) char rebc_buf[UEVENT_BUFFER_SIZE]; int len; + /* Reload rules if SIGHUP was received */ + if (reload_rules) { + reload_rules = 0; + rules_free(&rules); + rules_load_all(&rules, rules_dir); + } + if (-1 == poll(&pfd, 1, -1)) { if (errno == EINTR) continue; @@ -592,6 +663,7 @@ int main(int argc, char *argv[]) if (rebc_fd != -1) close(rebc_fd); close(pfd.fd); + rules_free(&rules); logit(LOG_NOTICE, "keventd shutting down"); return 0; diff --git a/keventd/keventd.h b/keventd/keventd.h index f1156905..e96733e8 100644 --- a/keventd/keventd.h +++ b/keventd/keventd.h @@ -29,11 +29,29 @@ #else # include /* BSD sys/queue.h API */ #endif +#include +#include #include /* Maximum uevent buffer size (kernel uses 8192 internally) */ #define UEVENT_BUFFER_SIZE 8192 +/* Capacity of the generic env store on struct uevent */ +#define UEVENT_ENV_MAX 64 + +/* Maximum TAG+= entries per device event */ +#define UEVENT_TAG_MAX 16 + +/* Maximum SYMLINK+= and RUN+= entries set by the rules engine */ +#define RULE_SYMLINK_MAX 8 +#define RULE_RUN_MAX 8 + +/* RUN{type} subtypes — also used in struct rule_ctx below */ +typedef enum { + RUN_PROGRAM = 0, /* execute external program */ + RUN_BUILTIN, /* call builtin function */ +} run_type_t; + /* Uevent actions from kernel */ typedef enum { ACT_UNKNOWN = 0, @@ -47,9 +65,36 @@ typedef enum { ACT_UNBIND, } uevent_action_t; +/* + * Rule-applied permissions and context, set by the rules engine. + * Zero-initialised means "nothing set by rules", fall back to built-in table. + */ +struct rule_ctx { + /* Device permissions */ + mode_t mode; + uid_t uid; + gid_t gid; + int has_mode; + int has_owner; + int has_group; + int final_mode; /* := operator locks against further overrides */ + int final_owner; + int final_group; + /* NAME= override for device node path (heap-allocated) */ + char *name; + /* SYMLINK+= accumulator (heap-allocated strings) */ + int nsymlinks; + char *symlinks[RULE_SYMLINK_MAX]; + /* RUN+= command list (heap-allocated strings, executed post-event) */ + int nruncmds; + char *run_cmds[RULE_RUN_MAX]; + run_type_t run_types[RULE_RUN_MAX]; +}; + /* * Parsed uevent structure. - * Pointers are into the receive buffer, zero-copy. + * Named field pointers point into the receive buffer (zero-copy). + * The env store holds all KEY=VALUE pairs plus any rule-set variables. */ struct uevent { uevent_action_t action; @@ -63,6 +108,23 @@ struct uevent { char *firmware; /* firmware file name request */ char *seqnum; /* kernel sequence number */ char *driver; /* driver name */ + + /* Generic env store: all KEY=VALUE from the netlink buffer + rule-set vars */ + int nenv; + char *env_key[UEVENT_ENV_MAX]; + char *env_val[UEVENT_ENV_MAX]; + uint8_t env_key_alloc[UEVENT_ENV_MAX]; /* 1 if heap-allocated, 0 if buf ptr */ + uint8_t env_val_alloc[UEVENT_ENV_MAX]; + + /* PROGRAM= output, stored for subsequent RESULT== matching */ + char *result; + + /* TAG+= accumulator */ + int ntags; + char *tags[UEVENT_TAG_MAX]; + + /* Rule-applied permissions from the rules engine */ + struct rule_ctx applied; }; /* Tracked symlink for cleanup on device removal */ @@ -73,19 +135,28 @@ struct dev_symlink { }; /* Function prototypes - uevent.c */ -int uevent_parse (char *buf, size_t len, struct uevent *ev); +void rule_ctx_free (struct rule_ctx *ctx); + +int uevent_parse (char *buf, size_t len, struct uevent *ev); const char *uevent_action_str(uevent_action_t action); +const char *uevent_sysname (const struct uevent *ev); + +const char *uevent_getenv (const struct uevent *ev, const char *key); +int uevent_setenv (struct uevent *ev, const char *key, const char *val); +void uevent_env_free (struct uevent *ev); -int devnode_add (struct uevent *ev); -int devnode_del (struct uevent *ev); +int devnode_add (struct uevent *ev); +int devnode_del (struct uevent *ev); +int netdev_add (struct uevent *ev); -int symlink_add (struct uevent *ev); -int symlink_del (struct uevent *ev); +int symlink_add (struct uevent *ev); +int symlink_del (struct uevent *ev); +void symlink_write_db (const char *devpath, FILE *fp); -int firmware_load (struct uevent *ev); -int modprobe_load (const char *modalias); +int firmware_load (struct uevent *ev); +int modprobe_load (const char *modalias); -int coldplug (void); +int coldplug (void); #endif /* FINIT_KEVENTD_H_ */ diff --git a/keventd/rules.c b/keventd/rules.c new file mode 100644 index 00000000..31580a47 --- /dev/null +++ b/keventd/rules.c @@ -0,0 +1,1621 @@ +/* udev rules file parser and matcher for keventd + * + * Copyright (c) 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#ifdef _LIBITE_LITE +# include +#else +# include +#endif + +#include "builtin.h" +#include "rules.h" +#include "sysfs.h" +#include "udevdb.h" + +/* Forward declaration from keventd.c */ +void logit(int prio, const char *fmt, ...); + +/* ----- key-name lookup -------------------------------------------------- */ + +static const struct { + const char *name; + rule_key_class_t cls; +} key_map[] = { + { "ACTION", KEY_ACTION }, + { "DEVPATH", KEY_DEVPATH }, + { "KERNEL", KEY_KERNEL }, + { "KERNELS", KEY_KERNELS }, + { "NAME", KEY_NAME }, + { "SUBSYSTEM", KEY_SUBSYSTEM }, + { "SUBSYSTEMS", KEY_SUBSYSTEMS }, + { "DRIVER", KEY_DRIVER }, + { "DRIVERS", KEY_DRIVERS }, + { "ATTR", KEY_ATTR }, + { "ATTRS", KEY_ATTRS }, + { "SYSCTL", KEY_SYSCTL }, + { "ENV", KEY_ENV }, + { "CONST", KEY_CONST }, + { "TAG", KEY_TAG }, + { "TAGS", KEY_TAGS }, + { "TEST", KEY_TEST }, + { "PROGRAM", KEY_PROGRAM }, + { "RESULT", KEY_RESULT }, + { "SYMLINK", KEY_SYMLINK }, + { "OWNER", KEY_OWNER }, + { "GROUP", KEY_GROUP }, + { "MODE", KEY_MODE }, + { "SECLABEL", KEY_SECLABEL }, + { "RUN", KEY_RUN }, + { "LABEL", KEY_LABEL }, + { "GOTO", KEY_GOTO }, + { "IMPORT", KEY_IMPORT }, + { "OPTIONS", KEY_OPTIONS }, +}; + +static rule_key_class_t lookup_key(const char *name) +{ + size_t i; + + for (i = 0; i < NELEMS(key_map); i++) { + if (!strcmp(key_map[i].name, name)) + return key_map[i].cls; + } + return KEY_UNKNOWN; +} + +/* ----- helpers ---------------------------------------------------------- */ + +static pat_type_t detect_pat_type(const char *val) +{ + if (strchr(val, '|')) + return PAT_SPLIT; + if (strpbrk(val, "*?[")) + return PAT_GLOB; + return PAT_PLAIN; +} + +static import_type_t parse_import_type(const char *attr) +{ + if (!attr || !*attr) return IMPORT_PROGRAM; + if (!strcmp(attr, "program")) return IMPORT_PROGRAM; + if (!strcmp(attr, "file")) return IMPORT_FILE; + if (!strcmp(attr, "db")) return IMPORT_DB; + if (!strcmp(attr, "builtin")) return IMPORT_BUILTIN; + if (!strcmp(attr, "parent")) return IMPORT_PARENT; + if (!strcmp(attr, "cmdline")) return IMPORT_CMDLINE; + return IMPORT_PROGRAM; +} + +static run_type_t parse_run_type(const char *attr) +{ + if (!attr || !*attr) return RUN_PROGRAM; + if (!strcmp(attr, "program")) return RUN_PROGRAM; + if (!strcmp(attr, "builtin")) return RUN_BUILTIN; + return RUN_PROGRAM; +} + +/* ----- tokenizer -------------------------------------------------------- */ + +static char *skip_space(char *p) +{ + while (*p == ' ' || *p == '\t') + p++; + return p; +} + +/* Strip inline comment: first '#' not inside double quotes */ +static void strip_comment(char *line) +{ + int in_quote = 0; + char *p; + + for (p = line; *p; p++) { + if (*p == '"') + in_quote ^= 1; + else if (*p == '#' && !in_quote) { + *p = 0; + break; + } + } +} + +/* + * Parse the key name (uppercase letters and '_') and optional {attr} + * suffix. Advances *pp past the parsed portion. + */ +static int parse_key_name(char **pp, + char *name, size_t namesz, + char *attr, size_t attrsz) +{ + char *p = *pp, *end; + size_t n; + + end = p; + while (*end && (isupper((unsigned char)*end) || *end == '_')) + end++; + + n = (size_t)(end - p); + if (!n || n >= namesz) + return -1; + + memcpy(name, p, n); + name[n] = 0; + p = end; + + if (*p == '{') { + p++; + end = strchr(p, '}'); + if (!end) + return -1; + n = (size_t)(end - p); + if (n >= attrsz) + return -1; + memcpy(attr, p, n); + attr[n] = 0; + p = end + 1; + } else { + attr[0] = 0; + } + + *pp = p; + return 0; +} + +static rule_op_t parse_op(char **pp) +{ + char *p = *pp; + rule_op_t op; + + if (p[0] == '=' && p[1] == '=') { op = OP_MATCH_EQ; *pp = p + 2; } + else if (p[0] == '!' && p[1] == '=') { op = OP_MATCH_NE; *pp = p + 2; } + else if (p[0] == '+' && p[1] == '=') { op = OP_ASSIGN_ADD; *pp = p + 2; } + else if (p[0] == '-' && p[1] == '=') { op = OP_ASSIGN_DEL; *pp = p + 2; } + else if (p[0] == ':' && p[1] == '=') { op = OP_ASSIGN_FINAL; *pp = p + 2; } + else if (p[0] == '=') { op = OP_ASSIGN; *pp = p + 1; } + else { op = OP_INVALID; } + + return op; +} + +/* Parse a double-quoted value, advancing *pp past the closing '"'. */ +static int parse_value(char **pp, char *val, size_t valsz) +{ + char *p = *pp, *end; + size_t n; + + if (*p != '"') + return -1; + p++; + + end = strchr(p, '"'); + if (!end) + return -1; + + n = (size_t)(end - p); + if (n >= valsz) + n = valsz - 1; + + memcpy(val, p, n); + val[n] = 0; + *pp = end + 1; + return 0; +} + +/* Free key resources within a rule without freeing the rule itself. */ +static void rule_keys_free(struct rule *r) +{ + int j; + + for (j = 0; j < r->nkeys; j++) { + free(r->keys[j].attr); + free(r->keys[j].value); + r->keys[j].attr = NULL; + r->keys[j].value = NULL; + } + r->nkeys = 0; +} + +/* + * Parse one logical rule line into r. + * Returns 0 if at least one key was successfully parsed. + */ +static int parse_rule_line(struct rule *r, char *src) +{ + char buf[4096]; + char *p; + size_t slen; + + slen = strlen(src); + if (slen >= sizeof(buf)) + slen = sizeof(buf) - 1; + memcpy(buf, src, slen); + buf[slen] = 0; + + p = skip_space(buf); + strip_comment(p); + p = skip_space(p); + + if (!*p) + return -1; + + while (*p) { + char name[64], attr[256], val[1024]; + rule_key_class_t cls; + rule_op_t op; + struct rule_key *k; + + p = skip_space(p); + if (!*p) + break; + + if (parse_key_name(&p, name, sizeof(name), attr, sizeof(attr)) < 0) { + logit(LOG_DEBUG, "rules:%s:%d: parse error near '%s'", + r->filename, r->lineno, p); + return -1; + } + + cls = lookup_key(name); + if (cls == KEY_UNKNOWN) { + logit(LOG_DEBUG, "rules:%s:%d: unknown key '%s'", + r->filename, r->lineno, name); + return -1; + } + + p = skip_space(p); + op = parse_op(&p); + if (op == OP_INVALID) { + logit(LOG_DEBUG, "rules:%s:%d: bad operator after '%s'", + r->filename, r->lineno, name); + return -1; + } + + p = skip_space(p); + if (parse_value(&p, val, sizeof(val)) < 0) { + logit(LOG_DEBUG, "rules:%s:%d: missing value for '%s'", + r->filename, r->lineno, name); + return -1; + } + + if (r->nkeys >= RULE_KEY_MAX) { + logit(LOG_WARNING, "rules:%s:%d: too many keys, truncating", + r->filename, r->lineno); + break; + } + + k = &r->keys[r->nkeys]; + k->cls = cls; + k->op = op; + k->attr = attr[0] ? strdup(attr) : NULL; + k->value = strdup(val); + + if (!k->value) { + free(k->attr); + k->attr = NULL; + break; + } + + k->pat_type = (op == OP_MATCH_EQ || op == OP_MATCH_NE) + ? detect_pat_type(val) : PAT_PLAIN; + k->import_type = (cls == KEY_IMPORT) ? parse_import_type(attr) : IMPORT_PROGRAM; + k->run_type = (cls == KEY_RUN) ? parse_run_type(attr) : RUN_PROGRAM; + + r->nkeys++; + + p = skip_space(p); + if (*p == ',') + p++; + } + + return r->nkeys > 0 ? 0 : -1; +} + +/* ----- file loading ----------------------------------------------------- */ + +static int rules_dirent_filter(const struct dirent *e) +{ + size_t l = strlen(e->d_name); + return l > 6 && !strcmp(e->d_name + l - 6, ".rules"); +} + +int rules_load(struct rule_list *list, const char *path) +{ + char line[2048], cont[4096]; + int lineno = 0, nrules = 0; + FILE *fp; + + fp = fopen(path, "r"); + if (!fp) + return -1; + + cont[0] = 0; + + while (fgets(line, sizeof(line), fp)) { + struct rule *r; + size_t clen, l; + char *p; + + lineno++; + chomp(line); + + /* Accumulate continuation lines */ + l = strlen(line); + if (l > 0 && line[l - 1] == '\\') { + line[l - 1] = 0; + clen = strlen(cont); + snprintf(cont + clen, sizeof(cont) - clen, "%s", line); + continue; + } + clen = strlen(cont); + snprintf(cont + clen, sizeof(cont) - clen, "%s", line); + + /* Skip blank lines and full-line comments */ + p = cont; + while (*p == ' ' || *p == '\t') + p++; + if (!*p || *p == '#') { + cont[0] = 0; + continue; + } + + r = calloc(1, sizeof(*r)); + if (!r) + break; + + r->filename = strdup(path); + r->lineno = lineno; + + if (r->filename && parse_rule_line(r, cont) == 0) { + TAILQ_INSERT_TAIL(list, r, link); + nrules++; + } else { + rule_keys_free(r); + free(r->filename); + free(r); + } + + cont[0] = 0; + } + + fclose(fp); + return nrules; +} + +int rules_load_all(struct rule_list *list, const char *extra_dir) +{ + static const char *std_dirs[] = { + "/lib/udev/rules.d", + "/run/udev/rules.d", + "/etc/udev/rules.d", + NULL + }; + const char *dirs[8]; + int ndirs = 0, total = 0, d; + + for (d = 0; std_dirs[d]; d++) + dirs[ndirs++] = std_dirs[d]; + + if (extra_dir) + dirs[ndirs++] = extra_dir; + dirs[ndirs] = NULL; + + for (d = 0; d < ndirs; d++) { + struct dirent **ents = NULL; + char path[PATH_MAX]; + int n, i; + + n = scandir(dirs[d], &ents, rules_dirent_filter, alphasort); + if (n < 0) + continue; + + for (i = 0; i < n; i++) { + int rc; + + snprintf(path, sizeof(path), "%s/%s", dirs[d], ents[i]->d_name); + rc = rules_load(list, path); + if (rc > 0) { + logit(LOG_DEBUG, "rules: %d rules from %s", rc, path); + total += rc; + } + free(ents[i]); + } + free(ents); + } + + if (total) + logit(LOG_NOTICE, "rules: %d rules loaded", total); + + return total; +} + +void rules_free(struct rule_list *list) +{ + struct rule *r, *tmp; + + TAILQ_FOREACH_SAFE(r, list, link, tmp) { + TAILQ_REMOVE(list, r, link); + rule_keys_free(r); + free(r->filename); + free(r); + } +} + +/* ===== Phase 3: full matcher ============================================ */ + +/* ----- variable substitution ------------------------------------------- */ + +static void append_str(char *buf, size_t *pos, size_t bufsz, const char *s) +{ + size_t l; + + if (!s || !*s) + return; + l = strlen(s); + if (*pos + l >= bufsz) + l = bufsz - *pos - 1; + if (l) { + memcpy(buf + *pos, s, l); + *pos += l; + buf[*pos] = 0; + } +} + +/* + * Expand udev-format substitution operators from src into buf. + * Handles both the %X short form and the $name long form. + */ +static void subst_value(const char *src, char *buf, size_t bufsz, + const struct uevent *ev) +{ + const char *p = src; + size_t pos = 0; + + buf[0] = 0; + if (!src) + return; + + while (*p && pos + 1 < bufsz) { + char tmp[256]; + + if (*p != '%' && *p != '$') { + buf[pos++] = *p++; + buf[pos] = 0; + continue; + } + + if (*p == '%') { + char c = *++p; + + switch (c) { + case '%': + buf[pos++] = '%'; buf[pos] = 0; p++; + break; + case 'k': + append_str(buf, &pos, bufsz, uevent_sysname(ev)); + p++; + break; + case 'p': + append_str(buf, &pos, bufsz, ev->devpath); + p++; + break; + case 'D': case 'N': + append_str(buf, &pos, bufsz, ev->devname); + p++; + break; + case 'M': + snprintf(tmp, sizeof(tmp), "%d", ev->major); + append_str(buf, &pos, bufsz, tmp); + p++; + break; + case 'm': + snprintf(tmp, sizeof(tmp), "%d", ev->minor); + append_str(buf, &pos, bufsz, tmp); + p++; + break; + case 'b': + snprintf(tmp, sizeof(tmp), "%d:%d", ev->major, ev->minor); + append_str(buf, &pos, bufsz, tmp); + p++; + break; + case 'd': + append_str(buf, &pos, bufsz, ev->driver); + p++; + break; + case 'n': { + /* trailing digit suffix of sysname */ + const char *sn = uevent_sysname(ev); + if (sn) { + const char *e = sn + strlen(sn); + while (e > sn && isdigit((unsigned char)*(e - 1))) + e--; + append_str(buf, &pos, bufsz, e); + } + p++; + break; + } + case 's': + /* %s{attr} — sysfs attribute of current device */ + if (p[1] == '{') { + const char *as = p + 2; + const char *ae = strchr(as, '}'); + + if (ae && ev->devpath) { + char attr[256]; + size_t al = (size_t)(ae - as); + + if (al >= sizeof(attr)) + al = sizeof(attr) - 1; + memcpy(attr, as, al); + attr[al] = 0; + if (!sysfs_read_attr(ev->devpath, attr, + tmp, sizeof(tmp))) + append_str(buf, &pos, bufsz, tmp); + p = ae + 1; + } else { + buf[pos++] = '%'; buf[pos] = 0; + } + } else { + buf[pos++] = '%'; buf[pos] = 0; + } + break; + case 'c': + /* %c or %c{n} — PROGRAM result or nth space-separated field */ + if (ev->result) { + if (p[1] == '{') { + const char *ns = p + 2; + const char *ne = strchr(ns, '}'); + + if (ne) { + int field = atoi(ns); + const char *rp = ev->result; + int f = 0; + + while (*rp && f < field) { + while (*rp && *rp != ' ') + rp++; + while (*rp == ' ') + rp++; + f++; + } + const char *re = rp; + while (*re && *re != ' ') + re++; + size_t fl = (size_t)(re - rp); + if (fl >= sizeof(tmp)) + fl = sizeof(tmp) - 1; + memcpy(tmp, rp, fl); + tmp[fl] = 0; + append_str(buf, &pos, bufsz, tmp); + p = ne + 1; + } else { + append_str(buf, &pos, bufsz, ev->result); + p++; + } + } else { + append_str(buf, &pos, bufsz, ev->result); + p++; + } + } else { + p++; + } + break; + default: + /* unknown specifier — pass '%' through, re-parse next char */ + buf[pos++] = '%'; buf[pos] = 0; + break; + } + + } else { /* '$' */ + const char *kw = ++p; + + if (*kw == '$') { + buf[pos++] = '$'; buf[pos] = 0; + p++; + } else if (!strncmp(kw, "attr{", 5)) { + int off = 5; + const char *as = kw + off; + const char *ae = strchr(as, '}'); + + if (ae && ev->devpath) { + char attr[256]; + size_t al = (size_t)(ae - as); + + if (al >= sizeof(attr)) + al = sizeof(attr) - 1; + memcpy(attr, as, al); + attr[al] = 0; + if (!sysfs_read_attr(ev->devpath, attr, tmp, sizeof(tmp))) + append_str(buf, &pos, bufsz, tmp); + p = ae + 1; + } else { + buf[pos++] = '$'; buf[pos] = 0; + } + } else if (!strncmp(kw, "env{", 4)) { + const char *ks = kw + 4; + const char *ke = strchr(ks, '}'); + + if (ke) { + char key[256]; + size_t kl = (size_t)(ke - ks); + + if (kl >= sizeof(key)) + kl = sizeof(key) - 1; + memcpy(key, ks, kl); + key[kl] = 0; + append_str(buf, &pos, bufsz, uevent_getenv(ev, key)); + p = ke + 1; + } else { + buf[pos++] = '$'; buf[pos] = 0; + } + } else { + /* Named scalar variables */ + static const struct { + const char *name; + size_t len; + } named[] = { + { "kernel", 6 }, { "name", 4 }, + { "devpath", 7 }, { "driver", 6 }, + { "result", 6 }, { "root", 4 }, + { "sys", 3 }, { "major", 5 }, + { "minor", 5 }, + }; + size_t i; + int handled = 0; + + for (i = 0; i < NELEMS(named); i++) { + if (strncmp(kw, named[i].name, named[i].len)) + continue; + switch (i) { + case 0: + append_str(buf, &pos, bufsz, uevent_sysname(ev)); + break; + case 1: + append_str(buf, &pos, bufsz, ev->devname); + break; + case 2: + append_str(buf, &pos, bufsz, ev->devpath); + break; + case 3: + append_str(buf, &pos, bufsz, ev->driver); + break; + case 4: + append_str(buf, &pos, bufsz, ev->result); + break; + case 5: + append_str(buf, &pos, bufsz, "/dev"); + break; + case 6: + append_str(buf, &pos, bufsz, "/sys"); + break; + case 7: + snprintf(tmp, sizeof(tmp), "%d", ev->major); + append_str(buf, &pos, bufsz, tmp); + break; + case 8: + snprintf(tmp, sizeof(tmp), "%d", ev->minor); + append_str(buf, &pos, bufsz, tmp); + break; + } + p = kw + named[i].len; + handled = 1; + break; + } + if (!handled) { + buf[pos++] = '$'; buf[pos] = 0; + /* don't advance p — let next iter handle kw[0] */ + } + } + } + } +} + +/* ----- pattern matching ------------------------------------------------- */ + +/* + * Match subject against a single pattern (no pipe splitting). + * Always uses fnmatch so that plain strings and globs work uniformly. + */ +static int pattern_match_one(const char *pat, const char *subject) +{ + return fnmatch(pat, subject, 0) == 0; +} + +static int pattern_match(const char *pat, const char *subject, pat_type_t type) +{ + if (type == PAT_PLAIN) + return !strcmp(pat, subject); + + if (type == PAT_SPLIT) { + char copy[1024]; + char *p, *tok; + + snprintf(copy, sizeof(copy), "%s", pat); + p = copy; + while ((tok = strsep(&p, "|")) != NULL) { + if (pattern_match_one(tok, subject)) + return 1; + } + return 0; + } + + /* PAT_GLOB */ + return pattern_match_one(pat, subject); +} + +/* ----- program execution ------------------------------------------------ */ + +/* + * Fork and exec cmd via /bin/sh, capture stdout into result. + * Temporarily restores SIGCHLD (keventd uses SIG_IGN) so waitpid works. + */ +static int run_program(const char *cmd, char *result, size_t rlen) +{ + struct sigaction sa_dfl, sa_old; + int pipefd[2]; + pid_t pid; + int rc = -1; + + if (!cmd || !*cmd) + return -1; + + if (pipe(pipefd) < 0) + return -1; + + sigemptyset(&sa_dfl.sa_mask); + sa_dfl.sa_flags = 0; + sa_dfl.sa_handler = SIG_DFL; + sigaction(SIGCHLD, &sa_dfl, &sa_old); + + pid = fork(); + if (pid < 0) { + sigaction(SIGCHLD, &sa_old, NULL); + close(pipefd[0]); + close(pipefd[1]); + return -1; + } + + if (pid == 0) { + close(pipefd[0]); + dup2(pipefd[1], STDOUT_FILENO); + close(pipefd[1]); + execl("/bin/sh", "sh", "-c", cmd, NULL); + _exit(127); + } + + close(pipefd[1]); + + { + ssize_t n = read(pipefd[0], result, rlen - 1); + if (n < 0) + n = 0; + result[n] = 0; + chomp(result); + } + close(pipefd[0]); + + { + int status; + + if (waitpid(pid, &status, 0) > 0) + rc = WIFEXITED(status) ? WEXITSTATUS(status) : -1; + } + + sigaction(SIGCHLD, &sa_old, NULL); + return rc; +} + +/* ----- parent-chain matching -------------------------------------------- */ + +struct parent_ctx { + const struct rule_key *k; + const struct uevent *ev; + int found; +}; + +static int parent_cb(const char *syspath, void *data) +{ + struct parent_ctx *ctx = data; + const struct rule_key *k = ctx->k; + char buf[256]; + const char *subject = NULL; + + switch (k->cls) { + case KEY_KERNELS: { + const char *p = strrchr(syspath, '/'); + subject = p ? p + 1 : syspath; + break; + } + case KEY_SUBSYSTEMS: + if (sysfs_read_subsystem(syspath, buf, sizeof(buf)) < 0) + return 1; + subject = buf; + break; + case KEY_DRIVERS: + if (sysfs_read_driver(syspath, buf, sizeof(buf)) < 0) + return 1; + subject = buf; + break; + case KEY_ATTRS: { + /* Read sysfs attribute directly from this level of the tree */ + char path[PATH_MAX]; + FILE *fp; + + snprintf(path, sizeof(path), "%s/%s", syspath, k->attr ?: ""); + fp = fopen(path, "r"); + if (!fp) + return 1; + if (!fgets(buf, sizeof(buf), fp)) { + fclose(fp); + return 1; + } + fclose(fp); + chomp(buf); + subject = buf; + break; + } + default: + return 1; + } + + if (!subject) + return 1; + + if (pattern_match(k->value, subject, k->pat_type)) { + ctx->found = 1; + return 0; /* stop walking */ + } + return 1; /* keep walking */ +} + +static int match_parent_chain(const struct rule_key *k, const struct uevent *ev) +{ + struct parent_ctx ctx = { k, ev, 0 }; + int matched; + + if (!ev->devpath) + return (k->op == OP_MATCH_NE); + + sysfs_parent_walk(ev->devpath, parent_cb, &ctx); + + matched = ctx.found; + return (k->op == OP_MATCH_EQ) ? matched : !matched; +} + +/* ----- per-key dispatch ------------------------------------------------- */ + +/* + * Read one line from path into buf (for SYSCTL matching). + */ +static int read_oneline(const char *path, char *buf, size_t len) +{ + FILE *fp = fopen(path, "r"); + + if (!fp) + return -1; + if (!fgets(buf, len, fp)) { + fclose(fp); + return -1; + } + fclose(fp); + chomp(buf); + return 0; +} + +/* + * Test one key against the event. + * Returns 1 if the key condition is satisfied, 0 if not. + */ +static int match_key(const struct rule *r, const struct rule_key *k, + struct uevent *ev) +{ + char subj_buf[1024] = ""; + const char *subject = NULL; + int matched; + + /* Parent-chain keys have their own walk logic */ + switch (k->cls) { + case KEY_KERNELS: + case KEY_SUBSYSTEMS: + case KEY_DRIVERS: + case KEY_ATTRS: + case KEY_TAGS: + return match_parent_chain(k, ev); + default: + break; + } + + /* PROGRAM= and TEST= have dedicated logic */ + if (k->cls == KEY_PROGRAM) { + char cmd[PATH_MAX], out[1024]; + int rc; + + subst_value(k->value, cmd, sizeof(cmd), ev); + rc = run_program(cmd, out, sizeof(out)); + free(ev->result); + ev->result = strdup(out); + matched = (rc == 0); + return (k->op == OP_MATCH_EQ) ? matched : !matched; + } + + if (k->cls == KEY_TEST) { + char path[PATH_MAX]; + struct stat st; + + subst_value(k->value, path, sizeof(path), ev); + if (stat(path, &st) < 0) { + matched = 0; + } else if (k->attr && k->attr[0]) { + mode_t req = (mode_t)strtoul(k->attr, NULL, 8); + matched = ((st.st_mode & req) == req); + } else { + matched = 1; + } + return (k->op == OP_MATCH_EQ) ? matched : !matched; + } + + /* All other match keys: compute subject string, then pattern match */ + switch (k->cls) { + case KEY_ACTION: + subject = uevent_action_str(ev->action); + break; + case KEY_DEVPATH: + subject = ev->devpath; + break; + case KEY_KERNEL: + subject = uevent_sysname(ev); + break; + case KEY_NAME: + subject = ev->devname; + break; + case KEY_SUBSYSTEM: + subject = ev->subsystem; + break; + case KEY_DRIVER: + subject = ev->driver; + break; + case KEY_ATTR: + if (!k->attr || !ev->devpath) + return (k->op == OP_MATCH_NE); + if (sysfs_read_attr(ev->devpath, k->attr, subj_buf, sizeof(subj_buf)) < 0) + return (k->op == OP_MATCH_NE); + subject = subj_buf; + break; + case KEY_SYSCTL: { + char path[PATH_MAX]; + char *dp; + + if (!k->attr) + return (k->op == OP_MATCH_NE); + snprintf(path, sizeof(path), "/proc/sys/%s", k->attr); + for (dp = path + 10; *dp; dp++) + if (*dp == '.') + *dp = '/'; + if (read_oneline(path, subj_buf, sizeof(subj_buf)) < 0) + return (k->op == OP_MATCH_NE); + subject = subj_buf; + break; + } + case KEY_ENV: + if (!k->attr) + return (k->op == OP_MATCH_NE); + subject = uevent_getenv(ev, k->attr); + break; + case KEY_CONST: { + if (!k->attr) { + subject = ""; + } else if (!strcmp(k->attr, "arch")) { + struct utsname uts; + if (uname(&uts) == 0) + snprintf(subj_buf, sizeof(subj_buf), "%s", uts.machine); + subject = subj_buf; + } else if (!strcmp(k->attr, "virt")) { + subject = (fexist("/.dockerenv") || + fexist("/run/.containerenv")) ? "container" : ""; + } else { + subject = ""; + } + break; + } + case KEY_TAG: { + int i; + matched = 0; + for (i = 0; i < ev->ntags; i++) { + if (pattern_match(k->value, ev->tags[i], k->pat_type)) { + matched = 1; + break; + } + } + return (k->op == OP_MATCH_EQ) ? matched : !matched; + } + case KEY_RESULT: + subject = ev->result; + break; + default: + /* Assignment-only key encountered in match position: skip */ + logit(LOG_DEBUG, "rules:%s:%d: skipping non-match key %d in match phase", + r->filename, r->lineno, k->cls); + return 1; + } + + if (!subject) + return (k->op == OP_MATCH_NE); + + matched = pattern_match(k->value, subject, k->pat_type); + return (k->op == OP_MATCH_EQ) ? matched : !matched; +} + +/* ----- rule-level AND logic --------------------------------------------- */ + +/* + * Returns 1 if all match-op keys in the rule are satisfied. + */ +static int rule_matches(const struct rule *r, struct uevent *ev) +{ + int i; + + for (i = 0; i < r->nkeys; i++) { + const struct rule_key *k = &r->keys[i]; + + /* Skip assignment-only operators */ + if (k->op != OP_MATCH_EQ && k->op != OP_MATCH_NE) + continue; + + if (!match_key(r, k, ev)) + return 0; + } + return 1; +} + +/* ===== Phase 4: executor ================================================ */ + +/* ----- UID/GID resolution ----------------------------------------------- */ + +static uid_t resolve_uid(const char *s) +{ + if (!s || !*s) + return 0; + if (*s >= '0' && *s <= '9') + return (uid_t)atoi(s); + { + struct passwd *pw = getpwnam(s); + return pw ? pw->pw_uid : 0; + } +} + +static gid_t resolve_gid(const char *s) +{ + if (!s || !*s) + return 0; + if (*s >= '0' && *s <= '9') + return (gid_t)atoi(s); + { + struct group *gr = getgrnam(s); + return gr ? gr->gr_gid : 0; + } +} + +/* ----- IMPORT helpers --------------------------------------------------- */ + +/* + * Fork, exec cmd, parse stdout as KEY=VALUE (or KEY="VALUE") pairs + * and import them into ev's env store. + */ +/* + * Helper-first, builtin-fallback for IMPORT{program}= / RUN+="...". + * + * Stock udev rules invoke /lib/udev/ (path_id, usb_id, blkid, ...) + * via IMPORT{program}=. keventd ships compatible builtins for many of + * those. If the helper is absent on this system, fall back to the + * matching builtin so the rule still works. If the helper exists, run + * it as-is -- this lets users override our builtins by dropping a + * custom helper into /lib/udev/. + * + * Returns 1 if the builtin handled the command, 0 if the caller should + * proceed with the normal fork+exec path. + */ +static int try_builtin_fallback(const char *cmd, struct uevent *ev) +{ + char first[PATH_MAX], rebuilt[PATH_MAX]; + const char *space, *base; + size_t len; + + space = strchr(cmd, ' '); + len = space ? (size_t)(space - cmd) : strlen(cmd); + if (len == 0 || len >= sizeof(first)) + return 0; + memcpy(first, cmd, len); + first[len] = '\0'; + + /* Helper exists -- let caller fork+exec it (allows user override). */ + if (access(first, X_OK) == 0) + return 0; + + base = strrchr(first, '/'); + base = base ? base + 1 : first; + + if (!builtin_has(base)) + return 0; + + if (space) + snprintf(rebuilt, sizeof(rebuilt), "%s%s", base, space); + else + snprintf(rebuilt, sizeof(rebuilt), "%s", base); + + logit(LOG_DEBUG, "rules: %s not found, falling back to builtin %s", first, base); + builtin_run(ev, rebuilt); + return 1; +} + +static void import_from_program(const char *cmd, struct uevent *ev) +{ + struct sigaction sa_dfl, sa_old; + int pipefd[2]; + pid_t pid; + FILE *fp; + char line[512]; + + if (!cmd || !*cmd) + return; + if (try_builtin_fallback(cmd, ev)) + return; + if (pipe(pipefd) < 0) + return; + + sigemptyset(&sa_dfl.sa_mask); + sa_dfl.sa_flags = 0; + sa_dfl.sa_handler = SIG_DFL; + sigaction(SIGCHLD, &sa_dfl, &sa_old); + + pid = fork(); + if (pid < 0) { + sigaction(SIGCHLD, &sa_old, NULL); + close(pipefd[0]); + close(pipefd[1]); + return; + } + + if (pid == 0) { + close(pipefd[0]); + dup2(pipefd[1], STDOUT_FILENO); + close(pipefd[1]); + execl("/bin/sh", "sh", "-c", cmd, NULL); + _exit(127); + } + + close(pipefd[1]); + fp = fdopen(pipefd[0], "r"); + if (fp) { + while (fgets(line, sizeof(line), fp)) { + char *eq, *val; + + chomp(line); + if (!*line) + continue; + eq = strchr(line, '='); + if (!eq) + continue; + *eq++ = 0; + val = eq; + /* Strip surrounding quotes from value */ + if (*val == '"') { + val++; + char *end = strrchr(val, '"'); + if (end) + *end = 0; + } + uevent_setenv(ev, line, val); + } + fclose(fp); + } else { + close(pipefd[0]); + } + + { + int status; + waitpid(pid, &status, 0); + } + sigaction(SIGCHLD, &sa_old, NULL); +} + +/* + * Read KEY=VALUE (or KEY="VALUE") pairs from a file into ev's env store. + */ +static void import_from_file(const char *path, struct uevent *ev) +{ + FILE *fp; + char line[512]; + + fp = fopen(path, "r"); + if (!fp) + return; + + while (fgets(line, sizeof(line), fp)) { + char *eq, *val; + + chomp(line); + if (!*line || *line == '#') + continue; + eq = strchr(line, '='); + if (!eq) + continue; + *eq++ = 0; + val = eq; + if (*val == '"') { + val++; + char *end = strrchr(val, '"'); + if (end) + *end = 0; + } + uevent_setenv(ev, line, val); + } + + fclose(fp); +} + +/* + * Import a matching key (or key=value) from /proc/cmdline. + * The pattern in val is matched against each key on the cmdline. + */ +static void import_from_cmdline(const char *key_pattern, struct uevent *ev) +{ + FILE *fp; + char line[2048]; + char *p, *tok; + + fp = fopen("/proc/cmdline", "r"); + if (!fp) + return; + + if (!fgets(line, sizeof(line), fp)) { + fclose(fp); + return; + } + fclose(fp); + + p = line; + while ((tok = strsep(&p, " \t\n")) != NULL) { + char *eq = strchr(tok, '='); + + if (eq) { + *eq++ = 0; + if (fnmatch(key_pattern, tok, 0) == 0) + uevent_setenv(ev, tok, eq); + } else if (*tok && fnmatch(key_pattern, tok, 0) == 0) { + uevent_setenv(ev, tok, "1"); + } + } +} + +static void exec_import(const struct rule_key *k, struct uevent *ev, + const char *val) +{ + switch (k->import_type) { + case IMPORT_PROGRAM: + import_from_program(val, ev); + break; + case IMPORT_FILE: + import_from_file(val, ev); + break; + case IMPORT_DB: + udevdb_read(ev); + break; + case IMPORT_PARENT: + udevdb_read_parent(ev); + break; + case IMPORT_CMDLINE: + import_from_cmdline(val, ev); + break; + case IMPORT_BUILTIN: + builtin_run(ev, val); + break; + } +} + +/* ----- assignment executor ---------------------------------------------- */ + +static void exec_rule(const struct rule *r, struct uevent *ev) +{ + int i; + + for (i = 0; i < r->nkeys; i++) { + const struct rule_key *k = &r->keys[i]; + char val[PATH_MAX]; + + /* Skip match-only operators */ + if (k->op == OP_MATCH_EQ || k->op == OP_MATCH_NE) + continue; + + subst_value(k->value, val, sizeof(val), ev); + + switch (k->cls) { + case KEY_MODE: + if (!ev->applied.final_mode) { + ev->applied.mode = (mode_t)strtoul(val, NULL, 8); + ev->applied.has_mode = 1; + if (k->op == OP_ASSIGN_FINAL) + ev->applied.final_mode = 1; + } + break; + + case KEY_OWNER: + if (!ev->applied.final_owner) { + ev->applied.uid = resolve_uid(val); + ev->applied.has_owner = 1; + if (k->op == OP_ASSIGN_FINAL) + ev->applied.final_owner = 1; + } + break; + + case KEY_GROUP: + if (!ev->applied.final_group) { + ev->applied.gid = resolve_gid(val); + ev->applied.has_group = 1; + if (k->op == OP_ASSIGN_FINAL) + ev->applied.final_group = 1; + } + break; + + case KEY_NAME: + if (k->op == OP_ASSIGN || k->op == OP_ASSIGN_FINAL) { + free(ev->applied.name); + ev->applied.name = strdup(val); + } + break; + + case KEY_SYMLINK: + if (k->op == OP_ASSIGN) { + /* Clear existing, set single entry */ + int j; + for (j = 0; j < ev->applied.nsymlinks; j++) { + free(ev->applied.symlinks[j]); + ev->applied.symlinks[j] = NULL; + } + ev->applied.nsymlinks = 0; + /* Fall through to add */ + } + if (k->op == OP_ASSIGN || k->op == OP_ASSIGN_ADD) { + /* Value may be space-separated list */ + char copy[PATH_MAX], *p, *tok; + snprintf(copy, sizeof(copy), "%s", val); + p = copy; + while ((tok = strsep(&p, " ")) != NULL) { + if (!*tok) + continue; + if (ev->applied.nsymlinks >= RULE_SYMLINK_MAX) + break; + ev->applied.symlinks[ev->applied.nsymlinks] = strdup(tok); + if (ev->applied.symlinks[ev->applied.nsymlinks]) + ev->applied.nsymlinks++; + } + } + break; + + case KEY_ENV: + if (!k->attr) + break; + if (k->op == OP_ASSIGN || k->op == OP_ASSIGN_FINAL) { + uevent_setenv(ev, k->attr, val); + } else if (k->op == OP_ASSIGN_ADD) { + const char *cur = uevent_getenv(ev, k->attr); + if (cur && *cur) { + char combined[1024]; + snprintf(combined, sizeof(combined), "%s%s", cur, val); + uevent_setenv(ev, k->attr, combined); + } else { + uevent_setenv(ev, k->attr, val); + } + } else if (k->op == OP_ASSIGN_DEL) { + uevent_setenv(ev, k->attr, ""); + } + break; + + case KEY_TAG: + if (k->op == OP_ASSIGN_ADD && + ev->ntags < UEVENT_TAG_MAX) { + ev->tags[ev->ntags] = strdup(val); + if (ev->tags[ev->ntags]) + ev->ntags++; + } else if (k->op == OP_ASSIGN_DEL) { + int j; + for (j = 0; j < ev->ntags; j++) { + if (!strcmp(ev->tags[j], val)) { + free(ev->tags[j]); + ev->tags[j] = ev->tags[--ev->ntags]; + ev->tags[ev->ntags] = NULL; + break; + } + } + } + break; + + case KEY_RUN: + if ((k->op == OP_ASSIGN_ADD || k->op == OP_ASSIGN) && + ev->applied.nruncmds < RULE_RUN_MAX) { + /* Store raw value; substitution happens at execution time */ + ev->applied.run_cmds[ev->applied.nruncmds] = strdup(k->value); + if (ev->applied.run_cmds[ev->applied.nruncmds]) { + ev->applied.run_types[ev->applied.nruncmds] = k->run_type; + ev->applied.nruncmds++; + } + } + break; + + case KEY_IMPORT: + exec_import(k, ev, val); + break; + + case KEY_ATTR: + /* ATTR{file}= writes value to sysfs attribute */ + if (k->attr && ev->devpath) { + char path[PATH_MAX]; + FILE *fp; + + snprintf(path, sizeof(path), "/sys%s/%s", + ev->devpath, k->attr); + fp = fopen(path, "w"); + if (fp) { + fputs(val, fp); + fclose(fp); + } + } + break; + + case KEY_SYSCTL: + /* SYSCTL{param}= writes value to /proc/sys/ */ + if (k->attr) { + char path[PATH_MAX]; + char *dp; + FILE *fp; + + snprintf(path, sizeof(path), "/proc/sys/%s", k->attr); + for (dp = path + 10; *dp; dp++) + if (*dp == '.') + *dp = '/'; + fp = fopen(path, "w"); + if (fp) { + fputs(val, fp); + fclose(fp); + } + } + break; + + case KEY_SECLABEL: + logit(LOG_DEBUG, "rules: %s:%d: SECLABEL not supported", + r->filename, r->lineno); + break; + + case KEY_OPTIONS: + logit(LOG_DEBUG, "rules: %s:%d: OPTIONS not supported: %s", + r->filename, r->lineno, k->value ?: ""); + break; + + case KEY_LABEL: + case KEY_GOTO: + /* Handled at loop level in rules_apply() */ + break; + + default: + break; + } + } +} + +/* ----- post-event RUN executor ------------------------------------------ */ + +void rules_run_cmds(struct uevent *ev) +{ + int i; + + for (i = 0; i < ev->applied.nruncmds; i++) { + char cmd[PATH_MAX]; + + if (!ev->applied.run_cmds[i]) + continue; + + subst_value(ev->applied.run_cmds[i], cmd, sizeof(cmd), ev); + logit(LOG_DEBUG, "rules: RUN %s", cmd); + + if (ev->applied.run_types[i] == RUN_BUILTIN) { + builtin_run(ev, cmd); + } else if (!try_builtin_fallback(cmd, ev)) { + pid_t pid = fork(); + + if (pid == 0) { + execl("/bin/sh", "sh", "-c", cmd, NULL); + _exit(127); + } + /* Parent: SIGCHLD=SIG_IGN in keventd auto-reaps the child */ + } + } +} + +/* ----- main applier ----------------------------------------------------- */ + +int rules_apply(struct rule_list *list, struct uevent *ev) +{ + const char *goto_label = NULL; + struct rule *r; + int i; + + TAILQ_FOREACH(r, list, link) { + /* GOTO: skip rules until matching LABEL= is found */ + if (goto_label) { + const char *lbl = NULL; + + for (i = 0; i < r->nkeys; i++) { + if (r->keys[i].cls == KEY_LABEL && + r->keys[i].op == OP_ASSIGN) + lbl = r->keys[i].value; + } + if (!lbl || strcmp(lbl, goto_label)) + continue; + goto_label = NULL; + } + + if (!rule_matches(r, ev)) + continue; + + logit(LOG_DEBUG, "rules: %s:%d matched %s@%s", + r->filename, r->lineno, + uevent_action_str(ev->action), ev->devpath ?: ""); + + exec_rule(r, ev); + + /* GOTO takes effect after this rule's assignments are applied */ + goto_label = NULL; + for (i = 0; i < r->nkeys; i++) { + if (r->keys[i].cls == KEY_GOTO && + r->keys[i].op == OP_ASSIGN) { + goto_label = r->keys[i].value; + break; + } + } + } + + return 0; +} + +/** + * Local Variables: + * indent-tabs-mode: t + * c-file-style: "linux" + * End: + */ diff --git a/keventd/rules.h b/keventd/rules.h new file mode 100644 index 00000000..60ff8ee8 --- /dev/null +++ b/keventd/rules.h @@ -0,0 +1,163 @@ +/* udev rules file parser for keventd + * + * Copyright (c) 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. + */ + +#ifndef KEVENTD_RULES_H_ +#define KEVENTD_RULES_H_ + +#ifdef _LIBITE_LITE +# include +#else +# include +#endif + +#include "keventd.h" + +/* Operators from udev rules syntax */ +typedef enum { + OP_INVALID = 0, + OP_MATCH_EQ, /* == */ + OP_MATCH_NE, /* != */ + OP_ASSIGN, /* = */ + OP_ASSIGN_ADD, /* += */ + OP_ASSIGN_DEL, /* -= */ + OP_ASSIGN_FINAL, /* := */ +} rule_op_t; + +/* All udev match and assignment key types */ +typedef enum { + KEY_UNKNOWN = 0, + /* Match keys — current device */ + KEY_ACTION, + KEY_DEVPATH, + KEY_KERNEL, + KEY_NAME, + KEY_SUBSYSTEM, + KEY_DRIVER, + KEY_ATTR, /* ATTR{filename} */ + KEY_SYSCTL, /* SYSCTL{parameter} */ + KEY_ENV, /* ENV{variable} */ + KEY_CONST, /* CONST{key} */ + KEY_TAG, + KEY_TEST, /* TEST{mode} */ + KEY_PROGRAM, + KEY_RESULT, + /* Match keys — parent-chain walk */ + KEY_KERNELS, + KEY_SUBSYSTEMS, + KEY_DRIVERS, + KEY_ATTRS, /* ATTRS{filename} */ + KEY_TAGS, + /* Assignment keys */ + KEY_SYMLINK, + KEY_OWNER, + KEY_GROUP, + KEY_MODE, + KEY_SECLABEL, /* SECLABEL{module} */ + KEY_RUN, /* RUN{type} */ + KEY_LABEL, + KEY_GOTO, + KEY_IMPORT, /* IMPORT{type} */ + KEY_OPTIONS, +} rule_key_class_t; + +/* Pattern matching strategy for match-op keys */ +typedef enum { + PAT_PLAIN = 0, /* literal strcmp */ + PAT_GLOB, /* fnmatch */ + PAT_SPLIT, /* pipe-separated alternatives */ +} pat_type_t; + +/* IMPORT{type} subtypes */ +typedef enum { + IMPORT_PROGRAM = 0, /* run program, parse KEY=VALUE output */ + IMPORT_FILE, /* read KEY=VALUE from file */ + IMPORT_DB, /* restore properties from udev database */ + IMPORT_BUILTIN, /* run builtin, parse output */ + IMPORT_PARENT, /* copy from parent device's database entry */ + IMPORT_CMDLINE, /* import matching key from /proc/cmdline */ +} import_type_t; + +/* One key–operator–value triplet within a rule */ +struct rule_key { + rule_key_class_t cls; + rule_op_t op; + char *attr; /* ATTR{x}/ENV{x}/… — heap-allocated, may be NULL */ + char *value; /* pattern or assignment value — heap-allocated */ + pat_type_t pat_type; + import_type_t import_type; + run_type_t run_type; +}; + +#define RULE_KEY_MAX 32 + +/* One parsed rule (a single non-empty, non-comment line) */ +struct rule { + TAILQ_ENTRY(rule) link; + char *filename; /* heap-allocated source path */ + int lineno; + int nkeys; + struct rule_key keys[RULE_KEY_MAX]; +}; + +TAILQ_HEAD(rule_list, rule); + +/* + * Load rules from a single .rules file, appending to list. + * Returns the number of rules loaded, or -1 on open failure. + */ +int rules_load (struct rule_list *list, const char *path); + +/* + * Load all *.rules files from the standard udev directories + * (/lib/udev/rules.d, /run/udev/rules.d, /etc/udev/rules.d) + * plus extra_dir if non-NULL. Files are loaded in alphanumeric + * order within each directory; directories are processed in order. + * Returns total number of rules loaded. + */ +int rules_load_all(struct rule_list *list, const char *extra_dir); + +/* + * Free all rules and release their resources. + */ +void rules_free (struct rule_list *list); + +/* + * Apply all matching rules from list to ev, updating ev->applied, + * ev->result, env store, and tag list. + */ +int rules_apply (struct rule_list *list, struct uevent *ev); + +/* + * Execute the RUN+= commands accumulated in ev->applied.run_cmds. + * Call after all device handling (nodes, symlinks) is complete. + */ +void rules_run_cmds(struct uevent *ev); + +#endif /* KEVENTD_RULES_H_ */ + +/** + * Local Variables: + * indent-tabs-mode: t + * c-file-style: "linux" + * End: + */ diff --git a/keventd/sysfs.c b/keventd/sysfs.c new file mode 100644 index 00000000..a9e88326 --- /dev/null +++ b/keventd/sysfs.c @@ -0,0 +1,131 @@ +/* sysfs attribute reader and parent-chain walker + * + * Copyright (c) 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 +#include +#include +#include + +#ifdef _LIBITE_LITE +# include +#else +# include +#endif + +#include "sysfs.h" + +int sysfs_read_file(const char *path, char *buf, size_t len) +{ + FILE *fp; + + fp = fopen(path, "r"); + if (!fp) + return -1; + + if (!fgets(buf, len, fp)) { + fclose(fp); + return -1; + } + + fclose(fp); + chomp(buf); + return 0; +} + +int sysfs_read_attr(const char *devpath, const char *attr, + char *buf, size_t len) +{ + char path[PATH_MAX]; + + snprintf(path, sizeof(path), "/sys%s/%s", devpath, attr); + return sysfs_read_file(path, buf, len); +} + +int sysfs_parent_walk(const char *devpath, + int (*cb)(const char *syspath, void *data), + void *data) +{ + char syspath[PATH_MAX]; + char *p; + + snprintf(syspath, sizeof(syspath), "/sys%s", devpath); + + while (1) { + if (cb(syspath, data) == 0) + return 0; + + /* Strip last path component */ + p = strrchr(syspath, '/'); + if (!p || p == syspath) + break; + *p = 0; + + /* Do not walk above /sys/devices */ + if (!strcmp(syspath, "/sys/devices") || + !strcmp(syspath, "/sys")) + break; + } + + return -1; +} + +int sysfs_read_subsystem(const char *syspath, char *buf, size_t len) +{ + char lpath[PATH_MAX], target[PATH_MAX]; + const char *p; + ssize_t n; + + snprintf(lpath, sizeof(lpath), "%s/subsystem", syspath); + n = readlink(lpath, target, sizeof(target) - 1); + if (n < 0) + return -1; + target[n] = 0; + + p = strrchr(target, '/'); + snprintf(buf, len, "%s", p ? p + 1 : target); + return 0; +} + +int sysfs_read_driver(const char *syspath, char *buf, size_t len) +{ + char lpath[PATH_MAX], target[PATH_MAX]; + const char *p; + ssize_t n; + + snprintf(lpath, sizeof(lpath), "%s/driver", syspath); + n = readlink(lpath, target, sizeof(target) - 1); + if (n < 0) + return -1; + target[n] = 0; + + p = strrchr(target, '/'); + snprintf(buf, len, "%s", p ? p + 1 : target); + return 0; +} + +/** + * Local Variables: + * indent-tabs-mode: t + * c-file-style: "linux" + * End: + */ diff --git a/keventd/sysfs.h b/keventd/sysfs.h new file mode 100644 index 00000000..576a7914 --- /dev/null +++ b/keventd/sysfs.h @@ -0,0 +1,75 @@ +/* sysfs attribute reader and parent-chain walker for keventd + * + * Copyright (c) 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. + */ + +#ifndef KEVENTD_SYSFS_H_ +#define KEVENTD_SYSFS_H_ + +#include + +/* + * Read the first line from an absolute path. + * Returns 0 on success, -1 on failure. + */ +int sysfs_read_file(const char *path, char *buf, size_t len); + +/* + * Read a sysfs attribute for the device at /sys{devpath}. + * attr may be a relative path like "device/vendor" or "../serial". + * Returns 0 on success, -1 if the attribute does not exist. + */ +int sysfs_read_attr(const char *devpath, const char *attr, + char *buf, size_t len); + +/* + * Walk the sysfs device tree upward from devpath toward /sys/devices, + * calling cb at each level. cb receives the absolute syspath and the + * caller-supplied data pointer. Walk stops when cb returns 0 (found) + * or the tree root is reached. + * Returns 0 if cb returned 0, -1 if nothing matched. + */ +int sysfs_parent_walk(const char *devpath, + int (*cb)(const char *syspath, void *data), + void *data); + +/* + * Read the subsystem name at syspath by following the "subsystem" + * symlink and returning its basename. + * Returns 0 on success, -1 if there is no subsystem link. + */ +int sysfs_read_subsystem(const char *syspath, char *buf, size_t len); + +/* + * Read the driver name at syspath by following the "driver" symlink + * and returning its basename. + * Returns 0 on success, -1 if there is no driver link. + */ +int sysfs_read_driver(const char *syspath, char *buf, size_t len); + +#endif /* KEVENTD_SYSFS_H_ */ + +/** + * Local Variables: + * indent-tabs-mode: t + * c-file-style: "linux" + * End: + */ diff --git a/keventd/udevdb.c b/keventd/udevdb.c new file mode 100644 index 00000000..bafe925c --- /dev/null +++ b/keventd/udevdb.c @@ -0,0 +1,243 @@ +/* udev-compatible per-device database in /run/udev/data/ + * + * Copyright (c) 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 +#include +#include +#include +#include +#include + +#ifdef _LIBITE_LITE +# include +#else +# include +#endif + +#include "keventd.h" +#include "sysfs.h" +#include "udevdb.h" + +/* Forward declaration from keventd.c */ +void logit(int prio, const char *fmt, ...); + +/* + * Compute the database file path for a device. + * + * udev's keying scheme: + * b: block device + * c: character device + * n network interface + * +: everything else + * + * This matches the layout that udevadm info, libudev, and eudev expect. + */ +static void udevdb_path(const struct uevent *ev, char *buf, size_t len) +{ + const char *sysname = uevent_sysname(ev); + const char *ifindex = NULL; + + if (ev->subsystem && !strcmp(ev->subsystem, "net")) + ifindex = uevent_getenv(ev, "IFINDEX"); + + if (ev->major >= 0 && ev->minor >= 0) { + const char *t = (ev->subsystem && !strcmp(ev->subsystem, "block")) + ? "b" : "c"; + snprintf(buf, len, UDEVDB_PATH "/%s%d:%d", t, ev->major, ev->minor); + } else if (ifindex) { + snprintf(buf, len, UDEVDB_PATH "/n%s", ifindex); + } else { + snprintf(buf, len, UDEVDB_PATH "/+%s:%s", + ev->subsystem ? ev->subsystem : "unknown", + sysname ? sysname : "unknown"); + } +} + +/* + * Write device properties to the database. + * + * Called after devnode_add() and symlink_add() so that all S: records + * can be captured via symlink_write_db(). + */ +int udevdb_write(const struct uevent *ev) +{ + char path[PATH_MAX]; + struct timeval tv; + FILE *fp; + int i; + + if (!ev->devpath) + return -1; + + if (mkpath(UDEVDB_PATH, 0755) && errno != EEXIST) + return -1; + + udevdb_path(ev, path, sizeof(path)); + + fp = fopen(path, "w"); + if (!fp) { + logit(LOG_DEBUG, "udevdb: cannot write %s: %s", path, strerror(errno)); + return -1; + } + + /* Timestamp in microseconds since epoch */ + gettimeofday(&tv, NULL); + fprintf(fp, "I:%llu\n", + (unsigned long long)(tv.tv_sec * 1000000ULL + tv.tv_usec)); + + /* All environment variables / device properties */ + for (i = 0; i < ev->nenv; i++) + fprintf(fp, "E:%s=%s\n", ev->env_key[i], ev->env_val[i]); + + /* Symlinks created for this device */ + symlink_write_db(ev->devpath, fp); + + /* Tags */ + for (i = 0; i < ev->ntags; i++) { + fprintf(fp, "G:%s\n", ev->tags[i]); + fprintf(fp, "Q:%s\n", ev->tags[i]); + } + + fprintf(fp, "V:1\n"); + fclose(fp); + + return 0; +} + +/* + * Read E: records from a database file into ev's env store. + */ +static int udevdb_load(const char *path, struct uevent *ev) +{ + char line[1024]; + FILE *fp; + + fp = fopen(path, "r"); + if (!fp) + return -1; + + while (fgets(line, sizeof(line), fp)) { + char *val; + + chomp(line); + + if (line[0] != 'E' || line[1] != ':') + continue; + + val = strchr(line + 2, '='); + if (!val) + continue; + *val++ = 0; + + uevent_setenv(ev, line + 2, val); + } + + fclose(fp); + return 0; +} + +/* + * Read device properties back from the database into ev's env store. + * + * Used by IMPORT{db} in the rules engine (Phase 4) to recover properties + * that were set during a previous event for the same device. + */ +int udevdb_read(struct uevent *ev) +{ + char path[PATH_MAX]; + + if (!ev->devpath) + return -1; + + udevdb_path(ev, path, sizeof(path)); + + return udevdb_load(path, ev); +} + +/* + * Read the parent device's properties into ev's env store, for + * IMPORT{parent} in the rules engine. The parent has no uevent to key + * from, so its device ID is composed from sysfs (see udevdb_path()). + */ +int udevdb_read_parent(struct uevent *ev) +{ + char ppath[PATH_MAX], syspath[PATH_MAX], dbfile[PATH_MAX]; + char subsys[64] = "unknown"; + char attr[64]; + const char *sn; + char *slash; + + if (!ev->devpath) + return -1; + + snprintf(ppath, sizeof(ppath), "%s", ev->devpath); + slash = strrchr(ppath, '/'); + if (!slash || slash == ppath) + return -1; + *slash = 0; + + sn = strrchr(ppath, '/'); + sn = sn ? sn + 1 : ppath; + + snprintf(syspath, sizeof(syspath), "/sys%s", ppath); + sysfs_read_subsystem(syspath, subsys, sizeof(subsys)); + + snprintf(dbfile, sizeof(dbfile), "%s/dev", syspath); + if (!sysfs_read_file(dbfile, attr, sizeof(attr))) { + snprintf(dbfile, sizeof(dbfile), UDEVDB_PATH "/%s%s", + strcmp(subsys, "block") ? "c" : "b", attr); + } else if (!strcmp(subsys, "net")) { + snprintf(dbfile, sizeof(dbfile), "%s/ifindex", syspath); + if (sysfs_read_file(dbfile, attr, sizeof(attr))) + return -1; + snprintf(dbfile, sizeof(dbfile), UDEVDB_PATH "/n%s", attr); + } else { + snprintf(dbfile, sizeof(dbfile), UDEVDB_PATH "/+%s:%s", + subsys, sn); + } + + return udevdb_load(dbfile, ev); +} + +/* + * Remove a device's database entry on ACT_REMOVE. + */ +void udevdb_delete(const struct uevent *ev) +{ + char path[PATH_MAX]; + + if (!ev->devpath) + return; + + udevdb_path(ev, path, sizeof(path)); + + if (unlink(path) && errno != ENOENT) + logit(LOG_DEBUG, "udevdb: cannot remove %s: %s", path, strerror(errno)); +} + +/** + * Local Variables: + * indent-tabs-mode: t + * c-file-style: "linux" + * End: + */ diff --git a/keventd/udevdb.h b/keventd/udevdb.h new file mode 100644 index 00000000..037eabbc --- /dev/null +++ b/keventd/udevdb.h @@ -0,0 +1,72 @@ +/* udev-compatible per-device database in /run/udev/data/ + * + * Copyright (c) 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. + */ + +#ifndef KEVENTD_UDEVDB_H_ +#define KEVENTD_UDEVDB_H_ + +#include "keventd.h" + +/* + * Database root directory, compatible with systemd/eudev's /run/udev/data/. + * One text file per device, keyed by device ID (see udevdb_path()). + * + * Record format (one per line): + * I: initialisation timestamp + * E:= environment / property + * S: symlink (relative to /dev/) + * G: tag (for G: records) + * Q: tag (for Q: query records, mirrors G:) + * V:1 format version sentinel, always last + */ +#define UDEVDB_PATH "/run/udev/data" + +/* + * Write a device's properties to the database after event processing. + * Creates /run/udev/data/ with I:, E:, S:, G:, Q:, V: records. + */ +int udevdb_write (const struct uevent *ev); + +/* + * Read a device's properties from the database into ev's env store. + * Used by IMPORT{db} and IMPORT{parent} in the rules engine (Phase 4). + */ +int udevdb_read (struct uevent *ev); + +/* + * Like udevdb_read(), but for the parent device of ev. + */ +int udevdb_read_parent (struct uevent *ev); + +/* + * Delete a device's database entry on ACT_REMOVE. + */ +void udevdb_delete (const struct uevent *ev); + +#endif /* KEVENTD_UDEVDB_H_ */ + +/** + * Local Variables: + * indent-tabs-mode: t + * c-file-style: "linux" + * End: + */ diff --git a/keventd/uevent.c b/keventd/uevent.c index 801ed07f..b5c935c1 100644 --- a/keventd/uevent.c +++ b/keventd/uevent.c @@ -34,12 +34,16 @@ #include #include +#include +#include #include #include #include #include #include +#include + #ifdef _LIBITE_LITE # include #else @@ -229,6 +233,16 @@ int uevent_parse(char *buf, size_t len, struct uevent *ev) hdrlen = strlen(buf) + 1 + strlen(ev->devpath) + 1; i = hdrlen; + /* Seed env store with header fields not present as KEY=VALUE pairs */ + if (ev->nenv < UEVENT_ENV_MAX - 1) { + ev->env_key[ev->nenv] = (char *)"ACTION"; + ev->env_val[ev->nenv] = buf; /* points to null-term'd action string */ + ev->nenv++; + ev->env_key[ev->nenv] = (char *)"DEVPATH"; + ev->env_val[ev->nenv] = ev->devpath; + ev->nenv++; + } + /* Parse KEY=VALUE pairs */ while (i < len) { char *eq; @@ -262,6 +276,14 @@ int uevent_parse(char *buf, size_t len, struct uevent *ev) ev->seqnum = eq; else if (!strcmp(line, "DRIVER")) ev->driver = eq; + + /* Store every pair in the env store for rule matching */ + if (ev->nenv < UEVENT_ENV_MAX) { + ev->env_key[ev->nenv] = line; + ev->env_val[ev->nenv] = eq; + /* alloc flags stay 0: buffer pointers, never freed */ + ev->nenv++; + } } i += strlen(line) + (eq ? strlen(eq) + 1 : 1) + 1; @@ -270,6 +292,108 @@ int uevent_parse(char *buf, size_t len, struct uevent *ev) return 0; } +const char *uevent_sysname(const struct uevent *ev) +{ + const char *p; + + if (!ev->devpath) + return NULL; + p = strrchr(ev->devpath, '/'); + return p ? p + 1 : ev->devpath; +} + +const char *uevent_getenv(const struct uevent *ev, const char *key) +{ + int i; + + for (i = 0; i < ev->nenv; i++) { + if (!strcmp(ev->env_key[i], key)) + return ev->env_val[i]; + } + return NULL; +} + +int uevent_setenv(struct uevent *ev, const char *key, const char *val) +{ + char *v; + int i; + + /* Update value of an existing key */ + for (i = 0; i < ev->nenv; i++) { + if (!strcmp(ev->env_key[i], key)) { + if (!strcmp(ev->env_val[i], val)) + return 0; + v = strdup(val); + if (!v) + return -1; + if (ev->env_val_alloc[i]) + free(ev->env_val[i]); + ev->env_val[i] = v; + ev->env_val_alloc[i] = 1; + return 0; + } + } + + /* Add new key — both key and value are heap-allocated */ + if (ev->nenv >= UEVENT_ENV_MAX) + return -1; + + i = ev->nenv; + ev->env_key[i] = strdup(key); + ev->env_val[i] = strdup(val); + if (!ev->env_key[i] || !ev->env_val[i]) { + free(ev->env_key[i]); + free(ev->env_val[i]); + return -1; + } + ev->env_key_alloc[i] = 1; + ev->env_val_alloc[i] = 1; + ev->nenv++; + return 0; +} + +void rule_ctx_free(struct rule_ctx *ctx) +{ + int i; + + free(ctx->name); + ctx->name = NULL; + + for (i = 0; i < ctx->nsymlinks; i++) { + free(ctx->symlinks[i]); + ctx->symlinks[i] = NULL; + } + ctx->nsymlinks = 0; + + for (i = 0; i < ctx->nruncmds; i++) { + free(ctx->run_cmds[i]); + ctx->run_cmds[i] = NULL; + } + ctx->nruncmds = 0; +} + +void uevent_env_free(struct uevent *ev) +{ + int i; + + for (i = 0; i < ev->nenv; i++) { + if (ev->env_key_alloc[i]) + free(ev->env_key[i]); + if (ev->env_val_alloc[i]) + free(ev->env_val[i]); + } + ev->nenv = 0; + + free(ev->result); + ev->result = NULL; + + for (i = 0; i < ev->ntags; i++) + free(ev->tags[i]); + ev->ntags = 0; + + rule_ctx_free(&ev->applied); +} + static struct devrule *find_rule(struct uevent *ev) { struct devrule *rule; @@ -308,6 +432,7 @@ int devnode_add(struct uevent *ev) { struct devrule *rule; char path[PATH_MAX]; + const char *devname; char *dir; mode_t mode; dev_t dev; @@ -316,7 +441,9 @@ int devnode_add(struct uevent *ev) if (!ev->devname || ev->major < 0 || ev->minor < 0) return -1; - snprintf(path, sizeof(path), "/dev/%s", ev->devname); + /* NAME= in a rule overrides the kernel-supplied device name */ + devname = ev->applied.name ?: ev->devname; + snprintf(path, sizeof(path), "/dev/%s", devname); /* Create parent directories if needed (e.g., /dev/input/) */ dir = strdupa(path); @@ -329,10 +456,10 @@ int devnode_add(struct uevent *ev) } } - /* Find matching rule for permissions */ + /* Built-in table provides defaults; rules engine may override */ rule = find_rule(ev); - mode = rule->mode; - dev = makedev(ev->major, ev->minor); + mode = ev->applied.has_mode ? ev->applied.mode : rule->mode; + dev = makedev(ev->major, ev->minor); /* Remove existing node if present */ unlink(path); @@ -355,14 +482,19 @@ int devnode_add(struct uevent *ev) logit(LOG_WARNING, "Failed chmod %s: %s", path, strerror(errno)); /* Set ownership */ - if (chown(path, rule->uid, rule->gid)) - logit(LOG_WARNING, "Failed chown %s: %s", path, strerror(errno)); + { + uid_t uid = ev->applied.has_owner ? ev->applied.uid : rule->uid; + gid_t gid = ev->applied.has_group ? ev->applied.gid : rule->gid; + + if (chown(path, uid, gid)) + logit(LOG_WARNING, "Failed chown %s: %s", path, strerror(errno)); + } logit(LOG_DEBUG, "Created %s (%d:%d) mode %04o", - path, ev->major, ev->minor, rule->mode); + path, ev->major, ev->minor, mode & ~S_IFMT); - /* Set dev/ condition */ - dev_cond(ev->devname, 1); + /* Set dev/ condition using the actual node name */ + dev_cond(devname, 1); return 0; } @@ -372,15 +504,18 @@ int devnode_add(struct uevent *ev) */ int devnode_del(struct uevent *ev) { + const char *devname; char path[PATH_MAX]; if (!ev->devname) return -1; - snprintf(path, sizeof(path), "/dev/%s", ev->devname); + /* Mirror devnode_add: NAME= overrides the kernel-supplied name */ + devname = ev->applied.name ?: ev->devname; + snprintf(path, sizeof(path), "/dev/%s", devname); /* Clear dev/ condition first */ - dev_cond(ev->devname, 0); + dev_cond(devname, 0); if (unlink(path) && errno != ENOENT) { logit(LOG_WARNING, "Failed removing %s: %s", path, strerror(errno)); @@ -391,6 +526,53 @@ int devnode_del(struct uevent *ev) return 0; } +/* + * Handle network interface add: rename if NAME= was set by a rule, then + * set the dev/ condition so Finit services can depend on the interface. + * + * The interface must be DOWN for SIOCSIFNAME to succeed. Freshly added + * interfaces always are, so we do not attempt to bring the link down. + */ +int netdev_add(struct uevent *ev) +{ + const char *new_name; + + if (!ev->devname) + return -1; + + new_name = ev->applied.name; + + if (new_name && strcmp(ev->devname, new_name)) { + struct ifreq ifr; + int sock, rc; + + sock = socket(AF_INET, SOCK_DGRAM | SOCK_CLOEXEC, 0); + if (sock < 0) { + logit(LOG_WARNING, "Cannot open socket for netif rename: %s", + strerror(errno)); + goto cond; + } + + memset(&ifr, 0, sizeof(ifr)); + strncpy(ifr.ifr_name, ev->devname, IFNAMSIZ - 1); + strncpy(ifr.ifr_newname, new_name, IFNAMSIZ - 1); + + rc = ioctl(sock, SIOCSIFNAME, &ifr); + close(sock); + + if (rc < 0) + logit(LOG_WARNING, "Failed renaming %s -> %s: %s", + ev->devname, new_name, strerror(errno)); + else + logit(LOG_NOTICE, "Renamed interface %s -> %s", + ev->devname, new_name); + } + +cond: + dev_cond(new_name ?: ev->devname, 1); + return 0; +} + /* * Track symlink for removal when device is unplugged. */ @@ -625,18 +807,72 @@ static int symlink_add_input(struct uevent *ev) } /* - * Create appropriate symlinks based on device subsystem. + * Write S: symlink records for a device into an already-open database file. + * Called by udevdb_write() when building the per-device database entry. */ -int symlink_add(struct uevent *ev) +void symlink_write_db(const char *devpath, FILE *fp) { - if (!ev->subsystem) - return 0; + struct dev_symlink *sl; - if (!strcmp(ev->subsystem, "block")) - return symlink_add_disk(ev); + TAILQ_FOREACH(sl, &symlinks, link) { + if (strcmp(sl->devpath, devpath)) + continue; + /* Store path relative to /dev/ */ + const char *rel = sl->linkpath; + if (!strncmp(rel, "/dev/", 5)) + rel += 5; + fprintf(fp, "S:%s\n", rel); + } +} - if (!strcmp(ev->subsystem, "input")) - return symlink_add_input(ev); +/* + * Create appropriate symlinks based on device subsystem and rules. + */ +int symlink_add(struct uevent *ev) +{ + int i; + + /* Built-in subsystem symlinks */ + if (ev->subsystem) { + if (!strcmp(ev->subsystem, "block")) + symlink_add_disk(ev); + else if (!strcmp(ev->subsystem, "input")) + symlink_add_input(ev); + } + + /* SYMLINK+= from matched rules */ + for (i = 0; i < ev->applied.nsymlinks; i++) { + const char *devname, *sl; + char link[PATH_MAX], target[PATH_MAX]; + size_t toff = 0; + int depth = 0; + const char *p; + + sl = ev->applied.symlinks[i]; + if (!sl || !*sl) + continue; + + devname = ev->applied.name ?: ev->devname; + if (!devname) + continue; + + /* basename of devname for the symlink target */ + const char *base = strrchr(devname, '/'); + base = base ? base + 1 : devname; + + /* Count '/' in symlink path to determine relative depth */ + for (p = sl; *p; p++) + if (*p == '/') + depth++; + + target[0] = 0; + for (int d = 0; d < depth; d++) + toff += snprintf(target + toff, sizeof(target) - toff, "../"); + snprintf(target + toff, sizeof(target) - toff, "%s", base); + + snprintf(link, sizeof(link), "/dev/%s", sl); + symlink_create(target, link, ev->devpath ?: ""); + } return 0; } diff --git a/mkdocs.yml b/mkdocs.yml index 2ccce539..eb003da7 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -56,6 +56,7 @@ nav: - Plugins: plugins.md - Watchdog: watchdog.md - keventd: keventd.md + - udev Rule Matching: udev-matching.md - Service State Machine: state-machine.md - Distributions: distro.md - Requirements: requirements.md From e1c07a59eab0434c98b8c6369b3b6c45a15b4eeb Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 18 May 2026 07:59:42 +0200 Subject: [PATCH 13/24] keventd: ship curated udev rules derived from eudev 3.2.14 Add 27 stock rules installed to /lib/udev/rules.d/. Rules invoking /lib/udev/ fall back to keventd builtins (path_id, usb_id, blkid, hwdb, kmod, net_id, input_id) when the helper binary is absent; user-supplied helpers in /lib/udev/ still take precedence. Signed-off-by: Joachim Wiberg --- configure.ac | 4 + keventd/Makefile.am | 36 +++++ keventd/rules.d/50-udev-default.rules | 121 ++++++++++++++++ keventd/rules.d/60-autosuspend.rules | 22 +++ keventd/rules.d/60-block.rules | 17 +++ keventd/rules.d/60-cdrom_id.rules | 33 +++++ keventd/rules.d/60-drm.rules | 12 ++ keventd/rules.d/60-evdev.rules | 29 ++++ keventd/rules.d/60-fido-id.rules | 18 +++ keventd/rules.d/60-input-id.rules | 12 ++ keventd/rules.d/60-persistent-alsa.rules | 18 +++ keventd/rules.d/60-persistent-input.rules | 46 ++++++ .../rules.d/60-persistent-storage-tape.rules | 40 ++++++ keventd/rules.d/60-persistent-storage.rules | 133 ++++++++++++++++++ keventd/rules.d/60-persistent-v4l.rules | 24 ++++ keventd/rules.d/60-sensor.rules | 38 +++++ keventd/rules.d/60-serial.rules | 30 ++++ keventd/rules.d/64-btrfs.rules | 21 +++ keventd/rules.d/70-camera.rules | 13 ++ keventd/rules.d/70-joystick.rules | 16 +++ keventd/rules.d/70-memory.rules | 12 ++ keventd/rules.d/70-mouse.rules | 22 +++ keventd/rules.d/70-touchpad.rules | 17 +++ keventd/rules.d/75-net-description.rules | 18 +++ keventd/rules.d/75-probe_mtd.rules | 11 ++ keventd/rules.d/78-sound-card.rules | 100 +++++++++++++ keventd/rules.d/80-drivers.rules | 17 +++ keventd/rules.d/80-net-name-slot.rules | 18 +++ keventd/rules.d/81-net-dhcp.rules | 18 +++ 29 files changed, 916 insertions(+) create mode 100644 keventd/rules.d/50-udev-default.rules create mode 100644 keventd/rules.d/60-autosuspend.rules create mode 100644 keventd/rules.d/60-block.rules create mode 100644 keventd/rules.d/60-cdrom_id.rules create mode 100644 keventd/rules.d/60-drm.rules create mode 100644 keventd/rules.d/60-evdev.rules create mode 100644 keventd/rules.d/60-fido-id.rules create mode 100644 keventd/rules.d/60-input-id.rules create mode 100644 keventd/rules.d/60-persistent-alsa.rules create mode 100644 keventd/rules.d/60-persistent-input.rules create mode 100644 keventd/rules.d/60-persistent-storage-tape.rules create mode 100644 keventd/rules.d/60-persistent-storage.rules create mode 100644 keventd/rules.d/60-persistent-v4l.rules create mode 100644 keventd/rules.d/60-sensor.rules create mode 100644 keventd/rules.d/60-serial.rules create mode 100644 keventd/rules.d/64-btrfs.rules create mode 100644 keventd/rules.d/70-camera.rules create mode 100644 keventd/rules.d/70-joystick.rules create mode 100644 keventd/rules.d/70-memory.rules create mode 100644 keventd/rules.d/70-mouse.rules create mode 100644 keventd/rules.d/70-touchpad.rules create mode 100644 keventd/rules.d/75-net-description.rules create mode 100644 keventd/rules.d/75-probe_mtd.rules create mode 100644 keventd/rules.d/78-sound-card.rules create mode 100644 keventd/rules.d/80-drivers.rules create mode 100644 keventd/rules.d/80-net-name-slot.rules create mode 100644 keventd/rules.d/81-net-dhcp.rules diff --git a/configure.ac b/configure.ac index 9f00b36b..a41d5517 100644 --- a/configure.ac +++ b/configure.ac @@ -174,6 +174,9 @@ AC_ARG_WITH(random-seed, AC_ARG_WITH(keventd, AS_HELP_STRING([--with-keventd], [Enable built-in keventd device manager, default: yes]),, [with_keventd=yes]) +AC_ARG_WITH(udev-rules, + AS_HELP_STRING([--without-udev-rules], [Skip install of curated udev rules under /lib/udev/rules.d, default: yes when keventd enabled]),, [with_udev_rules=yes]) + AC_ARG_WITH(sulogin, AS_HELP_STRING([--with-sulogin@<:@=USER@:>@], [Enable built-in sulogin, optional USER to request password for (default root), default: no.]),[sulogin=$withval],[with_sulogin=no]) @@ -356,6 +359,7 @@ AS_IF([test "x$with_plugin_path" != "xno"], [ AM_CONDITIONAL(BASH, [test "x$bash_dir" != "xno"]) AM_CONDITIONAL(STATIC, [test "x$enable_static" = "xyes"]) AM_CONDITIONAL(KEVENTD, [test "x$with_keventd" != "xno"]) +AM_CONDITIONAL(UDEV_RULES, [test "x$with_keventd" != "xno" -a "x$with_udev_rules" != "xno"]) AM_CONDITIONAL(SULOGIN, [test "x$with_sulogin" != "xno"]) AM_CONDITIONAL(WATCHDOGD, [test "x$with_watchdog" != "xno"]) AM_CONDITIONAL(LIBSYSTEMD, [test "x$with_libsystemd" != "xno"]) diff --git a/keventd/Makefile.am b/keventd/Makefile.am index 0b4f1166..0efbb4ad 100644 --- a/keventd/Makefile.am +++ b/keventd/Makefile.am @@ -18,3 +18,39 @@ keventd_CFLAGS += $(lite_CFLAGS) keventd_CFLAGS += $(blkid_CFLAGS) keventd_LDADD = $(lite_LIBS) keventd_LDADD += $(blkid_LIBS) + +# 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 +# precedence. See .notes/curated-rules-manifest.md. +if UDEV_RULES +udevrulesdir = $(prefix)/lib/udev/rules.d +dist_udevrules_DATA = \ + rules.d/50-udev-default.rules \ + rules.d/60-autosuspend.rules \ + rules.d/60-block.rules \ + rules.d/60-cdrom_id.rules \ + rules.d/60-drm.rules \ + rules.d/60-evdev.rules \ + rules.d/60-fido-id.rules \ + rules.d/60-input-id.rules \ + rules.d/60-persistent-alsa.rules \ + rules.d/60-persistent-input.rules \ + rules.d/60-persistent-storage.rules \ + rules.d/60-persistent-storage-tape.rules \ + rules.d/60-persistent-v4l.rules \ + rules.d/60-sensor.rules \ + rules.d/60-serial.rules \ + rules.d/64-btrfs.rules \ + rules.d/70-camera.rules \ + rules.d/70-joystick.rules \ + rules.d/70-memory.rules \ + rules.d/70-mouse.rules \ + rules.d/70-touchpad.rules \ + rules.d/75-net-description.rules \ + rules.d/75-probe_mtd.rules \ + rules.d/78-sound-card.rules \ + rules.d/80-drivers.rules \ + rules.d/80-net-name-slot.rules \ + rules.d/81-net-dhcp.rules +endif diff --git a/keventd/rules.d/50-udev-default.rules b/keventd/rules.d/50-udev-default.rules new file mode 100644 index 00000000..14e39126 --- /dev/null +++ b/keventd/rules.d/50-udev-default.rules @@ -0,0 +1,121 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Derived from eudev 3.2.14 rules/50-udev-default.rules +# Modifications by the Finit project: none +# +# do not edit this file, it will be overwritten on update + +# run a command on remove events +ACTION=="remove", ENV{REMOVE_CMD}!="", RUN+="$env{REMOVE_CMD}" +ACTION=="remove", GOTO="default_end" + +SUBSYSTEM=="virtio-ports", KERNEL=="vport*", ATTR{name}=="?*", SYMLINK+="virtio-ports/$attr{name}" + +# select "system RTC" or just use the first one +SUBSYSTEM=="rtc", ATTR{hctosys}=="1", SYMLINK+="rtc" +SUBSYSTEM=="rtc", KERNEL=="rtc0", SYMLINK+="rtc", OPTIONS+="link_priority=-100" + +SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", IMPORT{builtin}="usb_id", IMPORT{builtin}="hwdb --subsystem=usb" +ENV{MODALIAS}!="", IMPORT{builtin}="hwdb --subsystem=$env{SUBSYSTEM}" + +ACTION!="add", GOTO="default_end" + +SUBSYSTEM=="tty", KERNEL=="ptmx", GROUP="tty", MODE="0666" +SUBSYSTEM=="tty", KERNEL=="tty", GROUP="tty", MODE="0666" +SUBSYSTEM=="tty", KERNEL=="tty[0-9]*", GROUP="tty", MODE="0620" +SUBSYSTEM=="tty", KERNEL=="sclp_line[0-9]*", GROUP="tty", MODE="0620" +SUBSYSTEM=="tty", KERNEL=="ttysclp[0-9]*", GROUP="tty", MODE="0620" +SUBSYSTEM=="tty", KERNEL=="3270/tty[0-9]*", GROUP="tty", MODE="0620" +SUBSYSTEM=="vc", KERNEL=="vcs*|vcsa*", GROUP="tty" +KERNEL=="tty[A-Z]*[0-9]|ttymxc[0-9]*|pppox[0-9]*|ircomm[0-9]*|noz[0-9]*|rfcomm[0-9]*", GROUP="dialout" + +SUBSYSTEM=="mem", KERNEL=="mem|kmem|port", GROUP="kmem", MODE="0640" + +SUBSYSTEM=="input", GROUP="input" +SUBSYSTEM=="input", KERNEL=="js[0-9]*", MODE="0664" + +SUBSYSTEM=="video4linux", GROUP="video" +SUBSYSTEM=="graphics", GROUP="video" +SUBSYSTEM=="drm", KERNEL!="renderD*", GROUP="video" +SUBSYSTEM=="dvb", GROUP="video" +SUBSYSTEM=="media", GROUP="video" +SUBSYSTEM=="cec", GROUP="video" + +SUBSYSTEM=="drm", KERNEL=="renderD*", GROUP="video", MODE="0666" +SUBSYSTEM=="kfd", GROUP="video", MODE="0666" + +SUBSYSTEM=="misc", KERNEL=="sgx_enclave", GROUP="sgx", MODE="0660" +SUBSYSTEM=="misc", KERNEL=="sgx_vepc", GROUP="sgx", MODE="0660" + +# When using static_node= with non-default permissions, also update +# tmpfiles.d/static-nodes-permissions.conf.in to keep permissions synchronized. + +SUBSYSTEM=="sound", GROUP="audio", \ + OPTIONS+="static_node=snd/seq", OPTIONS+="static_node=snd/timer" + +SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", MODE="0664" + +SUBSYSTEM=="firewire", TEST=="units", TEST=="model", \ + IMPORT{builtin}="hwdb 'ieee1394:node:ven$attr{vendor}mo$attr{model}units$attr{units}'" + +SUBSYSTEM=="firewire", TEST=="units", TEST!="model", \ + IMPORT{builtin}="hwdb 'ieee1394:node:ven$attr{vendor}units$attr{units}'" + +SUBSYSTEM=="firewire", TEST=="units", ENV{IEEE1394_UNIT_FUNCTION_MIDI}=="1", GROUP="audio" +SUBSYSTEM=="firewire", TEST=="units", ENV{IEEE1394_UNIT_FUNCTION_AUDIO}=="1", GROUP="audio" +SUBSYSTEM=="firewire", TEST=="units", ENV{IEEE1394_UNIT_FUNCTION_VIDEO}=="1", GROUP="video" + +KERNEL=="parport[0-9]*", GROUP="lp" +SUBSYSTEM=="printer", KERNEL=="lp*", GROUP="lp" +SUBSYSTEM=="ppdev", GROUP="lp" +KERNEL=="lp[0-9]*", GROUP="lp" +KERNEL=="irlpt[0-9]*", GROUP="lp" +SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ENV{ID_USB_INTERFACES}=="*:0701??:*", GROUP="lp" + +SUBSYSTEM=="block", GROUP="disk" +SUBSYSTEM=="block", KERNEL=="sr[0-9]*", GROUP="cdrom" +SUBSYSTEM=="scsi_generic", SUBSYSTEMS=="scsi", ATTRS{type}=="4|5", GROUP="cdrom" +KERNEL=="sch[0-9]*", GROUP="cdrom" +KERNEL=="pktcdvd[0-9]*", GROUP="cdrom" +KERNEL=="pktcdvd", GROUP="cdrom" + +SUBSYSTEM=="scsi_generic|scsi_tape", SUBSYSTEMS=="scsi", ATTRS{type}=="1|8", GROUP="tape" +SUBSYSTEM=="scsi_generic", SUBSYSTEMS=="scsi", ATTRS{type}=="0", GROUP="disk" +KERNEL=="qft[0-9]*|nqft[0-9]*|zqft[0-9]*|nzqft[0-9]*|rawqft[0-9]*|nrawqft[0-9]*", GROUP="disk" +KERNEL=="loop-control", GROUP="disk", OPTIONS+="static_node=loop-control" +KERNEL=="btrfs-control", GROUP="disk" +KERNEL=="rawctl", GROUP="disk" +SUBSYSTEM=="raw", KERNEL=="raw[0-9]*", GROUP="disk" +SUBSYSTEM=="aoe", GROUP="disk", MODE="0220" +SUBSYSTEM=="aoe", KERNEL=="err", MODE="0440" + +KERNEL=="rfkill", MODE="0664" +KERNEL=="tun", MODE="0666", OPTIONS+="static_node=net/tun" + +KERNEL=="fuse", MODE="0666", OPTIONS+="static_node=fuse" + +# The static_node is required on s390x and ppc (they are using MODULE_ALIAS) +KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm" + +KERNEL=="vfio", MODE="0666", OPTIONS+="static_node=vfio/vfio" + +KERNEL=="vsock", MODE="0666" +KERNEL=="vhost-vsock", GROUP="kvm", MODE="0666", OPTIONS+="static_node=vhost-vsock" + +KERNEL=="vhost-net", GROUP="kvm", MODE="0666", OPTIONS+="static_node=vhost-net" + +KERNEL=="udmabuf", GROUP="kvm" + +SUBSYSTEM=="ptp", ATTR{clock_name}=="KVM virtual PTP", SYMLINK += "ptp_kvm" + +SUBSYSTEM=="ptp", ATTR{clock_name}=="hyperv", SYMLINK += "ptp_hyperv" + +SUBSYSTEM!="dmi", GOTO="dmi_end" +ENV{ID_VENDOR}="$attr{sys_vendor}" +ENV{ID_SYSFS_ATTRIBUTE_MODEL}=="|product_name", ENV{ID_MODEL}="$attr{product_name}" +ENV{ID_SYSFS_ATTRIBUTE_MODEL}=="product_version", ENV{ID_MODEL}="$attr{product_version}" +# fallback to board information +ENV{ID_VENDOR}=="", ENV{ID_VENDOR}="$attr{board_vendor}" +ENV{ID_MODEL}=="", ENV{ID_MODEL}="$attr{board_name}" +LABEL="dmi_end" + +LABEL="default_end" diff --git a/keventd/rules.d/60-autosuspend.rules b/keventd/rules.d/60-autosuspend.rules new file mode 100644 index 00000000..36b1b70a --- /dev/null +++ b/keventd/rules.d/60-autosuspend.rules @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Derived from eudev 3.2.14 rules/60-autosuspend.rules +# Modifications by the Finit project: none +# +# do not edit this file, it will be overwritten on update + +ACTION!="add", GOTO="autosuspend_end" + +# I2C rules +SUBSYSTEM=="i2c", ATTR{name}=="cyapa", \ + ATTR{power/control}="on", GOTO="autosuspend_end" + +# Enable autosuspend if hwdb says so. Here we are relying on +# the hwdb import done earlier based on MODALIAS. +ENV{ID_AUTOSUSPEND}=="1", TEST=="power/control", \ + ATTR{power/control}="auto" + +# Disable USB persist if hwdb says so. +ENV{ID_PERSIST}=="0", TEST=="power/persist", \ + ATTR{power/persist}="0" + +LABEL="autosuspend_end" diff --git a/keventd/rules.d/60-block.rules b/keventd/rules.d/60-block.rules new file mode 100644 index 00000000..bbad83b9 --- /dev/null +++ b/keventd/rules.d/60-block.rules @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Derived from eudev 3.2.14 rules/60-block.rules +# Modifications by the Finit project: none +# +# do not edit this file, it will be overwritten on update + +# enable in-kernel media-presence polling +ACTION=="add", SUBSYSTEM=="module", KERNEL=="block", ATTR{parameters/events_dfl_poll_msecs}=="0", \ + ATTR{parameters/events_dfl_poll_msecs}="2000" + +# forward scsi device event to corresponding block device +ACTION=="change", SUBSYSTEM=="scsi", ENV{DEVTYPE}=="scsi_device", TEST=="block", ATTR{block/*/uevent}="change" + +# watch metadata changes, caused by tools closing the device node which was opened for writing +ACTION!="remove", SUBSYSTEM=="block", \ + KERNEL=="loop*|mmcblk*[0-9]|msblk*[0-9]|mspblk*[0-9]|nvme*|sd*|vd*|xvd*|bcache*|cciss*|dasd*|ubd*|ubi*|scm*|pmem*|nbd*|zd*", \ + OPTIONS+="watch" diff --git a/keventd/rules.d/60-cdrom_id.rules b/keventd/rules.d/60-cdrom_id.rules new file mode 100644 index 00000000..59142ab0 --- /dev/null +++ b/keventd/rules.d/60-cdrom_id.rules @@ -0,0 +1,33 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Derived from eudev 3.2.14 rules/60-cdrom_id.rules +# Modifications by the Finit project: none +# +# do not edit this file, it will be overwritten on update + +ACTION=="remove", GOTO="cdrom_end" +SUBSYSTEM!="block", GOTO="cdrom_end" +KERNEL!="sr[0-9]*|vdisk*|xvd*", GOTO="cdrom_end" +ENV{DEVTYPE}!="disk", GOTO="cdrom_end" + +# unconditionally tag device as CDROM +KERNEL=="sr[0-9]*", ENV{ID_CDROM}="1" + +# stop automatically any mount units bound to the device if the media eject +# button is pressed. +ENV{ID_CDROM}=="1", ENV{SYSTEMD_MOUNT_DEVICE_BOUND}="1" + +# media eject button pressed +ENV{DISK_EJECT_REQUEST}=="?*", RUN+="cdrom_id --eject-media $devnode", GOTO="cdrom_end" + +# import device and media properties and lock tray to +# enable the receiving of media eject button events +IMPORT{program}="cdrom_id --lock-media $devnode" + +# ejecting a CD does not remove the device node, so mark the systemd device +# unit as inactive while there is no medium; this automatically cleans up of +# stale mounts after ejecting +ENV{DISK_MEDIA_CHANGE}=="?*", ENV{ID_CDROM_MEDIA}!="?*", ENV{SYSTEMD_READY}="0" + +KERNEL=="sr0", SYMLINK+="cdrom", OPTIONS+="link_priority=-100" + +LABEL="cdrom_end" diff --git a/keventd/rules.d/60-drm.rules b/keventd/rules.d/60-drm.rules new file mode 100644 index 00000000..f62c193b --- /dev/null +++ b/keventd/rules.d/60-drm.rules @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Derived from eudev 3.2.14 rules/60-drm.rules +# Modifications by the Finit project: none +# +# do not edit this file, it will be overwritten on update + +ACTION!="remove", SUBSYSTEM=="drm", SUBSYSTEMS=="pci|usb|platform", IMPORT{builtin}="path_id" + +# by-path +ENV{ID_PATH}=="?*", KERNEL=="card*", SYMLINK+="dri/by-path/$env{ID_PATH}-card" +ENV{ID_PATH}=="?*", KERNEL=="controlD*", SYMLINK+="dri/by-path/$env{ID_PATH}-control" +ENV{ID_PATH}=="?*", KERNEL=="renderD*", SYMLINK+="dri/by-path/$env{ID_PATH}-render" diff --git a/keventd/rules.d/60-evdev.rules b/keventd/rules.d/60-evdev.rules new file mode 100644 index 00000000..6c8cffaa --- /dev/null +++ b/keventd/rules.d/60-evdev.rules @@ -0,0 +1,29 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Derived from eudev 3.2.14 rules/60-evdev.rules +# Modifications by the Finit project: none +# +# do not edit this file, it will be overwritten on update + +ACTION=="remove", GOTO="evdev_end" +KERNEL!="event*", GOTO="evdev_end" + +# skip later rules when we find something for this input device +IMPORT{builtin}="hwdb --subsystem=input --lookup-prefix=evdev:", \ + RUN{builtin}+="keyboard", GOTO="evdev_end" + +# AT keyboard matching by the machine's DMI data +DRIVERS=="atkbd", \ + IMPORT{builtin}="hwdb 'evdev:atkbd:$attr{[dmi/id]modalias}'", \ + RUN{builtin}+="keyboard", GOTO="evdev_end" + +# device matching the input device name + properties + the machine's DMI data +KERNELS=="input*", \ + IMPORT{builtin}="hwdb 'evdev:name:$attr{name}:phys:$attr{phys}:ev:$attr{capabilities/ev}:$attr{[dmi/id]modalias}'", \ + RUN{builtin}+="keyboard", GOTO="evdev_end" + +# device matching the input device name and the machine's DMI data +KERNELS=="input*", \ + IMPORT{builtin}="hwdb 'evdev:name:$attr{name}:$attr{[dmi/id]modalias}'", \ + RUN{builtin}+="keyboard", GOTO="evdev_end" + +LABEL="evdev_end" diff --git a/keventd/rules.d/60-fido-id.rules b/keventd/rules.d/60-fido-id.rules new file mode 100644 index 00000000..da0e543e --- /dev/null +++ b/keventd/rules.d/60-fido-id.rules @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Derived from eudev 3.2.14 rules/60-fido-id.rules +# Modifications by the Finit project: none +# +# do not edit this file, it will be overwritten on update + +ACTION=="remove", GOTO="fido_id_end" + +SUBSYSTEM=="hidraw", IMPORT{program}="fido_id" + +# Tag any form of security token as such +ENV{ID_SECURITY_TOKEN}=="1", TAG+="security-device" + +SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ENV{ID_USB_INTERFACES}=="*:0b????:*", ENV{ID_SMARTCARD_READER}="1" +# Tag any CCID device (i.e. Smartcard Reader) as security token +ENV{ID_SMARTCARD_READER}=="1", TAG+="security-device" + +LABEL="fido_id_end" diff --git a/keventd/rules.d/60-input-id.rules b/keventd/rules.d/60-input-id.rules new file mode 100644 index 00000000..bbe64fe9 --- /dev/null +++ b/keventd/rules.d/60-input-id.rules @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Derived from eudev 3.2.14 rules/60-input-id.rules +# Modifications by the Finit project: none +# +# do not edit this file, it will be overwritten on update + +ACTION=="remove", GOTO="id_input_end" + +SUBSYSTEM=="input", ENV{ID_INPUT}=="", IMPORT{builtin}="input_id" +SUBSYSTEM=="input", IMPORT{builtin}="hwdb --subsystem=input --lookup-prefix=id-input:modalias:" + +LABEL="id_input_end" diff --git a/keventd/rules.d/60-persistent-alsa.rules b/keventd/rules.d/60-persistent-alsa.rules new file mode 100644 index 00000000..17c458df --- /dev/null +++ b/keventd/rules.d/60-persistent-alsa.rules @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Derived from eudev 3.2.14 rules/60-persistent-alsa.rules +# Modifications by the Finit project: none +# +# do not edit this file, it will be overwritten on update + +ACTION=="remove", GOTO="persistent_alsa_end" +SUBSYSTEM!="sound", GOTO="persistent_alsa_end" +KERNEL!="controlC[0-9]*", GOTO="persistent_alsa_end" + +SUBSYSTEMS=="usb", ENV{ID_MODEL}=="", IMPORT{builtin}="usb_id" +ENV{ID_SERIAL}=="?*", ENV{ID_USB_INTERFACE_NUM}=="?*", SYMLINK+="snd/by-id/$env{ID_BUS}-$env{ID_SERIAL}-$env{ID_USB_INTERFACE_NUM}" +ENV{ID_SERIAL}=="?*", ENV{ID_USB_INTERFACE_NUM}=="", SYMLINK+="snd/by-id/$env{ID_BUS}-$env{ID_SERIAL}" + +IMPORT{builtin}="path_id" +ENV{ID_PATH}=="?*", SYMLINK+="snd/by-path/$env{ID_PATH}" + +LABEL="persistent_alsa_end" diff --git a/keventd/rules.d/60-persistent-input.rules b/keventd/rules.d/60-persistent-input.rules new file mode 100644 index 00000000..9c2d9292 --- /dev/null +++ b/keventd/rules.d/60-persistent-input.rules @@ -0,0 +1,46 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Derived from eudev 3.2.14 rules/60-persistent-input.rules +# Modifications by the Finit project: none +# +# do not edit this file, it will be overwritten on update + +ACTION=="remove", GOTO="persistent_input_end" +SUBSYSTEM!="input", GOTO="persistent_input_end" +SUBSYSTEMS=="bluetooth", ENV{ID_BUS}="bluetooth", GOTO="persistent_input_end" +# Bluetooth devices don't always have the bluetooth subsystem +ATTRS{id/bustype}=="0005", ENV{ID_BUS}="bluetooth", GOTO="persistent_input_end" +SUBSYSTEMS=="rmi4", ENV{ID_BUS}="rmi" +SUBSYSTEMS=="serio", ENV{ID_BUS}="i8042" + +SUBSYSTEMS=="usb", ENV{ID_BUS}=="", IMPORT{builtin}="usb_id" + +# determine class name for persistent symlinks +ENV{ID_INPUT_KEYBOARD}=="?*", ENV{.INPUT_CLASS}="kbd" +ENV{ID_INPUT_MOUSE}=="?*", ENV{.INPUT_CLASS}="mouse" +ENV{ID_INPUT_TOUCHPAD}=="?*", ENV{.INPUT_CLASS}="mouse" +ENV{ID_INPUT_TABLET}=="?*", ENV{.INPUT_CLASS}="mouse" +ENV{ID_INPUT_JOYSTICK}=="?*", ENV{.INPUT_CLASS}="joystick" +DRIVERS=="pcspkr", ENV{.INPUT_CLASS}="spkr" +ATTRS{name}=="*dvb*|*DVB*|* IR *", ENV{.INPUT_CLASS}="ir" + +# fill empty serial number +ENV{.INPUT_CLASS}=="?*", ENV{ID_SERIAL}=="", ENV{ID_SERIAL}="noserial" + +# by-id links +KERNEL=="mouse*|js*", ENV{ID_BUS}=="?*", ENV{.INPUT_CLASS}=="?*", ATTRS{bInterfaceNumber}=="|00", SYMLINK+="input/by-id/$env{ID_BUS}-$env{ID_SERIAL}-$env{.INPUT_CLASS}" +KERNEL=="mouse*|js*", ENV{ID_BUS}=="?*", ENV{.INPUT_CLASS}=="?*", ATTRS{bInterfaceNumber}=="?*", ATTRS{bInterfaceNumber}!="00", SYMLINK+="input/by-id/$env{ID_BUS}-$env{ID_SERIAL}-if$attr{bInterfaceNumber}-$env{.INPUT_CLASS}" +KERNEL=="event*", ENV{ID_BUS}=="?*", ENV{.INPUT_CLASS}=="?*", ATTRS{bInterfaceNumber}=="|00", SYMLINK+="input/by-id/$env{ID_BUS}-$env{ID_SERIAL}-event-$env{.INPUT_CLASS}" +KERNEL=="event*", ENV{ID_BUS}=="?*", ENV{.INPUT_CLASS}=="?*", ATTRS{bInterfaceNumber}=="?*", ATTRS{bInterfaceNumber}!="00", SYMLINK+="input/by-id/$env{ID_BUS}-$env{ID_SERIAL}-if$attr{bInterfaceNumber}-event-$env{.INPUT_CLASS}" +# allow empty class for USB devices, by appending the interface number +SUBSYSTEMS=="usb", ENV{ID_BUS}=="?*", KERNEL=="event*", ENV{.INPUT_CLASS}=="", ATTRS{bInterfaceNumber}=="?*", \ + SYMLINK+="input/by-id/$env{ID_BUS}-$env{ID_SERIAL}-event-if$attr{bInterfaceNumber}" + +# by-path +SUBSYSTEMS=="pci|usb|platform|acpi", IMPORT{builtin}="path_id" +ENV{ID_PATH}=="?*", KERNEL=="mouse*|js*", ENV{.INPUT_CLASS}=="?*", SYMLINK+="input/by-path/$env{ID_PATH}-$env{.INPUT_CLASS}" +ENV{ID_PATH}=="?*", KERNEL=="event*", ENV{.INPUT_CLASS}=="?*", SYMLINK+="input/by-path/$env{ID_PATH}-event-$env{.INPUT_CLASS}" +# allow empty class for platform, usb and i2c devices; platform supports only a single interface that way +SUBSYSTEMS=="usb|platform|i2c", ENV{ID_PATH}=="?*", KERNEL=="event*", ENV{.INPUT_CLASS}=="", \ + SYMLINK+="input/by-path/$env{ID_PATH}-event" + +LABEL="persistent_input_end" diff --git a/keventd/rules.d/60-persistent-storage-tape.rules b/keventd/rules.d/60-persistent-storage-tape.rules new file mode 100644 index 00000000..5546c3f4 --- /dev/null +++ b/keventd/rules.d/60-persistent-storage-tape.rules @@ -0,0 +1,40 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Derived from eudev 3.2.14 rules/60-persistent-storage-tape.rules +# Modifications by the Finit project: none +# +# do not edit this file, it will be overwritten on update + +# persistent storage links: /dev/tape/{by-id,by-path} + +ACTION=="remove", GOTO="persistent_storage_tape_end" +ENV{UDEV_DISABLE_PERSISTENT_STORAGE_RULES_FLAG}=="1", GOTO="persistent_storage_tape_end" + +# type 8 devices are "Medium Changers" +SUBSYSTEM=="scsi_generic", SUBSYSTEMS=="scsi", ATTRS{type}=="8", IMPORT{program}="scsi_id --sg-version=3 --export --whitelisted -d $devnode", \ + SYMLINK+="tape/by-id/scsi-$env{ID_SERIAL} tape/by-id/scsi-$env{ID_SERIAL}-changer" + +# iSCSI devices from the same host have all the same ID_SERIAL, +# but additionally a property named ID_SCSI_SERIAL. +SUBSYSTEM=="scsi_generic", SUBSYSTEMS=="scsi", ATTRS{type}=="8", ENV{ID_SCSI_SERIAL}=="?*", \ + SYMLINK+="tape/by-id/scsi-$env{ID_SCSI_SERIAL}" + +SUBSYSTEM=="scsi_generic", SUBSYSTEMS=="scsi", ATTRS{type}=="8", IMPORT{builtin}="path_id", \ + SYMLINK+="tape/by-path/$env{ID_PATH}-changer" + +SUBSYSTEM!="scsi_tape", GOTO="persistent_storage_tape_end" + +KERNEL=="st*[0-9]|nst*[0-9]", ATTRS{ieee1394_id}=="?*", ENV{ID_SERIAL}="$attr{ieee1394_id}", ENV{ID_BUS}="ieee1394" +KERNEL=="st*[0-9]|nst*[0-9]", ENV{ID_SERIAL}!="?*", SUBSYSTEMS=="usb", IMPORT{builtin}="usb_id" +KERNEL=="st*[0-9]|nst*[0-9]", ENV{ID_SERIAL}!="?*", SUBSYSTEMS=="scsi", KERNELS=="[0-9]*:*[0-9]", ENV{.BSG_DEV}="$root/bsg/$id" +KERNEL=="st*[0-9]|nst*[0-9]", ENV{ID_SERIAL}!="?*", IMPORT{program}="scsi_id --whitelisted --export --device=$env{.BSG_DEV}", ENV{ID_BUS}="scsi" +KERNEL=="st*[0-9]", ENV{ID_SERIAL}=="?*", SYMLINK+="tape/by-id/$env{ID_BUS}-$env{ID_SERIAL}", OPTIONS+="link_priority=10" +KERNEL=="st*[0-9]", ENV{ID_SCSI_SERIAL}=="?*", SYMLINK+="tape/by-id/$env{ID_BUS}-$env{ID_SCSI_SERIAL}" +KERNEL=="nst*[0-9]", ENV{ID_SERIAL}=="?*", SYMLINK+="tape/by-id/$env{ID_BUS}-$env{ID_SERIAL}-nst" +KERNEL=="nst*[0-9]", ENV{ID_SCSI_SERIAL}=="?*", SYMLINK+="tape/by-id/$env{ID_BUS}-$env{ID_SCSI_SERIAL}-nst" + +# by-path (parent device path) +KERNEL=="st*[0-9]|nst*[0-9]", IMPORT{builtin}="path_id" +KERNEL=="st*[0-9]", ENV{ID_PATH}=="?*", SYMLINK+="tape/by-path/$env{ID_PATH}" +KERNEL=="nst*[0-9]", ENV{ID_PATH}=="?*", SYMLINK+="tape/by-path/$env{ID_PATH}-nst" + +LABEL="persistent_storage_tape_end" diff --git a/keventd/rules.d/60-persistent-storage.rules b/keventd/rules.d/60-persistent-storage.rules new file mode 100644 index 00000000..91199341 --- /dev/null +++ b/keventd/rules.d/60-persistent-storage.rules @@ -0,0 +1,133 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Derived from eudev 3.2.14 rules/60-persistent-storage.rules +# Modifications by the Finit project: none +# +# do not edit this file, it will be overwritten on update + +# persistent storage links: /dev/disk/{by-id,by-uuid,by-label,by-path} +# scheme based on "Linux persistent device names", 2004, Hannes Reinecke + +ACTION=="remove", GOTO="persistent_storage_end" +ENV{UDEV_DISABLE_PERSISTENT_STORAGE_RULES_FLAG}=="1", GOTO="persistent_storage_end" + +SUBSYSTEM!="block|ubi", GOTO="persistent_storage_end" +KERNEL!="loop*|mmcblk*[0-9]|msblk*[0-9]|mspblk*[0-9]|nvme*|sd*|sr*|vd*|xvd*|bcache*|cciss*|dasd*|ubd*|ubi*|scm*|pmem*|nbd*|zd*", GOTO="persistent_storage_end" + +# ignore partitions that span the entire disk +TEST=="whole_disk", GOTO="persistent_storage_end" + +# For partitions import parent disk ID_* information, except ID_FS_*. +# +# This is particularly important on media where a filesystem superblock and +# partition table are found on the same level, e.g. common Linux distro ISO +# installation media. +# +# In the case where a partition device points to the same filesystem that +# was detected on the parent disk, the ID_FS_* information is already +# present on the partition devices as well as the parent, so no need to +# propagate it. In the case where the partition device points to a different +# filesystem, merging the parent ID_FS_ properties would lead to +# inconsistencies, so we avoid doing so. +ENV{DEVTYPE}=="partition", \ + IMPORT{parent}="ID_[!F]*", IMPORT{parent}="ID_", \ + IMPORT{parent}="ID_F[!S]*", IMPORT{parent}="ID_F", \ + IMPORT{parent}="ID_FS[!_]*", IMPORT{parent}="ID_FS" + +# NVMe +KERNEL=="nvme*[0-9]n*[0-9]", ATTR{wwid}=="?*", SYMLINK+="disk/by-id/nvme-$attr{wwid}" +KERNEL=="nvme*[0-9]n*[0-9]p*[0-9]", ENV{DEVTYPE}=="partition", ATTRS{wwid}=="?*", SYMLINK+="disk/by-id/nvme-$attr{wwid}-part%n" + +KERNEL=="nvme*[0-9]n*[0-9]", ENV{DEVTYPE}=="disk", ATTRS{serial}=="?*", ENV{ID_SERIAL_SHORT}="$attr{serial}" +KERNEL=="nvme*[0-9]n*[0-9]", ENV{DEVTYPE}=="disk", ATTRS{wwid}=="?*", ENV{ID_WWN}="$attr{wwid}" +KERNEL=="nvme*[0-9]n*[0-9]", ENV{DEVTYPE}=="disk", ATTRS{model}=="?*", ENV{ID_MODEL}="$attr{model}" +KERNEL=="nvme*[0-9]n*[0-9]", ENV{DEVTYPE}=="disk", ATTRS{firmware_rev}=="?*", ENV{ID_REVISION}="$attr{firmware_rev}" +KERNEL=="nvme*[0-9]n*[0-9]", ENV{DEVTYPE}=="disk", ENV{ID_MODEL}=="?*", ENV{ID_SERIAL_SHORT}=="?*", \ + OPTIONS="string_escape=replace", ENV{ID_SERIAL}="$env{ID_MODEL}_$env{ID_SERIAL_SHORT}", SYMLINK+="disk/by-id/nvme-$env{ID_SERIAL}" + +KERNEL=="nvme*[0-9]n*[0-9]p*[0-9]", ENV{DEVTYPE}=="partition", ATTRS{serial}=="?*", ENV{ID_SERIAL_SHORT}="$attr{serial}" +KERNEL=="nvme*[0-9]n*[0-9]p*[0-9]", ENV{DEVTYPE}=="partition", ATTRS{model}=="?*", ENV{ID_MODEL}="$attr{model}" +KERNEL=="nvme*[0-9]n*[0-9]p*[0-9]", ENV{DEVTYPE}=="partition", ATTRS{firmware_rev}=="?*", ENV{ID_REVISION}="$attr{firmware_rev}" +KERNEL=="nvme*[0-9]n*[0-9]p*[0-9]", ENV{DEVTYPE}=="partition", ENV{ID_MODEL}=="?*", ENV{ID_SERIAL_SHORT}=="?*", \ + OPTIONS="string_escape=replace", ENV{ID_SERIAL}="$env{ID_MODEL}_$env{ID_SERIAL_SHORT}", SYMLINK+="disk/by-id/nvme-$env{ID_SERIAL}-part%n" + +# virtio-blk +KERNEL=="vd*[!0-9]", ATTRS{serial}=="?*", ENV{ID_SERIAL}="$attr{serial}", SYMLINK+="disk/by-id/virtio-$env{ID_SERIAL}" +KERNEL=="vd*[0-9]", ATTRS{serial}=="?*", ENV{ID_SERIAL}="$attr{serial}", SYMLINK+="disk/by-id/virtio-$env{ID_SERIAL}-part%n" + +# ATA +KERNEL=="sd*[!0-9]|sr*", ENV{ID_SERIAL}!="?*", SUBSYSTEMS=="scsi", ATTRS{vendor}=="ATA", IMPORT{program}="ata_id --export $devnode" + +# ATAPI devices (SPC-3 or later) +KERNEL=="sd*[!0-9]|sr*", ENV{ID_SERIAL}!="?*", SUBSYSTEMS=="scsi", ATTRS{type}=="5", ATTRS{scsi_level}=="[6-9]*", IMPORT{program}="ata_id --export $devnode" + +# Run ata_id on non-removable USB Mass Storage (SATA/PATA disks in enclosures) +KERNEL=="sd*[!0-9]|sr*", ENV{ID_SERIAL}!="?*", ATTR{removable}=="0", SUBSYSTEMS=="usb", IMPORT{program}="ata_id --export $devnode" + +# Fall back usb_id for USB devices +KERNEL=="sd*[!0-9]|sr*", ENV{ID_SERIAL}!="?*", SUBSYSTEMS=="usb", IMPORT{builtin}="usb_id" + +# SCSI devices +KERNEL=="sd*[!0-9]|sr*", ENV{ID_SERIAL}!="?*", IMPORT{program}="scsi_id --export --whitelisted -d $devnode", ENV{ID_BUS}="scsi" +KERNEL=="cciss*", ENV{DEVTYPE}=="disk", ENV{ID_SERIAL}!="?*", IMPORT{program}="scsi_id --export --whitelisted -d $devnode", ENV{ID_BUS}="cciss" +KERNEL=="sd*|sr*|cciss*", ENV{DEVTYPE}=="disk", ENV{ID_SERIAL}=="?*", SYMLINK+="disk/by-id/$env{ID_BUS}-$env{ID_SERIAL}" +KERNEL=="sd*|cciss*", ENV{DEVTYPE}=="partition", ENV{ID_SERIAL}=="?*", SYMLINK+="disk/by-id/$env{ID_BUS}-$env{ID_SERIAL}-part%n" + +# PMEM devices +KERNEL=="pmem*", ENV{DEVTYPE}=="disk", ATTRS{uuid}=="?*", SYMLINK+="disk/by-id/pmem-$attr{uuid}" + +# FireWire +KERNEL=="sd*[!0-9]|sr*", ATTRS{ieee1394_id}=="?*", SYMLINK+="disk/by-id/ieee1394-$attr{ieee1394_id}" +KERNEL=="sd*[0-9]", ATTRS{ieee1394_id}=="?*", SYMLINK+="disk/by-id/ieee1394-$attr{ieee1394_id}-part%n" + +# MMC +KERNEL=="mmcblk[0-9]", SUBSYSTEMS=="mmc", ATTRS{serial}=="?*", ENV{ID_SERIAL}="$attr{serial}" +KERNEL=="mmcblk[0-9]", SUBSYSTEMS=="mmc", ATTRS{name}=="?*", ENV{ID_NAME}="$attr{name}" +KERNEL=="mmcblk[0-9]", ENV{ID_NAME}=="?*", ENV{ID_SERIAL}=="?*", SYMLINK+="disk/by-id/mmc-$env{ID_NAME}_$env{ID_SERIAL}" +KERNEL=="mmcblk[0-9]p[0-9]*", ENV{ID_NAME}=="?*", ENV{ID_SERIAL}=="?*", SYMLINK+="disk/by-id/mmc-$env{ID_NAME}_$env{ID_SERIAL}-part%n" + +# Memstick +KERNEL=="msblk[0-9]|mspblk[0-9]", SUBSYSTEMS=="memstick", ATTRS{name}=="?*", ATTRS{serial}=="?*", \ + ENV{ID_NAME}="$attr{name}", ENV{ID_SERIAL}="$attr{serial}", SYMLINK+="disk/by-id/memstick-$env{ID_NAME}_$env{ID_SERIAL}" +KERNEL=="msblk[0-9]p[0-9]|mspblk[0-9]p[0-9]", ENV{ID_NAME}=="?*", ENV{ID_SERIAL}=="?*", SYMLINK+="disk/by-id/memstick-$env{ID_NAME}_$env{ID_SERIAL}-part%n" + +# by-path +ENV{DEVTYPE}=="disk", DEVPATH!="*/virtual/*", IMPORT{builtin}="path_id" +KERNEL=="mmcblk[0-9]boot[0-9]", ENV{DEVTYPE}=="disk", ENV{ID_PATH}=="?*", SYMLINK+="disk/by-path/$env{ID_PATH}-boot%n" +KERNEL!="mmcblk[0-9]boot[0-9]", ENV{DEVTYPE}=="disk", ENV{ID_PATH}=="?*", SYMLINK+="disk/by-path/$env{ID_PATH}" +ENV{DEVTYPE}=="partition", ENV{ID_PATH}=="?*", SYMLINK+="disk/by-path/$env{ID_PATH}-part%n" +# compatible links for ATA devices +KERNEL!="mmcblk[0-9]boot[0-9]", ENV{DEVTYPE}=="disk", ENV{ID_PATH_ATA_COMPAT}=="?*", SYMLINK+="disk/by-path/$env{ID_PATH_ATA_COMPAT}" +ENV{DEVTYPE}=="partition", ENV{ID_PATH_ATA_COMPAT}=="?*", SYMLINK+="disk/by-path/$env{ID_PATH_ATA_COMPAT}-part%n" + +# legacy virtio-pci by-path links (deprecated) +KERNEL=="vd*[!0-9]", ENV{ID_PATH}=="pci-*", SYMLINK+="disk/by-path/virtio-$env{ID_PATH}" +KERNEL=="vd*[0-9]", ENV{ID_PATH}=="pci-*", SYMLINK+="disk/by-path/virtio-$env{ID_PATH}-part%n" + +# probe filesystem metadata of optical drives which have a media inserted +KERNEL=="sr*", ENV{DISK_EJECT_REQUEST}!="?*", ENV{ID_CDROM_MEDIA_TRACK_COUNT_DATA}=="?*", ENV{ID_CDROM_MEDIA_SESSION_LAST_OFFSET}=="?*", \ + IMPORT{builtin}="blkid --hint=session_offset=$env{ID_CDROM_MEDIA_SESSION_LAST_OFFSET}" +# single-session CDs do not have ID_CDROM_MEDIA_SESSION_LAST_OFFSET +KERNEL=="sr*", ENV{DISK_EJECT_REQUEST}!="?*", ENV{ID_CDROM_MEDIA_TRACK_COUNT_DATA}=="?*", ENV{ID_CDROM_MEDIA_SESSION_LAST_OFFSET}=="", \ + IMPORT{builtin}="blkid --noraid" + +# probe filesystem metadata of disks +KERNEL!="sr*|mmcblk[0-9]boot[0-9]", IMPORT{builtin}="blkid" + +# by-label/by-uuid links (filesystem metadata) +ENV{ID_FS_USAGE}=="filesystem|other|crypto", ENV{ID_FS_UUID_ENC}=="?*", SYMLINK+="disk/by-uuid/$env{ID_FS_UUID_ENC}" +ENV{ID_FS_USAGE}=="filesystem|other|crypto", ENV{ID_FS_LABEL_ENC}=="?*", SYMLINK+="disk/by-label/$env{ID_FS_LABEL_ENC}" + +# by-id (World Wide Name) +ENV{DEVTYPE}=="disk", ENV{ID_WWN_WITH_EXTENSION}=="?*", SYMLINK+="disk/by-id/wwn-$env{ID_WWN_WITH_EXTENSION}" +ENV{DEVTYPE}=="partition", ENV{ID_WWN_WITH_EXTENSION}=="?*", SYMLINK+="disk/by-id/wwn-$env{ID_WWN_WITH_EXTENSION}-part%n" + +# by-partlabel/by-partuuid links (partition metadata) +ENV{ID_PART_ENTRY_UUID}=="?*", SYMLINK+="disk/by-partuuid/$env{ID_PART_ENTRY_UUID}" +ENV{ID_PART_ENTRY_SCHEME}=="gpt", ENV{ID_PART_ENTRY_NAME}=="?*", SYMLINK+="disk/by-partlabel/$env{ID_PART_ENTRY_NAME}" + +# by-diskseq link (if an app is told to open a path like this, they may parse +# the diskseq number from the path, then issue BLKGETDISKSEQ to verify they really got +# the right device, to access specific disks in a race-free fashion) +ENV{DISKSEQ}=="?*", ENV{DEVTYPE}!="partition", SYMLINK+="disk/by-diskseq/$env{DISKSEQ}" + +LABEL="persistent_storage_end" diff --git a/keventd/rules.d/60-persistent-v4l.rules b/keventd/rules.d/60-persistent-v4l.rules new file mode 100644 index 00000000..87d7030b --- /dev/null +++ b/keventd/rules.d/60-persistent-v4l.rules @@ -0,0 +1,24 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Derived from eudev 3.2.14 rules/60-persistent-v4l.rules +# Modifications by the Finit project: none +# +# do not edit this file, it will be overwritten on update + +ACTION=="remove", GOTO="persistent_v4l_end" +SUBSYSTEM!="video4linux", GOTO="persistent_v4l_end" +ENV{MAJOR}=="", GOTO="persistent_v4l_end" + +IMPORT{program}="v4l_id $devnode" + +SUBSYSTEMS=="usb", IMPORT{builtin}="usb_id" +KERNEL=="video*", ENV{ID_SERIAL}=="?*", SYMLINK+="v4l/by-id/$env{ID_BUS}-$env{ID_SERIAL}-video-index$attr{index}" + +# check for valid "index" number +TEST!="index", GOTO="persistent_v4l_end" +ATTR{index}!="?*", GOTO="persistent_v4l_end" + +IMPORT{builtin}="path_id" +ENV{ID_PATH}=="?*", KERNEL=="video*|vbi*", SYMLINK+="v4l/by-path/$env{ID_PATH}-video-index$attr{index}" +ENV{ID_PATH}=="?*", KERNEL=="audio*", SYMLINK+="v4l/by-path/$env{ID_PATH}-audio-index$attr{index}" + +LABEL="persistent_v4l_end" diff --git a/keventd/rules.d/60-sensor.rules b/keventd/rules.d/60-sensor.rules new file mode 100644 index 00000000..97564cf2 --- /dev/null +++ b/keventd/rules.d/60-sensor.rules @@ -0,0 +1,38 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Derived from eudev 3.2.14 rules/60-sensor.rules +# Modifications by the Finit project: none +# +# do not edit this file, it will be overwritten on update + +ACTION=="remove", GOTO="sensor_end" + +# device matching the sensor's label, name and the machine's DMI data for IIO devices +SUBSYSTEM=="iio", KERNEL=="iio*", SUBSYSTEMS=="usb|i2c|platform", ATTR{label}!="", \ + IMPORT{builtin}="hwdb 'sensor:$attr{label}:modalias:$attr{modalias}:$attr{[dmi/id]modalias}'", \ + GOTO="sensor_end" + +# Before Linux v6.0, cros-ec-accel used a non-standard 'location' sysfs file +SUBSYSTEM=="iio", KERNEL=="iio*", SUBSYSTEMS=="platform", \ + ATTR{name}=="cros-ec-accel|cros-ec-accel-legacy", ATTR{location}=="base", \ + IMPORT{builtin}="hwdb 'sensor:accel-base:modalias:$attr{modalias}:$attr{[dmi/id]modalias}'", \ + GOTO="sensor_end" + +SUBSYSTEM=="iio", KERNEL=="iio*", SUBSYSTEMS=="platform", \ + ATTR{name}=="cros-ec-accel|cros-ec-accel-legacy", ATTR{location}=="lid", \ + IMPORT{builtin}="hwdb 'sensor:accel-display:modalias:$attr{modalias}:$attr{[dmi/id]modalias}'", \ + GOTO="sensor_end" + +# device matching the sensor's name and the machine's DMI data for IIO devices +SUBSYSTEM=="iio", KERNEL=="iio*", SUBSYSTEMS=="usb|i2c|platform", \ + IMPORT{builtin}="hwdb 'sensor:modalias:$attr{modalias}:$attr{[dmi/id]modalias}'", \ + GOTO="sensor_end" + +SUBSYSTEM=="input", ENV{ID_INPUT_ACCELEROMETER}=="1", SUBSYSTEMS=="acpi", \ + IMPORT{builtin}="hwdb 'sensor:modalias:acpi:$attr{hid}:$attr{[dmi/id]modalias}'", \ + GOTO="sensor_end" + +SUBSYSTEM=="input", ENV{ID_INPUT_ACCELEROMETER}=="1", SUBSYSTEMS=="platform", \ + IMPORT{builtin}="hwdb 'sensor:modalias:platform:$id:$attr{[dmi/id]modalias}'", \ + GOTO="sensor_end" + +LABEL="sensor_end" diff --git a/keventd/rules.d/60-serial.rules b/keventd/rules.d/60-serial.rules new file mode 100644 index 00000000..26c37fc1 --- /dev/null +++ b/keventd/rules.d/60-serial.rules @@ -0,0 +1,30 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Derived from eudev 3.2.14 rules/60-serial.rules +# Modifications by the Finit project: none +# +# do not edit this file, it will be overwritten on update + +ACTION=="remove", GOTO="serial_end" +SUBSYSTEM!="tty", GOTO="serial_end" + +SUBSYSTEMS=="pci", ENV{ID_BUS}="pci", ENV{ID_VENDOR_ID}="$attr{vendor}", ENV{ID_MODEL_ID}="$attr{device}" +SUBSYSTEMS=="pci", IMPORT{builtin}="hwdb --subsystem=pci" +SUBSYSTEMS=="usb", IMPORT{builtin}="usb_id", IMPORT{builtin}="hwdb --subsystem=usb" + +# /dev/serial/by-path/, /dev/serial/by-id/ for USB devices +KERNEL!="ttyUSB[0-9]*|ttyACM[0-9]*", GOTO="serial_end" + +SUBSYSTEMS=="usb-serial", ENV{.ID_PORT}="$attr{port_number}" + +IMPORT{builtin}="path_id" +ENV{ID_PATH}=="?*", ENV{.ID_PORT}=="", SYMLINK+="serial/by-path/$env{ID_PATH}" +ENV{ID_PATH}=="?*", ENV{.ID_PORT}=="?*", SYMLINK+="serial/by-path/$env{ID_PATH}-port$env{.ID_PORT}" + +IMPORT{builtin}="usb_id" +ENV{ID_SERIAL}=="", GOTO="serial_end" +SUBSYSTEMS=="usb", ENV{ID_USB_INTERFACE_NUM}="$attr{bInterfaceNumber}" +ENV{ID_USB_INTERFACE_NUM}=="", GOTO="serial_end" +ENV{.ID_PORT}=="", SYMLINK+="serial/by-id/$env{ID_BUS}-$env{ID_SERIAL}-if$env{ID_USB_INTERFACE_NUM}" +ENV{.ID_PORT}=="?*", SYMLINK+="serial/by-id/$env{ID_BUS}-$env{ID_SERIAL}-if$env{ID_USB_INTERFACE_NUM}-port$env{.ID_PORT}" + +LABEL="serial_end" diff --git a/keventd/rules.d/64-btrfs.rules b/keventd/rules.d/64-btrfs.rules new file mode 100644 index 00000000..a7a03ef9 --- /dev/null +++ b/keventd/rules.d/64-btrfs.rules @@ -0,0 +1,21 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Derived from eudev 3.2.14 rules/64-btrfs.rules +# Modifications by the Finit project: none +# +# do not edit this file, it will be overwritten on update + +SUBSYSTEM!="block", GOTO="btrfs_end" +ACTION=="remove", GOTO="btrfs_end" +ENV{ID_FS_TYPE}!="btrfs", GOTO="btrfs_end" +ENV{SYSTEMD_READY}=="0", GOTO="btrfs_end" + +# let the kernel know about this btrfs filesystem, and check if it is complete +IMPORT{builtin}="btrfs ready $devnode" + +# mark the device as not ready to be used by the system +ENV{ID_BTRFS_READY}=="0", ENV{SYSTEMD_READY}="0" + +# reconsider pending devices in case when multidevice volume awaits +ENV{ID_BTRFS_READY}=="1", RUN+="/usr/bin/udevadm trigger -s block -p ID_BTRFS_READY=0" + +LABEL="btrfs_end" diff --git a/keventd/rules.d/70-camera.rules b/keventd/rules.d/70-camera.rules new file mode 100644 index 00000000..9e5fd5a3 --- /dev/null +++ b/keventd/rules.d/70-camera.rules @@ -0,0 +1,13 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Derived from eudev 3.2.14 rules/70-camera.rules +# Modifications by the Finit project: none +# +# do not edit this file, it will be overwritten on update + +ACTION=="remove", GOTO="camera_end" + +SUBSYSTEM=="video4linux", ENV{ID_BUS}="usb" , \ + IMPORT{builtin}="hwdb 'camera:usb:v$env{ID_VENDOR_ID}p$env{ID_MODEL_ID}:name:$attr{name}:'", \ + GOTO="camera_end" + +LABEL="camera_end" diff --git a/keventd/rules.d/70-joystick.rules b/keventd/rules.d/70-joystick.rules new file mode 100644 index 00000000..f9129d56 --- /dev/null +++ b/keventd/rules.d/70-joystick.rules @@ -0,0 +1,16 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Derived from eudev 3.2.14 rules/70-joystick.rules +# Modifications by the Finit project: none +# +# do not edit this file, it will be overwritten on update + +ACTION=="remove", GOTO="joystick_end" +ENV{ID_INPUT_JOYSTICK}=="", GOTO="joystick_end" +KERNEL!="event*", GOTO="joystick_end" + +# joystick::vp:name::* +KERNELS=="input*", ENV{ID_BUS}!="", \ + IMPORT{builtin}="hwdb 'joystick:$env{ID_BUS}:v$attr{id/vendor}p$attr{id/product}:name:$attr{name}:'", \ + GOTO="joystick_end" + +LABEL="joystick_end" diff --git a/keventd/rules.d/70-memory.rules b/keventd/rules.d/70-memory.rules new file mode 100644 index 00000000..11382e06 --- /dev/null +++ b/keventd/rules.d/70-memory.rules @@ -0,0 +1,12 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Derived from eudev 3.2.14 rules/70-memory.rules +# Modifications by the Finit project: none +# +# do not edit this file, it will be overwritten on update + +ACTION=="remove", GOTO="memory_end" +SUBSYSTEM!="dmi", GOTO="memory_end" + +IMPORT{program}="dmi_memory_id" + +LABEL="memory_end" diff --git a/keventd/rules.d/70-mouse.rules b/keventd/rules.d/70-mouse.rules new file mode 100644 index 00000000..97da3951 --- /dev/null +++ b/keventd/rules.d/70-mouse.rules @@ -0,0 +1,22 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Derived from eudev 3.2.14 rules/70-mouse.rules +# Modifications by the Finit project: none +# +# do not edit this file, it will be overwritten on update + +ACTION=="remove", GOTO="mouse_end" +KERNEL!="event*", GOTO="mouse_end" +ENV{ID_INPUT_MOUSE}=="", GOTO="mouse_end" + +# mouse::vp:name::* +KERNELS=="input*", ENV{ID_BUS}=="usb", \ + IMPORT{builtin}="hwdb 'mouse:$env{ID_BUS}:v$attr{id/vendor}p$attr{id/product}:name:$attr{name}:'", \ + GOTO="mouse_end" +KERNELS=="input*", ENV{ID_BUS}=="bluetooth", \ + IMPORT{builtin}="hwdb 'mouse:$env{ID_BUS}:v$attr{id/vendor}p$attr{id/product}:name:$attr{name}:'", \ + GOTO="mouse_end" +DRIVERS=="psmouse", SUBSYSTEMS=="serio", \ + IMPORT{builtin}="hwdb 'mouse:ps2::name:$attr{device/name}:'", \ + GOTO="mouse_end" + +LABEL="mouse_end" diff --git a/keventd/rules.d/70-touchpad.rules b/keventd/rules.d/70-touchpad.rules new file mode 100644 index 00000000..a0da2a06 --- /dev/null +++ b/keventd/rules.d/70-touchpad.rules @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Derived from eudev 3.2.14 rules/70-touchpad.rules +# Modifications by the Finit project: none +# +# do not edit this file, it will be overwritten on update + +ACTION=="remove", GOTO="touchpad_end" +ENV{ID_INPUT}=="", GOTO="touchpad_end" +ENV{ID_INPUT_TOUCHPAD}=="", GOTO="touchpad_end" +KERNEL!="event*", GOTO="touchpad_end" + +# touchpad::vp:name::* +KERNELS=="input*", ENV{ID_BUS}!="", \ + IMPORT{builtin}="hwdb 'touchpad:$env{ID_BUS}:v$attr{id/vendor}p$attr{id/product}:name:$attr{name}:'", \ + GOTO="touchpad_end" + +LABEL="touchpad_end" diff --git a/keventd/rules.d/75-net-description.rules b/keventd/rules.d/75-net-description.rules new file mode 100644 index 00000000..72e28d6d --- /dev/null +++ b/keventd/rules.d/75-net-description.rules @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Derived from eudev 3.2.14 rules/75-net-description.rules +# Modifications by the Finit project: none +# +# do not edit this file, it will be overwritten on update + +ACTION=="remove", GOTO="net_end" +SUBSYSTEM!="net", GOTO="net_end" + +IMPORT{builtin}="net_id" + +SUBSYSTEMS=="usb", IMPORT{builtin}="usb_id", IMPORT{builtin}="hwdb --subsystem=usb" +SUBSYSTEMS=="usb", GOTO="net_end" + +SUBSYSTEMS=="pci", ENV{ID_BUS}="pci", ENV{ID_VENDOR_ID}="$attr{vendor}", ENV{ID_MODEL_ID}="$attr{device}" +SUBSYSTEMS=="pci", IMPORT{builtin}="hwdb --subsystem=pci" + +LABEL="net_end" diff --git a/keventd/rules.d/75-probe_mtd.rules b/keventd/rules.d/75-probe_mtd.rules new file mode 100644 index 00000000..c4a159c2 --- /dev/null +++ b/keventd/rules.d/75-probe_mtd.rules @@ -0,0 +1,11 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Derived from eudev 3.2.14 rules/75-probe_mtd.rules +# Modifications by the Finit project: none +# +# do not edit this file, it will be overwritten on update + +ACTION!="add", GOTO="mtd_probe_end" + +KERNEL=="mtd*ro", IMPORT{program}="mtd_probe $devnode" + +LABEL="mtd_probe_end" diff --git a/keventd/rules.d/78-sound-card.rules b/keventd/rules.d/78-sound-card.rules new file mode 100644 index 00000000..c2ac54f2 --- /dev/null +++ b/keventd/rules.d/78-sound-card.rules @@ -0,0 +1,100 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Derived from eudev 3.2.14 rules/78-sound-card.rules +# Modifications by the Finit project: none +# +# do not edit this file, it will be overwritten on update + +SUBSYSTEM!="sound", GOTO="sound_end" + +ACTION=="add|change", KERNEL=="controlC*", ATTR{../uevent}="change" +ACTION!="change", GOTO="sound_end" + +# Ok, we probably need a little explanation here for what the two lines above +# are good for. +# +# The story goes like this: when ALSA registers a new sound card it emits a +# series of 'add' events to userspace, for the main card device and for all the +# child device nodes that belong to it. udev relays those to applications, +# however only maintains the order between father and child, but not between +# the siblings. The control device node creation can be used as synchronization +# point. All other devices that belong to a card are created in the kernel +# before it. However unfortunately due to the fact that siblings are forwarded +# out of order by udev this fact is lost to applications. +# +# OTOH before an application can open a device it needs to make sure that all +# its device nodes are completely created and set up. +# +# As a workaround for this issue we have added the udev rule above which will +# generate a 'change' event on the main card device from the 'add' event of the +# card's control device. Due to the ordering semantics of udev this event will +# only be relayed after all child devices have finished processing properly. +# When an application needs to listen for appearing devices it can hence look +# for 'change' events only, and ignore the actual 'add' events. +# +# When the application is initialized at the same time as a device is plugged +# in it may need to figure out if the 'change' event has already been triggered +# or not for a card. To find that out we store the flag environment variable +# SOUND_INITIALIZED on the device which simply tells us if the card 'change' +# event has already been processed. + +KERNEL!="card*", GOTO="sound_end" + +ENV{SOUND_INITIALIZED}="1" + +IMPORT{builtin}="hwdb" +SUBSYSTEMS=="usb", IMPORT{builtin}="usb_id" +SUBSYSTEMS=="usb", GOTO="skip_pci" + +SUBSYSTEMS=="firewire", ATTRS{guid}=="?*", \ + ENV{ID_BUS}="firewire", ENV{ID_SERIAL}="$attr{guid}", ENV{ID_SERIAL_SHORT}="$attr{guid}", \ + ENV{ID_VENDOR_ID}="$attr{vendor}", ENV{ID_MODEL_ID}="$attr{model}", \ + ENV{ID_VENDOR}="$attr{vendor_name}", ENV{ID_MODEL}="$attr{model_name}" +SUBSYSTEMS=="firewire", GOTO="skip_pci" + +SUBSYSTEMS=="pci", ENV{ID_BUS}="pci", ENV{ID_VENDOR_ID}="$attr{vendor}", ENV{ID_MODEL_ID}="$attr{device}" +SUBSYSTEMS=="pci", GOTO="skip_pci" + +# If we reach here, the device nor any of its parents are USB/PCI/firewire bus devices. +# If we now find a parent that is a platform device, assume that we're working with +# an internal sound card. +SUBSYSTEMS=="platform", ENV{SOUND_FORM_FACTOR}="internal", GOTO="sound_end" + +LABEL="skip_pci" + +# Define ID_ID if ID_BUS and ID_SERIAL are set. This will work for both +# USB and firewire. +ENV{ID_SERIAL}=="?*", ENV{ID_USB_INTERFACE_NUM}=="?*", ENV{ID_ID}="$env{ID_BUS}-$env{ID_SERIAL}-$env{ID_USB_INTERFACE_NUM}" +ENV{ID_SERIAL}=="?*", ENV{ID_USB_INTERFACE_NUM}=="", ENV{ID_ID}="$env{ID_BUS}-$env{ID_SERIAL}" + +IMPORT{builtin}="path_id" + +# The values used here for $SOUND_FORM_FACTOR and $SOUND_CLASS should be kept +# in sync with those defined for PulseAudio's src/pulse/proplist.h +# PA_PROP_DEVICE_FORM_FACTOR, PA_PROP_DEVICE_CLASS properties. + +# If the first PCM device of this card has the pcm class 'modem', then the card is a modem +ATTR{pcmC%nD0p/pcm_class}=="modem", ENV{SOUND_CLASS}="modem", GOTO="sound_end" + +# Identify cards on the internal PCI bus as internal +SUBSYSTEMS=="pci", DEVPATH=="*/0000:00:??.?/sound/*", ENV{SOUND_FORM_FACTOR}="internal", GOTO="sound_end" + +# Devices that also support Image/Video interfaces are most likely webcams +SUBSYSTEMS=="usb", ENV{ID_USB_INTERFACES}=="*:0e????:*", ENV{SOUND_FORM_FACTOR}="webcam", GOTO="sound_end" + +# Matching on the model strings is a bit ugly, I admit +ENV{ID_MODEL}=="*[Ss]peaker*", ENV{SOUND_FORM_FACTOR}="speaker", GOTO="sound_end" +ENV{ID_MODEL_FROM_DATABASE}=="*[Ss]peaker*", ENV{SOUND_FORM_FACTOR}="speaker", GOTO="sound_end" + +ENV{ID_MODEL}=="*[Hh]eadphone*", ENV{SOUND_FORM_FACTOR}="headphone", GOTO="sound_end" +ENV{ID_MODEL_FROM_DATABASE}=="*[Hh]eadphone*", ENV{SOUND_FORM_FACTOR}="headphone", GOTO="sound_end" + +ENV{ID_MODEL}=="*[Hh]eadset*", ENV{SOUND_FORM_FACTOR}="headset", GOTO="sound_end" +ENV{ID_MODEL_FROM_DATABASE}=="*[Hh]eadset*", ENV{SOUND_FORM_FACTOR}="headset", GOTO="sound_end" + +ENV{ID_MODEL}=="*[Hh]andset*", ENV{SOUND_FORM_FACTOR}="handset", GOTO="sound_end" +ENV{ID_MODEL_FROM_DATABASE}=="*[Hh]andset*", ENV{SOUND_FORM_FACTOR}="handset", GOTO="sound_end" + +ENV{ID_MODEL}=="*[Mm]icrophone*", ENV{SOUND_FORM_FACTOR}="microphone", GOTO="sound_end" +ENV{ID_MODEL_FROM_DATABASE}=="*[Mm]icrophone*", ENV{SOUND_FORM_FACTOR}="microphone", GOTO="sound_end" + +LABEL="sound_end" diff --git a/keventd/rules.d/80-drivers.rules b/keventd/rules.d/80-drivers.rules new file mode 100644 index 00000000..28140a23 --- /dev/null +++ b/keventd/rules.d/80-drivers.rules @@ -0,0 +1,17 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Derived from eudev 3.2.14 rules/80-drivers.rules +# Modifications by the Finit project: none +# +# do not edit this file, it will be overwritten on update + +ACTION!="add", GOTO="drivers_end" + +ENV{MODALIAS}=="?*", RUN{builtin}+="kmod load '$env{MODALIAS}'" +SUBSYSTEM=="tifm", ENV{TIFM_CARD_TYPE}=="SD", RUN{builtin}+="kmod load tifm_sd" +SUBSYSTEM=="tifm", ENV{TIFM_CARD_TYPE}=="MS", RUN{builtin}+="kmod load tifm_ms" +SUBSYSTEM=="memstick", RUN{builtin}+="kmod load ms_block mspro_block" +SUBSYSTEM=="i2o", RUN{builtin}+="kmod load i2o_block" +SUBSYSTEM=="module", KERNEL=="parport_pc", RUN{builtin}+="kmod load ppdev" +KERNEL=="mtd*ro", ENV{MTD_FTL}=="smartmedia", RUN{builtin}+="kmod load sm_ftl" + +LABEL="drivers_end" diff --git a/keventd/rules.d/80-net-name-slot.rules b/keventd/rules.d/80-net-name-slot.rules new file mode 100644 index 00000000..8eb4cf8c --- /dev/null +++ b/keventd/rules.d/80-net-name-slot.rules @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Derived from eudev 3.2.14 rules/80-net-name-slot.rules +# Modifications by the Finit project: none +# +# do not edit this file, it will be overwritten on update + +ACTION!="add", GOTO="net_name_slot_end" +SUBSYSTEM!="net", GOTO="net_name_slot_end" +NAME!="", GOTO="net_name_slot_end" + +IMPORT{cmdline}="net.ifnames" +ENV{net.ifnames}=="0", GOTO="net_name_slot_end" + +NAME=="", ENV{ID_NET_NAME_ONBOARD}!="", NAME="$env{ID_NET_NAME_ONBOARD}" +NAME=="", ENV{ID_NET_NAME_SLOT}!="", NAME="$env{ID_NET_NAME_SLOT}" +NAME=="", ENV{ID_NET_NAME_PATH}!="", NAME="$env{ID_NET_NAME_PATH}" + +LABEL="net_name_slot_end" diff --git a/keventd/rules.d/81-net-dhcp.rules b/keventd/rules.d/81-net-dhcp.rules new file mode 100644 index 00000000..15939c66 --- /dev/null +++ b/keventd/rules.d/81-net-dhcp.rules @@ -0,0 +1,18 @@ +# SPDX-License-Identifier: GPL-2.0-or-later +# Derived from eudev 3.2.14 rules/81-net-dhcp.rules +# Modifications by the Finit project: none +# +# do not edit this file, it will be overwritten on update + +ACTION=="remove", GOTO="net_dhcp_end" +SUBSYSTEM!="net", GOTO="net_dhcp_end" + +# Network interfaces requiring DHCPOFFER messages to be broadcast +# must set ID_NET_DHCP_BROADCAST to "1". This property will be +# checked by the networkd DHCP4 client to set the DHCP option + +# s390 ccwgroup interfaces in layer3 mode need broadcast DHCPOFFER +# using the link driver to detect this condition +ENV{ID_NET_DRIVER}=="qeth_l3", ENV{ID_NET_DHCP_BROADCAST}="1" + +LABEL="net_dhcp_end" From f8c584259f67693a142359fddfb799f9ef4f6f2c Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 19 May 2026 07:29:31 +0200 Subject: [PATCH 14/24] .github: install libblkid-dev for keventd build Signed-off-by: Joachim Wiberg --- .github/workflows/build.yml | 2 +- .github/workflows/coverity.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/weekly.yml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cec7aeab..eee780ae 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,7 +34,7 @@ jobs: - name: Install dependencies run: | sudo apt-get -y update - sudo apt-get -y install pkg-config tree jq libcap-dev libconfuse-dev + sudo apt-get -y install pkg-config tree jq libcap-dev libconfuse-dev libblkid-dev wget https://github.com/troglobit/libuev/releases/download/v2.4.1/libuev-2.4.1.tar.xz wget https://github.com/troglobit/libite/releases/download/v2.6.2/libite-2.6.2.tar.gz tar xf libuev-2.4.1.tar.xz diff --git a/.github/workflows/coverity.yml b/.github/workflows/coverity.yml index f9b51781..7d977459 100644 --- a/.github/workflows/coverity.yml +++ b/.github/workflows/coverity.yml @@ -55,7 +55,7 @@ jobs: - name: Install dependencies run: | sudo apt-get -y update - sudo apt-get -y install pkg-config libcap-dev libconfuse-dev + sudo apt-get -y install pkg-config libcap-dev libconfuse-dev libblkid-dev wget https://github.com/troglobit/libuev/releases/download/v2.4.1/libuev-2.4.1.tar.xz wget https://github.com/troglobit/libite/releases/download/v2.6.2/libite-2.6.2.tar.gz tar xf libuev-2.4.1.tar.xz diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d0bfcbd4..3ddbb383 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -18,7 +18,7 @@ jobs: - name: Installing dependencies ... run: | sudo apt-get -y update - sudo apt-get -y install pkg-config jq libcap-dev libconfuse-dev + sudo apt-get -y install pkg-config jq libcap-dev libconfuse-dev libblkid-dev wget https://github.com/troglobit/libuev/releases/download/v2.4.1/libuev-2.4.1.tar.xz wget https://github.com/troglobit/libite/releases/download/v2.6.2/libite-2.6.2.tar.gz tar xf libuev-2.4.1.tar.xz diff --git a/.github/workflows/weekly.yml b/.github/workflows/weekly.yml index d2d8cbe4..2e532a55 100644 --- a/.github/workflows/weekly.yml +++ b/.github/workflows/weekly.yml @@ -34,7 +34,7 @@ jobs: - name: Install dependencies run: | sudo apt-get -y update - sudo apt-get -y install pkg-config jq libcap-dev libconfuse-dev + sudo apt-get -y install pkg-config jq libcap-dev libconfuse-dev libblkid-dev wget https://github.com/troglobit/libuev/releases/download/v2.4.1/libuev-2.4.1.tar.xz wget https://github.com/troglobit/libite/releases/download/v2.6.2/libite-2.6.2.tar.gz tar xf libuev-2.4.1.tar.xz From b16ab84b4b4d718ed8213d19b017f6958ad569d5 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sat, 30 May 2026 16:14:12 +0200 Subject: [PATCH 15/24] keventd: convert main loop to libuev Replace the raw poll() + global running/reload_rules flag dance with libuev. All four signals (SIGUSR1, SIGTERM, SIGHUP, SIGCHLD) are now handled via uev_signal_init() -- libuev uses signalfd internally so the handlers run in normal main-loop context, not async signal context. The SIGCHLD handler waitpid(-1, WNOHANG) reaps any subprocess (modprobe loads, rule RUN+= helpers) instead of the old SIG_IGN auto-reap trick. Synchronous waitpid() in modprobe_load() and run_program() still works because signalfd queues the signal -- it only reaches sigchld_cb on the next main-loop iteration. The netlink receive loop becomes uevent_cb() registered via uev_io_init(), with the receive buffer passed via the watcher's arg slot (avoids file-static state). ENOBUFS and EINTR/EAGAIN are still tolerated, any other recv() error still panic()s. Signed-off-by: Joachim Wiberg --- keventd/Makefile.am | 2 + keventd/keventd.c | 163 +++++++++++++++++++++++--------------------- 2 files changed, 88 insertions(+), 77 deletions(-) diff --git a/keventd/Makefile.am b/keventd/Makefile.am index 0efbb4ad..46f2c70f 100644 --- a/keventd/Makefile.am +++ b/keventd/Makefile.am @@ -15,8 +15,10 @@ keventd_CFLAGS = -W -Wall -Wextra -std=gnu99 # handled via explicit checks where present. Matches systemd-udev policy. keventd_CFLAGS += -Wno-format-truncation keventd_CFLAGS += $(lite_CFLAGS) +keventd_CFLAGS += $(uev_CFLAGS) keventd_CFLAGS += $(blkid_CFLAGS) keventd_LDADD = $(lite_LIBS) +keventd_LDADD += $(uev_LIBS) keventd_LDADD += $(blkid_LIBS) # Curated udev rules derived from eudev 3.2.14 (GPL-2.0-or-later). diff --git a/keventd/keventd.c b/keventd/keventd.c index bfb3001f..e3317658 100644 --- a/keventd/keventd.c +++ b/keventd/keventd.c @@ -38,7 +38,6 @@ #include #include #include -#include #include #include #include @@ -50,6 +49,7 @@ #include #include #include +#include #include #include @@ -60,6 +60,8 @@ # include #endif +#include + #include "keventd.h" #include "rules.h" #include "udevdb.h" @@ -76,14 +78,12 @@ static int num_ac_online; static int num_ac; -static int running = 1; 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 */ -static volatile sig_atomic_t reload_rules; int debug; /* debug in other modules as well */ @@ -449,24 +449,79 @@ static void set_logging(int prio) level = prio; } -static void toggle_debug(int signo) +static void sigusr1_cb(uev_t *w, void *arg, int events) { - (void)signo; - + (void)w; (void)arg; (void)events; debug ^= 1; set_logging(debug ? LOG_DEBUG : LOG_NOTICE); } -static void shut_down(int signo) +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)signo; - running = 0; + (void)w; (void)arg; (void)events; + rules_free(&rules); + rules_load_all(&rules, rules_dir); +} + +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 sighup_handler(int signo) +static void uevent_cb(uev_t *w, void *arg, int events) { - (void)signo; - reload_rules = 1; + 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); } static int usage(int rc) @@ -503,12 +558,15 @@ static int usage(int rc) */ int main(int argc, char *argv[]) { + static char uevent_buf[UEVENT_BUFFER_SIZE]; unsigned int nlgroups = REBC_DEFAULT_NLGROUP; struct sockaddr_nl nls = { 0 }; - char buf[UEVENT_BUFFER_SIZE]; + uev_t sigusr1_w, sigterm_w, sighup_w, sigchld_w; + uev_t netlink_w; + uev_ctx_t ctx; int do_coldplug = 0; int foreground = 0; - struct pollfd pfd; + int nlfd; int c; /* Device nodes are created with explicit MODE= from rules or the @@ -558,10 +616,12 @@ int main(int argc, char *argv[]) set_logging(debug ? LOG_DEBUG : LOG_NOTICE); } - signal(SIGUSR1, toggle_debug); - signal(SIGTERM, shut_down); - signal(SIGHUP, sighup_handler); - signal(SIGCHLD, SIG_IGN); /* Don't wait for modprobe children */ + uev_init(&ctx); + + uev_signal_init(&ctx, &sigusr1_w, sigusr1_cb, NULL, SIGUSR1); + uev_signal_init(&ctx, &sigterm_w, sigterm_cb, NULL, SIGTERM); + uev_signal_init(&ctx, &sighup_w, sighup_cb, NULL, SIGHUP); + uev_signal_init(&ctx, &sigchld_w, sigchld_cb, NULL, SIGCHLD); /* Initialize condition directories */ init_power_supply(); @@ -573,23 +633,24 @@ int main(int argc, char *argv[]) disable_uevent_helper(); /* Set up netlink socket for kernel uevents */ - pfd.events = POLLIN; - pfd.fd = socket(PF_NETLINK, SOCK_DGRAM | SOCK_CLOEXEC, NETLINK_KOBJECT_UEVENT); - if (pfd.fd == -1) + 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(pfd.fd, (void *)&nls, sizeof(struct sockaddr_nl))) + if (bind(nlfd, (void *)&nls, sizeof(struct sockaddr_nl))) panic("bind failed"); /* Increase receive buffer to reduce event loss */ { int rcvbuf = 1024 * 1024; - setsockopt(pfd.fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf)); + setsockopt(nlfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf)); } + uev_io_init(&ctx, &netlink_w, 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) @@ -606,63 +667,11 @@ int main(int argc, char *argv[]) pidfile(NULL); logit(LOG_NOTICE, "keventd v%s started, waiting for events...", KEVENTD_VERSION); - while (running) { - char rebc_buf[UEVENT_BUFFER_SIZE]; - int len; - - /* Reload rules if SIGHUP was received */ - if (reload_rules) { - reload_rules = 0; - rules_free(&rules); - rules_load_all(&rules, rules_dir); - } - - if (-1 == poll(&pfd, 1, -1)) { - if (errno == EINTR) - continue; - break; - } - - len = recv(pfd.fd, buf, sizeof(buf) - 1, MSG_DONTWAIT); - if (len == -1) { - switch (errno) { - case EINTR: - continue; - case ENOBUFS: - warn("lost events, buffer overflow"); - continue; - default: - panic("recv failed"); - continue; - } - } - buf[len] = 0; - - /* Skip libudev events (start with "libudev") */ - if (!strncmp(buf, "libudev", 7)) - continue; - - /* - * 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, buf, len); - - handle_uevent(buf, len); - - /* - * Rebroadcast after processing so that device nodes and - * symlinks exist by the time consumers receive the event. - */ - if (rebc_fd != -1) - rebc_event(rebc_buf, len); - } + uev_run(&ctx, 0); if (rebc_fd != -1) close(rebc_fd); - close(pfd.fd); + close(nlfd); rules_free(&rules); logit(LOG_NOTICE, "keventd shutting down"); From c9200c4a4997cfc826618639d6ce8d0084bf0024 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sat, 30 May 2026 16:14:53 +0200 Subject: [PATCH 16/24] keventd: drop SIGCHLD save/restore dance, libuev reaps for us modprobe_load() and run_program() used to temporarily restore the default SIGCHLD handler around fork+waitpid because the main loop set SIGCHLD=SIG_IGN. With the libuev conversion, sigchld_cb in keventd.c handles reaping via signalfd -- and signalfd does not interfere with synchronous waitpid(pid, ...) -- so the dance is dead code. Signed-off-by: Joachim Wiberg --- keventd/rules.c | 13 +------------ keventd/uevent.c | 11 ----------- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/keventd/rules.c b/keventd/rules.c index 31580a47..48e46294 100644 --- a/keventd/rules.c +++ b/keventd/rules.c @@ -782,13 +782,9 @@ static int pattern_match(const char *pat, const char *subject, pat_type_t type) /* ----- program execution ------------------------------------------------ */ -/* - * Fork and exec cmd via /bin/sh, capture stdout into result. - * Temporarily restores SIGCHLD (keventd uses SIG_IGN) so waitpid works. - */ +/* Fork and exec cmd via /bin/sh, capture stdout into result. */ static int run_program(const char *cmd, char *result, size_t rlen) { - struct sigaction sa_dfl, sa_old; int pipefd[2]; pid_t pid; int rc = -1; @@ -799,14 +795,8 @@ static int run_program(const char *cmd, char *result, size_t rlen) if (pipe(pipefd) < 0) return -1; - sigemptyset(&sa_dfl.sa_mask); - sa_dfl.sa_flags = 0; - sa_dfl.sa_handler = SIG_DFL; - sigaction(SIGCHLD, &sa_dfl, &sa_old); - pid = fork(); if (pid < 0) { - sigaction(SIGCHLD, &sa_old, NULL); close(pipefd[0]); close(pipefd[1]); return -1; @@ -838,7 +828,6 @@ static int run_program(const char *cmd, char *result, size_t rlen) rc = WIFEXITED(status) ? WEXITSTATUS(status) : -1; } - sigaction(SIGCHLD, &sa_old, NULL); return rc; } diff --git a/keventd/uevent.c b/keventd/uevent.c index b5c935c1..866cce08 100644 --- a/keventd/uevent.c +++ b/keventd/uevent.c @@ -1039,13 +1039,9 @@ int firmware_load(struct uevent *ev) * Synchronous: waits for modprobe to complete before returning so * that rules running after `kmod load` (e.g. IMPORT{builtin}=blkid * once the filesystem driver is in) see the post-load state. - * keventd's main loop uses SIGCHLD=SIG_IGN to auto-reap stray - * children, so we temporarily restore the default handler here to - * make waitpid() observable. */ int modprobe_load(const char *modalias) { - struct sigaction sa_dfl, sa_old; pid_t pid; int status = 0; @@ -1054,15 +1050,9 @@ int modprobe_load(const char *modalias) logit(LOG_DEBUG, "Loading module for %s", modalias); - sigemptyset(&sa_dfl.sa_mask); - sa_dfl.sa_flags = 0; - sa_dfl.sa_handler = SIG_DFL; - sigaction(SIGCHLD, &sa_dfl, &sa_old); - pid = fork(); if (pid < 0) { logit(LOG_ERR, "fork failed: %s", strerror(errno)); - sigaction(SIGCHLD, &sa_old, NULL); return -1; } @@ -1073,7 +1063,6 @@ int modprobe_load(const char *modalias) while (waitpid(pid, &status, 0) < 0 && errno == EINTR) ; - sigaction(SIGCHLD, &sa_old, NULL); if (!WIFEXITED(status) || WEXITSTATUS(status) != 0) return -1; From 9e79ca6b5169220c675fff1bd93380fb09e6c71a Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sat, 30 May 2026 16:15:23 +0200 Subject: [PATCH 17/24] keventd: skip futile fork for missing /lib/udev/ When a rule references an absolute helper path (e.g. /lib/udev/fido_id, /lib/udev/scsi_id) that the system does not ship and we have no matching builtin either, return 1 from try_builtin_fallback() so the caller does not fork /bin/sh on a binary that's known to be missing. Previously each such uevent left a zombie 127 child for keventd's sigchld_cb to reap -- harmless but noisy and wasteful at coldplug. Signed-off-by: Joachim Wiberg --- keventd/rules.c | 49 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/keventd/rules.c b/keventd/rules.c index 48e46294..040f722a 100644 --- a/keventd/rules.c +++ b/keventd/rules.c @@ -1132,19 +1132,28 @@ static gid_t resolve_gid(const char *s) * * Stock udev rules invoke /lib/udev/ (path_id, usb_id, blkid, ...) * via IMPORT{program}=. keventd ships compatible builtins for many of - * those. If the helper is absent on this system, fall back to the - * matching builtin so the rule still works. If the helper exists, run - * it as-is -- this lets users override our builtins by dropping a - * custom helper into /lib/udev/. + * those. Dispatch order: * - * Returns 1 if the builtin handled the command, 0 if the caller should - * proceed with the normal fork+exec path. + * 1. Absolute helper path that exists -> let caller fork+exec it + * (lets users override builtins by dropping a custom helper) + * 2. Basename matches a known builtin -> run the builtin in-process + * 3. Absolute helper path that does NOT exist and has no matching + * builtin -> skip silently with a debug + * log; forking /bin/sh on a missing binary just yields a 127 + * child for every event (e.g. /lib/udev/fido_id, /lib/udev/scsi_id + * on systems that don't ship them). + * 4. Anything else (relative command, shell snippet, ...) -> caller + * forks /bin/sh -c which resolves via PATH. + * + * Returns 1 if the case has been handled here (caller skips fork), + * 0 if the caller should proceed with the normal fork+exec path. */ static int try_builtin_fallback(const char *cmd, struct uevent *ev) { char first[PATH_MAX], rebuilt[PATH_MAX]; const char *space, *base; size_t len; + int helper_missing; space = strchr(cmd, ' '); len = space ? (size_t)(space - cmd) : strlen(cmd); @@ -1157,20 +1166,30 @@ static int try_builtin_fallback(const char *cmd, struct uevent *ev) if (access(first, X_OK) == 0) return 0; + helper_missing = (first[0] == '/'); /* absolute path that's not there */ + base = strrchr(first, '/'); base = base ? base + 1 : first; - if (!builtin_has(base)) - return 0; + if (builtin_has(base)) { + if (space) + snprintf(rebuilt, sizeof(rebuilt), "%s%s", base, space); + else + snprintf(rebuilt, sizeof(rebuilt), "%s", base); + + logit(LOG_DEBUG, "rules: %s not found, falling back to builtin %s", + first, base); + builtin_run(ev, rebuilt); + return 1; + } - if (space) - snprintf(rebuilt, sizeof(rebuilt), "%s%s", base, space); - else - snprintf(rebuilt, sizeof(rebuilt), "%s", base); + if (helper_missing) { + logit(LOG_DEBUG, "rules: %s not found and no matching builtin, skipping", + first); + return 1; + } - logit(LOG_DEBUG, "rules: %s not found, falling back to builtin %s", first, base); - builtin_run(ev, rebuilt); - return 1; + return 0; } static void import_from_program(const char *cmd, struct uevent *ev) From 8b31369c4800ffa53bd42e89955a30e8214a04ce Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sat, 30 May 2026 16:18:29 +0200 Subject: [PATCH 18/24] keventd: add class// and driver/ conditions dev/ only fires when a device gets a /dev node, which leaves a lot of embedded-relevant hardware uncoverable: DSA switch cores, IIO sensors, LEDs, backlight, PHYs, regulators -- all live purely under sysfs. Two new condition namespaces: class// asserted on every sysfs class device add (e.g. ) driver/ asserted while the driver is bound to at least one device (e.g. ) A driver can bind to several devices, so driver/ conditions are refcounted: asserted on first bind, cleared when the last device is unbound. dev_cond() is generalized into a static cond_emit(prefix, rel, set) so class_cond() and driver_cond() share the same mkpath + symlink/erase code. The name avoids colliding with src/cond.h's public cond_path() helper (unrelated function that returns a condition's filesystem path). Signed-off-by: Joachim Wiberg --- doc/conditions.md | 10 ++++- doc/keventd.md | 35 +++++++++++++++ keventd/keventd.c | 13 +++++- keventd/keventd.h | 3 ++ keventd/uevent.c | 106 +++++++++++++++++++++++++++++++++++++++++----- src/cond.h | 4 ++ 6 files changed, 158 insertions(+), 13 deletions(-) diff --git a/doc/conditions.md b/doc/conditions.md index e61687bb..c56dab18 100644 --- a/doc/conditions.md +++ b/doc/conditions.md @@ -177,7 +177,8 @@ Built-in Conditions Finit comes with a set of plugins for conditions: - - `keventd`: provides `` and `` + - `keventd`: provides ``, ``, ``, and + `` - `devmon` (built-in fallback for `` without keventd) - `netlink`: provides `` - `pidfile`: provides `` @@ -191,6 +192,11 @@ condition is cleared. If keventd is not in use (an external device manager like udevd is used instead), the `devmon` built-in provides the same conditions by monitoring `/dev` and `/dev/dir` with inotify. +keventd also asserts `class//` for devices that have +no `/dev` node, e.g. LEDs and DSA switch ports, and `driver/` +while a driver is bound to at least one device. See +[keventd](keventd.md) for details. + The `pidfile` plugin recursively watches `/run/` for PID files created by the monitored services, and sets a corresponding condition in the `pid/` namespace. @@ -234,6 +240,8 @@ Built-in conditions: - `usr/foo` - `boot/arg` - `dev/node` and `dev/dir/node` +- `class//` +- `driver/` > [!NOTE] > Here, `up` means administratively up, the interface flag `IFF_UP`. diff --git a/doc/keventd.md b/doc/keventd.md index 5722fff2..b4c63ef8 100644 --- a/doc/keventd.md +++ b/doc/keventd.md @@ -201,6 +201,41 @@ devices: When the device is removed, the condition is cleared and Finit stops the dependent services. +### Class Conditions (`class/`) + +Many devices live in sysfs without a `/dev/` node -- DSA switch ports, +IIO sensors, LEDs, backlight, PHYs, regulators. For those, keventd +asserts `class//` on every `add` event so services +can still wait for them: + + service blink-blue { + description = "LED driver" + runlevel = "2345" + conditions = { "class/leds/blue" } + command = "/usr/sbin/blink-blue" + } + +The condition is cleared on `remove`. + +### Driver Conditions (`driver/`) + +`driver/` is asserted while the driver `` is bound to at +least one device, from the kernel's `bind`/`unbind` uevents. Use this +to gate on slow-probing hardware whose readiness isn't marked by a +class device or `/dev` node, such as a switch core or complex PHY: + + service dsa-probe { + description = "DSA topology probe" + runlevel = "2345" + conditions = { "driver/mt7530" } + command = "/usr/sbin/dsa-probe" + } + +A driver bound to several devices keeps the condition asserted until +the last device is unbound. To wait for one specific device instance, +gate on what its probe creates instead: the `class/` condition or +`/dev` node of the child device. + ### Power Supply Conditions (`sys/pwr/`) keventd monitors the `power_supply` subsystem and provides: diff --git a/keventd/keventd.c b/keventd/keventd.c index e3317658..fe09cb5b 100644 --- a/keventd/keventd.c +++ b/keventd/keventd.c @@ -334,11 +334,17 @@ static void handle_uevent(char *buf, size_t len) /* 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); @@ -355,8 +361,13 @@ static void handle_uevent(char *buf, size_t len) break; case ACT_BIND: + if (!passive) + driver_cond(&ev, 1); + break; + case ACT_UNBIND: - /* Driver bind/unbind - could trigger conditions */ + if (!passive) + driver_cond(&ev, 0); break; default: diff --git a/keventd/keventd.h b/keventd/keventd.h index e96733e8..d6554a72 100644 --- a/keventd/keventd.h +++ b/keventd/keventd.h @@ -149,6 +149,9 @@ int devnode_add (struct uevent *ev); int devnode_del (struct uevent *ev); int netdev_add (struct uevent *ev); +void class_cond (const struct uevent *ev, int set); +void driver_cond (const struct uevent *ev, int set); + int symlink_add (struct uevent *ev); int symlink_del (struct uevent *ev); void symlink_write_db (const char *devpath, FILE *fp); diff --git a/keventd/uevent.c b/keventd/uevent.c index 866cce08..3f4b56ad 100644 --- a/keventd/uevent.c +++ b/keventd/uevent.c @@ -58,35 +58,119 @@ void logit(int prio, const char *fmt, ...); /* - * Set/clear a device condition by creating/removing a symlink. + * Set/clear a Finit condition by creating/removing a symlink. * keventd is a standalone daemon, so we manipulate the filesystem directly * rather than using Finit's internal cond_set()/cond_clear() API. + * + * prefix is the namespace path fragment (COND_DEV, COND_CLASS, + * COND_DRIVER, ...) ending in '/'. rel is the relative path under that + * namespace (e.g. "sda", "input/event0", "net/eth0", "mt7530"). */ -static void dev_cond(const char *devname, int set) +static void cond_emit(const char *prefix, const char *rel, int set) { char cond[PATH_MAX]; char *dir; - if (!devname) + if (!prefix || !rel || !*rel) return; - snprintf(cond, sizeof(cond), "%s%s", _PATH_CONDDEV, devname); + snprintf(cond, sizeof(cond), "%s%s%s", _PATH_COND, prefix, rel); - /* Create parent directory if needed (e.g., dev/input/) */ + /* mkpath is idempotent (EEXIST-tolerant) so just always call it. */ dir = strdupa(cond); dir = dirname(dir); - if (strcmp(dir, _PATH_CONDDEV)) { - if (mkpath(dir, 0755) && errno != EEXIST) - logit(LOG_WARNING, "Failed creating condition dir %s", dir); - } + if (mkpath(dir, 0755) && errno != EEXIST) + logit(LOG_WARNING, "Failed creating condition dir %s", dir); if (set) { if (symlink(_PATH_RECONF, cond) && errno != EEXIST) - logit(LOG_WARNING, "Failed setting dev/%s condition", devname); + logit(LOG_WARNING, "Failed setting %s%s condition", prefix, rel); } else { if (erase(cond) && errno != ENOENT) - logit(LOG_WARNING, "Failed clearing dev/%s condition", devname); + logit(LOG_WARNING, "Failed clearing %s%s condition", prefix, rel); + } +} + +static void dev_cond(const char *devname, int set) +{ + cond_emit(COND_DEV, devname, set); +} + +/* + * class// -- fires for any sysfs class device add, + * regardless of whether the device gets a /dev node. Lets services + * depend on sysfs-only hardware (DSA switch ports, LEDs, IIO sensors, + * PHYs, etc.). + */ +void class_cond(const struct uevent *ev, int set) +{ + const char *sysname; + char rel[256]; + + if (!ev->subsystem || !ev->devpath) + return; + + sysname = strrchr(ev->devpath, '/'); + sysname = sysname ? sysname + 1 : ev->devpath; + if (!*sysname) + return; + + snprintf(rel, sizeof(rel), "%s/%s", ev->subsystem, sysname); + cond_emit(COND_CLASS, rel, set); +} + +/* + * driver/ -- asserted while the driver is bound to at least one + * device, from the kernel's bind/unbind uevents. Lets services depend + * on a specific driver having probed its hardware (e.g. ) + * even when no class device or /dev node marks the moment. + * + * A driver can bind to several devices, so the condition is refcounted: + * asserted on the first bind, cleared when the last device is unbound. + */ +struct drv_bind { + TAILQ_ENTRY(drv_bind) link; + char *name; + size_t count; +}; + +static TAILQ_HEAD(, drv_bind) drv_binds = TAILQ_HEAD_INITIALIZER(drv_binds); + +void driver_cond(const struct uevent *ev, int set) +{ + struct drv_bind *db; + + if (!ev->driver) + return; + + TAILQ_FOREACH(db, &drv_binds, link) { + if (!strcmp(db->name, ev->driver)) + break; + } + + if (set) { + if (!db) { + db = calloc(1, sizeof(*db)); + if (!db) + goto emit; + db->name = strdup(ev->driver); + if (!db->name) { + free(db); + goto emit; + } + TAILQ_INSERT_TAIL(&drv_binds, db, link); + } + if (db->count++) + return; + } else if (db) { + if (--db->count) + return; + TAILQ_REMOVE(&drv_binds, db, link); + free(db->name); + free(db); } +emit: + cond_emit(COND_DRIVER, ev->driver, set); } /* Symlink tracking for cleanup on device removal */ diff --git a/src/cond.h b/src/cond.h index 0ba5c2da..5c5e3af4 100644 --- a/src/cond.h +++ b/src/cond.h @@ -7,12 +7,16 @@ #define COND_BASE "finit/cond" #define COND_DEV "dev/" +#define COND_DRIVER "driver/" +#define COND_CLASS "class/" #define COND_PID "pid/" #define COND_SYS "sys/" #define COND_USR "usr/" #define _PATH_COND _PATH_VARRUN COND_BASE "/" #define _PATH_CONDDEV _PATH_COND COND_DEV +#define _PATH_CONDDRIVER _PATH_COND COND_DRIVER +#define _PATH_CONDCLASS _PATH_COND COND_CLASS #define _PATH_CONDPID _PATH_COND COND_PID #define _PATH_CONDSYS _PATH_COND COND_SYS #define _PATH_CONDUSR _PATH_COND COND_USR From 55cc969774ed74ee49e5df678c27db1a15918b24 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sat, 30 May 2026 16:19:53 +0200 Subject: [PATCH 19/24] keventd: add -S settle mode polling /sys/kernel/uevent_seqnum Stop-gap "settle" equivalent of udevadm settle for migration scenarios. Polls /sys/kernel/uevent_seqnum every 50ms and exits zero when the sequence number has been stable for 200ms (or non-zero after -t SECONDS timeout, default 30s). This is racy by design -- a slow probe firing after we return still races -- so the doc steers users toward dev/, class/, and bind/ conditions for any service they control. Settle is for legacy boot scripts and init transitions where condition wiring isn't feasible. Signed-off-by: Joachim Wiberg --- doc/keventd.md | 11 +++++++- keventd/keventd.c | 65 +++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/doc/keventd.md b/doc/keventd.md index b4c63ef8..96e7f371 100644 --- a/doc/keventd.md +++ b/doc/keventd.md @@ -256,7 +256,7 @@ battery: Usage ----- - keventd [-cdGhnv] [-g GROUP] + keventd [-cdGhnSv] [-g GROUP] [-t SECONDS] Options: -c Run coldplug at startup @@ -265,12 +265,21 @@ Usage -G Disable netlink rebroadcast entirely -h Show help text -n Run in foreground (no daemon) + -S Settle mode: wait for kernel uevent queue to quiet, then exit + -t SEC Settle timeout (default 30s, used with -S) -v Show version In normal operation, Finit starts keventd automatically via its system configuration. The `-d` flag is useful for debugging device issues -- it runs keventd in the foreground and logs all received uevents. +The `-S` flag offers a `udevadm settle`-equivalent for migration +scenarios: it polls `/sys/kernel/uevent_seqnum` until no events have +arrived for 200ms, then exits zero (or non-zero on timeout). Prefer +the condition-based model (``, ``, ``) over +settle when you control the service definition -- settle is racy with +slow probes that fire after the queue appears quiet. + Debug logging can also be toggled at runtime by sending `SIGUSR1`: kill -USR1 $(pidof keventd) diff --git a/keventd/keventd.c b/keventd/keventd.c index fe09cb5b..d558b976 100644 --- a/keventd/keventd.c +++ b/keventd/keventd.c @@ -44,6 +44,7 @@ #include #include #include +#include #include #include @@ -535,10 +536,57 @@ static void uevent_cb(uev_t *w, void *arg, int events) rebc_event(rebc_buf, len); } +/* + * 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 timespec start, last_change, now; + unsigned long long last_seq = 0, cur_seq = 0; + FILE *fp; + + clock_gettime(CLOCK_MONOTONIC, &start); + last_change = start; + + while (1) { + fp = fopen("/sys/kernel/uevent_seqnum", "r"); + if (!fp) { + 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); + + if (cur_seq != last_seq) { + last_seq = cur_seq; + last_change = now; + } else { + long dt_ms = (now.tv_sec - last_change.tv_sec) * 1000 + + (now.tv_nsec - last_change.tv_nsec) / 1000000; + if (dt_ms >= stable_ms) + return 0; + } + + if (now.tv_sec - start.tv_sec >= timeout_s) + return 1; + + usleep(50000); /* 50ms */ + } +} + static int usage(int rc) { fprintf(stderr, - "Usage: keventd [-dGhnpv] [-c] [-g GROUP] [-r DIR]\n" + "Usage: keventd [-dGhnpSv] [-c] [-g GROUP] [-r DIR] [-t SECONDS]\n" "\n" "Options:\n" " -c Run coldplug at startup\n" @@ -549,6 +597,8 @@ static int usage(int rc) " -n Run in foreground (no daemon)\n" " -p Passive mode: power supply events only (no device management)\n" " -r DIR Extra rules directory (in addition to 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); @@ -576,6 +626,8 @@ int main(int argc, char *argv[]) uev_t netlink_w; uev_ctx_t ctx; int do_coldplug = 0; + int do_settle = 0; + int settle_timeout = 30; int foreground = 0; int nlfd; int c; @@ -585,7 +637,7 @@ int main(int argc, char *argv[]) * requested bits verbatim instead of masking them. */ umask(0); - while ((c = getopt(argc, argv, "cdg:Ghnpr:v")) != -1) { + while ((c = getopt(argc, argv, "cdg:Ghnpr:St:v")) != -1) { switch (c) { case 'c': do_coldplug = 1; @@ -611,6 +663,12 @@ int main(int argc, char *argv[]) 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; @@ -619,6 +677,9 @@ int main(int argc, char *argv[]) } } + if (do_settle) + return cmd_settle(settle_timeout, 200); + if (!foreground) { openlog("keventd", LOG_PID, LOG_DAEMON); set_logging(LOG_NOTICE); From e648ea2a66117c38a301173f127f28ec03e22e29 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sat, 30 May 2026 16:20:19 +0200 Subject: [PATCH 20/24] doc: document keventd net interface conditions and flags - works for network interfaces too -- the condition fires when the kernel device appears, before any link state. Add an example next to and cross-reference the netlink plugin's parallel net//{exist,up,running} - The Usage block was missing -p (passive mode) and -r DIR (extra rules dir). Add them so the synopsis matches `keventd -h` - Clarify that `keventd -S` is a one-shot command, not a flag to the running daemon. It does not talk to keventd, it just polls the kernel's uevent_seqnum. Spell out the typical usage and the rationale for preferring conditions over settle Signed-off-by: Joachim Wiberg --- doc/keventd.md | 43 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/doc/keventd.md b/doc/keventd.md index 96e7f371..956ad04c 100644 --- a/doc/keventd.md +++ b/doc/keventd.md @@ -198,6 +198,22 @@ devices: command = "/usr/sbin/gps-daemon" } +Network interfaces use the same namespace -- the condition fires when +the interface exists in the kernel, before any link-up: + + service dhcpcd { + description = "DHCP client" + runlevel = "2345" + conditions = { "dev/wan" } + command = "/usr/sbin/dhcpcd" + } + +The `netlink` plugin provides a parallel `net//exist` condition +with the same meaning, plus `net//up` (admin up) and +`net//running` (carrier present). Either `` or +`` works to wait for the interface to appear; use the +latter when you also need to gate on link state. + When the device is removed, the condition is cleared and Finit stops the dependent services. @@ -256,7 +272,7 @@ battery: Usage ----- - keventd [-cdGhnSv] [-g GROUP] [-t SECONDS] + keventd [-cdGhnpSv] [-g GROUP] [-r DIR] [-t SECONDS] Options: -c Run coldplug at startup @@ -265,6 +281,8 @@ Usage -G Disable netlink rebroadcast entirely -h Show help text -n Run in foreground (no daemon) + -p Passive mode: power supply events only + -r DIR Extra rules directory -S Settle mode: wait for kernel uevent queue to quiet, then exit -t SEC Settle timeout (default 30s, used with -S) -v Show version @@ -273,12 +291,23 @@ In normal operation, Finit starts keventd automatically via its system configuration. The `-d` flag is useful for debugging device issues -- it runs keventd in the foreground and logs all received uevents. -The `-S` flag offers a `udevadm settle`-equivalent for migration -scenarios: it polls `/sys/kernel/uevent_seqnum` until no events have -arrived for 200ms, then exits zero (or non-zero on timeout). Prefer -the condition-based model (``, ``, ``) over -settle when you control the service definition -- settle is racy with -slow probes that fire after the queue appears quiet. +`keventd -S` is a one-shot command, not a flag to the running daemon. +It is the `udevadm settle` equivalent for migration scenarios: + + keventd -S -t 10 && start-graphical-session + +It does not talk to the running keventd (or any device manager) -- it +simply opens `/sys/kernel/uevent_seqnum` and polls until the kernel's +sequence counter has been stable for 200ms, then exits zero. After +the `-t SECONDS` timeout (default 30) it exits non-zero instead. +Because `uevent_seqnum` is maintained by the kernel itself, `-S` works +regardless of which device manager is active, or even if none is. + +Prefer the condition-based model (``, ``, +``) over settle when you control the service definition -- +settle is racy with slow probes that fire after the queue appears +quiet. It is provided for legacy boot scripts and init transitions +where condition wiring isn't feasible. Debug logging can also be toggled at runtime by sending `SIGUSR1`: From 6bd70680b719f5310818e8ffef69d77c7b3f3f77 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sat, 30 May 2026 17:14:59 +0200 Subject: [PATCH 21/24] keventd: defer pidfile until coldplug queue is drained When started with -c (the default when keventd is the device manager), gate the pidfile on the kernel's uevent_seqnum having been stable for 200ms. Up to now ready signaling with the pidfile was done right after coldplug() triggered the kernel to re-emit events, but before uev_run() had drained any of them, so really only meant "listening on netlink". With the gate, services that depend on can now assume /dev is populated and persistent symlinks are live. Signed-off-by: Joachim Wiberg --- doc/keventd.md | 6 +++ keventd/keventd.c | 101 ++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 91 insertions(+), 16 deletions(-) diff --git a/doc/keventd.md b/doc/keventd.md index 956ad04c..cb1474ed 100644 --- a/doc/keventd.md +++ b/doc/keventd.md @@ -125,6 +125,12 @@ kernel to re-emit add events for all existing devices. This replaces the separate `coldplug` script previously used with mdev. +When `-c` is used, keventd defers its pidfile (and the `pid/keventd` +condition Finit asserts from it) until the coldplug event queue has +been fully drained. Services that depend on `` can +therefore assume `/dev` is populated and persistent symlinks are live, +without needing a separate `settle` step. + Netlink Rebroadcast ------------------- diff --git a/keventd/keventd.c b/keventd/keventd.c index d558b976..8d0bf821 100644 --- a/keventd/keventd.c +++ b/keventd/keventd.c @@ -76,6 +76,13 @@ /* 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; + struct timespec last_change; + int primed; +}; + static int num_ac_online; static int num_ac; @@ -536,6 +543,55 @@ static void uevent_cb(uev_t *w, void *arg, int events) 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". + */ +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) + return; + + if (fscanf(fp, "%llu", &cur) != 1) + cur = cg->last_seq; + fclose(fp); + + clock_gettime(CLOCK_MONOTONIC, &now); + + if (!cg->primed) { + cg->last_seq = cur; + cg->last_change = now; + cg->primed = 1; + return; + } + + if (cur != cg->last_seq) { + cg->last_seq = cur; + cg->last_change = now; + return; + } + + dt_ms = (now.tv_sec - cg->last_change.tv_sec) * 1000 + + (now.tv_nsec - cg->last_change.tv_nsec) / 1000000; + if (dt_ms < 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` @@ -546,14 +602,15 @@ static void uevent_cb(uev_t *w, void *arg, int events) */ static int cmd_settle(int timeout_s, int stable_ms) { - struct timespec start, last_change, now; unsigned long long last_seq = 0, cur_seq = 0; - FILE *fp; + struct timespec start, last_change, now; clock_gettime(CLOCK_MONOTONIC, &start); last_change = start; while (1) { + FILE *fp; + fp = fopen("/sys/kernel/uevent_seqnum", "r"); if (!fp) { fprintf(stderr, "keventd: cannot read uevent_seqnum: %s\n", @@ -579,7 +636,7 @@ static int cmd_settle(int timeout_s, int stable_ms) if (now.tv_sec - start.tv_sec >= timeout_s) return 1; - usleep(50000); /* 50ms */ + usleep(50000); } } @@ -619,16 +676,17 @@ static int usage(int rc) */ int main(int argc, char *argv[]) { - static char uevent_buf[UEVENT_BUFFER_SIZE]; + 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 }; - uev_t sigusr1_w, sigterm_w, sighup_w, sigchld_w; - uev_t netlink_w; - uev_ctx_t ctx; - int do_coldplug = 0; - int do_settle = 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; @@ -690,10 +748,10 @@ int main(int argc, char *argv[]) uev_init(&ctx); - uev_signal_init(&ctx, &sigusr1_w, sigusr1_cb, NULL, SIGUSR1); - uev_signal_init(&ctx, &sigterm_w, sigterm_cb, NULL, SIGTERM); - uev_signal_init(&ctx, &sighup_w, sighup_cb, NULL, SIGHUP); - uev_signal_init(&ctx, &sigchld_w, sigchld_cb, NULL, SIGCHLD); + 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(); @@ -721,7 +779,7 @@ int main(int argc, char *argv[]) setsockopt(nlfd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf)); } - uev_io_init(&ctx, &netlink_w, uevent_cb, uevent_buf, nlfd, UEV_READ); + 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. */ @@ -736,8 +794,19 @@ int main(int argc, char *argv[]) if (!passive) rules_load_all(&rules, rules_dir); - pidfile(NULL); - logit(LOG_NOTICE, "keventd v%s started, waiting for events...", KEVENTD_VERSION); + /* + * 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); From c7b75b8e50c4945388c8b0caa0befb90c8f2cca9 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Sat, 1 Aug 2026 14:15:39 +0200 Subject: [PATCH 22/24] test: include bundled helpers' libraries in the sysroot keventd links libblkid, which finit itself does not, but sysroot.mk only copied the libraries finit links. Inside the sysroot keventd then fails to start: Service keventd[18] died (exit status: 127) Collect libraries from finit and everything installed under libexec/finit/ instead. Signed-off-by: Joachim Wiberg --- test/lib/sysroot.mk | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/lib/sysroot.mk b/test/lib/sysroot.mk index ea2d8c7c..de74abb0 100644 --- a/test/lib/sysroot.mk +++ b/test/lib/sysroot.mk @@ -39,7 +39,9 @@ BBURL ?= $(BBHOME)/$(BBVER)/$(BBBIN) _libs_nss = $(firstword $(wildcard /lib/$(ARCH)-linux-gnu/libnss_files.so.2 \ /usr/lib/$(ARCH)-linux-gnu/libnss_files.so.2 \ /lib64/libnss_files.so.2 /lib/libnss_files.so.2)) -_libs_src = $(shell ldd $(FINITBIN) | grep -Eo '/[^ ]+') $(_libs_nss) +# Bundled helpers like keventd link more libraries than finit itself +_bins = $(FINITBIN) $(shell find $(abspath $(DEST)) -path '*libexec/finit*' -type f -perm -u+x 2>/dev/null) +_libs_src = $(sort $(foreach bin,$(_bins),$(shell ldd $(bin) 2>/dev/null | grep -Eo '/[^ ]+'))) $(_libs_nss) libs = $(foreach path,$(_libs_src),$(abspath $(DEST))$(path)) all: $(libs) $(DEST)/bin/$(BBBIN) From 7f06e2f29eabca79a5ea8b2f13dc8bbd19bca263 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 6 Aug 2026 08:30:50 +0200 Subject: [PATCH 23/24] devmon: bridge keventd conditions to the service engine A service with a , or any other keventd-provided, condition is never started when the device appears. keventd asserts the condition file, but devmon only watches /dev, so no cond_update() ever reaches affected services. Reload made it worse: devmon_reconf() clears any registered dev/ condition without a /dev node behind it. Watch the dev/, class/, and bind/ condition directories, like the sys and usr plugins do for their namespaces, and treat an existing condition file as device presence in devmon_reconf(). While at it, drop the condition netdev_add() asserted: network interfaces are not device nodes. A /dev/wan node created by a user and a wan interface would fight over the same condition. Worse, interface remove events flowed through devnode_del(), which unlinks /dev/ -- removing the wan interface would delete a user's /dev/wan node. Interfaces already have a home: from the netlink plugin, and . Gate node removal on major:minor, like node creation. Signed-off-by: Joachim Wiberg --- doc/keventd.md | 16 ++++++------ keventd/keventd.c | 5 ++-- keventd/uevent.c | 9 +++---- src/devmon.c | 65 +++++++++++++++++++++++++++++++++++++---------- 4 files changed, 67 insertions(+), 28 deletions(-) diff --git a/doc/keventd.md b/doc/keventd.md index cb1474ed..7c25113c 100644 --- a/doc/keventd.md +++ b/doc/keventd.md @@ -204,21 +204,21 @@ devices: command = "/usr/sbin/gps-daemon" } -Network interfaces use the same namespace -- the condition fires when -the interface exists in the kernel, before any link-up: +Network interfaces are not device nodes and do not live in the `dev/` +namespace -- a `/dev/wan` node created by a user must not be confused +with a `wan` interface. To wait for an interface, use the `netlink` +plugin's `net//exist` condition, plus `net//up` (admin +up) and `net//running` (carrier present) to gate on link state: service dhcpcd { description = "DHCP client" runlevel = "2345" - conditions = { "dev/wan" } + conditions = { "net/wan/exist" } command = "/usr/sbin/dhcpcd" } -The `netlink` plugin provides a parallel `net//exist` condition -with the same meaning, plus `net//up` (admin up) and -`net//running` (carrier present). Either `` or -`` works to wait for the interface to appear; use the -latter when you also need to gate on link state. +keventd provides the parallel `class/net/` condition, like for +any other sysfs class device. When the device is removed, the condition is cleared and Finit stops the dependent services. diff --git a/keventd/keventd.c b/keventd/keventd.c index 8d0bf821..a779a50b 100644 --- a/keventd/keventd.c +++ b/keventd/keventd.c @@ -356,8 +356,9 @@ static void handle_uevent(char *buf, size_t len) /* Remove symlinks first */ symlink_del(&ev); - /* Remove device node */ - if (ev.devname) + /* 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; diff --git a/keventd/uevent.c b/keventd/uevent.c index 3f4b56ad..6f7ba7c5 100644 --- a/keventd/uevent.c +++ b/keventd/uevent.c @@ -611,8 +611,9 @@ int devnode_del(struct uevent *ev) } /* - * Handle network interface add: rename if NAME= was set by a rule, then - * set the dev/ condition so Finit services can depend on the interface. + * Handle network interface add: rename if NAME= was set by a rule. + * Interfaces are not device nodes, so no dev/ condition is set here; + * services depend on or instead. * * The interface must be DOWN for SIOCSIFNAME to succeed. Freshly added * interfaces always are, so we do not attempt to bring the link down. @@ -634,7 +635,7 @@ int netdev_add(struct uevent *ev) if (sock < 0) { logit(LOG_WARNING, "Cannot open socket for netif rename: %s", strerror(errno)); - goto cond; + return -1; } memset(&ifr, 0, sizeof(ifr)); @@ -652,8 +653,6 @@ int netdev_add(struct uevent *ev) ev->devname, new_name); } -cond: - dev_cond(new_name ?: ev->devname, 1); return 0; } diff --git a/src/devmon.c b/src/devmon.c index 92b8e430..3c113eb7 100644 --- a/src/devmon.c +++ b/src/devmon.c @@ -58,6 +58,30 @@ static struct dev_node *find_node(const char *cond) return NULL; } +static int devmon_cond(const char *cond) +{ + return !strncmp(cond, COND_DEV, strlen(COND_DEV)) || + !strncmp(cond, COND_CLASS, strlen(COND_CLASS)) || + !strncmp(cond, COND_DRIVER, strlen(COND_DRIVER)); +} + +/* + * keventd asserts class/ and driver/ conditions for devices that never + * get a /dev node, e.g. LEDs and DSA switch ports. For those the + * condition file is the only record of the device. + */ +static int dev_exists(const char *cond) +{ + char path[PATH_MAX], tmp[PATH_MAX]; + + snprintf(path, sizeof(path), "/%s", cond); + if (fexist(path)) + return 1; + + snprintf(tmp, sizeof(tmp), _PATH_COND "%s", cond); + return fexist(pid_runpath(tmp, path, sizeof(path))); +} + static void drop_node(struct dev_node *node) { if (!node) @@ -75,9 +99,8 @@ static void drop_node(struct dev_node *node) void devmon_add_cond(const char *cond) { struct dev_node *node; - char path[PATH_MAX]; - if (!cond || strncmp(cond, "dev/", 4)) { + if (!cond || !devmon_cond(cond)) { // dbg("no match %s", cond ?: ""); return; } @@ -105,14 +128,13 @@ void devmon_add_cond(const char *cond) TAILQ_INSERT_TAIL(&dev_node_list, node, link); - snprintf(path, sizeof(path), "/%s", cond); - if (fexist(path)) + if (dev_exists(cond)) cond_set(cond); } void devmon_del_cond(const char *cond) { - if (!cond || strncmp(cond, COND_DEV, strlen(COND_DEV))) + if (!cond || !devmon_cond(cond)) return; drop_node(find_node(cond)); @@ -121,11 +143,9 @@ void devmon_del_cond(const char *cond) void devmon_reconf(void) { struct dev_node *node, *tmp; - char path[PATH_MAX]; TAILQ_FOREACH_SAFE(node, &dev_node_list, link, tmp) { - snprintf(path, sizeof(path), "/%s", node->name); - if (fexist(path)) + if (dev_exists(node->name)) cond_set(node->name); else cond_clear(node->name); @@ -161,6 +181,18 @@ static void devmon_update_conds(char *dir, char *name, uint32_t mask) char *cond; paste(fn, sizeof(fn), dir, name); + + /* + * Events in the condition tree come from keventd, the file is + * the condition state so only kick affected services, like the + * sys/usr plugins do. + */ + cond = strstr(fn, COND_BASE); + if (cond) { + cond_update(cond + strlen(COND_BASE) + 1); + return; + } + cond = &fn[1]; // dbg("path: %s, mask: %08x, cond: %s", fn, mask, cond); @@ -273,23 +305,30 @@ static void devmon_cb(uev_t *w, void *arg, int events) void devmon_init(uev_ctx_t *ctx) { + const char *cond_dirs[] = { _PATH_CONDDEV, _PATH_CONDCLASS, _PATH_CONDDRIVER }; char dir[MAX_ARG_LEN]; + size_t i; fd = iwatch_init(&iw_devmon); if (fd < 0) return; - if (mkpath(pid_runpath(_PATH_CONDDEV, dir, sizeof(dir)), 0755) && errno != EEXIST) { - err(1, "Failed creating %s condition directory, %s", COND_DEV, _PATH_CONDDEV); - return; - } - if (uev_io_init(ctx, &devw, devmon_cb, NULL, fd, UEV_READ)) { err(1, "Failed setting up I/O callback for /dev watcher"); close(fd); return; } + for (i = 0; i < NELEMS(cond_dirs); i++) { + pid_runpath(cond_dirs[i], dir, sizeof(dir)); + if (mkpath(dir, 0755) && errno != EEXIST) { + err(1, "Failed creating condition directory %s", dir); + continue; + } + + devmon_add_path(&iw_devmon, dir); + } + strlcpy(dir, _PATH_DEV, sizeof(dir)); if (devmon_add_path(&iw_devmon, dir)) iwatch_exit(&iw_devmon); From 6fdb0472f3fb435af553312156605836ccf0b229 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 6 Aug 2026 08:35:37 +0200 Subject: [PATCH 24/24] test: add keventd device manager test The unified keventd has no automated coverage, only the devmon fallback is exercised by the test suite. Network interfaces are the one device class an unprivileged test can hotplug: the sandbox has its own network namespace, so 'ip link add' makes the kernel emit genuine uevents. Verify keventd readiness, driving a service -- and NOT asserted, interfaces are not device nodes -- libudev-compatible n keying in /run/udev/data, conditions surviving initctl reload, and cleanup on interface remove. Signed-off-by: Joachim Wiberg --- test/Makefile.am | 4 +++ test/keventd.sh | 73 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100755 test/keventd.sh diff --git a/test/Makefile.am b/test/Makefile.am index a12c296b..644bab97 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -48,6 +48,7 @@ EXTRA_DIST += failing-sysv.sh EXTRA_DIST += svc-env.sh EXTRA_DIST += global-envs.sh EXTRA_DIST += initctl-status-subset.sh +EXTRA_DIST += keventd.sh EXTRA_DIST += notify.sh EXTRA_DIST += pidfile.sh EXTRA_DIST += stale-pidfile.sh @@ -101,6 +102,9 @@ TESTS += failing-sysv.sh TESTS += svc-env.sh TESTS += global-envs.sh TESTS += initctl-status-subset.sh +if KEVENTD +TESTS += keventd.sh +endif TESTS += notify.sh TESTS += pidfile.sh TESTS += stale-pidfile.sh diff --git a/test/keventd.sh b/test/keventd.sh new file mode 100755 index 00000000..b00f6d63 --- /dev/null +++ b/test/keventd.sh @@ -0,0 +1,73 @@ +#!/bin/sh +# +# Verify keventd, the bundled device manager. Network interfaces are +# the only devices an unprivileged test can hotplug: the sandbox has +# its own network namespace, so `ip link add` makes the kernel emit +# genuine uevents for keventd to process. +# +# Verifies that keventd signals readiness, that an interface add event +# asserts to start a dependent service -- but never +# , interfaces are not device nodes -- that the +# /run/udev/data entry uses libudev-style n keying, and that +# the remove event reverses it all. +# + +set -eu + +TEST_DIR=$(dirname "$0") + +test_teardown() +{ + say "Running test teardown." + + run "ip link del dummy0 2>/dev/null || true" + run "rm -f $FINIT_CONF" +} + +# shellcheck source=/dev/null +. "$TEST_DIR/lib/setup.sh" + +if ! texec initctl -p status keventd >/dev/null 2>&1; then + skip "keventd not enabled in this build" +fi + +if ! run "ip link add probe0 type dummy 2>/dev/null"; then + skip "cannot create dummy interfaces in test namespace" +fi +run "ip link del probe0" + +sep "keventd readiness" +retry 'assert_ready "keventd"' 50 0.2 + +sep "interface add asserts " +cat >> "$SYSROOT$FINIT_CONF" < keying" +ifindex=$(texec cat /sys/class/net/dummy0/ifindex) +assert_file_exists "/run/udev/data/n$ifindex" +assert_file_contains "/run/udev/data/n$ifindex" "E:INTERFACE=dummy0" + +sep "conditions survive initctl reload" +run "initctl reload" +retry 'assert_cond "class/net/dummy0"' +retry 'assert_status "serv" "running"' + +sep "interface remove clears " +run "ip link del dummy0" +retry 'assert_nocond "class/net/dummy0"' +retry 'assert_status "serv" "waiting"' +retry "texec test ! -e /run/udev/data/n$ifindex"