I have to admit: until recently, udev completely intimidated me. I needed udev rules for changing device permissions or creating symlinks with special names, but usually I had to use Google to find somebody else’s rule. Sometimes a rule I found wouldn’t work because the udev syntax changed–for example, you need to use ATTR or ATTRS instead of SYSFS with recent versions. It still scares me a little bit, but at least I feel like I actually understand what’s going on with it now. I’d like to share a few tips and tricks I’ve picked up about making udev rules.

First of all, the man page for udev is extremely useful. I know man pages can be boring, but this one has just about everything you need to know, and it’s not too long.

Where udev rules are stored

There are two directories where you can find udev rules:

  • /etc/udev/rules.d
  • /lib/udev/rules.d

The rules in /lib/udev/rules.d are bundled rules that you probably shouldn’t be messing with. Instead, put your custom rules in /etc/udev/rules.d. You’ll notice they are named in the form “number-description.rules”. The number is used for ordering the rules, so you can pick an order that makes sense for your needs. It’s a good idea to use that same form so that you have a good idea of the order in which the rules will be parsed. This can definitely matter–I have had troubles with creating symlinks for USB serial devices if the number is too small, probably due to some bundled rule that overrides something my rule does.

If you want to override a bundled rule file that’s in /lib/udev/rules.d, you should create a file with the same name in /etc/udev/rules.d. This will cause the new file in /etc/udev/rules.d to be used instead of the file in /lib/udev/rules.d.

The format of udev rules

udev rules are basically a comma-separated list of things–conditions and assignments. If all of the conditions in a rule are true, the rule is a match and all of the assignments are performed. Similar to many programming languages, a single equal sign (=) represents an assignment, while two equal signs (==) represent a comparison. != represents a “not equals” comparison, += adds a value to a list, and := assigns a value for the final time, disallowing any higher-numbered rules from modifying whatever you assigned. Every condition or assignment is a key-value pair separated by one of the operators listed above.

Keys you can match against

This section, I believe, is the most important part to understand. If you don’t understand the intricacies of how these matches work, especially with the keys ending in S, you will probably make mistaken assumptions about how they work (I did!). You can match against these keys:

  • ACTION — what happened to the device that caused udev to be invoked. Common actions checked against are “add” and “remove”.
  • KERNEL — the name of the device as given by the kernel
  • KERNELS — the name of the device or a parent device as given by the kernel
  • SUBSYSTEM — the kernel subsystem of the device (example: tty or usb)
  • SUBSYSTEMS — the subsystem of the device or a parent device
  • DRIVER — the name of the device’s driver
  • DRIVERS — the name of the device’s driver or the name of a parent device’s driver
  • ATTR{name} — a sysfs attribute of the device
  • ATTRS{name} — a sysfs attribute of the device or a parent device
  • IMPORTANT NOTE: If you are using any of the keys above that match against a parent device (KERNELS, SUBSYSTEMS, DRIVERS, ATTRS) in a rule, the parent device you’re matching against must be the same. For example, you can’t search for ATTRS of two different parent devices in the same rule. You also can’t search for the ATTRS of one parent device and the SUBSYSTEMS of a different parent device in the same rule. You can work around this limitation by creating multiple rules and using GOTO, but you can’t do it all in the same rule. This is actually a good thing because it allows you to do things like make sure you’re looking at a USB serial number instead of a PCI serial number for example.

This is very much an incomplete list, but these are some of the more important ones.

How to find values to match against

The info above is great, but it doesn’t really help much unless you know which driver, subsystem, and event attribute you’re trying to match against. That’s where a really handy command comes into play:

udevadm info --attribute-walk --name=ttyUSB0

ttyUSB0 is an example device name in this case — a USB serial port. This attribute walk command will find all of the keys/attributes for a particular device and all of its parent devices. If you run this command on a USB serial port, you will notice it finds the tty device at the top of the output, which is actually fairly boring as far as matching goes. Then as you scroll down, you will see udevadm walk all the way up the tree of parent devices.. You will see it reach the actual USB device that provides the serial port, the USB hub it’s connected to, the USB host controller the hub is connected to, and then probably a PCI controller or something like that depending on your host system.

