Skip to content

fix(linux): load uinput rather than rely on another daemon - #756

Open
AalmanSadath wants to merge 4 commits into
AprilNEA:masterfrom
AalmanSadath:fix/linux-uinput-module-load
Open

fix(linux): load uinput rather than rely on another daemon#756
AalmanSadath wants to merge 4 commits into
AprilNEA:masterfrom
AalmanSadath:fix/linux-uinput-module-load

Conversation

@AalmanSadath

Copy link
Copy Markdown

Summary

The Linux packages ship 70-openlogi.rules, which grants /dev/uinput with
TAG+="uaccess" — an ACL logind applies in response to a device event. On a
machine that has never loaded the uinput module there is no such device, so
postinstall's udevadm trigger --attr-match=name=uinput matches nothing. It is
a no-op, and its || true hides that. OPTIONS+="static_node=uinput" does create
the node, but a static node gets default permissions and no ACL: opening it would
autoload the module, except opening is exactly what the missing permission forbids.

Nothing in the package breaks that cycle, so today it is broken by whatever else
happens to open /dev/uinput first. On a typical desktop something does —
bluetoothd opens it seconds after boot, and Steam and logiops do too — and
OpenLogi works. Where nothing does, the agent starts, reports the device,
battery and DPI normally over HID++, and button remapping silently never works
,
with only a journal warning to say so.

systemd's own rules show the shape of the fix: snd/seq and snd/timer carry a
real GROUP= in 50-udev-default.rules alongside TAG+="uaccess" in
70-uaccess.rules, precisely because uaccess cannot cover the pre-module node.

Changes

  • packaging/linux/modules-load/openlogi.conf (new) — loads uinput at boot.
    Package content rather than something the script writes, so uninstalling removes
    it.
  • packaging/linux/nfpm.yaml — ships the above to /etc/modules-load.d/ at
    0644.
  • packaging/linux/nfpm-scripts/postinstall.shmodprobe uinput before the
    udevadm block, so the session that installed the package works too and not
    only the next boot.

No Rust changes; .deb, .rpm and .pkg.tar.zst all pick this up from the
shared nfpm config.

Reproducing

Any machine, ~30 seconds. Unloading the module and dropping the ACL leaves the
static node at root:root 0600 with no ACL — byte-for-byte the state a
never-loaded machine boots into.

systemctl --user stop openlogi-agent.service
sudo systemctl stop bluetooth          # or whatever holds /dev/uinput; check with: sudo lsof /dev/uinput
sudo modprobe -r uinput
sudo setfacl -x "u:$USER" /dev/uinput

test -w /dev/uinput && echo "WRITABLE — precondition not met" || echo "NOT WRITABLE — ready"

systemctl --user start openlogi-agent.service
journalctl --user -u openlogi-agent -n 5 --no-pager

Before this change:

WARN openlogi_agent_core::hook_runtime: could not install OS input hook —
  events will not be captured error=Linux input error: Permission denied (os error 13)

openlogi list still shows the mouse and its battery throughout, which is what
makes this easy to miss.

Restore with sudo systemctl start bluetooth && sudo modprobe uinput && sudo udevadm trigger.

Check the mouse's /dev/input/event* node is readable first. If it is not, the
hook fails earlier with no mouse device found under /dev/input and never
reaches /dev/uinput — a different problem.

Testing

Fedora 44, x86_64, on real hardware (MX Master 3S over Bluetooth).

Bug reproduced on two machines, both running released packages, neither with
this change:

  • openlogi-v0.7.4-linux-amd64.rpm, with logiops and Solaar removed
  • a second machine running the 0.7.3 package, with the mouse's event node
    confirmed readable first so the failure could only be the uinput side

Both produced Permission denied (os error 13) and created no virtual input
device. In its untouched state the second machine does not fail — bluetoothd
had held /dev/uinput since 14 s after boot.

Fix verified as shipped, not just by hand:

cargo run -p xtask -- linux package
rpm -qlp target/release/openlogi-0.7.4-1.x86_64.rpm | grep modules-load
#   /etc/modules-load.d/openlogi.conf
rpm -qp --dump target/release/openlogi-0.7.4-1.x86_64.rpm | grep modules-load
#   mode 0100644 root root
rpm -qp --scripts target/release/openlogi-0.7.4-1.x86_64.rpm | grep modprobe
#   modprobe uinput || true

sudo dnf install -y target/release/openlogi-0.7.4-1.x86_64.rpm
#   -> uinput module loaded, /dev/uinput writable, hook installs,
#      "OpenLogi virtual mouse" node created
sudo dnf remove -y openlogi
#   -> /etc/modules-load.d/openlogi.conf removed, no .rpmsave left behind

The .deb was checked the same way (ar p … data.tar.gz | tar tzf -) and carries
both the config file and the modprobe line.

CI:

cargo xtask ci
#   7 passed, 0 failed, 2 skipped

Includes the shell job (shellcheck + shfmt -d), which gates
postinstall.sh. Not run: tests (macos) (wrong host) and cargo-deny
(not installed locally) — neither is affected by a packaging-only change, but
neither was executed.

The packages ship 70-openlogi.rules, which grants /dev/uinput with
TAG+="uaccess" — an ACL logind applies in response to a device event. A machine
that has never loaded the uinput module has no such device, so there is nothing
for postinstall's `udevadm trigger --attr-match=name=uinput` to match; it is a
no-op, and its `|| true` hides that. OPTIONS+="static_node=uinput" does create
the node, but a static node gets default permissions and no ACL: opening it
would autoload the module, except opening is what the missing permission
forbids.

