[Bug] OrangePi NEO-01: Controller unresponsive after suspend/resume due to devhide permission loop + ACL mask conflict
Device: OrangePi NEO-01
USB Controller: OrangePi USB Controller 045e:028e
System: Bazzite (Fedora Atomic / immutable), kernel 6.17.7-ba29.fc43.x86_64
HHD version: v4.1.5
systemd version: 258 (258.7-1.fc43)
Summary
After suspend/resume, the physical gamepad (xpad, 045e:028e) becomes completely unresponsive. HHD enters a loop of Using cached controller node / OSError: [Errno 19] No such device and never recovers on its own (or only recovers after 10–30+ minutes). Fan curves also fail until HHD is restarted.
Root Cause Analysis
The failure chain involves three compounding issues:
Issue 1: USB interface 1-5:1.1 enters a reconnect loop after resume
On resume from suspend, the device's secondary USB interface (1-5:1.1, bInterfaceClass=03, generic HID) begins disconnecting and reconnecting every ~8 seconds. This is likely firmware-level behavior specific to the NEO-01 after S2idle resume.
kernel: usb 1-5: New USB device found, idVendor=045e, idProduct=028e
kernel: hid-generic 0003:045E:028E.003D: input,hidraw0: USB HID v1.10 Keyboard on usb-0000:c3:00.3-5/input1
kernel: usb 1-5: New USB device found, idVendor=045e, idProduct=028e
kernel: hid-generic 0003:045E:028E.003E: input,hidraw0: USB HID v1.10 Keyboard on usb-0000:c3:00.3-5/input1
... (repeating every ~8 seconds indefinitely)
Each reconnection creates a new HID input node (input27, input29, input31, ...) which gets a new eventX device node. Note: 1-5:1.0 (the xpad joystick interface) remains stable throughout — event3/js0 stays correctly bound to xpad. The reconnect loop is exclusively on 1-5:1.1.
Issue 2: HHD's devhide rule is triggered on every reconnection, chmod 000 on event3
Each time a new input node from 1-5:1.1 appears, HHD calls hide_gamepad() which writes a udev rule to /run/udev/rules.d/95-hhd-devhide-*.rules. This rule contains:
KERNEL=="js[0-9]*|event[0-9]*", SUBSYSTEM=="input", MODE="000", GROUP="root", \
TAG-="uaccess", RUN+="/bin/chmod 000 /dev/input/%k"
Because this KERNEL== match is not scoped to the specific input node (due to a syntax error described below), it matches all event* and js* nodes — including event3 (the working xpad node). As a result, event3 gets chmod 000 repeatedly.
Syntax bug in HHD_HIDE_ALL=1 mode (the default on Bazzite):
The generated rule has a missing comma between ENV{ID_BUS}=="usb" and ATTRS{id/vendor}:
# Generated rule (broken):
SUBSYSTEMS=="input", ENV{ID_BUS}=="usb"ATTRS{id/vendor}=="045e", ATTRS{id/product}=="028e", GOTO="hhd_valid"
The missing comma causes the GOTO="hhd_valid" condition to partially fail, making the subsequent MODE="000" apply more broadly than intended.
Source file: /usr/lib/python3.14/site-packages/hhd/controller/lib/hide.py
if HIDE_ALL:
root = f"{vid:04x}-{pid:04x}"
extra = 'ENV{ID_BUS}=="usb"' # <-- missing trailing comma
The correct line should be:
extra = 'ENV{ID_BUS}=="usb", '
Issue 3: systemd-logind ACL (uaccess) with mask::--- makes chmod 000 irreversible from userspace
71-microsoft-controllers.rules tags the xpad input node with TAG+="uaccess", which causes systemd-logind to add an ACL entry user:<current_user>:rw- to event3. When HHD then applies TAG-="uaccess" and chmod 000, the resulting ACL state becomes:
# file: dev/input/event3
# owner: root
# group: root
user::---
user:lyymmo:rw- #effective:---
group::---
mask::--- # <-- masks all permissions to 000, including the rw- ACL entry
other::---
With mask::---, even chmod 660 has no effect because the ACL overrides it. setfacl -b (which removes all ACLs) does temporarily restore the permissions, but logind immediately re-applies the ACL on the next udev event, restoring mask::---.
This creates an unbreakable loop: 1-5:1.1 reconnects → devhide rule fires → chmod 000 + TAG-="uaccess" → mask::--- locks the node → HHD cannot open event3 → OSError: [Errno 19] No such device → loop.
Observed HHD Log (after resume)
ORPI WARNING Caching controller to avoid reconnection.
ORPI ERROR Received the following error:
<class 'OSError'>: [Errno 19] No such device
ORPI ERROR Assuming controllers disconnected, restarting after 1s.
ORPI INFO Launching emulated controller.
ORPI WARNING Using cached controller node for Steamdeck Controller.
ORPI INFO Starting 'Steam Controller (HHD)'.
ORPI INFO Emulated controller launched, have fun!
ORPI WARNING Caching controller to avoid reconnection.
ORPI ERROR Received the following error:
<class 'OSError'>: [Errno 19] No such device
... (repeating every ~8 seconds)
Current Workarounds Attempted (all insufficient)
| Attempt |
Result |
systemctl restart hhd after resume |
HHD restarts but finds event3 is chmod 000, loop continues |
rm -f /run/udev/rules.d/95-hhd-devhide-*.rules then restart HHD |
Rule is regenerated immediately, chmod 000 reapplied |
chmod 660 /dev/input/event3 |
Ineffective due to ACL mask::--- override |
setfacl -b /dev/input/event3 + chmod 660 |
Works momentarily, but logind re-applies ACL within seconds |
HHD_HIDE_ALL=0 via systemd drop-in |
Partially helps (rule now scoped to specific input node), but 1-5:1.1 reconnect loop continues to trigger devhide on new nodes, and mask::--- issue persists |
sleep hook to rmmod xpad before suspend |
Caused display/graphics stack failure on resume, abandoned |
udev rule to block hid-generic on 1-5:1.1 |
Ineffective; device re-enumerates and re-binds regardless |
modprobe usbhid quirks=0x045e:0x028e:0x0004 |
No effect on reconnect loop |
What does work: The controller eventually recovers on its own after 10–40 minutes (unknown trigger), or if the system is fully rebooted.
System Configuration (Normal/Working State)
# Normal state — both interfaces stable, no reconnect loop:
/sys/bus/usb/drivers/xpad/ → 1-5:1.0 (joystick interface, event3/js0)
/sys/bus/usb/drivers/usbhid/ → 1-5:1.1 (HID keyboard interface)
$ udevadm info /dev/input/event3 | grep -E "DRIVER|ID_INPUT"
E: ID_INPUT=1
E: ID_INPUT_JOYSTICK=1
E: ID_USB_DRIVER=xpad
E: ID_INPUT_JOYSTICK_INTEGRATION=internal
$ ls -la /dev/input/event3
crw-rw----. 1 root input 13, 67 /dev/input/event3 # permissions correct
# Failed state — after resume:
$ ls -la /dev/input/event3
c---------+ 1 root root 13, 67 /dev/input/event3 # chmod 000 + ACL locked
$ getfacl /dev/input/event3
user::---
user:lyymmo:rw- #effective:---
group::---
mask::---
other::---
Requested Fixes
Fix 1 (Critical): Syntax error in hide.py — missing comma in HIDE_ALL mode
File: hhd/controller/lib/hide.py
# Current (broken):
extra = 'ENV{ID_BUS}=="usb"'
# Fix:
extra = 'ENV{ID_BUS}=="usb", '
Fix 2 (Important): unhide_all() / unhide_gamepad() should restore permissions, not just delete the rule file
Currently unhide_all() removes the rule file and calls reload_children(), but does not restore the chmod 000 that was already applied by the RUN+= action. The device node remains inaccessible even after the rule is removed.
Suggested addition in unhide_all() and unhide_gamepad():
import subprocess
# After removing rule files, restore permissions on all input nodes
for node in os.listdir("/dev/input"):
path = os.path.join("/dev/input", node)
if node.startswith("event") or node.startswith("js"):
try:
subprocess.run(["setfacl", "-b", path], capture_output=True)
os.chmod(path, 0o660)
except Exception:
pass
Fix 3 (Enhancement): Add a configuration option to disable devhide for specific devices
For devices like the OrangePi NEO-01 where a secondary USB interface causes a reconnect loop that repeatedly triggers devhide, it would be helpful to have a per-device option to skip the devhide mechanism entirely, relying instead on evdev exclusive grab (grab=True) for isolation.
Fix 4 (Enhancement): Do not re-hide on reconnect if the physical controller node (1-5:1.0 / event3) has not changed
The reconnect loop is on 1-5:1.1 (a HID keyboard interface), not on 1-5:1.0 (the xpad joystick interface). HHD could check whether the reconnecting device is the same physical joystick interface before triggering a new devhide rule.
Environment
$ systemctl cat hhd.service | grep -E "Environ|HIDE"
Environment="HHD_HIDE_ALL=0" # set via drop-in zz-hide-fix.conf
Environment="HHD_HIDE_ALL=1" # set by /usr/lib/systemd/system/hhd.service.d/override.conf (Bazzite default)
# Note: override.conf loads AFTER our drop-in, so HHD_HIDE_ALL=1 wins unless
# the drop-in filename sorts after "override" alphabetically (e.g. "zz-hide-fix.conf")
$ cat /sys/bus/usb/devices/1-5/power/control
on
$ cat /sys/bus/usb/devices/1-5/power/autosuspend_delay_ms
-1000 # autosuspend disabled via kernel param usbcore.autosuspend=-1
Additional Notes
- This issue does not occur on cold boot — only after suspend/resume
- The
1-5:1.0 xpad interface remains stable throughout; only 1-5:1.1 (HID/keyboard) enters the reconnect loop
- The reconnect loop on
1-5:1.1 appears to be a firmware behavior of the OrangePi NEO-01 hardware after S2idle, not a software issue
- The fix most likely to help end users immediately is Fix 1 (the missing comma) combined with Fix 2 (restoring permissions in
unhide_all())
- Bazzite sets
HHD_HIDE_ALL=1 by default via /usr/lib/systemd/system/hhd.service.d/override.conf; this triggers the broken code path
[Bug] OrangePi NEO-01: Controller unresponsive after suspend/resume due to devhide permission loop + ACL mask conflict
Device: OrangePi NEO-01
USB Controller: OrangePi USB Controller
045e:028eSystem: Bazzite (Fedora Atomic / immutable), kernel
6.17.7-ba29.fc43.x86_64HHD version: v4.1.5
systemd version: 258 (258.7-1.fc43)
Summary
After suspend/resume, the physical gamepad (xpad,
045e:028e) becomes completely unresponsive. HHD enters a loop ofUsing cached controller node/OSError: [Errno 19] No such deviceand never recovers on its own (or only recovers after 10–30+ minutes). Fan curves also fail until HHD is restarted.Root Cause Analysis
The failure chain involves three compounding issues:
Issue 1: USB interface
1-5:1.1enters a reconnect loop after resumeOn resume from suspend, the device's secondary USB interface (
1-5:1.1,bInterfaceClass=03, generic HID) begins disconnecting and reconnecting every ~8 seconds. This is likely firmware-level behavior specific to the NEO-01 after S2idle resume.Each reconnection creates a new HID input node (
input27,input29,input31, ...) which gets a neweventXdevice node. Note:1-5:1.0(the xpad joystick interface) remains stable throughout —event3/js0stays correctly bound to xpad. The reconnect loop is exclusively on1-5:1.1.Issue 2: HHD's devhide rule is triggered on every reconnection, chmod 000 on event3
Each time a new input node from
1-5:1.1appears, HHD callshide_gamepad()which writes a udev rule to/run/udev/rules.d/95-hhd-devhide-*.rules. This rule contains:Because this
KERNEL==match is not scoped to the specific input node (due to a syntax error described below), it matches allevent*andjs*nodes — includingevent3(the working xpad node). As a result,event3getschmod 000repeatedly.Syntax bug in
HHD_HIDE_ALL=1mode (the default on Bazzite):The generated rule has a missing comma between
ENV{ID_BUS}=="usb"andATTRS{id/vendor}:The missing comma causes the
GOTO="hhd_valid"condition to partially fail, making the subsequentMODE="000"apply more broadly than intended.Source file:
/usr/lib/python3.14/site-packages/hhd/controller/lib/hide.pyThe correct line should be:
Issue 3: systemd-logind ACL (uaccess) with
mask::---makes chmod 000 irreversible from userspace71-microsoft-controllers.rulestags the xpad input node withTAG+="uaccess", which causes systemd-logind to add an ACL entryuser:<current_user>:rw-toevent3. When HHD then appliesTAG-="uaccess"andchmod 000, the resulting ACL state becomes:With
mask::---, evenchmod 660has no effect because the ACL overrides it.setfacl -b(which removes all ACLs) does temporarily restore the permissions, but logind immediately re-applies the ACL on the next udev event, restoringmask::---.This creates an unbreakable loop:
1-5:1.1reconnects → devhide rule fires →chmod 000+TAG-="uaccess"→mask::---locks the node → HHD cannot openevent3→OSError: [Errno 19] No such device→ loop.Observed HHD Log (after resume)
Current Workarounds Attempted (all insufficient)
systemctl restart hhdafter resumeevent3ischmod 000, loop continuesrm -f /run/udev/rules.d/95-hhd-devhide-*.rulesthen restart HHDchmod 000reappliedchmod 660 /dev/input/event3mask::---overridesetfacl -b /dev/input/event3+chmod 660HHD_HIDE_ALL=0via systemd drop-in1-5:1.1reconnect loop continues to trigger devhide on new nodes, andmask::---issue persistsrmmod xpadbefore suspendhid-genericon1-5:1.1modprobe usbhid quirks=0x045e:0x028e:0x0004What does work: The controller eventually recovers on its own after 10–40 minutes (unknown trigger), or if the system is fully rebooted.
System Configuration (Normal/Working State)
Requested Fixes
Fix 1 (Critical): Syntax error in
hide.py— missing comma inHIDE_ALLmodeFile:
hhd/controller/lib/hide.pyFix 2 (Important):
unhide_all()/unhide_gamepad()should restore permissions, not just delete the rule fileCurrently
unhide_all()removes the rule file and callsreload_children(), but does not restore thechmod 000that was already applied by theRUN+=action. The device node remains inaccessible even after the rule is removed.Suggested addition in
unhide_all()andunhide_gamepad():Fix 3 (Enhancement): Add a configuration option to disable devhide for specific devices
For devices like the OrangePi NEO-01 where a secondary USB interface causes a reconnect loop that repeatedly triggers devhide, it would be helpful to have a per-device option to skip the devhide mechanism entirely, relying instead on
evdevexclusive grab (grab=True) for isolation.Fix 4 (Enhancement): Do not re-hide on reconnect if the physical controller node (
1-5:1.0/event3) has not changedThe reconnect loop is on
1-5:1.1(a HID keyboard interface), not on1-5:1.0(the xpad joystick interface). HHD could check whether the reconnecting device is the same physical joystick interface before triggering a new devhide rule.Environment
Additional Notes
1-5:1.0xpad interface remains stable throughout; only1-5:1.1(HID/keyboard) enters the reconnect loop1-5:1.1appears to be a firmware behavior of the OrangePi NEO-01 hardware after S2idle, not a software issueunhide_all())HHD_HIDE_ALL=1by default via/usr/lib/systemd/system/hhd.service.d/override.conf; this triggers the broken code path