In the last post in this series, I complained that the userspace driver needed to run as root in order to open the device files. It turns out that this is relatively easy to fix using Udev rules.
Udev is the device management subsystem of many modern Linux distributions. All Debian-derived, Fedora-derived, and Arch-derived distros use it. I can’t speak for other families of Linux, but I think it’s reasonably ubiquitous.
Using Udev, we can change the permissions of both the /dev/uinput
device file and the /dev/hidrawX
device file that the userspace driver needs to open so that it can open them without running as root.
I was recently playing with uinput
from Golang using this repository and their README provided a handy Udev rule to make the /dev/uinput
device file writable by the user.
echo KERNEL==\"uinput\", GROUP=\"$USER\", MODE:=\"0660\" | sudo tee /etc/udev/rules.d/99-$USER.rules sudo udevadm trigger
This rule specifies that devices that the kernel calls “uinput” should be owned by the current $USER
‘s group and that group should have read and write permission on them.
The udevadm
command asks for the effects of all rules to be re-appplied so that the new permissions take effect. Once you’ve configured Udev in this way, /dev/uinput
will always have those permissions (even after you restart).
Using that rule solved exactly half of my problems. I still needed to the /dev/hidrawX
file corresponding to the keyboard function keys to be readable by a normal user. To do this, I needed to write a custom Udev rule.
I used the Udev man pages and this somewhat outdated guide to initially construct the rule:
KERNEL=="hidraw*", ATTRS{idVendor}=="058f", ATTRS{idProduct}=="9410", GROUP="chris"
This rule asks for any file called hidraw
(followed by anything) that has the vendor ID and product ID of the Kinesis Freestyle 2 to be given the group chris
as an owner. Udev defaults to giving read/write permission when you add a group to a device, so we don’t have to add MODE="0660"
, but we could.
I screwed up writing this rule several times, and I (at first) found it incredibly difficult to debug Udev. All of the resources that I could find referenced tools like udevinfo
and udevtest
that don’t exist on my computer. To verify my rules, I needed to figure out how to query device information from Udev and to test whether my rules applied to the devices that I wanted them to. Let’s take these one at a time:
Getting Udev Device Info
Since udevinfo
no longer exists, we have to use a different tool: udevadm
.
udevadm
is the administration tool for the Udev subsystem. When trying to figure out how to get Udev info for the keyboard (so that I could match against it in my Udev rules), I found this ArchWiki section on the subject.
Apparently, udevadm info
has subsumed the functionality of udevinfo
. To get all of the properties of a device (and its parent devices), we can run: udevadm info -a -n
. For example, running it on the raw device file for the Kinesis function keys:
$ udevadm info -a -n /dev/hidraw1 Udevadm info starts with the device specified by the devpath and then walks up the chain of parent devices. It prints for every device found, all possible attributes in the udev rules key format. A rule to match, can be composed by the attributes of the device and the attributes from one single parent device. looking at device '/devices/pci0000:00/0000:00:14.0/usb1/1-7/1-7:1.1/0003:058F:9410.000E/hidraw/hidraw1': KERNEL=="hidraw1" SUBSYSTEM=="hidraw" DRIVER=="" looking at parent device '/devices/pci0000:00/0000:00:14.0/usb1/1-7/1-7:1.1/0003:058F:9410.000E': KERNELS=="0003:058F:9410.000E" SUBSYSTEMS=="hid" DRIVERS=="hid-generic" ATTRS{country}=="00" looking at parent device '/devices/pci0000:00/0000:00:14.0/usb1/1-7/1-7:1.1': KERNELS=="1-7:1.1" SUBSYSTEMS=="usb" DRIVERS=="usbhid" ATTRS{authorized}=="1" ATTRS{bAlternateSetting}==" 0" ATTRS{bInterfaceClass}=="03" ATTRS{bInterfaceNumber}=="01" ATTRS{bInterfaceProtocol}=="00" ATTRS{bInterfaceSubClass}=="00" ATTRS{bNumEndpoints}=="01" ATTRS{supports_autosuspend}=="1" looking at parent device '/devices/pci0000:00/0000:00:14.0/usb1/1-7': KERNELS=="1-7" SUBSYSTEMS=="usb" DRIVERS=="usb" ATTRS{authorized}=="1" ATTRS{avoid_reset_quirk}=="0" ATTRS{bConfigurationValue}=="1" ATTRS{bDeviceClass}=="00" ATTRS{bDeviceProtocol}=="00" ATTRS{bDeviceSubClass}=="00" ATTRS{bMaxPacketSize0}=="8" ATTRS{bMaxPower}=="50mA" ATTRS{bNumConfigurations}=="1" ATTRS{bNumInterfaces}==" 2" ATTRS{bcdDevice}=="0122" ATTRS{bmAttributes}=="a0" ATTRS{busnum}=="1" ATTRS{configuration}=="" ATTRS{devnum}=="10" ATTRS{devpath}=="7" ATTRS{idProduct}=="9410" ATTRS{idVendor}=="058f" ATTRS{ltm_capable}=="no" ATTRS{manufacturer}=="KINESIS FREESTYLE KB800" ATTRS{maxchild}=="0" ATTRS{product}=="KB800 Kinesis Freestyle" ATTRS{quirks}=="0x0" ATTRS{removable}=="removable" ATTRS{speed}=="1.5" ATTRS{urbnum}=="15" ATTRS{version}==" 1.10" looking at parent device '/devices/pci0000:00/0000:00:14.0/usb1': KERNELS=="usb1" SUBSYSTEMS=="usb" DRIVERS=="usb" ATTRS{authorized}=="1" ATTRS{authorized_default}=="1" ATTRS{avoid_reset_quirk}=="0" ATTRS{bConfigurationValue}=="1" ATTRS{bDeviceClass}=="09" ATTRS{bDeviceProtocol}=="01" ATTRS{bDeviceSubClass}=="00" ATTRS{bMaxPacketSize0}=="64" ATTRS{bMaxPower}=="0mA" ATTRS{bNumConfigurations}=="1" ATTRS{bNumInterfaces}==" 1" ATTRS{bcdDevice}=="0413" ATTRS{bmAttributes}=="e0" ATTRS{busnum}=="1" ATTRS{configuration}=="" ATTRS{devnum}=="1" ATTRS{devpath}=="0" ATTRS{idProduct}=="0002" ATTRS{idVendor}=="1d6b" ATTRS{interface_authorized_default}=="1" ATTRS{ltm_capable}=="no" ATTRS{manufacturer}=="Linux 4.13.0-16-generic xhci-hcd" ATTRS{maxchild}=="12" ATTRS{product}=="xHCI Host Controller" ATTRS{quirks}=="0x0" ATTRS{removable}=="unknown" ATTRS{serial}=="0000:00:14.0" ATTRS{speed}=="480" ATTRS{urbnum}=="243" ATTRS{version}==" 2.00" looking at parent device '/devices/pci0000:00/0000:00:14.0': KERNELS=="0000:00:14.0" SUBSYSTEMS=="pci" DRIVERS=="xhci_hcd" ATTRS{broken_parity_status}=="0" ATTRS{class}=="0x0c0330" ATTRS{consistent_dma_mask_bits}=="64" ATTRS{d3cold_allowed}=="1" ATTRS{device}=="0x9d2f" ATTRS{dma_mask_bits}=="64" ATTRS{driver_override}=="(null)" ATTRS{enable}=="1" ATTRS{irq}=="122" ATTRS{local_cpulist}=="0-3" ATTRS{local_cpus}=="f" ATTRS{msi_bus}=="1" ATTRS{numa_node}=="-1" ATTRS{revision}=="0x21" ATTRS{subsystem_device}=="0x1303" ATTRS{subsystem_vendor}=="0x1558" ATTRS{vendor}=="0x8086" looking at parent device '/devices/pci0000:00': KERNELS=="pci0000:00" SUBSYSTEMS=="" DRIVERS==""
If you’d like to see the output, click on the command above. It’s quite long.
Unfortunately, the information that I got from udevadm
only confirmed that my rule looked correct. The next logical step was trying to test it.
Testing Udev Rules
Once again, ArchWiki had the answers for how to properly test Udev rules using udevadm test
.
Since my raw device file was /dev/hidraw1
, I ran the following to test my rules:
$ udevadm test (udevadm info -q path -n /dev/hidraw1) calling: test version 234 === trie on-disk === tool version: 234 file size: 9117970 bytes header size 80 bytes strings 1906146 bytes nodes 7211744 bytes Load module index Failed to read $container of PID 1, ignoring: Permission denied Found container virtualization none. timestamp of '/etc/systemd/network' changed timestamp of '/lib/systemd/network' changed Parsed configuration file /lib/systemd/network/99-default.link Created link configuration context. timestamp of '/etc/udev/rules.d' changed timestamp of '/lib/udev/rules.d' changed Reading rules file: /lib/udev/rules.d/39-usbmuxd.rules Reading rules file: /lib/udev/rules.d/40-usb_modeswitch.rules Reading rules file: /lib/udev/rules.d/40-vm-hotadd.rules ... <output abridged> ... Reading rules file: /lib/udev/rules.d/97-hid2hci.rules Reading rules file: /etc/udev/rules.d/99-chris.rules Reading rules file: /lib/udev/rules.d/99-systemd.rules rules contain 393216 bytes tokens (32768 * 12 bytes), 35035 bytes strings 24112 strings (203963 bytes), 20923 de-duplicated (172118 bytes), 3190 trie nodes used value '[dmi/id]sys_vendor' is 'Notebook ' value '[dmi/id]sys_vendor' is 'Notebook ' GROUP 1000 /etc/udev/rules.d/99-chris.rules:2 handling device node '/dev/hidraw1', devnum=c246:1, mode=0660, uid=0, gid=1000 preserve permissions /dev/hidraw1, 020660, uid=0, gid=1000 preserve already existing symlink '/dev/char/246:1' to '../hidraw1' Unload module index Unloaded link configuration context. This program is for debugging only, it does not run any program specified by a RUN key. It may show incorrect results, because some values may be different, or not available at a simulation run. .MM_USBIFNUM=01 ACTION=add DEVNAME=/dev/hidraw1 DEVPATH=/devices/pci0000:00/0000:00:14.0/usb1/1-7/1-7:1.1/0003:058F:9410.0002/hidraw/hidraw1 MAJOR=246 MINOR=1 SUBSYSTEM=hidraw USEC_INITIALIZED=2702830805
By the highlighted lines, you can see that my rule is being applied. The test output says that Udev will set the gid
of the file to 1000, which is the group chris
that will own it. It is also setting the mode
to be 0660
, which allows both the owner (root) and the group (chris) to read and write the file.
Getting the rule to work
Armed with the knowledge that the rule is correct, I started looking for why it wasn’t being triggered. I’d run udevadm trigger
already a few times and it hadn’t helped.
The same ArchWiki page that helped with writing and testing the rules above suggested another fix for how to reload the rules:
$ udevadm control --reload root privileges required $ sudo udevadm control --reload $
After this, the rule started working. I think that udevadm trigger
would also have worked if I had thought to run it as root, but it doesn’t produce a helpful error message when run without permissions. You’ll notice that the instructions I used for the uinput
Udev rule included using sudo
to trigger the rules, but I was an idiot and didn’t realize that until now.
I’ve updated the README of my userspace driver with directions on how to set the permissions correctly so that the driver no longer needs to run as root.
Now only one task remains to make this driver really convenient: having it run automatically when the keyboard is plugged in. I’ll make that the subject of a future post when I have time to attempt it.
Image Credit: Michael Allen
Image License: CC BY-NC 2.0
[…] already had to tangle with udev once during this project, so it came as little surprise that the solution arose from […]
LikeLike