Here is some example output from a PL-2303-based USB to serial converter I have connected to a VMware virtual machine:

looking at device '/devices/pci0000:00/0000:00:11.0/0000:02:00.0/usb2/2-2/2-2.1/2-2.1:1.0/ttyUSB0/tty/ttyUSB0':
 KERNEL=="ttyUSB0"
 SUBSYSTEM=="tty"
 DRIVER==""

looking at parent device '/devices/pci0000:00/0000:00:11.0/0000:02:00.0/usb2/2-2/2-2.1/2-2.1:1.0/ttyUSB0':
 KERNELS=="ttyUSB0"
 SUBSYSTEMS=="usb-serial"
 DRIVERS=="pl2303"
 ATTRS{port_number}=="0"

looking at parent device '/devices/pci0000:00/0000:00:11.0/0000:02:00.0/usb2/2-2/2-2.1/2-2.1:1.0':
 KERNELS=="2-2.1:1.0"
 SUBSYSTEMS=="usb"
 DRIVERS=="pl2303"
 ATTRS{bInterfaceClass}=="ff"
 ATTRS{bInterfaceSubClass}=="00"
 ATTRS{bInterfaceProtocol}=="00"
 ATTRS{bNumEndpoints}=="03"
 ATTRS{supports_autosuspend}=="1"
 ATTRS{bAlternateSetting}==" 0"
 ATTRS{bInterfaceNumber}=="00"

looking at parent device '/devices/pci0000:00/0000:00:11.0/0000:02:00.0/usb2/2-2/2-2.1':
 KERNELS=="2-2.1"
 SUBSYSTEMS=="usb"
 DRIVERS=="usb"
 ATTRS{bDeviceSubClass}=="00"
 ATTRS{bDeviceProtocol}=="00"
 ATTRS{devpath}=="2.1"
 ATTRS{idVendor}=="067b"
 ATTRS{speed}=="12"
 ATTRS{bNumInterfaces}==" 1"
 ATTRS{bConfigurationValue}=="1"
 ATTRS{bMaxPacketSize0}=="64"
 ATTRS{busnum}=="2"
 ATTRS{devnum}=="4"
 ATTRS{configuration}==""
 ATTRS{bMaxPower}=="100mA"
 ATTRS{authorized}=="1"
 ATTRS{bmAttributes}=="80"
 ATTRS{bNumConfigurations}=="1"
 ATTRS{maxchild}=="0"
 ATTRS{bcdDevice}=="0300"
 ATTRS{avoid_reset_quirk}=="0"
 ATTRS{quirks}=="0x0"
 ATTRS{version}==" 1.10"
 ATTRS{urbnum}=="20"
 ATTRS{ltm_capable}=="no"
 ATTRS{manufacturer}=="Prolific Technology Inc."
 ATTRS{removable}=="unknown"
 ATTRS{idProduct}=="2303"
 ATTRS{bDeviceClass}=="00"
 ATTRS{product}=="USB-Serial Controller"

looking at parent device '/devices/pci0000:00/0000:00:11.0/0000:02:00.0/usb2/2-2':
 KERNELS=="2-2"
 SUBSYSTEMS=="usb"
 DRIVERS=="usb"
 ATTRS{bDeviceSubClass}=="00"
 ATTRS{bDeviceProtocol}=="00"
 ATTRS{devpath}=="2"
 ATTRS{idVendor}=="0e0f"
 ATTRS{speed}=="12"
 ATTRS{bNumInterfaces}==" 1"
 ATTRS{bConfigurationValue}=="1"
 ATTRS{bMaxPacketSize0}=="8"
 ATTRS{busnum}=="2"
 ATTRS{devnum}=="3"
 ATTRS{configuration}=="VMware Virtual USB Hub"
 ATTRS{bMaxPower}=="0mA"
 ATTRS{authorized}=="1"
 ATTRS{bmAttributes}=="e0"
 ATTRS{bNumConfigurations}=="1"
 ATTRS{maxchild}=="7"
 ATTRS{bcdDevice}=="0100"
 ATTRS{avoid_reset_quirk}=="0"
 ATTRS{quirks}=="0x0"
 ATTRS{version}==" 1.10"
 ATTRS{urbnum}=="48"
 ATTRS{ltm_capable}=="no"
 ATTRS{removable}=="unknown"
 ATTRS{idProduct}=="0002"
 ATTRS{bDeviceClass}=="09"
 ATTRS{product}=="VMware Virtual USB Hub"

