From 0349ed1e795cedb533bc7389c08a6611c1f76155 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 6 May 2026 23:50:12 +0200 Subject: [PATCH 01/14] 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 41f60723bc95e9b6152abbfa5f2635ec2e887a63 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 29 Jan 2026 12:13:03 +0100 Subject: [PATCH 02/14] 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 7b0943ac..ec81e3dc 100644 --- a/configure.ac +++ b/configure.ac @@ -112,7 +112,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]) @@ -165,7 +165,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 bd126600..1372f5d6 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 c9b5a30d29316af9404f3a98ee41cdc3264d8741 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 29 Jan 2026 12:16:33 +0100 Subject: [PATCH 03/14] 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 | 26 +++--- doc/keventd.md | 202 +++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 206 insertions(+), 22 deletions(-) diff --git a/doc/conditions.md b/doc/conditions.md index aac0eadf..5ab0df1d 100644 --- a/doc/conditions.md +++ b/doc/conditions.md @@ -135,26 +135,30 @@ 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/` (recursively) for PID -files created by the monitored services, and sets a corresponding -condition in the `pid/` namespace. +The `pidfile` plugin (recursively) watches `/run/` for PID files created +by the monitored services, and sets a corresponding condition in the +`pid/` namespace. Similarly, the `netlink` plugin provides basic conditions for when an interface is brought up/down and when a default route (gateway) is set, in the `net/` namespace. -The `sys` and `usr` plugins monitor are passive condition monitors where -the action is provided by `keventd`, signal handlers, and in the case of +The `sys` and `usr` plugins are passive condition monitors where the +action is provided by `keventd`, signal handlers, and in the case of `usr`, the end-user via the `initctl` tool. Additionally, the various states of a run/task/sysv/service can also be diff --git a/doc/keventd.md b/doc/keventd.md index 7651b0d8..73fe3f7b 100644 --- a/doc/keventd.md +++ b/doc/keventd.md @@ -1,14 +1,194 @@ -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 [2345] /usr/sbin/mdadm --monitor /dev/md0 -- RAID monitor + service [2345] /usr/sbin/gps-daemon -- 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 [2345] cron -f -- Cron daemon + + +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 a685c7212d7b67c53dfbeca708a7a5d22e740e41 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 29 Jan 2026 12:32:35 +0100 Subject: [PATCH 04/14] man: add fine manual for keventd(8) Signed-off-by: Joachim Wiberg --- man/Makefile.am | 4 + man/keventd.8 | 215 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 219 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..e7e5921f --- /dev/null +++ b/man/keventd.8 @@ -0,0 +1,215 @@ +.\" 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 [2345] /usr/sbin/mdadm -- RAID monitor +service [2345] /usr/sbin/gpsd -- GPS daemon +.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 [2345] cron -f -- Cron daemon +.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 c07a3bcab8685c4481b74c25cff0e0089650ae0c Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 29 Jan 2026 14:21:06 +0100 Subject: [PATCH 05/14] 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 985c12b399d27e1e9b79c484633a9fb804bcebd7 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 29 Jan 2026 14:22:33 +0100 Subject: [PATCH 06/14] 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 73fe3f7b..977279ac 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 ---------- @@ -143,7 +194,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: @@ -154,11 +205,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 @@ -177,7 +230,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: @@ -187,8 +240,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 e7e5921f..a3c238dd 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. @@ -182,6 +199,40 @@ Useful for preventing power-hungry services from running on battery: .Bd -literal -offset indent service [2345] cron -f -- Cron daemon .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 18c2ab26df8d593a32a8337b58448a514fc673ab Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Thu, 29 Jan 2026 14:36:47 +0100 Subject: [PATCH 07/14] 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 9b47388a2ac415814820b1a63641a471d8849c31 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 6 May 2026 22:09:29 +0200 Subject: [PATCH 08/14] 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 6194759073de5b4a396458035c1a3dad0584c91d Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 6 May 2026 22:32:23 +0200 Subject: [PATCH 09/14] 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 --- src/conf.c | 14 ++++++++++--- src/keventd.c | 56 +++++++++++++++++++++++++++++++-------------------- 2 files changed, 45 insertions(+), 25 deletions(-) diff --git a/src/conf.c b/src/conf.c index 6e8ec834..4f172aa3 100644 --- a/src/conf.c +++ b/src/conf.c @@ -1717,11 +1717,19 @@ 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, "[S12345789] cgroup.init notify:none " - FINIT_EXECPATH_ "/keventd -- Finit kernel event daemon", "keventd.conf"); + conf_save_service(SVC_TYPE_SERVICE, "[S0123456789] cgroup.init notify:none " + FINIT_EXECPATH_ "/keventd" +#ifdef HAVE_HOTPLUG_PLUGIN + " -p" +#else + " -c" +#endif + " -- kernel event daemon", "keventd.conf"); 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 b9e517a2cb1375bd4fbdb45030021fa4afa8c6ae Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Wed, 6 May 2026 23:24:10 +0200 Subject: [PATCH 10/14] 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 4f172aa3..ffa0f94b 100644 --- a/src/conf.c +++ b/src/conf.c @@ -1722,7 +1722,7 @@ int conf_init(uev_ctx_t *ctx) * only monitors power supply events (sys/pwr/ac condition). */ if (whichp(FINIT_EXECPATH_ "/keventd")) - conf_save_service(SVC_TYPE_SERVICE, "[S0123456789] cgroup.init notify:none " + conf_save_service(SVC_TYPE_SERVICE, "[S0123456789] cgroup.init notify:pid " FINIT_EXECPATH_ "/keventd" #ifdef HAVE_HOTPLUG_PLUGIN " -p" 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 a77b4360c5b30ce36374ce534c6504f76be6ae97 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 8 May 2026 11:03:31 +0200 Subject: [PATCH 11/14] 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 ec81e3dc..43d6a772 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 1372f5d6..c5139272 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 76818e61480bf65b758c41bdfa28712d7ee95c08 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 8 May 2026 14:59:14 +0200 Subject: [PATCH 12/14] 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/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 | 1663 ++++++++++++++++++++++++++++++++++++++++++ keventd/rules.h | 163 +++++ keventd/sysfs.c | 131 ++++ keventd/sysfs.h | 75 ++ keventd/udevdb.c | 180 +++++ keventd/udevdb.h | 67 ++ keventd/uevent.c | 274 ++++++- 14 files changed, 4405 insertions(+), 34 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 43d6a772..ca40e249 100644 --- a/configure.ac +++ b/configure.ac @@ -313,7 +313,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"], [ @@ -429,7 +433,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/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..8f855e32 --- /dev/null +++ b/keventd/rules.c @@ -0,0 +1,1663 @@ +/* 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: { + /* Read E: records from the parent device's udev database entry */ + if (ev->devpath) { + char ppath[PATH_MAX], dbfile[PATH_MAX]; + char sysname[64] = "unknown"; + char subsys[64] = "unknown"; + char syspath[PATH_MAX]; + const char *sn; + char *slash; + FILE *fp; + + snprintf(ppath, sizeof(ppath), "%s", ev->devpath); + slash = strrchr(ppath, '/'); + if (!slash || slash == ppath) + break; + *slash = 0; + + sn = strrchr(ppath, '/'); + snprintf(sysname, sizeof(sysname), "%s", sn ? sn + 1 : ppath); + + snprintf(syspath, sizeof(syspath), "/sys%s", ppath); + sysfs_read_subsystem(syspath, subsys, sizeof(subsys)); + + snprintf(dbfile, sizeof(dbfile), UDEVDB_PATH "/+%s:%s", + subsys, sysname); + + fp = fopen(dbfile, "r"); + if (fp) { + char line[1024]; + while (fgets(line, sizeof(line), fp)) { + char *eq; + chomp(line); + if (line[0] != 'E' || line[1] != ':') + continue; + eq = strchr(line + 2, '='); + if (eq) { + *eq++ = 0; + uevent_setenv(ev, line + 2, eq); + } + } + fclose(fp); + } + } + 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..259f971c --- /dev/null +++ b/keventd/udevdb.c @@ -0,0 +1,180 @@ +/* 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 "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 + * +: 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); + + 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 { + 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 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], line[1024]; + FILE *fp; + + if (!ev->devpath) + return -1; + + udevdb_path(ev, path, sizeof(path)); + + 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; +} + +/* + * 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..9b0565b3 --- /dev/null +++ b/keventd/udevdb.h @@ -0,0 +1,67 @@ +/* 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); + +/* + * 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; } From 6818b14abb1e0e053f8d04ed59732fd9f3a18807 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Mon, 18 May 2026 07:59:42 +0200 Subject: [PATCH 13/14] 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 ca40e249..4049b900 100644 --- a/configure.ac +++ b/configure.ac @@ -168,6 +168,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]) @@ -350,6 +353,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 2b0cacb2533ebf3e7fe1a0e5bb9d772ee7a01d26 Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Tue, 19 May 2026 07:29:31 +0200 Subject: [PATCH 14/14] .github: install libblkid-dev for keventd build Signed-off-by: Joachim Wiberg --- .github/workflows/build.yml | 3 +-- .github/workflows/coverity.yml | 2 +- .github/workflows/release.yml | 2 +- .github/workflows/weekly.yml | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 460d378a..76a30e51 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,8 +34,7 @@ jobs: - name: Install dependencies run: | sudo apt-get -y update - sudo apt-get -y install pkg-config tree jq libcap-dev - sudo apt-get -y install pkg-config + sudo apt-get -y install pkg-config tree jq libcap-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 6b4add0b..390d74fa 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 + sudo apt-get -y install pkg-config libcap-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 1f740e24..6764b10a 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 + sudo apt-get -y install pkg-config jq libcap-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 56991c2f..62d87684 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 + sudo apt-get -y install pkg-config jq libcap-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