Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions docs/linux-usb-prerequisites.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ the device world-writable.
1. From the repository root, copy the shipped rule and reload udev:

```sh
sudo install -Dm0644 packaging/udev/99-attack-shark-x6.rules /etc/udev/rules.d/99-attack-shark-x6.rules
sudo install -Dm0644 packaging/udev/60-attack-shark-x6-hidraw.rules /etc/udev/rules.d/60-attack-shark-x6-hidraw.rules
sudo udevadm control --reload-rules
sudo udevadm trigger
```
Expand Down Expand Up @@ -41,14 +41,14 @@ seat user. On systems without logind/uaccess support, use a static group instead
```sh
sudo groupadd --system attack-shark-x6
sudo usermod -aG attack-shark-x6 "$USER"
sudo install -Dm0644 packaging/udev/99-attack-shark-x6.rules /etc/udev/rules.d/99-attack-shark-x6.rules
sudo install -Dm0644 packaging/udev/60-attack-shark-x6-hidraw.rules /etc/udev/rules.d/60-attack-shark-x6-hidraw.rules
```

Then replace the installed rule's final action with this group policy, reload
udev, and replug the dongle:

```udev
SUBSYSTEM=="usb", ATTRS{idVendor}=="1d57", ATTRS{idProduct}=="fa60", GROUP="attack-shark-x6", MODE="0660"
SUBSYSTEM=="hidraw", ATTRS{idVendor}=="1d57", ATTRS{idProduct}=="fa60", GROUP="attack-shark-x6", MODE="0660"
```

```sh
Expand All @@ -66,7 +66,7 @@ The application maps `os.ErrPermission` to the UI error code
`permission_denied`. This means Linux denied access to the validated dongle; it
does not mean the app should be run as root. Check, in order:

- The rule is installed at `/etc/udev/rules.d/99-attack-shark-x6.rules`.
- The rule is installed at `/etc/udev/rules.d/60-attack-shark-x6-hidraw.rules`.
- Rules were reloaded, the trigger command ran, and the dongle was replugged.
- For group policy, the user belongs to `attack-shark-x6` and has logged in again.
- The dongle identity still appears as `1d57:fa60`.
Expand All @@ -79,11 +79,11 @@ Confirm the USB identity:
lsusb -d 1d57:fa60
```

Inspect udev attributes after locating the matching device node (replace the
placeholder with the bus/device path reported by your system):
Inspect udev attributes after locating an X6 hidraw node (replace the
placeholder with the hidraw node reported by your system):

```sh
udevadm info -a -n /dev/bus/usb/BBB/DDD
udevadm info -a -n /dev/hidrawN
```

The adapter reads status or acknowledgements from the vendor hidraw node only
Expand Down
55 changes: 55 additions & 0 deletions odd/tasks/udev-hidraw-uaccess.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Udev Hidraw Uaccess Fix

Repository-relative file locator: `odd/tasks/udev-hidraw-uaccess.md`

## Objective

Correct the packaged X6 Linux access policy so systemd processes `uaccess` before its late-seat rule and grants access to the hidraw nodes that the desktop application actually opens.

## Problem and evidence

The packaged file is named `99-attack-shark-x6.rules` and matches `SUBSYSTEM=="usb"`. On this host, `/usr/lib/udev/rules.d/73-seat-late.rules:16` queues its uaccess helper only when the tag already exists. Collective lexical evaluation therefore processes `73-...` before `99-...`, so the late tag cannot satisfy that earlier condition. The application uses `/dev/hidraw*`, not the USB device node.

The issue #77 reporter observed the ordering failure on CachyOS with Hyprland. Read-only host inspection confirmed an X6 USB device and four X6 hidraw nodes. The host's local policy uses separate USB and hidraw rules, but it is customized with `OWNER="alejandro"`; it is not a suitable shipped policy or runtime proof.

## Scope