looking at parent device '/devices/pci0000:00/0000:00:11.0/0000:02:00.0/usb2':
 KERNELS=="usb2"
 SUBSYSTEMS=="usb"
 DRIVERS=="usb"
 ATTRS{bDeviceSubClass}=="00"
 ATTRS{bDeviceProtocol}=="00"
 ATTRS{devpath}=="0"
 ATTRS{idVendor}=="1d6b"
 ATTRS{speed}=="12"
 ATTRS{bNumInterfaces}==" 1"
 ATTRS{bConfigurationValue}=="1"
 ATTRS{bMaxPacketSize0}=="64"
 ATTRS{authorized_default}=="1"
 ATTRS{busnum}=="2"
 ATTRS{devnum}=="1"
 ATTRS{configuration}==""
 ATTRS{bMaxPower}=="0mA"
 ATTRS{authorized}=="1"
 ATTRS{bmAttributes}=="e0"
 ATTRS{bNumConfigurations}=="1"
 ATTRS{maxchild}=="2"
 ATTRS{bcdDevice}=="0311"
 ATTRS{avoid_reset_quirk}=="0"
 ATTRS{quirks}=="0x0"
 ATTRS{serial}=="0000:02:00.0"
 ATTRS{version}==" 1.10"
 ATTRS{urbnum}=="37"
 ATTRS{ltm_capable}=="no"
 ATTRS{manufacturer}=="Linux 3.11.0-12-generic uhci_hcd"
 ATTRS{removable}=="unknown"
 ATTRS{idProduct}=="0001"
 ATTRS{bDeviceClass}=="09"
 ATTRS{product}=="UHCI Host Controller"

looking at parent device '/devices/pci0000:00/0000:00:11.0/0000:02:00.0':
 KERNELS=="0000:02:00.0"
 SUBSYSTEMS=="pci"
 DRIVERS=="uhci_hcd"
 ATTRS{irq}=="18"
 ATTRS{subsystem_vendor}=="0x15ad"
 ATTRS{broken_parity_status}=="0"
 ATTRS{class}=="0x0c0300"
 ATTRS{consistent_dma_mask_bits}=="32"
 ATTRS{dma_mask_bits}=="32"
 ATTRS{local_cpus}=="ff"
 ATTRS{device}=="0x0774"
 ATTRS{msi_bus}==""
 ATTRS{local_cpulist}=="0-7"
 ATTRS{vendor}=="0x15ad"
 ATTRS{subsystem_device}=="0x1976"
 ATTRS{d3cold_allowed}=="0"

looking at parent device '/devices/pci0000:00/0000:00:11.0':
 KERNELS=="0000:00:11.0"
 SUBSYSTEMS=="pci"
 DRIVERS==""
 ATTRS{irq}=="0"
 ATTRS{subsystem_vendor}=="0x15ad"
 ATTRS{broken_parity_status}=="0"
 ATTRS{class}=="0x060401"
 ATTRS{consistent_dma_mask_bits}=="32"
 ATTRS{dma_mask_bits}=="32"
 ATTRS{local_cpus}=="ff"
 ATTRS{device}=="0x0790"
 ATTRS{msi_bus}=="1"
 ATTRS{local_cpulist}=="0-7"
 ATTRS{vendor}=="0x15ad"
 ATTRS{subsystem_device}=="0x0790"
 ATTRS{d3cold_allowed}=="0"

