Sometimes you think you have a rough idea of how things are supposed to work, and then somehow they don’t. This post is being written as I figure out how this works and maybe this helps someone trying to learn about this. The goal is to allow a user to control the keyboard backlight (and probably later on other stuff). It involves: a recent archlinux, sysfs, systemd, udev, seats.
I already knew that sysfs is a mechanism used in Linux to control hardware through the filesystem. This means that under /sys/, there are lots of files that control devices by reading or writing things into the respective files. Some of these are specific to the manufacturer, because hard- and firmware handle things differently. On this (Lenovo Thinkpad) machine, /sys/class/leds/ contains a bunch of different leds:
$ls -la /sys/class/leds/
input4::capslock/ phy0-led/ tpacpi::kbd_backlight/ tpacpi::standby/
input4::numlock/ platform::micmute/ tpacpi::lid_logo_dot/ tpacpi::thinklight/
input4::scrolllock/ platform::mute/ tpacpi::power/ tpacpi::thinkvantage/
In this case, we’re interested in tpacpi::kbd_backlight (keyboard backlight). The folder is a symbolic link to the corresponding device in the sysfs:
$ls -la /sys/class/leds/tpacpi\:\:kbd_backlight
lrwxrwxrwx 1 root root 0 Jan 7 10:19 /sys/class/leds/tpacpi::kbd_backlight -> ../../devices/platform/thinkpad_acpi/leds/tpacpi::kbd_backlight
I checked – we can set the brightness (respecting the value in /sys/class/leds/tpacpi\:\:kbd_backlight/max_brightness – 2 on my system) of the backlight via:
echo 1 > /sys/class/leds/tpacpi\:\:kbd_backlight/brightness
but this only works as root, and indeed:
ls -la /sys/class/leds/tpacpi\:\:kbd_backlight/brightness
-rw-r--r-- 1 root root 4096 Jan 7 09:36 /sys/class/leds/tpacpi::kbd_backlight/brightness
First idea: change this to root:light, so that users in the light group can modify the file and done. Changing this directly doesn’t work, because sysfs is not really a filesystem, i.e., the permissions are not modifiable, they’re set by the system (I’m not sure if it is the kernel, some firmware or systemd). Some googling suggests that udev and its rules are the way to modify these permissions, see e.g. archwiki on (monitor) backlight. This page suggests changing access via a RUN+= rule, but reading the udev article, there are builtin commands. So I thought I’d just write one. I learned that standard udev rules are located in /usr/lib/udev/rules.d and that there is a difference between system and non-system groups (see man groupadd – in short this works via the groupid). I should have read the README first, but instead I wrote a short rule:
cat /etc/udev/rules.d/00-light.rules
# allow users in light to control backlights
KERNEL=="tpacpi::kbd_backlight" SUBSYSTEM=="leds" GROUP="light" MODE="0660"
The KERNEL and SUBSYSTEM information can be found via the command udevadm info, e.g.: udevadm info --attribute-walk /sys/class/leds/tpacpi::kbd_backlight, though it would have been smarter to leave out the attribute walk option, since the info command lists other info as well:
udevadm info /sys/class/leds/tpacpi::kbd_backlight
P: /devices/platform/thinkpad_acpi/leds/tpacpi::kbd_backlight
M: tpacpi::kbd_backlight
J: +leds:tpacpi::kbd_backlight
U: leds
E: DEVPATH=/devices/platform/thinkpad_acpi/leds/tpacpi::kbd_backlight
E: SUBSYSTEM=leds
E: USEC_INITIALIZED=XXXXXXXX
E: ID_PATH=platform-thinkpad_acpi
E: ID_PATH_TAG=platform-thinkpad_acpi
E: ID_FOR_SEAT=leds-platform-thinkpad_acpi
E: SYSTEMD_WANTS=systemd-backlight@leds:tpacpi::kbd_backlight.service
E: TAGS=:systemd:seat:
E: CURRENT_TAGS=:systemd:seat:
as the README file says, we can check the active udev configuration via:
systemd-analyze cat-config udev/rules.d
which outputs all active rules. I tried to use the rule I created above, but this did not change the permissions after reloading thee config (it is possible a reboot may have been required). However, I then found the “allowing regular users to use devices” section, which at the end explains that one should use a uaccess tag (for user access). /usr/lib/udev/rules.d/73-seat-late.rules is then the magic glue that should set the permission. Since rules in /etc/ are applied before these rules (at least according to udevadm test), we can hopefully just change our rule to:
ACTION!="remove", SUBSYSTEM=="leds", MODE="0660", TAG+="uaccess"
Make sure to udevadm test /sys/class/leds/tpacpi::kbd_backlight and verify there are no errors (such as a double = for mode, which shows an error, or missing commas, which means the tag doesn’t show up in the simulated config) and then udevadm control --reload. Unfortunately:
echo 1 > /sys/class/leds/tpacpi\:\:kbd_backlight/brightness
bash: /sys/class/leds/tpacpi::kbd_backlight/brightness: Permission denied
I tried to use temporary debugging to find out why, using udevadm control --log-priority=debug and then journalctl -f, which after an explicit trigger of the device udevadm trigger /sys/class/leds/tpacpi::kbd_backlight only showed output for the MODE command (and it looks like it isn’t triggering the rules for uaccess). However, the mode command also doesn’t seem to actually change the mode. I don’t know what to do here, but a reboot may work.
After reboot: it did not. Searching on, I learned that I can verify whether the tag is applied correctly via udevadm monitor -t uaccess -u -p -k followed by a udevadm trigger /sys/class/leds/tpacpi::kbd_backlight. The monitor then indeed outputs that the tags and current tags include uaccess. Similarly, journalctl shows that the rule (which I have moved to check whether the execution sequence was the issue) is executed after the trigger:
Jan 07 13:41:45 namnatop (udev-worker)[3395]: tpacpi::kbd_backlight: /etc/udev/rules.d/72-light.rules:3 GROUP:="light": Set group ID: 965
Jan 07 13:41:45 namnatop (udev-worker)[3395]: tpacpi::kbd_backlight: /etc/udev/rules.d/72-light.rules:3 MODE="0660": Set mode: 0660
but there is no file with this group:
find /sys -group light
This might suggest it is overwritten later. I thought that in theory, the seat mechanism is the intended way to get access, so I figured I’d try that way. This question notes that loginctl is supposed to be the place where the seat magic happens (see man sd-login(3) if you, like me, first need an intro on what seats are). Checking:
loginctl seat-status seat0 | grep -C1 kbd_b
│ leds:platform::mute
├─/sys/devices/platform/thinkpad_acpi/leds/tpacpi::kbd_backlight
│ leds:tpacpi::kbd_backlight
├─/sys/devices/platform/thinkpad_acpi/leds/tpacpi::lid_logo_dot
Since this suggests it should already work, I deleted my custom rule. After also looking at the list-seats, list-sessions and list-users commands, I thought the issue might be that my window manager is doing something weird, but even in a simple login shell, I still get an access denied. Going back to the arch wiki on udev again, this explains how the uaccess is supposed to work by pointing at the systemd udev code. As far as I can tell, this should print errors if it fails, unless it things systemd-logind isn’t running. But is this builtin ever used..?
grep -r -E "builtin.*uaccess" /usr/lib/udev/
/usr/lib/udev/rules.d/73-seat-late.rules:TAG=="uaccess", ENV{MAJOR}!="", RUN{builtin}+="uaccess"
Indeed this also matches the systemd-analyze output.
Some solutions for “I want to write this thing” suggest using chgrp and chown instead:
ACTION!="remove", SUBSYSTEM=="leds", RUN+="/bin/chgrp seat $sys$devpath/brightness", RUN+="/bin/chmod g+w $sys$devpath/brightness"
Interestingly, this works fine. I still have no idea why the intended mechanism (uaccess) doesn’t work, though…