Files
finit/doc/udev-matching.md
T
Joachim Wiberg 67d61a8282 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>
2026-08-16 22:03:31 +02:00

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:

  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.