bucklespring-libinput and access to the keyboard devices ======================================================== The executable in this package, buckle-libinput, reads key events straight from the raw input devices in /dev/input through libinput, rather than from X11, so that it also works under a Wayland compositor and on the console. It is registered as an alternative for /usr/games/buckle, so with both flavours installed pick it with # update-alternatives --config buckle The input devices are root:input 0660, so out of the box buckle-libinput can only read them as root. Running it as root is no good either, as then cannot find the PulseAudio/PipeWire socket of your session so cannot play audio. Input device access has to be granted to the normal user. Any way of doing that has security implications: processes able to read /dev/input/event* can log every keystroke on the machine, including passwords typed into other applications. Bucklespring is just for fun, so you should only do this if you accept the security risk for pure whimsy. Recommended: the uaccess udev rule ---------------------------------- The package installs /usr/lib/udev/rules.d/70-bucklespring-libinput.rules which tags the keyboard and pointer event devices with "uaccess", making systemd-logind put an ACL on them for the user who is physically logged in at the local seat. Access is limited to that active local session and is revoked at logout, so remote or background users of the machine do not get it. The rule does nothing until you enable it, and enabling it is a flag file rather than a copy of the rule. Installing the package asks whether to turn it on, at medium debconf priority and defaulting to no; to change that answer later: # dpkg-reconfigure bucklespring-libinput which creates or deletes /etc/bucklespring/uaccess and re-triggers the input devices for you. That answer is authoritative: the file is rewritten from it whenever the package is configured, so do it by hand only as a temporary change, and remember the trigger: # touch /etc/bucklespring/uaccess # udevadm trigger --subsystem-match=input --action=change Check that it took with "getfacl /dev/input/event3" (any keyboard device), which should now list your user with "rw-". Taking the access away again is the same in reverse: # rm /etc/bucklespring/uaccess # udevadm trigger --subsystem-match=input --action=change The pointer devices are tagged for the sake of the click sound on mouse buttons. To get key sounds without handing out the mouse and touchpad, copy the rule into /etc/udev/rules.d/ under the same name, delete the ID_INPUT_MOUSE and ID_INPUT_TOUCHPAD lines in the copy, and reload: # cp /usr/lib/udev/rules.d/70-bucklespring-libinput.rules /etc/udev/rules.d/ # editor /etc/udev/rules.d/70-bucklespring-libinput.rules # udevadm control --reload # udevadm trigger --subsystem-match=input --action=change A file of that name in /etc/udev/rules.d takes precedence over the one in /usr/lib and, unlike the shipped one, is not overwritten on upgrade. A symlink there pointing at /dev/null disables the rule outright. The 70 in the file name matters: it sorts the rule after 60-persistent-input.rules, which is what sets ID_INPUT_KEYBOARD, and before 73-seat-late.rules, which acts on the uaccess tag. Narrowing the grant to a group of users -------------------------------------- The uaccess mechanism is per seat rather than per user: it grants to whoever holds the active session at the local seat, and /etc/bucklespring/uaccess is a switch for the whole machine. On a desktop with several accounts you may want only some of them to have it. That is an intersection of two facts, "is in a group" and "is logged in at the local seat", and the only place a privileged process gets told both is the PAM session stack at login. The package does not set this up, because installing it means editing the system authentication configuration, which is more than a program that makes typing noises ought to do to your machine. Doing it by hand is not hard. Leave the debconf question answered no, so the shipped udev rule stays inert and logind grants nothing to anybody, and prepare the group: # apt-get install acl # addgroup --system bucklespring # adduser bucklespring Save this as /usr/local/sbin/bucklespring-acl, owned by root and mode 0755: #!/bin/sh # Give members of group bucklespring access to the input devices for the # length of a login at the local seat. Run as root by pam_exec(8). set -e : "${PAM_USER:?}" # Remote logins never get the access, and, just as important, logging out # of one must not take it away from the seat. [ -z "${PAM_RHOST:-}" ] || exit 0 groups=$(id -nG "$PAM_USER" 2> /dev/null || true) case " $groups " in *" bucklespring "*) ;; *) exit 0 ;; esac # The same devices the shipped udev rule would tag. Drop MOUSE and # TOUCHPAD here for key sounds without handing out the pointer devices. for_each_device() { for dev in /dev/input/event*; do [ -c "$dev" ] || continue udevadm info --query=property --name="$dev" 2> /dev/null | grep -qE '^ID_INPUT_(KEYBOARD|MOUSE|TOUCHPAD)=1$' || continue "$@" "$dev" || true done } # rw, not r, as libinput opens the event nodes O_RDWR; this is the same # access uaccess itself hands out. grant() { setfacl -m "u:$PAM_USER:rw" "$1"; } revoke() { setfacl -x "u:$PAM_USER" "$1"; } case "${PAM_TYPE:-}" in open_session) # Only once logind has actually put a session of this user on a seat. for s in $(loginctl show-user "$PAM_USER" \ --property=Sessions --value 2> /dev/null); do seat=$(loginctl show-session "$s" \ --property=Seat --value 2> /dev/null || true) if [ -n "$seat" ]; then for_each_device grant break fi done ;; close_session) for_each_device revoke ;; esac exit 0 Then add it to the end of /etc/pam.d/common-session, below the line reading "# end of pam-auth-update config": session optional pam_exec.so /usr/local/sbin/bucklespring-acl Local modules belong outside that block, which pam-auth-update rewrites, and being below it also puts the line after pam_systemd.so, so that the logind session exists by the time the script goes looking for it. Finally, the shipped user unit refuses to start while the flag file /etc/bucklespring/uaccess is missing, which it now always is, so drop that condition with $ systemctl --user edit buckle-libinput and a drop-in of [Unit] ConditionPathExists= Check the result with "getfacl /dev/input/event3" after a fresh login: a member of the group should be listed with "rw-" and anybody else not at all. Two things to know about it. Devices plugged in during a session do not get the ACL, as nothing runs between login and logout; log out and back in. And two simultaneous local logins by the same user, a desktop session and a text console say, share the one ACL, so leaving either takes the access away from both until the next login. Starting it automatically ------------------------- The package ships a systemd user unit, buckle-libinput.service, not enabled on install: $ systemctl --user enable --now buckle-libinput It follows the desktop session on the local seat, and does nothing until the access above has been granted. See buckle-libinput.service(8) for the details, including what to do about a login on a plain text console. Alternative: the input group ---------------------------- # adduser input This is coarser: it applies to all of that user's processes at all times, local session or not, so the uaccess rule above is preferable. It is however the only option on a system without systemd-logind or elogind. Why the binary is not setgid or capability marked ------------------------------------------------- Marking the binary itself ("chgrp input /usr/games/buckle-libinput" plus setgid, or setcap cap_dac_override) would confine the extra privilege to this one program rather than to everything the user runs, which sounds like the better idea, but it has a few issues: - It gives *every* local account, including system and ssh-only ones that never sit at this machine, a ready made way to read all keyboard input. - buckle links OpenAL and alure, which read configuration files and load backend plugins at runtime; that is a large and unaudited surface to run under a privilege the caller does not otherwise have. - cap_dac_override in particular is not "may read input devices", it is "may bypass all file permission checks", i.e., close to root. It does not drop the privilege after opening the devices, and hardening the bucklespring executable enough to be a trustworthy setgid program would entail considerable effort. Granting the access to the seat-local user through udev has a smaller vulnerability surface. Note that buckle-libinput prints "Failed to open /dev/input/eventN (Permission denied)" on startup for the input devices the rule above does not cover, and libinput just skips those. The message is harmless. -- Barak A. Pearlmutter , Tue, 1 Sep 2026 09:04:54 +0100