Skip to content

fix(install): grant raw USB node access for libusb-claimed devices (#355)#361

Merged
BANANASJIM merged 1 commit into
mainfrom
fix/355-libusb-udev-grant
Jun 3, 2026
Merged

fix(install): grant raw USB node access for libusb-claimed devices (#355)#361
BANANASJIM merged 1 commit into
mainfrom
fix/355-libusb-udev-grant

Conversation

@BANANASJIM

@BANANASJIM BANANASJIM commented Jun 3, 2026

Copy link
Copy Markdown
Owner

What

PR #357 moved the Flydigi Vader 5 from hidraw (class="hid") to libusb (class="vendor"/"suppress") so the kernel exposes no hidraw node for Steam to grab. libusb then needs write access to the raw USB device node (/dev/bus/usb/...) plus libusb_detach_kernel_driver — but the installer only granted the hidraw/input nodes. So an unprivileged systemd --user daemon could not claim the device: the bind failed and padctl status showed nothing (reported on #355 after building 731bc17).

Empirically confirmed: the generated udev rules granted hidraw+input for 37d7:2401 but no SUBSYSTEM=="usb" permission; raw USB nodes are root:root 0664, so a non-root daemon cannot open them.

Changes

  • udev (udev.zig): emit ACTION=="add", SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", ATTR{idVendor}==.., ATTR{idProduct}==.., TAG+="uaccess", GROUP="input", MODE="0660" for any device that declares a vendor/suppress interface (detected from the device TOML at install time). Mirrors the existing hidraw grant; emitted only for libusb devices (exactly 1 today: the Vader).
  • supervisor (supervisor.zig): a libusb claim failure no longer drops the device with a generic warning — logBindFailure logs an actionable message naming the device and the remedy (install udev rules / join input group / run privileged).
  • tests: rule emitted for vendor/suppress devices, absent for hidraw-only devices; usesLibusb helper coverage.

Verification

  • Built in the canonical Docker image; regenerated the install udev rules and confirmed the USB-node grant now appears for 37d7:2401 (exactly one, no HID device affected).
  • Full zig build test green in Docker; zig fmt --check clean (0.15.2); musl -Dlibusb=false build green.
  • Opus code review: no blocking findings (matcher uses ATTR not ATTRS, gated on DEVTYPE=="usb_device"; both catch sites keep their transient-retry; no std.log.err in test-exercised paths; new tests are falsifiable).

Known limitation (not addressed here)

#357 also changed block_kernel_drivers from ["xpad"] to ["xpad","hid_generic","usbhid"], so udev now eagerly unbinds all HID drivers on plug, removing the hidraw node. Discovery is still hidraw-only (no libusb enumeration fallback), so there is a likely race where the node is gone before the daemon reads it. This is unconfirmed without hardware and not changed here; the new actionable log will help disambiguate it from the permission gap in the field.

Refs #355

Summary by CodeRabbit

  • New Features

    • Added udev rule generation for devices requiring libusb access, with automatic detection of vendor and suppress device interfaces.
    • Devices now emit corresponding raw USB device node rules with proper permission and access control settings.
  • Tests

    • Added comprehensive tests for udev rule generation covering both libusb-enabled and HID-only device scenarios.
  • Improvements

    • Enhanced error messages when device binding fails, providing specific guidance on udev rules and user group permissions.

)

PR #357 moved the Vader 5 from hidraw (class="hid") to libusb
(class="vendor"/"suppress") so the kernel exposes no hidraw node for
Steam to grab. libusb then needs write access to the raw USB device node
(/dev/bus/usb/...) plus driver detach, but the installer only granted the
hidraw/input nodes — so the unprivileged --user daemon could not claim
the device and the bind failed, leaving `padctl status` empty.

- udev: emit a SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device" uaccess/
  GROUP/MODE grant for any device that declares a vendor/suppress
  interface (detected from the device TOML at install time)
- supervisor: a libusb claim failure no longer drops the device with a
  generic warning; log an actionable message naming the device and remedy
- tests: udev rule emitted for vendor/suppress devices and absent for
  hidraw-only devices; usesLibusb helper coverage

Refs #355
@coderabbitai

coderabbitai Bot commented Jun 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR implements end-to-end support for generating udev rules that grant raw USB access to devices requiring libusb. It detects vendor/suppress interface classes in device configs, tracks this requirement through udev rule generation, emits appropriate usb-subsystem rules with uaccess tags, and improves error messages when libusb device binding fails.

Changes

Libusb udev rules and error messaging

Layer / File(s) Summary
Device libusb detection helper
src/config/device.zig
usesLibusb() scans device interfaces for vendor or suppress classes; unit tests validate both true and false cases.
Udev parsing for libusb interface classes
src/cli/install/udev.zig
UdevEntry.needs_libusb field tracks libusb requirement; extractVidPid parser detects [[device.interface]] sections and sets flag when class is vendor or suppress; flag propagates to final entry construction.
Udev rule generation for raw USB nodes
src/cli/install/udev.zig
VID:PID deduplication OR-merges needs_libusb flags; rule generation conditionally emits usb-subsystem rules granting TAG+="uaccess", GROUP="input", and MODE="0660" to raw USB device nodes.
Supervisor error messaging for libusb failures
src/supervisor.zig
logBindFailure() helper classifies libusb claim errors and emits targeted guidance (udev rules, input group, privilege); integrated into cold-scan and hotplug device creation error paths.
Udev generation test cases
src/cli/install/tests.zig
Test validates libusb devices emit raw USB node uaccess rules; second test verifies pure HID devices omit ENV{DEVTYPE}=="usb_device" rules.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • BANANASJIM/padctl#192: Introduced the foundational UdevEntry struct and extractVidPid parser that this PR extends with libusb detection and rule generation.
  • BANANASJIM/padctl#357: Related libusb suppress handling enhancements including suppress interface routing and non-suppress device index mapping that complement this rule generation work.
  • BANANASJIM/padctl#50: Modified the same extractVidPid TOML parsing logic; this PR adds interface class detection on top of those parsing improvements.

Poem

🐰 With vendor classes detected and suppress rules in sight,
Raw USB devices now glow with udev's guiding light!
Parse the interfaces, merge the flags with care,
Then grant uaccess TAG—fair rules, fair and square.
When binding fails, we whisper guidance true,
Through input group and rule-check paths anew. ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: granting raw USB node access for libusb-claimed devices. It directly reflects the core objective of the PR and the primary implementation across udev, device config, and supervisor files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/355-libusb-udev-grant

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@qodo-code-review

Copy link
Copy Markdown

Review Summary by Qodo

Grant raw USB node access for libusb-claimed devices

🐞 Bug fix ✨ Enhancement 🧪 Tests

Grey Divider

Walkthroughs

Description
• Grant raw USB device node access for libusb-claimed devices via udev rules
  - Emit SUBSYSTEM=="usb" rules for vendor/suppress interface devices
  - Detect libusb usage from device TOML at install time
• Improve error messaging for libusb device binding failures
  - Log actionable diagnostics naming the device and remedy
  - Distinguish libusb claim errors from generic initialization failures
• Add comprehensive test coverage for udev rule generation
  - Verify USB node rules emitted for libusb devices
  - Verify USB node rules absent for hidraw-only devices
  - Add usesLibusb helper function with test coverage
Diagram
flowchart LR
  A["Device TOML<br/>vendor/suppress class"] -->|detectLibusb| B["UdevEntry<br/>needs_libusb flag"]
  B -->|generateRules| C["USB node rule<br/>SUBSYSTEM==usb"]
  D["DeviceInstance.init<br/>libusb claim fails"] -->|isLibusbClaimError| E["logBindFailure<br/>actionable message"]
  E -->|user sees| F["Install udev rules<br/>join input group"]

Loading

Grey Divider

File Changes

1. src/cli/install/tests.zig 🧪 Tests +90/-0

Add udev rule generation tests for libusb devices

• Add test verifying USB node udev rule emitted for libusb-claimed devices
• Add test verifying USB node rule absent for hidraw-only devices
• Define usb_node_rule constant for rule validation

src/cli/install/tests.zig


2. src/cli/install/udev.zig ✨ Enhancement +39/-0

Emit USB node udev rules for libusb devices

• Add needs_libusb boolean field to UdevEntry struct
• Detect vendor/suppress interface classes in extractVidPid function
• Generate raw USB device node udev rules for libusb-claimed devices
• Merge needs_libusb flag when consolidating duplicate VID:PID entries

src/cli/install/udev.zig


3. src/config/device.zig ✨ Enhancement +37/-0

Add libusb detection helper function

• Add usesLibusb public function to detect vendor/suppress interfaces
• Add test case verifying usesLibusb returns true for Vader 5 and false for hid-only devices

src/config/device.zig


View more (1)
4. src/supervisor.zig Error handling +25/-2

Improve libusb device binding error diagnostics

• Add isLibusbClaimError helper to identify libusb-specific failures
• Add logBindFailure function providing actionable error messages for libusb devices
• Replace generic warning logs with logBindFailure calls at two device binding sites
• Log remedy instructions (install udev rules, join input group, replug device)

src/supervisor.zig


Grey Divider

Qodo Logo

@qodo-code-review

qodo-code-review Bot commented Jun 3, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🔗 Cross-repo conflicts (0)

Context used

Grey Divider


Action required

1. Libusb Busy not retried 🐞 Bug ☼ Reliability
Description
isLibusbClaimError now treats error.Busy as a libusb claim failure, but the retry path still
depends on isTransientOpenError; since isTransientOpenError does not include error.Busy, a
LIBUSB_ERROR_BUSY bind failure will log and then drop without scheduling a retry. This can leave a
device missing from padctl status until the user manually replugs/restarts the daemon.
Code

src/supervisor.zig[R2317-2321]

Evidence
The PR introduces a libusb-specific error classifier that includes error.Busy, and usbraw can
return that error when libusb reports LIBUSB_ERROR_BUSY. However, supervisor retry scheduling is
guarded by isTransientOpenError, whose switch does not include error.Busy, so the retry is never
enqueued for this case.

src/io/usbraw.zig[95-114]
src/supervisor.zig[2178-2183]
src/supervisor.zig[2300-2315]
src/supervisor.zig[2317-2338]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`usbraw` can return `error.Busy` (mapped from `LIBUSB_ERROR_BUSY`). The supervisor currently enqueues a hotplug retry only when `isTransientOpenError(err)` is true; `error.Busy` is not considered transient there, so a busy libusb claim can cause a permanent drop until user intervention.

## Issue Context
This PR adds `isLibusbClaimError()` (including `error.Busy`) and `logBindFailure()` for libusb devices, but does not adjust the retry classification to match.

## Fix Focus Areas
- src/supervisor.zig[2300-2315]
- src/supervisor.zig[2317-2338]
- src/io/usbraw.zig[95-114]

### Suggested fix
Add `error.Busy` to `Supervisor.isTransientOpenError()` (or map `usbraw`'s busy condition to an existing transient error like `error.DeviceBusy`) so the existing retry path (`enqueueHotplugRetryForPath`) also covers libusb busy claim failures.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/cli/install/tests.zig (1)

369-369: 💤 Low value

Test constant missing ACTION=="add", prefix.

The generated rule (line 452 of udev.zig) includes ACTION=="add", at the start, but this test constant omits it. The test still passes via substring matching, but won't catch if the ACTION clause is accidentally removed from generation.

🔧 Suggested fix
-const usb_node_rule = "SUBSYSTEM==\"usb\", ENV{DEVTYPE}==\"usb_device\", ATTR{idVendor}==\"37d7\", ATTR{idProduct}==\"2401\", TAG+=\"uaccess\", GROUP=\"input\", MODE=\"0660\"";
+const usb_node_rule = "ACTION==\"add\", SUBSYSTEM==\"usb\", ENV{DEVTYPE}==\"usb_device\", ATTR{idVendor}==\"37d7\", ATTR{idProduct}==\"2401\", TAG+=\"uaccess\", GROUP=\"input\", MODE=\"0660\"";
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/install/tests.zig` at line 369, The test constant usb_node_rule is
missing the leading ACTION=="add", clause that the generated udev rule in
udev.zig expects; update the usb_node_rule string to include ACTION=="add", at
the start (so it exactly matches the generated rule including the ACTION clause)
to ensure the test fails if the ACTION clause is omitted by the generator.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/cli/install/tests.zig`:
- Line 369: The test constant usb_node_rule is missing the leading
ACTION=="add", clause that the generated udev rule in udev.zig expects; update
the usb_node_rule string to include ACTION=="add", at the start (so it exactly
matches the generated rule including the ACTION clause) to ensure the test fails
if the ACTION clause is omitted by the generator.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: eb78c9c0-a48e-4419-8975-4a735ca9e48f

📥 Commits

Reviewing files that changed from the base of the PR and between 66fa4a1 and e89c78b.

📒 Files selected for processing (4)
  • src/cli/install/tests.zig
  • src/cli/install/udev.zig
  • src/config/device.zig
  • src/supervisor.zig

@BANANASJIM BANANASJIM merged commit dc28146 into main Jun 3, 2026
38 checks passed
@BANANASJIM BANANASJIM deleted the fix/355-libusb-udev-grant branch June 3, 2026 06:35
Comment thread src/supervisor.zig
Comment on lines +2317 to +2321
fn isLibusbClaimError(err: anyerror) bool {
return switch (err) {
error.NotFound, error.Busy, error.ClaimFailed => true,
else => false,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Libusb busy not retried 🐞 Bug ☼ Reliability

isLibusbClaimError now treats error.Busy as a libusb claim failure, but the retry path still
depends on isTransientOpenError; since isTransientOpenError does not include error.Busy, a
LIBUSB_ERROR_BUSY bind failure will log and then drop without scheduling a retry. This can leave a
device missing from padctl status until the user manually replugs/restarts the daemon.
Agent Prompt
## Issue description
`usbraw` can return `error.Busy` (mapped from `LIBUSB_ERROR_BUSY`). The supervisor currently enqueues a hotplug retry only when `isTransientOpenError(err)` is true; `error.Busy` is not considered transient there, so a busy libusb claim can cause a permanent drop until user intervention.

## Issue Context
This PR adds `isLibusbClaimError()` (including `error.Busy`) and `logBindFailure()` for libusb devices, but does not adjust the retry classification to match.

## Fix Focus Areas
- src/supervisor.zig[2300-2315]
- src/supervisor.zig[2317-2338]
- src/io/usbraw.zig[95-114]

### Suggested fix
Add `error.Busy` to `Supervisor.isTransientOpenError()` (or map `usbraw`'s busy condition to an existing transient error like `error.DeviceBusy`) so the existing retry path (`enqueueHotplugRetryForPath`) also covers libusb busy claim failures.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant