A spiky green plant

Kinesis Freestyle 2 and Linux, Part 3: Permissions

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

1 thought on “Kinesis Freestyle 2 and Linux, Part 3: Permissions”

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.