The Basics of Writing udev Rules

According to Wikipedia, "udev (userspace /dev) is a device manager for the Linux kernel. As the successor of devfsd and hotplug, udev primarily manages device nodes in the /dev directory. At the same time, udev also handles all user space events raised when hardware devices are added into the system or removed from it, including firmware loading as required by certain devices." Cool, but ... what the hell does that mean? Almost everything in Linux is a "learn by doing" topic, but this more so than most ... Let's get to work.

Disclaimer: this was written and tested on Debian 12/bookworm. I think most of these processes are similar on other Linux distros, but I can't guarantee it.

Start Simple

Grab a USB stick. Run (as root) dmesg --follow which essentially "tails" the kernel messages. Take a look at the recent messages, to get an idea of what's been happening, and particularly what the current latest message is. In my case it was "[2279055.867682] PM: suspend exit". Plug the USB stick into your computer: you'll see a bunch of text similar to this:

[2280121.095999] usb 1-1.4: new high-speed USB device number 73 using xhci_hcd
[2280121.212225] usb 1-1.4: New USB device found, idVendor=0951, idProduct=1603, bcdDevice= 2.00
[2280121.212240] usb 1-1.4: New USB device strings: Mfr=1, Product=2, SerialNumber=5
[2280121.212246] usb 1-1.4: Product: DataTraveler 2.0
[2280121.212250] usb 1-1.4: Manufacturer: Kingston
[2280121.212254] usb 1-1.4: SerialNumber: 200801250000000000004315
[2280121.215472] usb-storage 1-1.4:1.0: USB Mass Storage device detected
[2280121.216050] scsi host4: usb-storage 1-1.4:1.0
[2280122.233164] scsi 4:0:0:0: Direct-Access     Kingston DataTraveler 2.0 1.00 PQ: 0 ANSI: 2
[2280122.233925] sd 4:0:0:0: Attached scsi generic sg3 type 0
[2280122.234562] sd 4:0:0:0: [sdd] 15769600 512-byte logical blocks: (8.07 GB/7.52 GiB)
[2280122.234997] sd 4:0:0:0: [sdd] Write Protect is off
[2280122.235003] sd 4:0:0:0: [sdd] Mode Sense: 23 00 00 00
[2280122.235440] sd 4:0:0:0: [sdd] No Caching mode page found
[2280122.235444] sd 4:0:0:0: [sdd] Assuming drive cache: write through
[2280122.463555]  sdd:
[2280122.463837] sd 4:0:0:0: [sdd] Attached SCSI removable disk

This gives you a fair bit of information about the USB stick ... although maybe not as much as we're eventually going to want to write udev rules. We need a pointer to the device, and given that this is a storage device, the place to look is /dev/disk/by-id/ where I found usb-Kingston_DataTraveler_2.0_200801250000000000004315-0:0, which was a link to ../../sdd. I'm beginning to really like /dev/disk/by-id/ (and its relative /dev/input/by-id/ - we'll get to that another day) because that ID will be the same if you plug the stick into another Linux machine, and unique - even if you have another 8G Kingston DataTraveler stick.

Hit Ctrl-C to break out of dmesg --follow.

Track "add" and "remove" USB Events

Before we get to writing event handlers, we need a simple logging script to keep track of our events. I placed this one at /usr/local/bin/udevlogger and the contents look like this:

#!/bin/bash
echo "$(/usr/bin/date +%Y%m%d%H%M.%S) $@" >> /tmp/udev.log

That's it: all it does is write a datestamp and all parameters passed to it into a file in the /tmp folder. Keep in mind that the /tmp/ folder is wiped every reboot: if you do this work as root, you could write to /var/log/ where logs are more traditionally placed, but this is temporary, an experiment - so for now I recommend temporary storage.

The other part of this is an actual udev rule. This will be in a text file that should be added to the /etc/udev/rules.d/ folder. The filename needs to end with .rules, and common practice is to call it something like 80-local.rules. If there are a lot of rules they'll usually be numbered, and the "80" indicates where in the sequence this would run - which is to say "not very important" as rules 00 through 79 will run before it. My /etc/udev/rules.d/ folder was empty, so it's kind of a moot point ...

Create /etc/udev/rules.d/80-local.rules:

SUBSYSTEM=="block", ACTION=="add",    RUN+="/usr/local/bin/udevlogger 'block add'"
SUBSYSTEM=="block", ACTION=="remove", RUN+="/usr/local/bin/udevlogger 'block remove'"

Again, this must be done as root, and since you're messing with the kernel's behaviour - be a bit careful. In this case, probably the most important thing is the + after the RUN: that means "do this also." If your system auto-mounts USB sticks, removing the + could potentially stop any other actions from happening, like that auto-mount. SUBSYSTEM is "block" because we're looking for a block device, namely USB storage. And we're looking for both the "add" and "remove" actions, and doing marginally different things in each case.

Whenever you make changes to udev rules, there's one more step needed: you need to reload the rules with udevadm control --reload. After you've done the reload, plug in (or unplug) your USB stick. Then look at /tmp/udev.log which should now include a dated notice about the add/remove of the USB stick. If you're going to end up tweaking the rules over and over, I would recommend you run tail -f /tmp/udev.log in another terminal so any changes to the file automatically appear in that terminal.

Identifying Specific USB Devices

Stop and think about what we've done: when you add or remove a USB block device, a shell script is run. This could allow us to copy data off, or to, a specific USB device. What if we could be more specific about which device? To do that, we need to be able to identify our devices. Examine the output of: udevadm info --attribute-walk --name=/dev/disk/by-id/usb-Kingston_DataTraveler_2.0_200801250000000000004315-0:0. We're looking for "ATTR" entries that look unique. Initially I tried 'ATTRS{vendor}=="Kingston"' and 'ATTRS{model}=="DataTraveler 2.0"' in the new rule below. First, they're not unique, second ... they didn't work.

This is a good time to pause and point out that the best piece of advice I got from all my reading on this subject is to CHANGE ONE THING AT A TIME. And lest it not be obvious: test after each change.

A couple caveats here: the ATTRS you're looking at may not look like those shown, for example I have another device (input, not block) that called these "ATTR{id/vendor}" and "ATTR{id/product}" and then the values were hexadecimal, so this stuff can vary considerably. Also: the listed output includes the whole USB tree, and the attributes that worked for me were under "looking at parent device" which was ... unexpected. What I used was found by searching for the specific serial number, and copying both that ATTR and the nearby "idVendor" ATTR. We take these values, and craft a new line in /etc/udev/rules.d/80-local.rules:

SUBSYSTEM=="block", ACTION=="add",    RUN+="/usr/local/bin/udevlogger 'block add'"
SUBSYSTEM=="block", ACTION=="remove", RUN+="/usr/local/bin/udevlogger 'block remove'"
SUBSYSTEM=="block" \
, ACTION=="add" \
, ATTRS{idVendor}=="0951" \
, ATTRS{serial}=="200801250000000000004315" \
, RUN+="/usr/local/bin/udevlogger 'vendor / serial number inserted'"

In this case, I've constructed the new line with backslashes for line continuation. This can be useful when lines would otherwise be very long. You should also notice that I'm now triggering two events on "insert" of the specific USB stick: one generic, which will log "block add" (and would do so for any USB stick or USB HD) and one highly specific that will trigger only on this USB stick. Yes - you can have multiple rules.

Join us next time for more on how to identify and utilize old/obscure USB input devices!

Bibliography