Nothing in the package breaks that cycle, so today it is broken by whatever else
happens to open /dev/uinput first. On a typical desktop something does —
bluetoothd opens it seconds after boot, and Steam and logiops do too — and
OpenLogi works. Where nothing does, the agent starts, reports the device,
battery and DPI normally over HID++, and button remapping silently never works,
with only a journal warning to say so.

systemd's own rules show the shape of the fix: snd/seq and snd/timer carry a
real GROUP= in 50-udev-default.rules alongside TAG+="uaccess" in
70-uaccess.rules, precisely because uaccess cannot cover the pre-module node.

Ship /etc/modules-load.d/openlogi.conf so the module loads from the next boot
on, and modprobe it in postinstall so the session that installed the package
works too. The config file is package content rather than something the script
writes, so removing the package removes it.

Verified on Fedora 44 against the released openlogi-v0.7.4-linux-amd64.rpm. With
logiops and solaar removed, uinput unloaded and no stale ACL, installing the
package leaves /dev/uinput at root:root 0600, and starting the agent as its own
post-install message instructs fails with

  could not install OS input hook — error=Linux input error: Permission denied (os error 13)

and creates no virtual input device. Applying this change to that same install
restores "OS input hook installed" and the "OpenLogi virtual mouse" node.

Reproduced independently on a second machine running the 0.7.3 package, with
its mouse's event node confirmed readable first so the failure could only be the
uinput side — same "Permission denied (os error 13)".

In its untouched state that machine does not fail: bluetoothd had held
/dev/uinput since 14 seconds after boot, so the module was loaded and the rules
had already tagged it. That is the common case, and it is why this has gone
unnoticed — the packages work by luck rather than by construction. Stopping
bluetoothd and unloading the module reproduces what a machine with no such
daemon looks like from a cold boot: the static node exists at root:root 0600
with no ACL, which is exactly the never-loaded state.
@AalmanSadath
AalmanSadath requested a review from AprilNEA as a code owner August 21, 2026 13:01
@greptile-apps

greptile-apps Bot commented Aug 21, 2026

Copy link
Copy Markdown

Greptile Summary

The PR ensures Linux installations load uinput, allowing the existing udev rule to grant access before OpenLogi creates its virtual input device.

  • Ships a modules-load configuration in Linux packages and the from-source installer.
  • Loads uinput during package and from-source installation for immediate use.
  • Enables uinput through the native NixOS module option.
  • Waits for triggered udev events and removes the boot configuration during from-source uninstall.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
packaging/linux/install.sh Installs the boot-time module configuration, loads uinput for the current session, and waits for queued udev events.
packaging/linux/modules-load/openlogi.conf Adds the declarative boot-time request to load the uinput kernel module.
packaging/linux/nfpm-scripts/postinstall.sh Loads uinput before retriggering udev rules during package installation.
packaging/linux/nfpm.yaml Includes the new modules-load configuration in all Linux packages produced from the shared nfpm descriptor.
packaging/linux/nixos-module.nix Uses the native NixOS kernel-module option to load uinput at boot.
packaging/linux/uninstall.sh Removes the from-source modules-load configuration without unloading a potentially shared live module.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Linux installation] --> B{Installation path}
  B -->|deb/rpm| C[Ship openlogi.conf]
  B -->|From source| D[Install openlogi.conf]
  B -->|NixOS module| E[Set boot.kernelModules]
  C --> F[modprobe uinput during postinstall]
  D --> G[modprobe uinput during install]
  E --> H[Load uinput during boot]
  F --> I[Trigger udev rules]
  G --> I
  H --> I
  I --> J[Apply uaccess ACL]
  J --> K[OpenLogi creates virtual input device]
Loading

Reviews (4): Last reviewed commit: "fix(linux): wait for udev in the from-so..." | Re-trigger Greptile

Comment thread packaging/linux/nfpm.yaml
The previous commit fixed only the nfpm packages. install.sh and the NixOS
module install the same udev rule and hit the same wall: TAG+="uaccess" grants
/dev/uinput in response to a device event, and a host that has never loaded the
module has no device for it to act on, so the node stays root-owned and the
agent cannot create the virtual device button remapping needs.

install.sh now installs /etc/modules-load.d/openlogi.conf and modprobes uinput,
matching what the packages do. uninstall.sh removes the config again but leaves
the module loaded, since other software may be relying on it by then.

On NixOS /etc/modules-load.d is not the mechanism, so the module asks for the
kernel module directly with boot.kernelModules.

Raised in review: the fix worked for package installs while the documented
alternative installers left the same hosts broken.
@davidbudnick davidbudnick added platform: linux Linux-specific issue platform: all Cross-platform issue labels Aug 21, 2026
davidbudnick
davidbudnick previously approved these changes Aug 21, 2026
@davidbudnick davidbudnick added this to the v0.7.5 milestone Aug 21, 2026
Comment thread packaging/linux/install.sh
install.sh reloaded the rules and fired the same three triggers as the package
post-install, but returned without settling. `udevadm trigger` only queues the
events, so the script could exit before udev had applied the uaccess ACLs, and
the next thing the user does — start the agent — would still hit EACCES on a
correctly configured system.

postinstall.sh has always settled here for exactly that reason. This makes the
two paths agree.
@AalmanSadath

Copy link
Copy Markdown
Author

Greptile flagged that install.sh reloaded and triggered udev but never settled, so the script could exit before the uaccess ACLs were applied. 34142fc adds the udevadm settle that postinstall.sh already had, so the two paths now agree.

The push dismissed your approval, so it would need re-approval.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

platform: all Cross-platform issue platform: linux Linux-specific issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants