udev Rules Explained: Making Devices Behave the Way You Want
Plug in a USB serial adapter and it appears as /dev/ttyUSB0. Plug in a second one and the first might become ttyUSB1 after a reboot. Try to open it as a normal user and you get permission denied.
udev is the piece that decides all of this, and rules are how you change its mind.
What udev is
The kernel emits an event whenever a device appears or disappears. systemd-udevd listens for those events and acts: creating the device node in /dev, setting ownership and permissions, adding symlinks, and optionally running something.
The rules that govern this live in three places, read in lexical order across all of them:
/usr/lib/udev/rules.d/for package-provided rules/run/udev/rules.d/for runtime rules/etc/udev/rules.d/for yours
Put your rules in /etc/udev/rules.d/. Files elsewhere get replaced on package updates.
Finding what to match on
This is the step people skip, and it is the reason most rules fail.
udevadm info -a -n /dev/ttyUSB0
The output walks up the device tree, starting with the device itself and continuing through each parent. It looks like this:
looking at device '/devices/.../ttyUSB0':
KERNEL=="ttyUSB0"
SUBSYSTEM=="tty"
DRIVER==""
looking at parent device '/devices/.../1-1.4:1.0':
KERNELS=="1-1.4:1.0"
SUBSYSTEMS=="usb"
ATTRS{idVendor}=="0403"
ATTRS{idProduct}=="6001"
ATTRS{serial}=="A50285BI"
The critical rule: you may only match attributes from one level. Taking KERNEL from the device and ATTRS{serial} from a grandparent produces a rule that never matches, and udev will not tell you why.
Note the singular and plural forms. KERNEL and SUBSYSTEM match the device itself; KERNELS, SUBSYSTEMS, and ATTRS search up through parents.
A stable name
sudo nano /etc/udev/rules.d/99-usb-serial.rules
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", ATTRS{idProduct}=="6001", ATTRS{serial}=="A50285BI", SYMLINK+="ttyArduino"
Now the device is always reachable at /dev/ttyArduino regardless of which ttyUSB number it received.
Reload and test:
sudo udevadm control --reload-rules
sudo udevadm trigger
# then unplug and replug
ls -l /dev/ttyArduino
Permissions
The other common need. By default serial devices are owned by root and a group most users are not in.
SUBSYSTEM=="tty", ATTRS{idVendor}=="0403", GROUP="dialout", MODE="0660"
Then add yourself to the group:
sudo usermod -aG dialout "$USER"
Log out and back in, because group membership is read at login. Our useradd builder generates that command correctly, including the -aG rather than -G that preserves your other groups.
For something like an SDR or a programmer where a group is overkill:
SUBSYSTEM=="usb", ATTRS{idVendor}=="1d50", MODE="0666"
0666 means any user can read and write it. Acceptable on a personal machine, inappropriate on a shared one.
Running something on plug-in
ACTION=="add", SUBSYSTEM=="block", ATTRS{idVendor}=="0951", RUN+="/usr/local/bin/backup-usb.sh"
Keep whatever runs very short. udev kills processes that take too long, and a blocked rule delays every subsequent device event on the system.
For anything substantial, hand off to systemd:
ACTION=="add", SUBSYSTEM=="block", ATTRS{serial}=="ABC123", TAG+="systemd", ENV{SYSTEMD_WANTS}="backup-usb.service"
That starts a normal service, which gets a proper timeout, logging in the journal, and no risk of stalling udev. The systemd unit builder generates the unit.
Testing without guessing
# Simulate a rule run against a real device
udevadm test /sys/class/tty/ttyUSB0
# Watch events live, then plug something in
udevadm monitor --environment --udev
# What does udev currently know about this device
udevadm info -q all -n /dev/ttyUSB0
udevadm monitor is the quickest way to discover what a device actually reports, particularly for hardware that appears as several devices at once.
Common rules
Ignore a device entirely, useful for a card reader that keeps mounting:
SUBSYSTEM=="block", ATTRS{idVendor}=="058f", ENV{UDISKS_IGNORE}="1"
Name a network interface predictably:
SUBSYSTEM=="net", ACTION=="add", ATTR{address}=="aa:bb:cc:dd:ee:ff", NAME="lan0"
Disable wake from a USB device that wakes the machine unprompted:
ACTION=="add", SUBSYSTEM=="usb", ATTRS{idVendor}=="046d", ATTR{power/wakeup}="disabled"
Why a rule silently does nothing
In order of likelihood:
- Rules were not reloaded, or the device was not replugged
- Attributes come from more than one level of the tree
- A
==was typed where=was meant, or the reverse - Another rule file with a higher number overrides it
- The attribute value has different case or whitespace than expected
For the last one, remember that matching is exact. Copy values from udevadm info rather than typing them.
Frequently Asked Questions
What does udev do?
It manages the /dev directory and reacts to device events from the kernel. When hardware appears or disappears, udev creates or removes device nodes, applies permissions, creates symlinks, and can run commands in response.
Why does my USB serial adapter change between ttyUSB0 and ttyUSB1?
Device numbering follows the order the kernel enumerates devices, which varies between boots and with what else is plugged in. Write a udev rule that creates a stable symlink based on the device serial number and use that name instead.
How do I find the attributes to match on?
Run udevadm info -a -n /dev/yourdevice, which walks up the device tree printing every attribute at each level. Match attributes from a single level only, since combining attributes from different parents will never match.
Why does my rule not take effect?
Usually because it was never reloaded, or because it matches attributes from more than one level of the device tree. Run udevadm control --reload-rules then unplug and replug the device, and test the match with udevadm test.
Where do my rules go?
In /etc/udev/rules.d/ with a name like 99-mydevice.rules. Files in /usr/lib/udev/rules.d/ belong to packages and are overwritten on update. Numbers control ordering, and a higher number runs later so it can override earlier rules.
Can udev rules run scripts?
Yes with RUN, but it should be brief. udev kills long-running processes, so anything substantial should start a systemd unit instead using TAG and SYSTEMD_WANTS, which hands the work to a proper service.