Files
Joachim Wiberg 59945b87f7 keventd: load udev rules in udev's order, with masking
The rules directories were read one after the other, each sorted on
its own.  udev instead sorts all files from all directories together
and lets a file mask one of the same name in a lower priority
directory.  The difference broke both standard override idioms
without a sound: copying a rules file to /etc/udev/rules.d/ to edit
it left the original running too, and symlinking a name to /dev/null
disabled nothing.

Collect everything with glob(), drop entries shadowed by a later
directory, and sort the survivors by filename, same as udev and the
tmpfiles.d handling in Finit itself.  The -r directory sits above
/etc/ so a test ruleset can override anything shipped.

Signed-off-by: Joachim Wiberg <troglobit@gmail.com>
2026-08-29 09:26:18 +02:00

1649 lines
36 KiB
C

/* udev rules file parser and matcher 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.
*/
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <fnmatch.h>
#include <glob.h>
#include <grp.h>
#include <limits.h>
#include <pwd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/utsname.h>
#include <sys/wait.h>
#ifdef _LIBITE_LITE
# include <libite/lite.h>
#else
# include <lite/lite.h>
#endif
#include "builtin.h"
#include "rules.h"
#include "sysfs.h"
#include "udevdb.h"
#include "util.h"
/* ----- 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 ----------------------------------------------------- */
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;
}
static int rule_file_cmp(const void *a, const void *b)
{
return strcmp(basenm(*(char *const *)a), basenm(*(char *const *)b));
}
int rules_load_all(struct rule_list *list, const char *extra_dir)
{
/*
* Like udev: a file masks one of the same name in an earlier
* (lower priority) directory, and the survivors are sorted
* together by filename, whichever directory they live in. A
* symlink to /dev/null thus disables the file it shadows.
*/
const char *dir[] = { /* in priority order, lowest first */
"/lib/udev/rules.d",
"/run/udev/rules.d",
"/etc/udev/rules.d",
extra_dir
};
int flags = GLOB_NOESCAPE, total = 0;
size_t i, j, nfiles = 0;
char **files;
glob_t gl;
for (i = 0; i < NELEMS(dir); i++) {
char pattern[PATH_MAX];
if (!dir[i])
continue;
snprintf(pattern, sizeof(pattern), "%s/*.rules", dir[i]);
glob(pattern, flags, NULL, &gl);
flags |= GLOB_APPEND;
}
files = calloc(gl.gl_pathc, sizeof(char *));
if (!files) {
globfree(&gl);
return 0;
}
for (i = 0; i < gl.gl_pathc; i++) {
/* masked by the same name in a later directory? */
for (j = i + 1; j < gl.gl_pathc; j++) {
if (!strcmp(basenm(gl.gl_pathv[i]), basenm(gl.gl_pathv[j])))
break;
}
if (j < gl.gl_pathc)
continue;
files[nfiles++] = gl.gl_pathv[i];
}
qsort(files, nfiles, sizeof(char *), rule_file_cmp);
for (i = 0; i < nfiles; i++) {
int rc;
rc = rules_load(list, files[i]);
if (rc > 0) {
logit(LOG_DEBUG, "rules: %d rules from %s", rc, files[i]);
total += rc;
}
}
free(files);
globfree(&gl);
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. */
static int run_program(const char *cmd, char *result, size_t rlen)
{
int pipefd[2];
pid_t pid;
int rc = -1;
if (!cmd || !*cmd)
return -1;
if (pipe(pipefd) < 0)
return -1;
pid = fork();
if (pid < 0) {
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;
}
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/<helper> (path_id, usb_id, blkid, ...)
* via IMPORT{program}=. keventd ships compatible builtins for many of
* those. Dispatch order:
*
* 1. Absolute helper path that exists -> let caller fork+exec it
* (lets users override builtins by dropping a custom helper)
* 2. Basename matches a known builtin -> run the builtin in-process
* 3. Absolute helper path that does NOT exist and has no matching
* builtin -> skip silently with a debug
* log; forking /bin/sh on a missing binary just yields a 127
* child for every event (e.g. /lib/udev/fido_id, /lib/udev/scsi_id
* on systems that don't ship them).
* 4. Anything else (relative command, shell snippet, ...) -> caller
* forks /bin/sh -c which resolves via PATH.
*
* Returns 1 if the case has been handled here (caller skips fork),
* 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;
int helper_missing;
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;
helper_missing = (first[0] == '/'); /* absolute path that's not there */
base = strrchr(first, '/');
base = base ? base + 1 : first;
if (builtin_has(base)) {
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;
}
if (helper_missing) {
logit(LOG_DEBUG, "rules: %s not found and no matching builtin, skipping",
first);
return 1;
}
return 0;
}
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:
*/