From 67d61a828271b1ff5d03b98c6012b65dc2e02f0b Mon Sep 17 00:00:00 2001 From: Joachim Wiberg Date: Fri, 8 May 2026 14:59:14 +0200 Subject: [PATCH] keventd: add udev rules engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Transform keventd from a power-supply monitor + basic hotplug handler into a full udev-compatible device manager. Rules engine (rules.c): - Full .rules file parser covering all udev key types: ACTION, KERNEL, SUBSYSTEM, DEVPATH, ENV, ATTR, SYSCTL, TAG, RESULT, PROGRAM, TEST, parent-chain KERNELS/SUBSYSTEMS/ATTRS/DRIVERS, and more - Pattern matching: plain string, fnmatch glob, and pipe-separated alternatives - Operators: ==, !=, =, +=, -=, := - Assignments: NAME=, MODE=, OWNER=, GROUP=, SYMLINK+=, ENV{k}=, TAG+=, RUN+= - IMPORT{program|file|builtin|parent|cmdline|db}= - PROGRAM= with stdout capture for subsequent RESULT== matching - GOTO=/LABEL= flow control - Loads *.rules from /lib/udev/rules.d, /run/udev/rules.d, /etc/udev/rules.d and an optional extra directory (-r DIR); reloads on SIGHUP Builtin framework (builtin.c): - kmod: load module by MODALIAS or explicit alias - hwdb: match device against *.hwdb text files in udev hwdb dirs; builds correct lookup key per subsystem — evdev:input:b*v*p*e* for input, usb:v*p* for USB, raw modalias for PCI/platform - path_id: build stable ID_PATH / ID_PATH_TAG from sysfs topology (PCI, USB, ATA, NVMe, platform, ACPI, virtio) - usb_id: read idVendor/idProduct/bcdDevice/serial from sysfs; look up ID_VENDOR_FROM_DATABASE and ID_MODEL_FROM_DATABASE from usb.ids (hwdata package) when available; silent fallback when absent - input_id: classify input devices (keyboard, mouse, joystick, touchscreen, touchpad) from evdev capability bitmasks in sysfs - net_id: generate predictable names — MAC-based enx and PCI-slot-based enps[f] - blkid: probe filesystem type, UUID, and label via libblkid; sets ID_FS_* and ID_PART_TABLE_* properties Network interface renaming (uevent.c): - netdev_add() renames interfaces via SIOCSIFNAME when a NAME= rule matched, then sets the Finit dev/ condition on the final name; and any setup using persistent interface naming via udev rules Device node and symlink improvements (uevent.c): - NAME=, MODE=, OWNER=, GROUP= overrides from matched rules applied at mknod/chown time, falling back to the built-in permission table - SYMLINK+= links from rules applied alongside built-in by-id/by-path links Device property database (udevdb.c): - Persist per-device E:/S:/I: records to /run/udev/data/ on ADD/CHANGE, delete on REMOVE; IMPORT{db}= restores saved properties into event env Build: - libblkid (util-linux) is now required for keventd Signed-off-by: Joachim Wiberg --- configure.ac | 8 +- doc/Makefile.am | 2 +- doc/keventd.md | 25 +- doc/udev-matching.md | 123 ++++ keventd/Makefile.am | 10 + keventd/builtin.c | 1530 +++++++++++++++++++++++++++++++++++++++ keventd/builtin.h | 46 ++ keventd/keventd.c | 85 ++- keventd/keventd.h | 89 ++- keventd/rules.c | 1621 ++++++++++++++++++++++++++++++++++++++++++ keventd/rules.h | 163 +++++ keventd/sysfs.c | 131 ++++ keventd/sysfs.h | 75 ++ keventd/udevdb.c | 243 +++++++ keventd/udevdb.h | 72 ++ keventd/uevent.c | 271 ++++++- mkdocs.yml | 1 + 17 files changed, 4451 insertions(+), 44 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 6c8948c2..d3f77798 100644 --- a/configure.ac +++ b/configure.ac @@ -329,7 +329,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"], [ @@ -445,7 +449,7 @@ Optional features: Install doc/..........: $enable_doc Install contrib/......: $enable_contrib Bash completion.......: $bash_completion_dir - Built-in keventd......: $with_keventd + Built-in keventd......: $with_keventd (blkid: $blkid_LIBS) Built-in sulogin......: $with_sulogin $sulogin Built-in watchdogd....: $with_watchdog $watchdog Built-in logrotate....: $enable_logrotate diff --git a/doc/Makefile.am b/doc/Makefile.am index a649398e..dc821c7d 100644 --- a/doc/Makefile.am +++ b/doc/Makefile.am @@ -6,6 +6,6 @@ dist_docs_DATA = build.md cmdline.md commands.md conditions.md \ distro.md example.md features.md index.md initctl.md \ keventd.md plugins.md requirements.md runlevels.md \ runparts.md service.md signals.md state-machine.md \ - watchdog.md + udev-matching.md watchdog.md dist_docs_DATA += img/logo.png img/svc-machine.png img/svc-machine.svg \ img/alpine-screenshot2.png diff --git a/doc/keventd.md b/doc/keventd.md index 1497ef3c..95d08a0a 100644 --- a/doc/keventd.md +++ b/doc/keventd.md @@ -20,9 +20,11 @@ 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 +- **udev rules**: matches events against rules read from + `/lib/udev/rules.d/`, `/run/udev/rules.d/`, and `/etc/udev/rules.d/`, + with a curated ruleset derived from eudev installed by default +- **Persistent symlinks**: creates `by-id` and `by-path` symlinks under + `/dev/disk/`, `/dev/input/`, and elsewhere, 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` @@ -70,19 +72,25 @@ device subsystem and name: Persistent Symlinks ------------------- -For block devices, keventd creates symlinks under `/dev/disk/`: +Symlinks come from two places. A handful are built into keventd and are +created for every device in the subsystem, whatever rules are loaded. + +For block devices, 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/`: +For input devices, 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. +The rest come from `SYMLINK+=` in the udev rules, so what you get +depends on the ruleset installed. + +Built-in and rule-provided symlinks alike are tracked internally and +removed when the corresponding device is unplugged. Firmware Loading @@ -221,7 +229,7 @@ battery: Usage ----- - keventd [-cdGhnpv] [-g GROUP] + keventd [-cdGhnpv] [-g GROUP] [-r DIR] Options: -c Run coldplug at startup @@ -231,6 +239,7 @@ Usage -h Show help text -n Run in foreground (no daemon) -p Passive mode: power supply events only + -r DIR Extra rules directory -v Show version In normal operation, Finit starts keventd automatically via its system 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..faf219b6 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); } @@ -307,8 +342,9 @@ static void handle_uevent(char *buf, size_t len) /* Remove symlinks first */ symlink_del(&ev); - /* Remove device node */ - if (ev.devname) + /* Remove device node, mirrors devnode_add(): net + * interfaces carry a devname but have no node. */ + if (ev.major >= 0 && ev.minor >= 0 && ev.devname) devnode_del(&ev); } break; @@ -327,6 +363,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 +464,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 +483,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 +517,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 +540,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 +561,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 +600,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 +611,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 +664,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); -int devnode_add (struct uevent *ev); -int devnode_del (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 symlink_add (struct uevent *ev); -int symlink_del (struct uevent *ev); +int devnode_add (struct uevent *ev); +int devnode_del (struct uevent *ev); +int netdev_add (struct uevent *ev); -int firmware_load (struct uevent *ev); -int modprobe_load (const char *modalias); +int symlink_add (struct uevent *ev); +int symlink_del (struct uevent *ev); +void symlink_write_db (const char *devpath, FILE *fp); -int coldplug (void); +int firmware_load (struct uevent *ev); +int modprobe_load (const char *modalias); + +int coldplug (void); #endif /* FINIT_KEVENTD_H_ */ diff --git a/keventd/rules.c b/keventd/rules.c new file mode 100644 index 00000000..31580a47 --- /dev/null +++ b/keventd/rules.c @@ -0,0 +1,1621 @@ +/* udev rules file parser and matcher for keventd + * + * Copyright (c) 2025 Joachim Wiberg + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#ifdef _LIBITE_LITE +# include +#else +# include +#endif + +#include "builtin.h" +#include "rules.h" +#include "sysfs.h" +#include "udevdb.h" + +/* Forward declaration from keventd.c */ +void logit(int prio, const char *fmt, ...); + +/* ----- key-name lookup -------------------------------------------------- */ + +static const struct { + const char *name; + rule_key_class_t cls; +} key_map[] = { + { "ACTION", KEY_ACTION }, + { "DEVPATH", KEY_DEVPATH }, + { "KERNEL", KEY_KERNEL }, + { "KERNELS", KEY_KERNELS }, + { "NAME", KEY_NAME }, + { "SUBSYSTEM", KEY_SUBSYSTEM }, + { "SUBSYSTEMS", KEY_SUBSYSTEMS }, + { "DRIVER", KEY_DRIVER }, + { "DRIVERS", KEY_DRIVERS }, + { "ATTR", KEY_ATTR }, + { "ATTRS", KEY_ATTRS }, + { "SYSCTL", KEY_SYSCTL }, + { "ENV", KEY_ENV }, + { "CONST", KEY_CONST }, + { "TAG", KEY_TAG }, + { "TAGS", KEY_TAGS }, + { "TEST", KEY_TEST }, + { "PROGRAM", KEY_PROGRAM }, + { "RESULT", KEY_RESULT }, + { "SYMLINK", KEY_SYMLINK }, + { "OWNER", KEY_OWNER }, + { "GROUP", KEY_GROUP }, + { "MODE", KEY_MODE }, + { "SECLABEL", KEY_SECLABEL }, + { "RUN", KEY_RUN }, + { "LABEL", KEY_LABEL }, + { "GOTO", KEY_GOTO }, + { "IMPORT", KEY_IMPORT }, + { "OPTIONS", KEY_OPTIONS }, +}; + +static rule_key_class_t lookup_key(const char *name) +{ + size_t i; + + for (i = 0; i < NELEMS(key_map); i++) { + if (!strcmp(key_map[i].name, name)) + return key_map[i].cls; + } + return KEY_UNKNOWN; +} + +/* ----- helpers ---------------------------------------------------------- */ + +static pat_type_t detect_pat_type(const char *val) +{ + if (strchr(val, '|')) + return PAT_SPLIT; + if (strpbrk(val, "*?[")) + return PAT_GLOB; + return PAT_PLAIN; +} + +static import_type_t parse_import_type(const char *attr) +{ + if (!attr || !*attr) return IMPORT_PROGRAM; + if (!strcmp(attr, "program")) return IMPORT_PROGRAM; + if (!strcmp(attr, "file")) return IMPORT_FILE; + if (!strcmp(attr, "db")) return IMPORT_DB; + if (!strcmp(attr, "builtin")) return IMPORT_BUILTIN; + if (!strcmp(attr, "parent")) return IMPORT_PARENT; + if (!strcmp(attr, "cmdline")) return IMPORT_CMDLINE; + return IMPORT_PROGRAM; +} + +static run_type_t parse_run_type(const char *attr) +{ + if (!attr || !*attr) return RUN_PROGRAM; + if (!strcmp(attr, "program")) return RUN_PROGRAM; + if (!strcmp(attr, "builtin")) return RUN_BUILTIN; + return RUN_PROGRAM; +} + +/* ----- tokenizer -------------------------------------------------------- */ + +static char *skip_space(char *p) +{ + while (*p == ' ' || *p == '\t') + p++; + return p; +} + +/* Strip inline comment: first '#' not inside double quotes */ +static void strip_comment(char *line) +{ + int in_quote = 0; + char *p; + + for (p = line; *p; p++) { + if (*p == '"') + in_quote ^= 1; + else if (*p == '#' && !in_quote) { + *p = 0; + break; + } + } +} + +/* + * Parse the key name (uppercase letters and '_') and optional {attr} + * suffix. Advances *pp past the parsed portion. + */ +static int parse_key_name(char **pp, + char *name, size_t namesz, + char *attr, size_t attrsz) +{ + char *p = *pp, *end; + size_t n; + + end = p; + while (*end && (isupper((unsigned char)*end) || *end == '_')) + end++; + + n = (size_t)(end - p); + if (!n || n >= namesz) + return -1; + + memcpy(name, p, n); + name[n] = 0; + p = end; + + if (*p == '{') { + p++; + end = strchr(p, '}'); + if (!end) + return -1; + n = (size_t)(end - p); + if (n >= attrsz) + return -1; + memcpy(attr, p, n); + attr[n] = 0; + p = end + 1; + } else { + attr[0] = 0; + } + + *pp = p; + return 0; +} + +static rule_op_t parse_op(char **pp) +{ + char *p = *pp; + rule_op_t op; + + if (p[0] == '=' && p[1] == '=') { op = OP_MATCH_EQ; *pp = p + 2; } + else if (p[0] == '!' && p[1] == '=') { op = OP_MATCH_NE; *pp = p + 2; } + else if (p[0] == '+' && p[1] == '=') { op = OP_ASSIGN_ADD; *pp = p + 2; } + else if (p[0] == '-' && p[1] == '=') { op = OP_ASSIGN_DEL; *pp = p + 2; } + else if (p[0] == ':' && p[1] == '=') { op = OP_ASSIGN_FINAL; *pp = p + 2; } + else if (p[0] == '=') { op = OP_ASSIGN; *pp = p + 1; } + else { op = OP_INVALID; } + + return op; +} + +/* Parse a double-quoted value, advancing *pp past the closing '"'. */ +static int parse_value(char **pp, char *val, size_t valsz) +{ + char *p = *pp, *end; + size_t n; + + if (*p != '"') + return -1; + p++; + + end = strchr(p, '"'); + if (!end) + return -1; + + n = (size_t)(end - p); + if (n >= valsz) + n = valsz - 1; + + memcpy(val, p, n); + val[n] = 0; + *pp = end + 1; + return 0; +} + +/* Free key resources within a rule without freeing the rule itself. */ +static void rule_keys_free(struct rule *r) +{ + int j; + + for (j = 0; j < r->nkeys; j++) { + free(r->keys[j].attr); + free(r->keys[j].value); + r->keys[j].attr = NULL; + r->keys[j].value = NULL; + } + r->nkeys = 0; +} + +/* + * Parse one logical rule line into r. + * Returns 0 if at least one key was successfully parsed. + */ +static int parse_rule_line(struct rule *r, char *src) +{ + char buf[4096]; + char *p; + size_t slen; + + slen = strlen(src); + if (slen >= sizeof(buf)) + slen = sizeof(buf) - 1; + memcpy(buf, src, slen); + buf[slen] = 0; + + p = skip_space(buf); + strip_comment(p); + p = skip_space(p); + + if (!*p) + return -1; + + while (*p) { + char name[64], attr[256], val[1024]; + rule_key_class_t cls; + rule_op_t op; + struct rule_key *k; + + p = skip_space(p); + if (!*p) + break; + + if (parse_key_name(&p, name, sizeof(name), attr, sizeof(attr)) < 0) { + logit(LOG_DEBUG, "rules:%s:%d: parse error near '%s'", + r->filename, r->lineno, p); + return -1; + } + + cls = lookup_key(name); + if (cls == KEY_UNKNOWN) { + logit(LOG_DEBUG, "rules:%s:%d: unknown key '%s'", + r->filename, r->lineno, name); + return -1; + } + + p = skip_space(p); + op = parse_op(&p); + if (op == OP_INVALID) { + logit(LOG_DEBUG, "rules:%s:%d: bad operator after '%s'", + r->filename, r->lineno, name); + return -1; + } + + p = skip_space(p); + if (parse_value(&p, val, sizeof(val)) < 0) { + logit(LOG_DEBUG, "rules:%s:%d: missing value for '%s'", + r->filename, r->lineno, name); + return -1; + } + + if (r->nkeys >= RULE_KEY_MAX) { + logit(LOG_WARNING, "rules:%s:%d: too many keys, truncating", + r->filename, r->lineno); + break; + } + + k = &r->keys[r->nkeys]; + k->cls = cls; + k->op = op; + k->attr = attr[0] ? strdup(attr) : NULL; + k->value = strdup(val); + + if (!k->value) { + free(k->attr); + k->attr = NULL; + break; + } + + k->pat_type = (op == OP_MATCH_EQ || op == OP_MATCH_NE) + ? detect_pat_type(val) : PAT_PLAIN; + k->import_type = (cls == KEY_IMPORT) ? parse_import_type(attr) : IMPORT_PROGRAM; + k->run_type = (cls == KEY_RUN) ? parse_run_type(attr) : RUN_PROGRAM; + + r->nkeys++; + + p = skip_space(p); + if (*p == ',') + p++; + } + + return r->nkeys > 0 ? 0 : -1; +} + +/* ----- file loading ----------------------------------------------------- */ + +static int rules_dirent_filter(const struct dirent *e) +{ + size_t l = strlen(e->d_name); + return l > 6 && !strcmp(e->d_name + l - 6, ".rules"); +} + +int rules_load(struct rule_list *list, const char *path) +{ + char line[2048], cont[4096]; + int lineno = 0, nrules = 0; + FILE *fp; + + fp = fopen(path, "r"); + if (!fp) + return -1; + + cont[0] = 0; + + while (fgets(line, sizeof(line), fp)) { + struct rule *r; + size_t clen, l; + char *p; + + lineno++; + chomp(line); + + /* Accumulate continuation lines */ + l = strlen(line); + if (l > 0 && line[l - 1] == '\\') { + line[l - 1] = 0; + clen = strlen(cont); + snprintf(cont + clen, sizeof(cont) - clen, "%s", line); + continue; + } + clen = strlen(cont); + snprintf(cont + clen, sizeof(cont) - clen, "%s", line); + + /* Skip blank lines and full-line comments */ + p = cont; + while (*p == ' ' || *p == '\t') + p++; + if (!*p || *p == '#') { + cont[0] = 0; + continue; + } + + r = calloc(1, sizeof(*r)); + if (!r) + break; + + r->filename = strdup(path); + r->lineno = lineno; + + if (r->filename && parse_rule_line(r, cont) == 0) { + TAILQ_INSERT_TAIL(list, r, link); + nrules++; + } else { + rule_keys_free(r); + free(r->filename); + free(r); + } + + cont[0] = 0; + } + + fclose(fp); + return nrules; +} + +int rules_load_all(struct rule_list *list, const char *extra_dir) +{ + static const char *std_dirs[] = { + "/lib/udev/rules.d", + "/run/udev/rules.d", + "/etc/udev/rules.d", + NULL + }; + const char *dirs[8]; + int ndirs = 0, total = 0, d; + + for (d = 0; std_dirs[d]; d++) + dirs[ndirs++] = std_dirs[d]; + + if (extra_dir) + dirs[ndirs++] = extra_dir; + dirs[ndirs] = NULL; + + for (d = 0; d < ndirs; d++) { + struct dirent **ents = NULL; + char path[PATH_MAX]; + int n, i; + + n = scandir(dirs[d], &ents, rules_dirent_filter, alphasort); + if (n < 0) + continue; + + for (i = 0; i < n; i++) { + int rc; + + snprintf(path, sizeof(path), "%s/%s", dirs[d], ents[i]->d_name); + rc = rules_load(list, path); + if (rc > 0) { + logit(LOG_DEBUG, "rules: %d rules from %s", rc, path); + total += rc; + } + free(ents[i]); + } + free(ents); + } + + if (total) + logit(LOG_NOTICE, "rules: %d rules loaded", total); + + return total; +} + +void rules_free(struct rule_list *list) +{ + struct rule *r, *tmp; + + TAILQ_FOREACH_SAFE(r, list, link, tmp) { + TAILQ_REMOVE(list, r, link); + rule_keys_free(r); + free(r->filename); + free(r); + } +} + +/* ===== Phase 3: full matcher ============================================ */ + +/* ----- variable substitution ------------------------------------------- */ + +static void append_str(char *buf, size_t *pos, size_t bufsz, const char *s) +{ + size_t l; + + if (!s || !*s) + return; + l = strlen(s); + if (*pos + l >= bufsz) + l = bufsz - *pos - 1; + if (l) { + memcpy(buf + *pos, s, l); + *pos += l; + buf[*pos] = 0; + } +} + +/* + * Expand udev-format substitution operators from src into buf. + * Handles both the %X short form and the $name long form. + */ +static void subst_value(const char *src, char *buf, size_t bufsz, + const struct uevent *ev) +{ + const char *p = src; + size_t pos = 0; + + buf[0] = 0; + if (!src) + return; + + while (*p && pos + 1 < bufsz) { + char tmp[256]; + + if (*p != '%' && *p != '$') { + buf[pos++] = *p++; + buf[pos] = 0; + continue; + } + + if (*p == '%') { + char c = *++p; + + switch (c) { + case '%': + buf[pos++] = '%'; buf[pos] = 0; p++; + break; + case 'k': + append_str(buf, &pos, bufsz, uevent_sysname(ev)); + p++; + break; + case 'p': + append_str(buf, &pos, bufsz, ev->devpath); + p++; + break; + case 'D': case 'N': + append_str(buf, &pos, bufsz, ev->devname); + p++; + break; + case 'M': + snprintf(tmp, sizeof(tmp), "%d", ev->major); + append_str(buf, &pos, bufsz, tmp); + p++; + break; + case 'm': + snprintf(tmp, sizeof(tmp), "%d", ev->minor); + append_str(buf, &pos, bufsz, tmp); + p++; + break; + case 'b': + snprintf(tmp, sizeof(tmp), "%d:%d", ev->major, ev->minor); + append_str(buf, &pos, bufsz, tmp); + p++; + break; + case 'd': + append_str(buf, &pos, bufsz, ev->driver); + p++; + break; + case 'n': { + /* trailing digit suffix of sysname */ + const char *sn = uevent_sysname(ev); + if (sn) { + const char *e = sn + strlen(sn); + while (e > sn && isdigit((unsigned char)*(e - 1))) + e--; + append_str(buf, &pos, bufsz, e); + } + p++; + break; + } + case 's': + /* %s{attr} — sysfs attribute of current device */ + if (p[1] == '{') { + const char *as = p + 2; + const char *ae = strchr(as, '}'); + + if (ae && ev->devpath) { + char attr[256]; + size_t al = (size_t)(ae - as); + + if (al >= sizeof(attr)) + al = sizeof(attr) - 1; + memcpy(attr, as, al); + attr[al] = 0; + if (!sysfs_read_attr(ev->devpath, attr, + tmp, sizeof(tmp))) + append_str(buf, &pos, bufsz, tmp); + p = ae + 1; + } else { + buf[pos++] = '%'; buf[pos] = 0; + } + } else { + buf[pos++] = '%'; buf[pos] = 0; + } + break; + case 'c': + /* %c or %c{n} — PROGRAM result or nth space-separated field */ + if (ev->result) { + if (p[1] == '{') { + const char *ns = p + 2; + const char *ne = strchr(ns, '}'); + + if (ne) { + int field = atoi(ns); + const char *rp = ev->result; + int f = 0; + + while (*rp && f < field) { + while (*rp && *rp != ' ') + rp++; + while (*rp == ' ') + rp++; + f++; + } + const char *re = rp; + while (*re && *re != ' ') + re++; + size_t fl = (size_t)(re - rp); + if (fl >= sizeof(tmp)) + fl = sizeof(tmp) - 1; + memcpy(tmp, rp, fl); + tmp[fl] = 0; + append_str(buf, &pos, bufsz, tmp); + p = ne + 1; + } else { + append_str(buf, &pos, bufsz, ev->result); + p++; + } + } else { + append_str(buf, &pos, bufsz, ev->result); + p++; + } + } else { + p++; + } + break; + default: + /* unknown specifier — pass '%' through, re-parse next char */ + buf[pos++] = '%'; buf[pos] = 0; + break; + } + + } else { /* '$' */ + const char *kw = ++p; + + if (*kw == '$') { + buf[pos++] = '$'; buf[pos] = 0; + p++; + } else if (!strncmp(kw, "attr{", 5)) { + int off = 5; + const char *as = kw + off; + const char *ae = strchr(as, '}'); + + if (ae && ev->devpath) { + char attr[256]; + size_t al = (size_t)(ae - as); + + if (al >= sizeof(attr)) + al = sizeof(attr) - 1; + memcpy(attr, as, al); + attr[al] = 0; + if (!sysfs_read_attr(ev->devpath, attr, tmp, sizeof(tmp))) + append_str(buf, &pos, bufsz, tmp); + p = ae + 1; + } else { + buf[pos++] = '$'; buf[pos] = 0; + } + } else if (!strncmp(kw, "env{", 4)) { + const char *ks = kw + 4; + const char *ke = strchr(ks, '}'); + + if (ke) { + char key[256]; + size_t kl = (size_t)(ke - ks); + + if (kl >= sizeof(key)) + kl = sizeof(key) - 1; + memcpy(key, ks, kl); + key[kl] = 0; + append_str(buf, &pos, bufsz, uevent_getenv(ev, key)); + p = ke + 1; + } else { + buf[pos++] = '$'; buf[pos] = 0; + } + } else { + /* Named scalar variables */ + static const struct { + const char *name; + size_t len; + } named[] = { + { "kernel", 6 }, { "name", 4 }, + { "devpath", 7 }, { "driver", 6 }, + { "result", 6 }, { "root", 4 }, + { "sys", 3 }, { "major", 5 }, + { "minor", 5 }, + }; + size_t i; + int handled = 0; + + for (i = 0; i < NELEMS(named); i++) { + if (strncmp(kw, named[i].name, named[i].len)) + continue; + switch (i) { + case 0: + append_str(buf, &pos, bufsz, uevent_sysname(ev)); + break; + case 1: + append_str(buf, &pos, bufsz, ev->devname); + break; + case 2: + append_str(buf, &pos, bufsz, ev->devpath); + break; + case 3: + append_str(buf, &pos, bufsz, ev->driver); + break; + case 4: + append_str(buf, &pos, bufsz, ev->result); + break; + case 5: + append_str(buf, &pos, bufsz, "/dev"); + break; + case 6: + append_str(buf, &pos, bufsz, "/sys"); + break; + case 7: + snprintf(tmp, sizeof(tmp), "%d", ev->major); + append_str(buf, &pos, bufsz, tmp); + break; + case 8: + snprintf(tmp, sizeof(tmp), "%d", ev->minor); + append_str(buf, &pos, bufsz, tmp); + break; + } + p = kw + named[i].len; + handled = 1; + break; + } + if (!handled) { + buf[pos++] = '$'; buf[pos] = 0; + /* don't advance p — let next iter handle kw[0] */ + } + } + } + } +} + +/* ----- pattern matching ------------------------------------------------- */ + +/* + * Match subject against a single pattern (no pipe splitting). + * Always uses fnmatch so that plain strings and globs work uniformly. + */ +static int pattern_match_one(const char *pat, const char *subject) +{ + return fnmatch(pat, subject, 0) == 0; +} + +static int pattern_match(const char *pat, const char *subject, pat_type_t type) +{ + if (type == PAT_PLAIN) + return !strcmp(pat, subject); + + if (type == PAT_SPLIT) { + char copy[1024]; + char *p, *tok; + + snprintf(copy, sizeof(copy), "%s", pat); + p = copy; + while ((tok = strsep(&p, "|")) != NULL) { + if (pattern_match_one(tok, subject)) + return 1; + } + return 0; + } + + /* PAT_GLOB */ + return pattern_match_one(pat, subject); +} + +/* ----- program execution ------------------------------------------------ */ + +/* + * Fork and exec cmd via /bin/sh, capture stdout into result. + * Temporarily restores SIGCHLD (keventd uses SIG_IGN) so waitpid works. + */ +static int run_program(const char *cmd, char *result, size_t rlen) +{ + struct sigaction sa_dfl, sa_old; + int pipefd[2]; + pid_t pid; + int rc = -1; + + if (!cmd || !*cmd) + return -1; + + if (pipe(pipefd) < 0) + return -1; + + sigemptyset(&sa_dfl.sa_mask); + sa_dfl.sa_flags = 0; + sa_dfl.sa_handler = SIG_DFL; + sigaction(SIGCHLD, &sa_dfl, &sa_old); + + pid = fork(); + if (pid < 0) { + sigaction(SIGCHLD, &sa_old, NULL); + close(pipefd[0]); + close(pipefd[1]); + return -1; + } + + if (pid == 0) { + close(pipefd[0]); + dup2(pipefd[1], STDOUT_FILENO); + close(pipefd[1]); + execl("/bin/sh", "sh", "-c", cmd, NULL); + _exit(127); + } + + close(pipefd[1]); + + { + ssize_t n = read(pipefd[0], result, rlen - 1); + if (n < 0) + n = 0; + result[n] = 0; + chomp(result); + } + close(pipefd[0]); + + { + int status; + + if (waitpid(pid, &status, 0) > 0) + rc = WIFEXITED(status) ? WEXITSTATUS(status) : -1; + } + + sigaction(SIGCHLD, &sa_old, NULL); + return rc; +} + +/* ----- parent-chain matching -------------------------------------------- */ + +struct parent_ctx { + const struct rule_key *k; + const struct uevent *ev; + int found; +}; + +static int parent_cb(const char *syspath, void *data) +{ + struct parent_ctx *ctx = data; + const struct rule_key *k = ctx->k; + char buf[256]; + const char *subject = NULL; + + switch (k->cls) { + case KEY_KERNELS: { + const char *p = strrchr(syspath, '/'); + subject = p ? p + 1 : syspath; + break; + } + case KEY_SUBSYSTEMS: + if (sysfs_read_subsystem(syspath, buf, sizeof(buf)) < 0) + return 1; + subject = buf; + break; + case KEY_DRIVERS: + if (sysfs_read_driver(syspath, buf, sizeof(buf)) < 0) + return 1; + subject = buf; + break; + case KEY_ATTRS: { + /* Read sysfs attribute directly from this level of the tree */ + char path[PATH_MAX]; + FILE *fp; + + snprintf(path, sizeof(path), "%s/%s", syspath, k->attr ?: ""); + fp = fopen(path, "r"); + if (!fp) + return 1; + if (!fgets(buf, sizeof(buf), fp)) { + fclose(fp); + return 1; + } + fclose(fp); + chomp(buf); + subject = buf; + break; + } + default: + return 1; + } + + if (!subject) + return 1; + + if (pattern_match(k->value, subject, k->pat_type)) { + ctx->found = 1; + return 0; /* stop walking */ + } + return 1; /* keep walking */ +} + +static int match_parent_chain(const struct rule_key *k, const struct uevent *ev) +{ + struct parent_ctx ctx = { k, ev, 0 }; + int matched; + + if (!ev->devpath) + return (k->op == OP_MATCH_NE); + + sysfs_parent_walk(ev->devpath, parent_cb, &ctx); + + matched = ctx.found; + return (k->op == OP_MATCH_EQ) ? matched : !matched; +} + +/* ----- per-key dispatch ------------------------------------------------- */ + +/* + * Read one line from path into buf (for SYSCTL matching). + */ +static int read_oneline(const char *path, char *buf, size_t len) +{ + FILE *fp = fopen(path, "r"); + + if (!fp) + return -1; + if (!fgets(buf, len, fp)) { + fclose(fp); + return -1; + } + fclose(fp); + chomp(buf); + return 0; +} + +/* + * Test one key against the event. + * Returns 1 if the key condition is satisfied, 0 if not. + */ +static int match_key(const struct rule *r, const struct rule_key *k, + struct uevent *ev) +{ + char subj_buf[1024] = ""; + const char *subject = NULL; + int matched; + + /* Parent-chain keys have their own walk logic */ + switch (k->cls) { + case KEY_KERNELS: + case KEY_SUBSYSTEMS: + case KEY_DRIVERS: + case KEY_ATTRS: + case KEY_TAGS: + return match_parent_chain(k, ev); + default: + break; + } + + /* PROGRAM= and TEST= have dedicated logic */ + if (k->cls == KEY_PROGRAM) { + char cmd[PATH_MAX], out[1024]; + int rc; + + subst_value(k->value, cmd, sizeof(cmd), ev); + rc = run_program(cmd, out, sizeof(out)); + free(ev->result); + ev->result = strdup(out); + matched = (rc == 0); + return (k->op == OP_MATCH_EQ) ? matched : !matched; + } + + if (k->cls == KEY_TEST) { + char path[PATH_MAX]; + struct stat st; + + subst_value(k->value, path, sizeof(path), ev); + if (stat(path, &st) < 0) { + matched = 0; + } else if (k->attr && k->attr[0]) { + mode_t req = (mode_t)strtoul(k->attr, NULL, 8); + matched = ((st.st_mode & req) == req); + } else { + matched = 1; + } + return (k->op == OP_MATCH_EQ) ? matched : !matched; + } + + /* All other match keys: compute subject string, then pattern match */ + switch (k->cls) { + case KEY_ACTION: + subject = uevent_action_str(ev->action); + break; + case KEY_DEVPATH: + subject = ev->devpath; + break; + case KEY_KERNEL: + subject = uevent_sysname(ev); + break; + case KEY_NAME: + subject = ev->devname; + break; + case KEY_SUBSYSTEM: + subject = ev->subsystem; + break; + case KEY_DRIVER: + subject = ev->driver; + break; + case KEY_ATTR: + if (!k->attr || !ev->devpath) + return (k->op == OP_MATCH_NE); + if (sysfs_read_attr(ev->devpath, k->attr, subj_buf, sizeof(subj_buf)) < 0) + return (k->op == OP_MATCH_NE); + subject = subj_buf; + break; + case KEY_SYSCTL: { + char path[PATH_MAX]; + char *dp; + + if (!k->attr) + return (k->op == OP_MATCH_NE); + snprintf(path, sizeof(path), "/proc/sys/%s", k->attr); + for (dp = path + 10; *dp; dp++) + if (*dp == '.') + *dp = '/'; + if (read_oneline(path, subj_buf, sizeof(subj_buf)) < 0) + return (k->op == OP_MATCH_NE); + subject = subj_buf; + break; + } + case KEY_ENV: + if (!k->attr) + return (k->op == OP_MATCH_NE); + subject = uevent_getenv(ev, k->attr); + break; + case KEY_CONST: { + if (!k->attr) { + subject = ""; + } else if (!strcmp(k->attr, "arch")) { + struct utsname uts; + if (uname(&uts) == 0) + snprintf(subj_buf, sizeof(subj_buf), "%s", uts.machine); + subject = subj_buf; + } else if (!strcmp(k->attr, "virt")) { + subject = (fexist("/.dockerenv") || + fexist("/run/.containerenv")) ? "container" : ""; + } else { + subject = ""; + } + break; + } + case KEY_TAG: { + int i; + matched = 0; + for (i = 0; i < ev->ntags; i++) { + if (pattern_match(k->value, ev->tags[i], k->pat_type)) { + matched = 1; + break; + } + } + return (k->op == OP_MATCH_EQ) ? matched : !matched; + } + case KEY_RESULT: + subject = ev->result; + break; + default: + /* Assignment-only key encountered in match position: skip */ + logit(LOG_DEBUG, "rules:%s:%d: skipping non-match key %d in match phase", + r->filename, r->lineno, k->cls); + return 1; + } + + if (!subject) + return (k->op == OP_MATCH_NE); + + matched = pattern_match(k->value, subject, k->pat_type); + return (k->op == OP_MATCH_EQ) ? matched : !matched; +} + +/* ----- rule-level AND logic --------------------------------------------- */ + +/* + * Returns 1 if all match-op keys in the rule are satisfied. + */ +static int rule_matches(const struct rule *r, struct uevent *ev) +{ + int i; + + for (i = 0; i < r->nkeys; i++) { + const struct rule_key *k = &r->keys[i]; + + /* Skip assignment-only operators */ + if (k->op != OP_MATCH_EQ && k->op != OP_MATCH_NE) + continue; + + if (!match_key(r, k, ev)) + return 0; + } + return 1; +} + +/* ===== Phase 4: executor ================================================ */ + +/* ----- UID/GID resolution ----------------------------------------------- */ + +static uid_t resolve_uid(const char *s) +{ + if (!s || !*s) + return 0; + if (*s >= '0' && *s <= '9') + return (uid_t)atoi(s); + { + struct passwd *pw = getpwnam(s); + return pw ? pw->pw_uid : 0; + } +} + +static gid_t resolve_gid(const char *s) +{ + if (!s || !*s) + return 0; + if (*s >= '0' && *s <= '9') + return (gid_t)atoi(s); + { + struct group *gr = getgrnam(s); + return gr ? gr->gr_gid : 0; + } +} + +/* ----- IMPORT helpers --------------------------------------------------- */ + +/* + * Fork, exec cmd, parse stdout as KEY=VALUE (or KEY="VALUE") pairs + * and import them into ev's env store. + */ +/* + * Helper-first, builtin-fallback for IMPORT{program}= / RUN+="...". + * + * Stock udev rules invoke /lib/udev/ (path_id, usb_id, blkid, ...) + * via IMPORT{program}=. keventd ships compatible builtins for many of + * those. If the helper is absent on this system, fall back to the + * matching builtin so the rule still works. If the helper exists, run + * it as-is -- this lets users override our builtins by dropping a + * custom helper into /lib/udev/. + * + * Returns 1 if the builtin handled the command, 0 if the caller should + * proceed with the normal fork+exec path. + */ +static int try_builtin_fallback(const char *cmd, struct uevent *ev) +{ + char first[PATH_MAX], rebuilt[PATH_MAX]; + const char *space, *base; + size_t len; + + space = strchr(cmd, ' '); + len = space ? (size_t)(space - cmd) : strlen(cmd); + if (len == 0 || len >= sizeof(first)) + return 0; + memcpy(first, cmd, len); + first[len] = '\0'; + + /* Helper exists -- let caller fork+exec it (allows user override). */ + if (access(first, X_OK) == 0) + return 0; + + base = strrchr(first, '/'); + base = base ? base + 1 : first; + + if (!builtin_has(base)) + return 0; + + if (space) + snprintf(rebuilt, sizeof(rebuilt), "%s%s", base, space); + else + snprintf(rebuilt, sizeof(rebuilt), "%s", base); + + logit(LOG_DEBUG, "rules: %s not found, falling back to builtin %s", first, base); + builtin_run(ev, rebuilt); + return 1; +} + +static void import_from_program(const char *cmd, struct uevent *ev) +{ + struct sigaction sa_dfl, sa_old; + int pipefd[2]; + pid_t pid; + FILE *fp; + char line[512]; + + if (!cmd || !*cmd) + return; + if (try_builtin_fallback(cmd, ev)) + return; + if (pipe(pipefd) < 0) + return; + + sigemptyset(&sa_dfl.sa_mask); + sa_dfl.sa_flags = 0; + sa_dfl.sa_handler = SIG_DFL; + sigaction(SIGCHLD, &sa_dfl, &sa_old); + + pid = fork(); + if (pid < 0) { + sigaction(SIGCHLD, &sa_old, NULL); + close(pipefd[0]); + close(pipefd[1]); + return; + } + + if (pid == 0) { + close(pipefd[0]); + dup2(pipefd[1], STDOUT_FILENO); + close(pipefd[1]); + execl("/bin/sh", "sh", "-c", cmd, NULL); + _exit(127); + } + + close(pipefd[1]); + fp = fdopen(pipefd[0], "r"); + if (fp) { + while (fgets(line, sizeof(line), fp)) { + char *eq, *val; + + chomp(line); + if (!*line) + continue; + eq = strchr(line, '='); + if (!eq) + continue; + *eq++ = 0; + val = eq; + /* Strip surrounding quotes from value */ + if (*val == '"') { + val++; + char *end = strrchr(val, '"'); + if (end) + *end = 0; + } + uevent_setenv(ev, line, val); + } + fclose(fp); + } else { + close(pipefd[0]); + } + + { + int status; + waitpid(pid, &status, 0); + } + sigaction(SIGCHLD, &sa_old, NULL); +} + +/* + * Read KEY=VALUE (or KEY="VALUE") pairs from a file into ev's env store. + */ +static void import_from_file(const char *path, struct uevent *ev) +{ + FILE *fp; + char line[512]; + + fp = fopen(path, "r"); + if (!fp) + return; + + while (fgets(line, sizeof(line), fp)) { + char *eq, *val; + + chomp(line); + if (!*line || *line == '#') + continue; + eq = strchr(line, '='); + if (!eq) + continue; + *eq++ = 0; + val = eq; + if (*val == '"') { + val++; + char *end = strrchr(val, '"'); + if (end) + *end = 0; + } + uevent_setenv(ev, line, val); + } + + fclose(fp); +} + +/* + * Import a matching key (or key=value) from /proc/cmdline. + * The pattern in val is matched against each key on the cmdline. + */ +static void import_from_cmdline(const char *key_pattern, struct uevent *ev) +{ + FILE *fp; + char line[2048]; + char *p, *tok; + + fp = fopen("/proc/cmdline", "r"); + if (!fp) + return; + + if (!fgets(line, sizeof(line), fp)) { + fclose(fp); + return; + } + fclose(fp); + + p = line; + while ((tok = strsep(&p, " \t\n")) != NULL) { + char *eq = strchr(tok, '='); + + if (eq) { + *eq++ = 0; + if (fnmatch(key_pattern, tok, 0) == 0) + uevent_setenv(ev, tok, eq); + } else if (*tok && fnmatch(key_pattern, tok, 0) == 0) { + uevent_setenv(ev, tok, "1"); + } + } +} + +static void exec_import(const struct rule_key *k, struct uevent *ev, + const char *val) +{ + switch (k->import_type) { + case IMPORT_PROGRAM: + import_from_program(val, ev); + break; + case IMPORT_FILE: + import_from_file(val, ev); + break; + case IMPORT_DB: + udevdb_read(ev); + break; + case IMPORT_PARENT: + udevdb_read_parent(ev); + break; + case IMPORT_CMDLINE: + import_from_cmdline(val, ev); + break; + case IMPORT_BUILTIN: + builtin_run(ev, val); + break; + } +} + +/* ----- assignment executor ---------------------------------------------- */ + +static void exec_rule(const struct rule *r, struct uevent *ev) +{ + int i; + + for (i = 0; i < r->nkeys; i++) { + const struct rule_key *k = &r->keys[i]; + char val[PATH_MAX]; + + /* Skip match-only operators */ + if (k->op == OP_MATCH_EQ || k->op == OP_MATCH_NE) + continue; + + subst_value(k->value, val, sizeof(val), ev); + + switch (k->cls) { + case KEY_MODE: + if (!ev->applied.final_mode) { + ev->applied.mode = (mode_t)strtoul(val, NULL, 8); + ev->applied.has_mode = 1; + if (k->op == OP_ASSIGN_FINAL) + ev->applied.final_mode = 1; + } + break; + + case KEY_OWNER: + if (!ev->applied.final_owner) { + ev->applied.uid = resolve_uid(val); + ev->applied.has_owner = 1; + if (k->op == OP_ASSIGN_FINAL) + ev->applied.final_owner = 1; + } + break; + + case KEY_GROUP: + if (!ev->applied.final_group) { + ev->applied.gid = resolve_gid(val); + ev->applied.has_group = 1; + if (k->op == OP_ASSIGN_FINAL) + ev->applied.final_group = 1; + } + break; + + case KEY_NAME: + if (k->op == OP_ASSIGN || k->op == OP_ASSIGN_FINAL) { + free(ev->applied.name); + ev->applied.name = strdup(val); + } + break; + + case KEY_SYMLINK: + if (k->op == OP_ASSIGN) { + /* Clear existing, set single entry */ + int j; + for (j = 0; j < ev->applied.nsymlinks; j++) { + free(ev->applied.symlinks[j]); + ev->applied.symlinks[j] = NULL; + } + ev->applied.nsymlinks = 0; + /* Fall through to add */ + } + if (k->op == OP_ASSIGN || k->op == OP_ASSIGN_ADD) { + /* Value may be space-separated list */ + char copy[PATH_MAX], *p, *tok; + snprintf(copy, sizeof(copy), "%s", val); + p = copy; + while ((tok = strsep(&p, " ")) != NULL) { + if (!*tok) + continue; + if (ev->applied.nsymlinks >= RULE_SYMLINK_MAX) + break; + ev->applied.symlinks[ev->applied.nsymlinks] = strdup(tok); + if (ev->applied.symlinks[ev->applied.nsymlinks]) + ev->applied.nsymlinks++; + } + } + break; + + case KEY_ENV: + if (!k->attr) + break; + if (k->op == OP_ASSIGN || k->op == OP_ASSIGN_FINAL) { + uevent_setenv(ev, k->attr, val); + } else if (k->op == OP_ASSIGN_ADD) { + const char *cur = uevent_getenv(ev, k->attr); + if (cur && *cur) { + char combined[1024]; + snprintf(combined, sizeof(combined), "%s%s", cur, val); + uevent_setenv(ev, k->attr, combined); + } else { + uevent_setenv(ev, k->attr, val); + } + } else if (k->op == OP_ASSIGN_DEL) { + uevent_setenv(ev, k->attr, ""); + } + break; + + case KEY_TAG: + if (k->op == OP_ASSIGN_ADD && + ev->ntags < UEVENT_TAG_MAX) { + ev->tags[ev->ntags] = strdup(val); + if (ev->tags[ev->ntags]) + ev->ntags++; + } else if (k->op == OP_ASSIGN_DEL) { + int j; + for (j = 0; j < ev->ntags; j++) { + if (!strcmp(ev->tags[j], val)) { + free(ev->tags[j]); + ev->tags[j] = ev->tags[--ev->ntags]; + ev->tags[ev->ntags] = NULL; + break; + } + } + } + break; + + case KEY_RUN: + if ((k->op == OP_ASSIGN_ADD || k->op == OP_ASSIGN) && + ev->applied.nruncmds < RULE_RUN_MAX) { + /* Store raw value; substitution happens at execution time */ + ev->applied.run_cmds[ev->applied.nruncmds] = strdup(k->value); + if (ev->applied.run_cmds[ev->applied.nruncmds]) { + ev->applied.run_types[ev->applied.nruncmds] = k->run_type; + ev->applied.nruncmds++; + } + } + break; + + case KEY_IMPORT: + exec_import(k, ev, val); + break; + + case KEY_ATTR: + /* ATTR{file}= writes value to sysfs attribute */ + if (k->attr && ev->devpath) { + char path[PATH_MAX]; + FILE *fp; + + snprintf(path, sizeof(path), "/sys%s/%s", + ev->devpath, k->attr); + fp = fopen(path, "w"); + if (fp) { + fputs(val, fp); + fclose(fp); + } + } + break; + + case KEY_SYSCTL: + /* SYSCTL{param}= writes value to /proc/sys/ */ + if (k->attr) { + char path[PATH_MAX]; + char *dp; + FILE *fp; + + snprintf(path, sizeof(path), "/proc/sys/%s", k->attr); + for (dp = path + 10; *dp; dp++) + if (*dp == '.') + *dp = '/'; + fp = fopen(path, "w"); + if (fp) { + fputs(val, fp); + fclose(fp); + } + } + break; + + case KEY_SECLABEL: + logit(LOG_DEBUG, "rules: %s:%d: SECLABEL not supported", + r->filename, r->lineno); + break; + + case KEY_OPTIONS: + logit(LOG_DEBUG, "rules: %s:%d: OPTIONS not supported: %s", + r->filename, r->lineno, k->value ?: ""); + break; + + case KEY_LABEL: + case KEY_GOTO: + /* Handled at loop level in rules_apply() */ + break; + + default: + break; + } + } +} + +/* ----- post-event RUN executor ------------------------------------------ */ + +void rules_run_cmds(struct uevent *ev) +{ + int i; + + for (i = 0; i < ev->applied.nruncmds; i++) { + char cmd[PATH_MAX]; + + if (!ev->applied.run_cmds[i]) + continue; + + subst_value(ev->applied.run_cmds[i], cmd, sizeof(cmd), ev); + logit(LOG_DEBUG, "rules: RUN %s", cmd); + + if (ev->applied.run_types[i] == RUN_BUILTIN) { + builtin_run(ev, cmd); + } else if (!try_builtin_fallback(cmd, ev)) { + pid_t pid = fork(); + + if (pid == 0) { + execl("/bin/sh", "sh", "-c", cmd, NULL); + _exit(127); + } + /* Parent: SIGCHLD=SIG_IGN in keventd auto-reaps the child */ + } + } +} + +/* ----- main applier ----------------------------------------------------- */ + +int rules_apply(struct rule_list *list, struct uevent *ev) +{ + const char *goto_label = NULL; + struct rule *r; + int i; + + TAILQ_FOREACH(r, list, link) { + /* GOTO: skip rules until matching LABEL= is found */ + if (goto_label) { + const char *lbl = NULL; + + for (i = 0; i < r->nkeys; i++) { + if (r->keys[i].cls == KEY_LABEL && + r->keys[i].op == OP_ASSIGN) + lbl = r->keys[i].value; + } + if (!lbl || strcmp(lbl, goto_label)) + continue; + goto_label = NULL; + } + + if (!rule_matches(r, ev)) + continue; + + logit(LOG_DEBUG, "rules: %s:%d matched %s@%s", + r->filename, r->lineno, + uevent_action_str(ev->action), ev->devpath ?: ""); + + exec_rule(r, ev); + + /* GOTO takes effect after this rule's assignments are applied */ + goto_label = NULL; + for (i = 0; i < r->nkeys; i++) { + if (r->keys[i].cls == KEY_GOTO && + r->keys[i].op == OP_ASSIGN) { + goto_label = r->keys[i].value; + break; + } + } + } + + return 0; +} + +/** + * Local Variables: + * indent-tabs-mode: t + * c-file-style: "linux" + * End: + */ diff --git a/keventd/rules.h b/keventd/rules.h new file mode 100644 index 00000000..60ff8ee8 --- /dev/null +++ b/keventd/rules.h @@ -0,0 +1,163 @@ +/* udev rules file parser for keventd + * + * Copyright (c) 2025 Joachim Wiberg + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef KEVENTD_RULES_H_ +#define KEVENTD_RULES_H_ + +#ifdef _LIBITE_LITE +# include +#else +# include +#endif + +#include "keventd.h" + +/* Operators from udev rules syntax */ +typedef enum { + OP_INVALID = 0, + OP_MATCH_EQ, /* == */ + OP_MATCH_NE, /* != */ + OP_ASSIGN, /* = */ + OP_ASSIGN_ADD, /* += */ + OP_ASSIGN_DEL, /* -= */ + OP_ASSIGN_FINAL, /* := */ +} rule_op_t; + +/* All udev match and assignment key types */ +typedef enum { + KEY_UNKNOWN = 0, + /* Match keys — current device */ + KEY_ACTION, + KEY_DEVPATH, + KEY_KERNEL, + KEY_NAME, + KEY_SUBSYSTEM, + KEY_DRIVER, + KEY_ATTR, /* ATTR{filename} */ + KEY_SYSCTL, /* SYSCTL{parameter} */ + KEY_ENV, /* ENV{variable} */ + KEY_CONST, /* CONST{key} */ + KEY_TAG, + KEY_TEST, /* TEST{mode} */ + KEY_PROGRAM, + KEY_RESULT, + /* Match keys — parent-chain walk */ + KEY_KERNELS, + KEY_SUBSYSTEMS, + KEY_DRIVERS, + KEY_ATTRS, /* ATTRS{filename} */ + KEY_TAGS, + /* Assignment keys */ + KEY_SYMLINK, + KEY_OWNER, + KEY_GROUP, + KEY_MODE, + KEY_SECLABEL, /* SECLABEL{module} */ + KEY_RUN, /* RUN{type} */ + KEY_LABEL, + KEY_GOTO, + KEY_IMPORT, /* IMPORT{type} */ + KEY_OPTIONS, +} rule_key_class_t; + +/* Pattern matching strategy for match-op keys */ +typedef enum { + PAT_PLAIN = 0, /* literal strcmp */ + PAT_GLOB, /* fnmatch */ + PAT_SPLIT, /* pipe-separated alternatives */ +} pat_type_t; + +/* IMPORT{type} subtypes */ +typedef enum { + IMPORT_PROGRAM = 0, /* run program, parse KEY=VALUE output */ + IMPORT_FILE, /* read KEY=VALUE from file */ + IMPORT_DB, /* restore properties from udev database */ + IMPORT_BUILTIN, /* run builtin, parse output */ + IMPORT_PARENT, /* copy from parent device's database entry */ + IMPORT_CMDLINE, /* import matching key from /proc/cmdline */ +} import_type_t; + +/* One key–operator–value triplet within a rule */ +struct rule_key { + rule_key_class_t cls; + rule_op_t op; + char *attr; /* ATTR{x}/ENV{x}/… — heap-allocated, may be NULL */ + char *value; /* pattern or assignment value — heap-allocated */ + pat_type_t pat_type; + import_type_t import_type; + run_type_t run_type; +}; + +#define RULE_KEY_MAX 32 + +/* One parsed rule (a single non-empty, non-comment line) */ +struct rule { + TAILQ_ENTRY(rule) link; + char *filename; /* heap-allocated source path */ + int lineno; + int nkeys; + struct rule_key keys[RULE_KEY_MAX]; +}; + +TAILQ_HEAD(rule_list, rule); + +/* + * Load rules from a single .rules file, appending to list. + * Returns the number of rules loaded, or -1 on open failure. + */ +int rules_load (struct rule_list *list, const char *path); + +/* + * Load all *.rules files from the standard udev directories + * (/lib/udev/rules.d, /run/udev/rules.d, /etc/udev/rules.d) + * plus extra_dir if non-NULL. Files are loaded in alphanumeric + * order within each directory; directories are processed in order. + * Returns total number of rules loaded. + */ +int rules_load_all(struct rule_list *list, const char *extra_dir); + +/* + * Free all rules and release their resources. + */ +void rules_free (struct rule_list *list); + +/* + * Apply all matching rules from list to ev, updating ev->applied, + * ev->result, env store, and tag list. + */ +int rules_apply (struct rule_list *list, struct uevent *ev); + +/* + * Execute the RUN+= commands accumulated in ev->applied.run_cmds. + * Call after all device handling (nodes, symlinks) is complete. + */ +void rules_run_cmds(struct uevent *ev); + +#endif /* KEVENTD_RULES_H_ */ + +/** + * Local Variables: + * indent-tabs-mode: t + * c-file-style: "linux" + * End: + */ diff --git a/keventd/sysfs.c b/keventd/sysfs.c new file mode 100644 index 00000000..a9e88326 --- /dev/null +++ b/keventd/sysfs.c @@ -0,0 +1,131 @@ +/* sysfs attribute reader and parent-chain walker + * + * Copyright (c) 2025 Joachim Wiberg + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include +#include +#include +#include + +#ifdef _LIBITE_LITE +# include +#else +# include +#endif + +#include "sysfs.h" + +int sysfs_read_file(const char *path, char *buf, size_t len) +{ + FILE *fp; + + fp = fopen(path, "r"); + if (!fp) + return -1; + + if (!fgets(buf, len, fp)) { + fclose(fp); + return -1; + } + + fclose(fp); + chomp(buf); + return 0; +} + +int sysfs_read_attr(const char *devpath, const char *attr, + char *buf, size_t len) +{ + char path[PATH_MAX]; + + snprintf(path, sizeof(path), "/sys%s/%s", devpath, attr); + return sysfs_read_file(path, buf, len); +} + +int sysfs_parent_walk(const char *devpath, + int (*cb)(const char *syspath, void *data), + void *data) +{ + char syspath[PATH_MAX]; + char *p; + + snprintf(syspath, sizeof(syspath), "/sys%s", devpath); + + while (1) { + if (cb(syspath, data) == 0) + return 0; + + /* Strip last path component */ + p = strrchr(syspath, '/'); + if (!p || p == syspath) + break; + *p = 0; + + /* Do not walk above /sys/devices */ + if (!strcmp(syspath, "/sys/devices") || + !strcmp(syspath, "/sys")) + break; + } + + return -1; +} + +int sysfs_read_subsystem(const char *syspath, char *buf, size_t len) +{ + char lpath[PATH_MAX], target[PATH_MAX]; + const char *p; + ssize_t n; + + snprintf(lpath, sizeof(lpath), "%s/subsystem", syspath); + n = readlink(lpath, target, sizeof(target) - 1); + if (n < 0) + return -1; + target[n] = 0; + + p = strrchr(target, '/'); + snprintf(buf, len, "%s", p ? p + 1 : target); + return 0; +} + +int sysfs_read_driver(const char *syspath, char *buf, size_t len) +{ + char lpath[PATH_MAX], target[PATH_MAX]; + const char *p; + ssize_t n; + + snprintf(lpath, sizeof(lpath), "%s/driver", syspath); + n = readlink(lpath, target, sizeof(target) - 1); + if (n < 0) + return -1; + target[n] = 0; + + p = strrchr(target, '/'); + snprintf(buf, len, "%s", p ? p + 1 : target); + return 0; +} + +/** + * Local Variables: + * indent-tabs-mode: t + * c-file-style: "linux" + * End: + */ diff --git a/keventd/sysfs.h b/keventd/sysfs.h new file mode 100644 index 00000000..576a7914 --- /dev/null +++ b/keventd/sysfs.h @@ -0,0 +1,75 @@ +/* sysfs attribute reader and parent-chain walker for keventd + * + * Copyright (c) 2025 Joachim Wiberg + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef KEVENTD_SYSFS_H_ +#define KEVENTD_SYSFS_H_ + +#include + +/* + * Read the first line from an absolute path. + * Returns 0 on success, -1 on failure. + */ +int sysfs_read_file(const char *path, char *buf, size_t len); + +/* + * Read a sysfs attribute for the device at /sys{devpath}. + * attr may be a relative path like "device/vendor" or "../serial". + * Returns 0 on success, -1 if the attribute does not exist. + */ +int sysfs_read_attr(const char *devpath, const char *attr, + char *buf, size_t len); + +/* + * Walk the sysfs device tree upward from devpath toward /sys/devices, + * calling cb at each level. cb receives the absolute syspath and the + * caller-supplied data pointer. Walk stops when cb returns 0 (found) + * or the tree root is reached. + * Returns 0 if cb returned 0, -1 if nothing matched. + */ +int sysfs_parent_walk(const char *devpath, + int (*cb)(const char *syspath, void *data), + void *data); + +/* + * Read the subsystem name at syspath by following the "subsystem" + * symlink and returning its basename. + * Returns 0 on success, -1 if there is no subsystem link. + */ +int sysfs_read_subsystem(const char *syspath, char *buf, size_t len); + +/* + * Read the driver name at syspath by following the "driver" symlink + * and returning its basename. + * Returns 0 on success, -1 if there is no driver link. + */ +int sysfs_read_driver(const char *syspath, char *buf, size_t len); + +#endif /* KEVENTD_SYSFS_H_ */ + +/** + * Local Variables: + * indent-tabs-mode: t + * c-file-style: "linux" + * End: + */ diff --git a/keventd/udevdb.c b/keventd/udevdb.c new file mode 100644 index 00000000..bafe925c --- /dev/null +++ b/keventd/udevdb.c @@ -0,0 +1,243 @@ +/* udev-compatible per-device database in /run/udev/data/ + * + * Copyright (c) 2025 Joachim Wiberg + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#include +#include +#include +#include +#include +#include + +#ifdef _LIBITE_LITE +# include +#else +# include +#endif + +#include "keventd.h" +#include "sysfs.h" +#include "udevdb.h" + +/* Forward declaration from keventd.c */ +void logit(int prio, const char *fmt, ...); + +/* + * Compute the database file path for a device. + * + * udev's keying scheme: + * b: block device + * c: character device + * n network interface + * +: everything else + * + * This matches the layout that udevadm info, libudev, and eudev expect. + */ +static void udevdb_path(const struct uevent *ev, char *buf, size_t len) +{ + const char *sysname = uevent_sysname(ev); + const char *ifindex = NULL; + + if (ev->subsystem && !strcmp(ev->subsystem, "net")) + ifindex = uevent_getenv(ev, "IFINDEX"); + + if (ev->major >= 0 && ev->minor >= 0) { + const char *t = (ev->subsystem && !strcmp(ev->subsystem, "block")) + ? "b" : "c"; + snprintf(buf, len, UDEVDB_PATH "/%s%d:%d", t, ev->major, ev->minor); + } else if (ifindex) { + snprintf(buf, len, UDEVDB_PATH "/n%s", ifindex); + } else { + snprintf(buf, len, UDEVDB_PATH "/+%s:%s", + ev->subsystem ? ev->subsystem : "unknown", + sysname ? sysname : "unknown"); + } +} + +/* + * Write device properties to the database. + * + * Called after devnode_add() and symlink_add() so that all S: records + * can be captured via symlink_write_db(). + */ +int udevdb_write(const struct uevent *ev) +{ + char path[PATH_MAX]; + struct timeval tv; + FILE *fp; + int i; + + if (!ev->devpath) + return -1; + + if (mkpath(UDEVDB_PATH, 0755) && errno != EEXIST) + return -1; + + udevdb_path(ev, path, sizeof(path)); + + fp = fopen(path, "w"); + if (!fp) { + logit(LOG_DEBUG, "udevdb: cannot write %s: %s", path, strerror(errno)); + return -1; + } + + /* Timestamp in microseconds since epoch */ + gettimeofday(&tv, NULL); + fprintf(fp, "I:%llu\n", + (unsigned long long)(tv.tv_sec * 1000000ULL + tv.tv_usec)); + + /* All environment variables / device properties */ + for (i = 0; i < ev->nenv; i++) + fprintf(fp, "E:%s=%s\n", ev->env_key[i], ev->env_val[i]); + + /* Symlinks created for this device */ + symlink_write_db(ev->devpath, fp); + + /* Tags */ + for (i = 0; i < ev->ntags; i++) { + fprintf(fp, "G:%s\n", ev->tags[i]); + fprintf(fp, "Q:%s\n", ev->tags[i]); + } + + fprintf(fp, "V:1\n"); + fclose(fp); + + return 0; +} + +/* + * Read E: records from a database file into ev's env store. + */ +static int udevdb_load(const char *path, struct uevent *ev) +{ + char line[1024]; + FILE *fp; + + fp = fopen(path, "r"); + if (!fp) + return -1; + + while (fgets(line, sizeof(line), fp)) { + char *val; + + chomp(line); + + if (line[0] != 'E' || line[1] != ':') + continue; + + val = strchr(line + 2, '='); + if (!val) + continue; + *val++ = 0; + + uevent_setenv(ev, line + 2, val); + } + + fclose(fp); + return 0; +} + +/* + * Read device properties back from the database into ev's env store. + * + * Used by IMPORT{db} in the rules engine (Phase 4) to recover properties + * that were set during a previous event for the same device. + */ +int udevdb_read(struct uevent *ev) +{ + char path[PATH_MAX]; + + if (!ev->devpath) + return -1; + + udevdb_path(ev, path, sizeof(path)); + + return udevdb_load(path, ev); +} + +/* + * Read the parent device's properties into ev's env store, for + * IMPORT{parent} in the rules engine. The parent has no uevent to key + * from, so its device ID is composed from sysfs (see udevdb_path()). + */ +int udevdb_read_parent(struct uevent *ev) +{ + char ppath[PATH_MAX], syspath[PATH_MAX], dbfile[PATH_MAX]; + char subsys[64] = "unknown"; + char attr[64]; + const char *sn; + char *slash; + + if (!ev->devpath) + return -1; + + snprintf(ppath, sizeof(ppath), "%s", ev->devpath); + slash = strrchr(ppath, '/'); + if (!slash || slash == ppath) + return -1; + *slash = 0; + + sn = strrchr(ppath, '/'); + sn = sn ? sn + 1 : ppath; + + snprintf(syspath, sizeof(syspath), "/sys%s", ppath); + sysfs_read_subsystem(syspath, subsys, sizeof(subsys)); + + snprintf(dbfile, sizeof(dbfile), "%s/dev", syspath); + if (!sysfs_read_file(dbfile, attr, sizeof(attr))) { + snprintf(dbfile, sizeof(dbfile), UDEVDB_PATH "/%s%s", + strcmp(subsys, "block") ? "c" : "b", attr); + } else if (!strcmp(subsys, "net")) { + snprintf(dbfile, sizeof(dbfile), "%s/ifindex", syspath); + if (sysfs_read_file(dbfile, attr, sizeof(attr))) + return -1; + snprintf(dbfile, sizeof(dbfile), UDEVDB_PATH "/n%s", attr); + } else { + snprintf(dbfile, sizeof(dbfile), UDEVDB_PATH "/+%s:%s", + subsys, sn); + } + + return udevdb_load(dbfile, ev); +} + +/* + * Remove a device's database entry on ACT_REMOVE. + */ +void udevdb_delete(const struct uevent *ev) +{ + char path[PATH_MAX]; + + if (!ev->devpath) + return; + + udevdb_path(ev, path, sizeof(path)); + + if (unlink(path) && errno != ENOENT) + logit(LOG_DEBUG, "udevdb: cannot remove %s: %s", path, strerror(errno)); +} + +/** + * Local Variables: + * indent-tabs-mode: t + * c-file-style: "linux" + * End: + */ diff --git a/keventd/udevdb.h b/keventd/udevdb.h new file mode 100644 index 00000000..037eabbc --- /dev/null +++ b/keventd/udevdb.h @@ -0,0 +1,72 @@ +/* udev-compatible per-device database in /run/udev/data/ + * + * Copyright (c) 2025 Joachim Wiberg + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +#ifndef KEVENTD_UDEVDB_H_ +#define KEVENTD_UDEVDB_H_ + +#include "keventd.h" + +/* + * Database root directory, compatible with systemd/eudev's /run/udev/data/. + * One text file per device, keyed by device ID (see udevdb_path()). + * + * Record format (one per line): + * I: initialisation timestamp + * E:= environment / property + * S: symlink (relative to /dev/) + * G: tag (for G: records) + * Q: tag (for Q: query records, mirrors G:) + * V:1 format version sentinel, always last + */ +#define UDEVDB_PATH "/run/udev/data" + +/* + * Write a device's properties to the database after event processing. + * Creates /run/udev/data/ with I:, E:, S:, G:, Q:, V: records. + */ +int udevdb_write (const struct uevent *ev); + +/* + * Read a device's properties from the database into ev's env store. + * Used by IMPORT{db} and IMPORT{parent} in the rules engine (Phase 4). + */ +int udevdb_read (struct uevent *ev); + +/* + * Like udevdb_read(), but for the parent device of ev. + */ +int udevdb_read_parent (struct uevent *ev); + +/* + * Delete a device's database entry on ACT_REMOVE. + */ +void udevdb_delete (const struct uevent *ev); + +#endif /* KEVENTD_UDEVDB_H_ */ + +/** + * Local Variables: + * indent-tabs-mode: t + * c-file-style: "linux" + * End: + */ diff --git a/keventd/uevent.c b/keventd/uevent.c index 801ed07f..6c6801aa 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,52 @@ int devnode_del(struct uevent *ev) return 0; } +/* + * Handle network interface add: rename if NAME= was set by a rule. + * Interfaces are not device nodes, so no dev/ condition is set here; + * services depend on or instead. + * + * The interface must be DOWN for SIOCSIFNAME to succeed. Freshly added + * interfaces always are, so we do not attempt to bring the link down. + */ +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)); + return -1; + } + + 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); + } + + return 0; +} + /* * Track symlink for removal when device is unplugged. */ @@ -625,18 +806,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. + */ +void symlink_write_db(const char *devpath, FILE *fp) +{ + struct dev_symlink *sl; + + 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); + } +} + +/* + * Create appropriate symlinks based on device subsystem and rules. */ int symlink_add(struct uevent *ev) { - if (!ev->subsystem) - return 0; + int i; - if (!strcmp(ev->subsystem, "block")) - return symlink_add_disk(ev); + /* 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); + } - if (!strcmp(ev->subsystem, "input")) - return symlink_add_input(ev); + /* SYMLINK+= from matched rules */ + for (i = 0; i < ev->applied.nsymlinks; i++) { + const char *devname, *sl; + char link[PATH_MAX], target[PATH_MAX]; + size_t toff = 0; + int depth = 0; + const char *p; + + sl = ev->applied.symlinks[i]; + if (!sl || !*sl) + continue; + + devname = ev->applied.name ?: ev->devname; + if (!devname) + continue; + + /* basename of devname for the symlink target */ + const char *base = strrchr(devname, '/'); + base = base ? base + 1 : devname; + + /* Count '/' in symlink path to determine relative depth */ + for (p = sl; *p; p++) + if (*p == '/') + depth++; + + target[0] = 0; + for (int d = 0; d < depth; d++) + toff += snprintf(target + toff, sizeof(target) - toff, "../"); + snprintf(target + toff, sizeof(target) - toff, "%s", base); + + snprintf(link, sizeof(link), "/dev/%s", sl); + symlink_create(target, link, ev->devpath ?: ""); + } return 0; } diff --git a/mkdocs.yml b/mkdocs.yml index 674a554a..4c6cf528 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -56,6 +56,7 @@ nav: - Plugins: plugins.md - Watchdog: watchdog.md - keventd: keventd.md + - udev Rule Matching: udev-matching.md - D-Bus Integration: dbus.md - Service State Machine: state-machine.md - Distributions: distro.md