- Remove the superseded USB-only `99-attack-shark-x6.rules` policy and rename the existing hidraw-specific `99-attack-shark-x6-hidraw.rules` policy to `60-attack-shark-x6-hidraw.rules`, preserving its X6 parent matching, `TAG+="uaccess"`, and `MODE="0660"` contract.
- Update installation, group-policy alternative, recovery, and troubleshooting documentation to use the new path and accurately state the hidraw target.
- Run a maintainer-authorized, reversible live end-to-end acceptance before repository policy edits; prove the candidate grants the active user ACL access to the X6 hidraw nodes and permits non-mutating desktop reads.
- Post one concise clarification to issue #77: the `99 → 60` ordering diagnosis is valid, and the delivered policy must also match hidraw.

## Constraints and non-goals

- Preserve least privilege: do not use `0666`, root execution, broad USB access, or user-specific `OWNER=` policy.
- Do not reload rules, trigger events, replug hardware, modify `/etc` or `/run` udev rules, or run a live udev simulation **unless the maintainer explicitly authorizes the reversible UDEV-2 acceptance procedure**.
- Do not change HID transport, device discovery, Wails bindings, or remapping behavior.
- The live acceptance procedure must use a volatile `/run` candidate and exact rollback. It must temporarily neutralize the host's existing X6-specific `OWNER=` rules, reload udev, require a physical replug, verify hidraw ACLs and non-mutating desktop reads, then restore the original rules and access state even on failure.
- A repository rule contract can be validated statically; live ACL confirmation is separate maintainer-operated evidence and must not be claimed until observed.

## Delivery strategy

- Forecast: approximately 80–140 authored changed lines, including documentation and static contract checks.
- Strategy: `ask-on-risk`; the forecast is below the 400-line delivery heuristic.
- Effective TDD mode: enabled by explicit maintainer instruction. Runner: `go test ./packaging/udev -run '^TestPackagedUdevPolicyContract$' -count=1`. The new adjacent test `packaging/udev/udev_policy_tdd_test.go` must produce observed RED evidence before production policy/documentation edits, then observed GREEN evidence afterward.

## Tasks

- [x] **UDEV-1 — Establish an isolated feature workspace from `origin/main`.** Created branch `fix/udev-hidraw-uaccess` at `e4ea092d8bc71f56b852f4fada310bc82404a24e`; worktree is clean. Evidence: `git worktree add -b fix/udev-hidraw-uaccess … origin/main`. Commit: pending explicit user authorization.
- [x] **UDEV-2 — Run the reversible live end-to-end acceptance.** PASS. The volatile `60-attack-shark-x6-hidraw.rules` candidate produced `uaccess` tags and `user:alejandro:rw-` ACLs on all four X6 hidraw nodes after physical replug. The desktop Device view read interface `dongle` and battery `100%` without applying any device change. Terminal closure interrupted automatic cleanup, but exact manual restoration was then verified read-only: both root-owned baseline rules exist, the candidate and backup are absent, and all four hidraw nodes are `alejandro:root` mode `0660` with active-user read/write access.
- [x] **UDEV-3 — RED, GREEN, and REFACTOR the packaged policy and documentation.** RED observed because the new policy path was absent. GREEN observed after explicit deletion authorization: removed both obsolete `99-...` policies, renamed the correct hidraw policy to `60-attack-shark-x6-hidraw.rules`, and updated the documentation. Focused test passed; `gofmt` completed; `git diff --check` passed.
- [x] **UDEV-4 — Verify delivery evidence and clarify issue #77.** Static contract and diff checks passed. Posted and read back the corrective issue comment: https://github.com/blak0p/attack-shark-linux/issues/77#issuecomment-5717223060. It records both the ordering and hidraw-target requirements and the live acceptance evidence.
- [x] **UDEV-5 — Create the work-unit commit.** Committed as `a516ba24f1635ac54b0ada4957fab58616d1badd` with `fix(udev): grant uaccess to X6 hidraw nodes`. Native assessment was unavailable/schema-incompatible, so independent verification ran and passed the focused policy test and committed-range diff check.
- [x] **UDEV-6 — Apply the verified policy to the local host.** PASS. Installed `/etc/udev/rules.d/60-attack-shark-x6-hidraw.rules`, removed both superseded local `99-...` rules, reloaded udev, physically replugged the dongle, and observed `user:alejandro:rw-` ACL access on all four X6 hidraw nodes. The temporary rollback backup was removed after verification.