looking at parent device '/devices/pci0000:00':
 KERNELS=="pci0000:00"
 SUBSYSTEMS==""
 DRIVERS==""

Let’s pretend we want to match against this device in order to give the device node in /dev permissions that allow it to be read and written by all users on the computer. Unfortunately, this PL-2303-based serial adapter does not seem to provide a serial number, so we’ll have to match against every PL-2303 device that uses the product and vendor ID that this device uses. (Side note: FTDI USB to serial chipsets such as the FT232RL also include a unique serial number, so you can identify a particular USB to serial converter dongle by its serial number. The PL-2303 chipset, at least in this case, does not seem to provide that capability)

First of all, notice that one of the parent devices (/devices/pci0000:00/0000:00:11.0/0000:02:00.0/usb2/2-2/2-2.1) contains the vendor and product IDs of the USB dongle:

  • ATTRS{idVendor}==”067b”
  • ATTRS{idProduct}==”2303″

That will be how we will identify this particular device. This is not the device we want to change the permissions of though; we want to change the permissions of the device at the top of the output that belongs to the “tty” subsystem. That is the actual character device shown in /dev for the port.

Everything above is all of the information we need. Let’s get started creating the rule. I’m going to put it in /etc/udev/rules.d/99-test-usb-dongle.rules. First of all, we will specify that we want to match a device that has a subsystem of tty:

SUBSYSTEM=="tty"

This will ensure we’re changing the permissions of the actual tty device rather than any of the parent USB or PCI devices. Next, we want to match the USB dongle by its USB product and vendor IDs. The USB dongle is represented by several parent devices. In order to match a parent device’s attributes, we will have to use the items that end in S (e.g. ATTRS, KERNELS, SUBSYSTEMS). In this case, we will use the idVendor and idProduct attributes of the device I talked about above:

SUBSYSTEM=="tty", ATTRS{idVendor}=="067b", ATTRS{idProduct}=="2303"

Luckily, both of the ATTRS{} values we need to match belong to the same parent device, which is exactly how it has to work with udev. Now that we’ve matched ATTRS{} contained in the device “/devices/pci0000:00/0000:00:11.0/0000:02:00.0/usb2/2-2/2-2.1”, we can’t match the ATTRS{} or other “ending in S” keys of any other parent device. In this case, we luckily don’t have to worry about that. In other cases, it may be useful to be able to match against keys of multiple parent devices. If you ever run into that kind of situation, you can use LABEL and GOTO to jump to another rule that does another check for other properties of a different parent device. I’m not going to talk about that in this post, but if you look at some of the bundled rules you should be able to figure out how GOTO and LABEL work.

Anyway, back to what we were doing. We have completed the match portion of our rule. All that is left to do is add an assignment to the permissions when the conditions are matched:

SUBSYSTEM=="tty", ATTRS{idVendor}=="067b", ATTRS{idProduct}=="2303", MODE="0666"

This sets the permissions of the tty device to allow reading and writing for the owner, the group, and everyone else. Notice that the assignment uses a single “=” rather than a double “==” like the comparisons used. I think that difference was the biggest stumbling block when I first began looking at udev rules. I was very familiar with the difference between “=” and “==” in C, but I didn’t expect udev to use the same distinction. It turns out that it does.

That’s really all it takes. If you save that file in the location I mentioned earlier (/etc/udev/rules.d/99-test-usb-dongle.rules) and reload the udev rules, the next time you plug a PL-2303 USB dongle in, it should have correct permissions.

To reload the udev rules type the following command:

sudo udevadm control --reload-rules

That’s really all there is to it. Now I’d like to do a more complicated example:

More complicated example

