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 committed 2026-08-16 22:03:31 +02:00
1 parent dda0d5c763
commit 67d61a8282
17 files changed
+4451 -44

No files matched your search

+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;
}