## Acceptance criteria and checks

- The installed filename is lexically earlier than `73-seat-late.rules`.
- The packaged rule matches `SUBSYSTEM=="hidraw"`, constrains the X6 VID/PID via parent attributes, has `TAG+="uaccess"`, and retains `MODE="0660"`.
- The packaged and documented filenames agree; no `99-attack-shark-x6.rules` reference remains.
- No broad USB-only rule, `OWNER=`, `0666`, root instruction, runtime udev reload/trigger, hardware communication, or generated-file change is introduced.
- Static checks and diff inspection are observed. Live ACL behavior is explicitly reported as maintainer verification, not automation evidence.

## Current progress and next step

TDD RED → GREEN → REFACTOR is complete. The live E2E acceptance passed, baseline recovery was verified, static checks passed, issue #77 was corrected, and work-unit commit `a516ba24f1635ac54b0ada4957fab58616d1badd` was independently verified. The final persistent local policy installation also passed. The host-install evidence update remains uncommitted; push and PR creation remain maintainer decisions.
5 changes: 0 additions & 5 deletions packaging/udev/99-attack-shark-x6.rules

This file was deleted.

91 changes: 91 additions & 0 deletions packaging/udev/udev_policy_tdd_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package udev

import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"
)

const (
policyFile = "60-attack-shark-x6-hidraw.rules"
policyLine = `SUBSYSTEM=="hidraw", ATTRS{idVendor}=="1d57", ATTRS{idProduct}=="fa60", TAG+="uaccess", MODE="0660"`
)

func TestPackagedUdevPolicyContract(t *testing.T) {
packageDir := packageDirectory(t)
policyPath := filepath.Join(packageDir, policyFile)
activeLines := activeRuleLines(t, policyPath)

if policyFile >= "73-seat-late.rules" {
t.Fatalf("policy filename %q must sort before 73-seat-late.rules", policyFile)
}
if len(activeLines) != 1 || activeLines[0] != policyLine {
t.Fatalf("active policy lines = %q, want exactly %q", activeLines, policyLine)
}

policyText := readFile(t, policyPath)
for _, forbidden := range []string{"SUBSYSTEM==\"usb\"", "OWNER="} {
if strings.Contains(policyText, forbidden) {
t.Errorf("policy contains forbidden token %q", forbidden)
}
}
for _, obsoleteFile := range []string{
"99-attack-shark-x6.rules",
"99-attack-shark-x6-hidraw.rules",
} {
if _, err := os.Stat(filepath.Join(packageDir, obsoleteFile)); !os.IsNotExist(err) {
t.Errorf("obsolete policy %q still exists (stat error: %v)", obsoleteFile, err)
}
}

repositoryRoot := filepath.Dir(filepath.Dir(packageDir))
documentation := readFile(t, filepath.Join(repositoryRoot, "docs", "linux-usb-prerequisites.md"))
for _, required := range []string{
"packaging/udev/60-attack-shark-x6-hidraw.rules",
"/etc/udev/rules.d/60-attack-shark-x6-hidraw.rules",
} {
if !strings.Contains(documentation, required) {
t.Errorf("documentation does not reference %q", required)
}
}
for _, staleReference := range []string{
"99-attack-shark-x6.rules",
"99-attack-shark-x6-hidraw.rules",
} {
if strings.Contains(documentation, staleReference) {
t.Errorf("documentation contains stale %s reference", staleReference)
}
}
}

func packageDirectory(t *testing.T) string {
t.Helper()
_, sourceFile, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("locating test source with runtime.Caller")
}
return filepath.Dir(sourceFile)
}

func activeRuleLines(t *testing.T, path string) []string {
t.Helper()
var active []string
for _, line := range strings.Split(readFile(t, path), "\n") {
line = strings.TrimSpace(line)
if line != "" && !strings.HasPrefix(line, "#") {
active = append(active, line)
}
}
return active
}

func readFile(t *testing.T, path string) string {
t.Helper()
contents, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
return string(contents)
}