Files
Joachim Wiberg c512d57df4 doc: describe the udev rules keventd actually implements
The engine covers most of the udev grammar but not all of it, and the
gaps are invisible until a rule silently does nothing.  Write down what
is implemented and where it parts ways with udev(7), rather than
leaving people to infer it from a ruleset that happens to work.  The
man page gets the directory precedence and a pointer to udev(7) and
the User's Guide, which hold the details.

Rename the menu entry to Device Manager while here, matching how the
watchdog daemon is listed.

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

24 KiB

Bundled Device Manager

The kernel event daemon keventd is a built-in device manager bundled with Finit. It replaces the need for external device managers like mdev, mdevd, or udevd on systems where a lighter-weight solution is preferred, particularly on embedded systems.

It is enabled by default since Finit v5, and needs libblkid from util-linux (the libblkid-dev package, or whatever your distribution calls it) to read the filesystem UUID and label behind the /dev/disk/by-uuid/ and /dev/disk/by-label/ symlinks. To disable it and use an external device manager instead:

./configure --without-keventd

Features

When started, keventd listens on a NETLINK_KOBJECT_UEVENT socket for kernel events and handles:

  • Device node creation: creates and removes /dev nodes with correct permissions on device add/remove events
  • 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 to load the appropriate kernel module
  • Coldplug: with the -c flag, walks /sys/devices and triggers add events for all devices present at boot
  • Power supply monitoring: tracks AC power status and provides the sys/pwr/ac condition
  • Device conditions: sets dev/* conditions in the Finit condition system when device nodes appear or disappear
  • Netlink rebroadcast: rebroadcasts processed uevents to netlink group 0x4 for libudev-zero consumers (enabled by default)
  • D-Bus interface: org.finit.Device1 -- settle, trigger, device info, rules reload, and queue state over its own bus socket

Device Nodes

On receiving an add event with MAJOR, MINOR, and DEVNAME fields, keventd creates the corresponding device node in /dev using mknod(). Parent directories are created automatically (e.g., /dev/input/ for /dev/input/event0).

On remove events, the device node and its associated symlinks and conditions are cleaned up.

Default Permissions

keventd applies permissions based on built-in rules that match on device subsystem and name:

Subsystem Pattern Mode Owner:Group
block sd*, vd*, nvme*, mmcblk*, loop*, dm-, md 0660 root:disk
tty tty[0-9]* 0620 root:tty
tty ttyS*, ttyUSB*, ttyACM* 0660 root:dialout
input event*, mouse*, mice 0660 root:root
sound * 0660 root:audio
video4linux * 0660 root:video
drm card*, render* 0660 root:video
(any) null, zero, full, random, urandom 0666 root:root
(any) console 0600 root:root
(default) 0660 root:root

udev Rules

Rules are read from /lib/udev/rules.d/, /run/udev/rules.d/, and /etc/udev/rules.d/, plus any directory passed with -r. Files need a .rules suffix. Like udev, all files sort together by filename, whichever directory they live in, and a file masks one of the same name in an earlier directory: /etc/ overrides /run/, which overrides /lib/, and the -r directory overrides them all. A symlink to /dev/null in /etc/udev/rules.d/ disables the /lib/ file of the same name.

The syntax is the one described in udev(7). What follows is the subset keventd understands, not a second copy of the grammar. To pick up edited rules, call RulesReload on org.finit.Device1. initctl reload does not touch them.

Match Keys

Key Matches
ACTION add, remove, change, move, bind, unbind, online, offline
DEVPATH kernel device path, without the /sys prefix
KERNEL sysfs name of the device
NAME device node name
SUBSYSTEM, DRIVER subsystem and bound driver
ATTR{file} sysfs attribute of the device
SYSCTL{param} value under /proc/sys/
ENV{key} uevent property, including ones set by earlier rules
CONST{key} arch; virt reports container or nothing
TAG tag set by an earlier TAG+=
TEST{octal} file exists, optionally with the given permission bits
PROGRAM exit status of a program, output kept for RESULT and %c
RESULT output of the last PROGRAM
KERNELS, SUBSYSTEMS, DRIVERS, ATTRS{file} the device or any of its parents, see below

Values match literally, as an fnmatch() glob, or as a|b|c alternatives. Both == and != work everywhere.

SECLABEL, OPTIONS, and TAGS parse but do nothing. Rules using them load without complaint and the key is skipped, so OPTIONS+="static_node=...", link_priority=, string_escape=, and watch have no effect, and a TAGS== match never succeeds.

The Parent Chain

ATTR{} reads an attribute of the device the event fired for. ATTRS{} also looks at the parents, walking up the sysfs tree until one matches. For a keyboard behind a hub:

/sys/devices/.../usb2/         <- root hub
              usb2/2-1/        <- hub
              usb2/2-1/2-1.4/  <- keyboard, event fires here

ATTRS{authorized_default}=="1" finds no such attribute on the keyboard or the hub, reaches the root hub, and matches there. KERNELS, SUBSYSTEMS, and DRIVERS do the same for a parent's name, subsystem, and driver.

Each of these keys walks the chain on its own. udev requires all the parent keys in one rule to match on the same parent, so a rule pairing SUBSYSTEMS=="usb" with ATTRS{idVendor}=="1d6b" is looser here: the two may land on different ancestors. Where that matters, match on one attribute specific enough to identify the device by itself.

Assignments

Key Effect
NAME= name of the device node
SYMLINK=, += symlinks to create, space separated, relative to /dev
OWNER=, GROUP=, MODE= ownership and permissions of the node
ENV{key}=, +=, -= set, append to, or clear a property
TAG+=, -= tags for later TAG== matching
RUN+= program to run once the event is handled, RUN{builtin}+= for a builtin
IMPORT{type} program, file, db, builtin, parent, cmdline
ATTR{file}= write to a sysfs attribute
SYSCTL{param}= write to /proc/sys/
LABEL=, GOTO= skip ahead to a label

:= locks MODE, OWNER, and GROUP against later rules the way udev does. On NAME and ENV{} it behaves like plain = and locks nothing, and SYMLINK:= is dropped without a word.

IMPORT{builtin} and RUN{builtin} can call blkid, hwdb, input_id, kmod, net_id, path_id, and usb_id.

Substitutions

Long Short Expands to
$kernel %k sysfs name of the device
$devpath %p kernel device path
$name %N, %D device node name
$major, $minor %M, %m device numbers
$driver %d bound driver
$attr{file} %s{file} sysfs attribute of the device
$env{key} uevent property
$result %c, %c{N} PROGRAM output, or its Nth field
$root /dev
$sys /sys
%n trailing digits of the sysfs name
%b major:minor

udev's $id, $parent, $links, and %E{} are not implemented, and %b here is the device number pair rather than udev's parent bus id. An unknown specifier is left in the string as written.

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, under /dev/input/:

  • by-id: based on the device name from sysfs
  • by-path: based on the physical device path

The rest come from SYMLINK+= in the udev rules, so what you get depends on the ruleset installed. The curated rules Finit ships add, among others:

Directory Links
/dev/disk/ by-uuid, by-label, by-partuuid, by-partlabel, by-diskseq, and further by-id and by-path names for NVMe, virtio, MMC, WWN, and FireWire
/dev/input/ longer by-id and by-path names than the built-in ones, down to the USB interface number
/dev/serial/ by-id, by-path
/dev/snd/ by-id, by-path
/dev/v4l/ by-id, by-path
/dev/tape/ by-id, by-path
/dev/dri/ by-path

They also set up a few fixed names: /dev/rtc, /dev/cdrom, and /dev/virtio-ports/<name>.

The by-uuid and by-label links are why keventd needs libblkid. The rules read the filesystem metadata off the device with IMPORT{builtin}="blkid", and libblkid is what that builtin calls.

Built-in and rule-provided symlinks alike are tracked internally and removed when the corresponding device is unplugged.

Firmware Loading

When a kernel driver requests firmware (via request_firmware()), the kernel sends a uevent with a FIRMWARE= field. keventd handles this by:

  1. Searching for the firmware file in order:
    • /lib/firmware/updates/<kernel-version>/<name>
    • /lib/firmware/updates/<name>
    • /lib/firmware/<kernel-version>/<name>
    • /lib/firmware/<name>
  2. Writing 1 to /sys/<devpath>/loading to signal start
  3. Copying the firmware data to /sys/<devpath>/data
  4. Writing 0 to /sys/<devpath>/loading on success (or -1 on failure)

This is particularly important early in boot when drivers for graphics cards, network adapters, and other hardware need firmware before they can operate.

Module Loading

When a device add event includes a MODALIAS field, keventd spawns modprobe -bq <modalias> to load the matching kernel module. Module loading is done asynchronously (keventd does not wait for modprobe to complete) to avoid blocking other event processing.

Coldplug

To handle devices that were present before keventd started, it supports a coldplug mode activated with the -c flag. This walks the entire /sys/devices tree and writes add to each uevent file, causing the kernel to re-emit add events for all existing devices.

This replaces the separate coldplug script previously used with mdev.

When -c is used, keventd defers its pidfile (and the pid/keventd condition Finit asserts from it) until the coldplug event queue has been fully drained. Services that depend on <pid/keventd> can therefore assume /dev is populated and persistent symlinks are live, without needing a separate settle step.

The Linux kernel sends uevents to netlink multicast group 1 (bit 0) of NETLINK_KOBJECT_UEVENT. Only the device manager listens on this raw kernel group. Userspace consumers — applications using libudev — expect to receive processed events on a separate netlink group.

systemd/udevd established the convention of rebroadcasting processed events to a separate group, and libudev-zero, a daemonless drop-in replacement for libudev, listens on group 0x4 for these events. Without a device manager rebroadcasting, graphical applications, Wayland/X11 compositors, libinput, and anything else using libudev to monitor device hotplug will never see any events.

keventd rebroadcasts by default to netlink group 4 (0x4). A second netlink socket is created at startup, and after each uevent has been fully processed (device nodes created, symlinks set up, modules loaded), the original raw event is sent to the configured group(s). Rebroadcasting after processing ensures that device nodes and symlinks already exist by the time consumers receive the notification.

Use -g GROUP to override the default group mask, or -G to disable rebroadcast entirely. Bit 0 of the group mask is always forced off to prevent a feedback loop with the kernel's own multicast group.

Background

The netlink uevent architecture uses separate multicast groups to isolate the kernel-to-device-manager channel from the device-manager-to- application channel:

Group Bit Purpose
1 0 Kernel events (device manager listens here)
4 2 Processed events (libudev consumers listen)

This two-group design was established by systemd/udevd and is the de facto standard. mdevd implements the same mechanism via its -O flag, and keventd follows the same convention.

For more details, see:

  • libudev-zero — daemonless replacement for libudev
  • mdevd — mdev-compatible device manager with rebroadcast support

Conditions

keventd provides conditions in two namespaces:

Device Conditions (dev/)

When a device node is created, keventd asserts a corresponding condition in /run/finit/cond/dev/. This allows services to wait for specific devices:

service mdadm {
    description = "RAID monitor"
    runlevel    = "2345"
    conditions  = { "dev/sda" }
    command     = "/usr/sbin/mdadm --monitor /dev/md0"
}

service gps-daemon {
    description = "GPS daemon"
    runlevel    = "2345"
    conditions  = { "dev/ttyUSB0" }
    command     = "/usr/sbin/gps-daemon"
}

Network interfaces are not device nodes and do not live in the dev/ namespace -- a /dev/wan node created by a user must not be confused with a wan interface. To wait for an interface, use the netlink plugin's net/<iface>/exist condition, plus net/<iface>/up (admin up) and net/<iface>/running (carrier present) to gate on link state:

service dhcpcd {
    description = "DHCP client"
    runlevel    = "2345"
    conditions  = { "net/wan/exist" }
    command     = "/usr/sbin/dhcpcd"
}

keventd provides the parallel class/net/<iface> condition, like for any other sysfs class device.

When the device is removed, the condition is cleared and Finit stops the dependent services.

Class Conditions (class/)

Many devices live in sysfs without a /dev/ node -- DSA switch ports, IIO sensors, LEDs, backlight, PHYs, regulators. For those, keventd asserts class/<subsystem>/<sysname> on every add event so services can still wait for them:

service blink-blue {
    description = "LED driver"
    runlevel    = "2345"
    conditions  = { "class/leds/blue" }
    command     = "/usr/sbin/blink-blue"
}

The condition is cleared on remove.

Driver Conditions (driver/)

driver/<name> is asserted while the driver <name> is bound to at least one device, from the kernel's bind/unbind uevents. Use this to gate on slow-probing hardware whose readiness isn't marked by a class device or /dev node, such as a switch core or complex PHY:

service dsa-probe {
    description = "DSA topology probe"
    runlevel    = "2345"
    conditions  = { "driver/mt7530" }
    command     = "/usr/sbin/dsa-probe"
}

A driver bound to several devices keeps the condition asserted until the last device is unbound. To wait for one specific device instance, gate on what its probe creates instead: the class/ condition or /dev node of the child device.

Power Supply Conditions (sys/pwr/)

keventd monitors the power_supply subsystem and provides:

  • sys/pwr/ac — asserted when AC power is connected

This is useful for preventing power-hungry services from running on battery:

service cron {
    description = "Cron daemon"
    runlevel    = "2345"
    conditions  = { "sys/pwr/ac", "pid/syslogd" }
    command     = "cron -f"
}

Usage

keventd [-cdGhnpSv] [-g GROUP] [-r DIR] [-t SECONDS]

Options:
  -c        Run coldplug at startup
  -d        Enable debug mode (foreground, verbose)
  -g GROUP  Override netlink rebroadcast group (default: 4)
  -G        Disable netlink rebroadcast entirely
  -h        Show help text
  -n        Run in foreground (no daemon)
  -p        Passive mode: power supply events only
  -r DIR    Extra rules directory, overrides the standard udev paths
  -S        Settle mode: wait for kernel uevent queue to quiet, then exit
  -t SEC    Settle timeout (default 30s, used with -S)
  -v        Show version

In normal operation, Finit starts keventd automatically via its system configuration. The -d flag is useful for debugging device issues -- it runs keventd in the foreground and logs all received uevents.

keventd -S is a one-shot command, not a flag to the running daemon. It is the udevadm settle equivalent for migration scenarios:

keventd -S -t 10 && start-graphical-session

With D-Bus support it asks the running keventd over Device1.Settle, which tracks the event queue where the events actually flow. When no bus answers -- another device manager, or a build without D-Bus -- it falls back to polling /sys/kernel/uevent_seqnum until the kernel's sequence counter has been stable for 200ms, then exits zero. After the -t SECONDS timeout (default 30) it exits non-zero instead. The fallback works regardless of which device manager is active, or even if none is.

Prefer the condition-based model (<dev/X>, <class/...>, <driver/...>) over settle when you control the service definition -- settle is racy with slow probes that fire after the queue appears quiet. It is provided for legacy boot scripts and init transitions where condition wiring isn't feasible.

Debug logging can also be toggled at runtime by sending SIGUSR1:

kill -USR1 $(pidof keventd)

D-Bus Interface (org.finit.Device1)

With D-Bus support (default, --disable-dbus opts out) keventd serves org.finit.Device1 at /org/finit/device on its own brokerless bus, unix:path=/run/keventd/bus, the same way Finit serves org.finit on /run/finit/bus. There is no forwarding between the two -- clients that want both connect to both.

Method In sig Out sig Priv. Notes
Settle u b no Wait until the event queue drains, timeout in seconds; false on timeout
Trigger ss — yes Replay events: action, subsystem glob (empty = all)
Info s a{ss} no /run/udev/data properties for a devpath
RulesReload — u yes Re-read the rules directories, returns rule count
Property Sig Notes
QueueEmpty b No device events in flight
SeqnumProcessed t Highest kernel seqnum keventd has handled
Signal Body Fires when
DeviceProcessed ss — devpath, action An event has been fully handled: node, symlinks, database

Example, wait up to ten seconds for the queue to drain:

dbus-send --address=unix:path=/run/keventd/bus \
          --type=method_call --print-reply --dest=org.finit \
          /org/finit/device org.finit.Device1.Settle uint32:10

In passive mode (-p) Trigger and RulesReload refuse -- device events are not handled here -- while Settle and the queue-state properties remain meaningful.

keventd's conditions are symlinks to Finit's reconf generation marker, so they read the current generation by construction and never enter flux -- like user-defined conditions, they need no reassert after initctl reload. Device state does not change because Finit re-read its configuration.

Integration with Finit

keventd is a standalone daemon started by Finit as an internal service. It communicates with Finit exclusively through the filesystem-based condition system — creating and removing symlinks in /run/finit/cond/.

This means keventd can also be tested independently:

# Run in debug mode to see all kernel events
keventd -d

# Run with coldplug to populate /dev from scratch
keventd -c -n

# Run without rebroadcast (e.g., headless embedded system)
keventd -c -G

Only one device manager should be active at a time. This is settled at build time: with the hotplug plugin enabled, Finit starts keventd in passive mode (-p), leaving device management to the udevd, mdevd, or mdev service from system/10-hotplug.conf and monitoring only power supply events. Without the hotplug plugin, keventd runs as the system device manager (keventd -c).