Skip to content

[Bug] OrangePi NEO-01: Controller unresponsive after suspend/resume due to devhide permission loop + ACL mask conflict #317

Description

@lyymo

[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 event3OSError: [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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions