diff --git a/configure.ac b/configure.ac index 3a0a971d..3e21a66e 100644 --- a/configure.ac +++ b/configure.ac @@ -124,7 +124,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]) @@ -177,7 +177,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 0c065e6a..81bd327a 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]; + char buf[UEVENT_BUFFER_SIZE]; + int do_coldplug = 0; + int foreground = 0; + int c; - if (argc > 1) { - if (!strcmp(argv[1], "-d")) + /* 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)) + /* Skip libudev events (start with "libudev") */ + if (!strncmp(buf, "libudev", 7)) continue; - /* XXX: currently limited to monitoring this subsystem */ - if (!strstr(buf, "power_supply")) - 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: + */