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>
6.0 KiB
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:
- 2-1.4/authorized_default — doesn't exist (not a hub/controller)
- 2-1/authorized_default — doesn't exist (downstream hubs don't have this attribute, only root hubs do)
- 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:
- ATTR{authorized}=="0" — matches (device is unauthorized)
- ATTRS{authorized_default}=="1" — walks up to usb2, finds 0 → no match
- 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.