Here’s a rule I created for giving certain USB-serial adapters a different name. If a USB-serial adapter with the serial number “12345678” is plugged in, I want a symlink of the form “ttyBlah#” to be created to point to it, where # is an integer. So if I plug three of them in, I want them to be called ttyBlah0, ttyBlah1, and ttyBlah2. This seems straightforward, but there might be other USB-serial adapters plugged in so I can’t just copy the kernel’s device number. For example, let’s pretend five USB-serial adapters are plugged in, named ttyUSB0 through ttyUSB4. ttyUSB0, ttyUSB1, and ttyUSB4 have the serial number “12345678” and ttyUSB2 and ttyUSB3 are other things that don’t use that serial number. I want my symlinks to be called ttyBlah0, ttyBlah1, and ttyBlah2. Notice that I can’t just blindly copy the number over because I want the third one to be called ttyBlah2, not ttyBlah4. It turns out udev can do this with a little bit of help.

I know this idea of having multiple USB devices with the same serial number seems goofy, but I actually did this in the real world. I programmed several FTDI chips to have the same serial number. It’s more convenient than getting a new USB device ID and not knowing if the Linux kernel is going to support it out of the box. The main reason I did it, though, was not Linux-related: it helps prevent Windows from creating a new COM port number each time a device with a different serial number is plugged in. In certain scenarios this behavior is undesirable.

Let’s get to the fun part. Here’s my example rule:

ACTION=="add", KERNEL="ttyUSB[0-9]*", SUBSYSTEMS=="usb", ATTRS{serial}=="12345678", PROGRAM="/home/doug/symlink-number.sh ttyBlah", SYMLINK+="ttyBlah%c"

This rule makes use of several new ideas. First of all, I have to match a device with a kernel name of ttyUSB followed by zero or more digits. This should match all USB serial ports. A parent device needs to be in the USB subsystem. This same parent device also needs to have a serial number of 12345678. The reason we’re checking for a subsystem of USB is to ensure that we don’t match against a device that has a PCI serial number (for example) of 12345678 instead. It’s probably overkill since ttyUSB devices are only going to be USB devices anyway, but it’s a good example of how the “ending in S” keys work. Anyway, we’re making sure we match against a ttyUSB* device which has a USB serial number of 12345678. Those three rules combined should match my three adapters that have that serial number.

If they match, we have two assignments. The first assignment is to PROGRAM. If you assign a value to PROGRAM, the value is interpreted as a command to run. The command is executed and the program’s output to stdout can be used in various ways in udev. This script (which we will see later) will print out the next available number to use for ttyBlah. Note that I passed a single argument to my script: ttyBlah. As you’ll see, I made this script generic so it could be used for numbering various different symlinks.

The second assignment uses the += operator, which as I said earlier adds a value to a list. The SYMLINK variable is a list of names of symlinks to create that will point to the matched device. We use += in case another rule has already set up a symlink for this same port.

You’ll notice that the symlink will be named ttyBlah%c. Several of the assigned values (one of which is SYMLINK) can be given names with printf-style formatters to add extra information. %c will be replaced with the stdout output from the program that was executed from the PROGRAM assignment. Another example of the available printf-style substitutions is %b which is the name of the parent device that was matched with the “ending in S” keys.

OK, so that’s pretty straightforward. Finally, I’ll give you the contents of my symlink-number.sh script (make sure you chmod +x it!):

#!/bin/sh

SYMPREFIX=$1

for i in `seq 0 100`
do
    if [ ! -e /dev/${SYMPREFIX}${i} ]; then
        echo ${i}
        exit 0
    fi
done

echo Unknown

This is a really crude script, but basically it searches for the next free index between 0 and 100. If for some reason it can’t find a free one, it prints out Unknown. This is actually a pretty lame way to write a script and there are probably better ways to do it. I was just lazy. I’ll never have more than about 5 USB-serial adapters plugged in at a time, so the crude technique doesn’t really bother me.

Conclusion

Don’t be scared by udev. It’s really not that bad. Just sit down, read the very helpful man page, and don’t be afraid to use udevadm’s attribute-walk feature to help you figure out what you need to do.