keventd: add udev rules engine

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<mac> and PCI-slot-based
	    enp<bus>s<dev>[f<func>]
- 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 <troglobit@gmail.com>
This commit is contained in:
Joachim Wiberg
2026-08-16 22:03:31 +02:00
parent dda0d5c763
commit 67d61a8282
17 changed files with 4451 additions and 44 deletions
+6 -2
View File
@@ -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
+1 -1
View File
@@ -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
+17 -8
View File
@@ -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
+123
View File
@@ -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.
+10
View File
@@ -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)
+1530
View File
File diff suppressed because it is too large Load Diff
+46
View File
@@ -0,0 +1,46 @@
/* Builtin command dispatch for keventd rules engine
*
* Copyright (c) 2025 Joachim Wiberg <troglobit@gmail.com>
*
* 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:
*/
+79 -6
View File
@@ -34,6 +34,7 @@
* THE SOFTWARE.
*/
#include <ctype.h>
#include <errno.h>
#include <dirent.h>
#include <getopt.h>
@@ -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;
+80 -9
View File
@@ -29,11 +29,29 @@
#else
# include <lite/queue.h> /* BSD sys/queue.h API */
#endif
#include <stdint.h>
#include <stdio.h>
#include <sys/types.h>
/* 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_ */
+1621
View File
File diff suppressed because it is too large Load Diff
+163
View File
@@ -0,0 +1,163 @@
/* udev rules file parser for keventd
*
* Copyright (c) 2025 Joachim Wiberg <troglobit@gmail.com>
*
* 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 <libite/queue.h>
#else
# include <lite/queue.h>
#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:
*/
+131
View File
@@ -0,0 +1,131 @@
/* sysfs attribute reader and parent-chain walker
*
* Copyright (c) 2025 Joachim Wiberg <troglobit@gmail.com>
*
* 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 <limits.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#ifdef _LIBITE_LITE
# include <libite/lite.h>
#else
# include <lite/lite.h>
#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:
*/
+75
View File
@@ -0,0 +1,75 @@
/* sysfs attribute reader and parent-chain walker for keventd
*
* Copyright (c) 2025 Joachim Wiberg <troglobit@gmail.com>
*
* 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 <stddef.h>
/*
* 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:
*/
+243
View File
@@ -0,0 +1,243 @@
/* udev-compatible per-device database in /run/udev/data/
*
* Copyright (c) 2025 Joachim Wiberg <troglobit@gmail.com>
*
* 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 <errno.h>
#include <stdio.h>
#include <string.h>
#include <sys/time.h>
#include <syslog.h>
#include <unistd.h>
#ifdef _LIBITE_LITE
# include <libite/lite.h>
#else
# include <lite/lite.h>
#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<maj>:<min> block device
* c<maj>:<min> character device
* n<ifindex> network interface
* +<subsystem>:<sysname> 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:
*/
+72
View File
@@ -0,0 +1,72 @@
/* udev-compatible per-device database in /run/udev/data/
*
* Copyright (c) 2025 Joachim Wiberg <troglobit@gmail.com>
*
* 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:<usec> initialisation timestamp
* E:<KEY>=<VAL> environment / property
* S:<path> symlink (relative to /dev/)
* G:<tag> tag (for G: records)
* Q:<tag> 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/<id> 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:
*/
+253 -18
View File
@@ -34,12 +34,16 @@
#include <syslog.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/sysmacros.h>
#include <sys/types.h>
#include <sys/utsname.h>
#include <sys/wait.h>
#include <net/if.h>
#ifdef _LIBITE_LITE
# include <libite/lite.h>
#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 <net/IFNAME/exist> or <class/net/IFNAME> 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;
}
+1
View File
@@ -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