Skip to main content

usb-kernel-recover

Use when a USB device or fixture on a HIL rig's Linux host (ci.lan, hifiphile/tusb, a bench PC) is wedged, not enumerating, or when processes touching USB (testusb, JLinkExe, uhubctl, openocd, libusb tools) hang in D state. Linux-host side only — a bus owned by a TinyUSB host is out of reach.

ソース情報

リポジトリ
hathach/tinyusb
ソースの最終更新活動
2026年10月7日 10:46
検出された SKILL.md の言語
英語
スター
7,192
フォーク
1,552

インストール方法

デフォルトでは、最初にソースを確認する Prompt が選択されています。直接コマンドに切り替えるか、ローカルコピーをダウンロードすることもできます。

ソースファイルを確認

インストールを決める前に、SKILL.md と SkillsMP に表示されている付属ファイルをお読みください。

ファイルエクスプローラー
2 ファイル

SKILL.md を表示中

SKILL.md
ソースの指示 · 読み取り専用プレビュー
name
usb-kernel-recover
description
Use when a USB device or fixture on a HIL rig's Linux host (ci.lan, hifiphile/tusb, a bench PC) is wedged, not enumerating, or when processes touching USB (testusb, JLinkExe, uhubctl, openocd, libusb tools) hang in D state. Linux-host side only — a bus owned by a TinyUSB host is out of reach.
# USB Recovery on the HIL Rig (Linux kernel side) **The rule:** a wedged usbfs ioctl holds that device's `device_lock` (`usbdev_do_ioctl` takes `usb_lock_device`, the uninterruptible variant — v6.12.96 devio.c:2609) and the driver under it waits in a plain `wait_for_completion()` with no timeout (usbtest.c:1404; `usb_sg_wait`, message.c:765). Nothing that also takes that lock can help. Only two levers don't: **failing the URB at the device** (rung 1) and **the port-side data-line drop** (rung 2). ## 1. Triage: find the holder ```bash ps -eo pid,stat,etimes,wchan:22,args | awk '$2 ~ /D/' sudo cat /proc/<pid>/stack # never opens the node, so it cannot block ``` - **`S` = victim.** Lock-taking sysfs *reads* use `usb_lock_device_interruptible` (sysfs.c:124-139, 11 sites), so readers are killable and `timeout` bounds them. Ignore them; they unwind by themselves. - **`D` = the holder, or a writer that took the uninterruptible path.** | Stack shows | Meaning | Go to | |---|---|---| | `usbdev_ioctl` + a driver module (`[usbtest]`) | **owner, holds the lock** | rung 3 — terminal | | `usbdev_ioctl`, no driver frames | owner waiting on a URB | rung 1 (DUT) / rung 2 (probe) | | `usbdev_open`, sysfs reads | victim | ignore | | `tee .../usbtest/new_id`, `bind`, `unbind` | **victim that SPREADS it** | stop issuing them | | `hub_event` in a kworker | teardown stuck behind an owner | rung 3 | Driver-bind writes are not passive: `__device_driver_lock` (drivers/base/dd.c) takes `device_lock()` uninterruptibly **and `device_lock(parent)`**, because `usb_bus_type` sets `.need_parent_lock = true` (driver.c:2048) — each one holds the HUB's lock, which is how one wedged port takes a whole bus down. Map the holder to a busport with **lock-free attrs only** (`devnum`, `idVendor`, `idProduct` are `usb_descriptor_attr*`, plain `sysfs_emit`, sysfs.c:688-705): ```bash for d in /sys/bus/usb/devices/<bus>-*/; do [ "$(cat $d/devnum)" = "<devnum>" ] && echo "$d $(cat $d/idVendor):$(cat $d/idProduct)" done grep -l <SERIAL> /sys/bus/usb/devices/*/serial # only on a HEALTHY device ``` ## 2. Shield first (prerequisite for anything using libusb) Repository paths in the commands below are relative to the checkout root. A wedged device blocks every enumerator that reads its locking attributes — JLinkExe, uhubctl, openocd's HID fallback. `chmod 000` makes the VFS reject the read before `->show()` runs, so they skip it and keep enumerating. `chmod` never blocks (inode setattr, no `show()`), so it works on a fully wedged device. ```bash sudo .claude/skills/usb-kernel-recover/scripts/usb_recover.sh shield <busport> $$ # leaf + parent hub + root hub, modes recorded first sudo .claude/skills/usb-kernel-recover/scripts/usb_recover.sh shield-status # who holds what, owner alive/dead, what re-enumerated sudo .claude/skills/usb-kernel-recover/scripts/usb_recover.sh unshield <busport> $$ # restore the recorded modes; the owner pid, or none once it is dead ``` - The owner pid is the recovery session (your shell, the orchestrating process), not the short-lived `sudo`: a stale shield is one whose owner is gone, and `shield-status` says so. Never unshield blindly; a live foreign owner is refused. - The root hub is shared, so a second shield touching it is refused: finish or unshield the first one. - **Run the recovery tool as NON-root**: root has `CAP_DAC_OVERRIDE`, ignores the `000`, and blocks anyway. - Leaf shields vanish on re-enumeration (new kobject, new inodes); `unshield` restores only the surviving originals and reports the rest as gone. It never copies modes from a sibling: two of the nine are 644 where the rest are 444. - **JLinkExe always needs it**, even with `-USB`/`-SelectEmuBySN`: to find its probe it reads `product`, `serial` and `bNumInterfaces` of every USB device on the host (strace, ci.lan 2026-09-21), and those are served under each device's lock. - **Not needed for openocd pinned with `vid_pid`**, over `interface/jlink.cfg`, or for `STM32_Programmer_CLI` (`hil_flash.convoy_safe`) — those skip a foreign device before `libusb_open` and read no other device's locking attributes. ## 3. The rungs — go straight to the one triage names **Rung 1 — wedged DUT: reset it through its own probe.** Resolve the board's recovery flasher from the HIL roster, its `flasher_recover` or else its primary `flasher` (`hil_flash.recover_flasher`; `<probe-sn>` is the primary's `uid` either way). A convoy-safe one (`hil_flash.convoy_safe`, section 2) needs no shield; for openocd: ```bash openocd -c "adapter serial <probe-sn>" <flasher args> -c "init; reset run; shutdown" ``` For stlink: `STM32_Programmer_CLI --connect port=swd sn=<probe-sn> --rst --go`. Any other runs only behind the shield (section 2), then unshield; for JLink: ```bash sudo .claude/skills/usb-kernel-recover/scripts/usb_recover.sh shield <busport> $$ printf "r\ng\nq\n" > /tmp/rec.jlink JLinkExe -device <DEV> -if SWD -speed 4000 -SelectEmuBySN <probe-sn> \ -autoconnect 1 -nogui 1 -CommandFile /tmp/rec.jlink sudo .claude/skills/usb-kernel-recover/scripts/usb_recover.sh unshield <busport> $$ ``` Reset **before** park-flash: non-destructive (the firmware under test survives for autopsy), no flash wear, and no bad park image — a `wfe`/`wfi` park has bricked SWD on mimxrt1064_evk and max32666fthr through a power cycle. `ResetTarget` measures 128-129 ms; cleared 57 → 0, 26 → 0 and 5 → 0 D-state processes, single shot each. Mechanism: chip reset drops the pull-up → `usb_hcd_flush_endpoint` unlinks the URB `-ESHUTDOWN` (hcd.c:1783) → the completion fires → the ioctl returns → the lock releases. Works on i.MX RT (`USBCMD.RS` = 0 detaches, RT1050 RM Rev 3 p.2453) **and on DWC2** — measured 2026-08-16 on stm32f407disco: `r; g` gave `usb 13-2.2: USB disconnect, device number 107`, re-enumerating 325 ms later. (A bare **halt** does not: the core keeps running with the pull-up asserted.) **Park-flash** (`--recover-board`/`--recover-fw`, what `usbtest.py` automates) is the fallback where the reset cannot reach the peripheral. Delivery must be convoy-safe: **openocd pinned with `vid_pid`** or over `interface/jlink.cfg`, `STM32_Programmer_CLI`, or esptool (`-p <ttyACM>`). JLinkExe needs the shield (section 2). **Rung 2 — wedged PROBE: `root-cycle`.** A probe has no probe to reset it, so the port-side drop is the only lever left that avoids the KERNEL device lock. It commands the ROOT hub and never touches the wedged device's `device_lock` — which is exactly why rungs 1 and 3 are dangerous and this one is not. That is a different lock from the rig's **board flocks**, and this rung still needs those: it bounces every fixture under the root port, including boards another job is mid-flash on. Take them first, and release after: ```bash python3 test/hil/helper/hil_lock.py hold --all --config <this host's config> --reason "root-cycle <busport>" sudo .claude/skills/usb-kernel-recover/scripts/usb_recover.sh root-cycle <busport> [expected-serial] python3 test/hil/helper/hil_lock.py release --all # no --config: it walks the lock dir ``` `--all` is coarse for one root port, but nothing maps a sysfs busport to a board name, so it is the only reservation that actually covers the blast radius; `hold` accepts any string, so a hand-listed "just the siblings" hold reserves nothing while reporting success. A refusal naming `hil_test.py` means CI is mid-test — wait, do not force. Give the script the wedged probe's own busport (e.g. `13-1.6`), not the `13-1` hub path: it derives the root port itself, and the expected-serial guard and the success check both read the path you pass. Bounces **every fixture under that root port** (up to 25 here). The Renesas cards advertise `ppps` but do not implement it: VBUS stays up and only D+/D− drop, so this is a forced re-enumeration, never a power cycle — a device whose firmware is wedged can ride it out. Success is the sysfs inode changing, not uhubctl's exit code. **Rung 3 — terminal case: a driver ioctl that OWNS the lock.** No software cure: the task is uninterruptible and SIGKILL is queued, not delivered. Reboot with **sysrq**, never `reboot(2)` — a graceful reboot runs `device_shutdown()`, which takes every device lock and stalls on the wedged one. ```bash echo b | sudo tee /proc/sysrq-trigger # after: sync; sudo umount -a ``` **Rung 4 — hypervisor.** ci.lan only, and never needed in eight recorded wedges: `qm stop <vmid> && qm start <vmid>` from the PVE host. A VM *reboot* is not reliable — hubs can latch across the PCIe reset. ## 3b. If the CONTROLLER is dead, not a device Signature: `xhci-pci-renesas <addr>: Timeout while waiting for setup device command`, devices on that controller failing to enumerate, or its buses gone — as opposed to ONE device wedged. The rungs above cannot help; the controller itself needs re-initialising. ```bash sudo .claude/skills/usb-kernel-recover/scripts/usb_recover.sh pci-rebind <pciaddr> # unbind + bind the whole xHCI sudo .claude/skills/usb-kernel-recover/scripts/usb_recover.sh pci-bind <pciaddr> # only if it ends up driverless ``` On a successful rebind every fixture re-enumerates, and **it renumbers every bus that controller owns** (buses 17 and 18 came back as 1 and 2 on ci.lan, 2026-08-17), so take the rig-wide hold as in rung 2 before the rebind, release it after, and re-derive busports. Do NOT reach for it while a device-lock convoy is live — see Common mistakes. ## 4. If nothing is in D state The device is dead or silent, not wedged. `sudo .claude/skills/usb-kernel-recover/scripts/usb_recover.sh authorized <busport>` unconfigures and reconfigures it (`usb_set_configuration(dev, -1)` then re-choose, hub.c) — it fixes stale driver/interface state, does **not** replug: the `usb_device` survives, so most probes keep their sysfs node. If that does not take, the device is wedged rather than silent — go to rung 1 or 2. `resolve <dev-node>` maps `/dev/ttyACM3` → busport. **It takes `usb_lock_device` uninterruptibly** (hub.c `usb_deauthorize_device`), so it is safe only while nothing is in D state. ## 5. Before declaring the rig healthy ```bash ps -eo stat,args | awk '$1 ~ /D/' | wc -l # must be 0 timeout 15 lsusb # rc 0 and a sane device count sudo uhubctl -l <bus> -p <port> # "0000 off" = never came back sudo uhubctl -l <bus> -p <port> -a on ``` Observed: 5 boards missing with a completely clean D-state list, because `usb17-port2` sat at `disable=1`. ## Common mistakes - **`uhubctl -a cycle` on a root port without `-S`.** It writes sysfs `disable`, and `disable_store` takes `usb_lock_device(hdev)` uninterruptibly then calls `usb_disconnect(child)` inside it (port.c) — against a wedged child that blocks while holding the root hub's lock, poisoning the bus. `usb_recover.sh` passes `-S`. - **`echo 1 > .../remove`** to make a wedged device "go away": `remove_store` is the one attribute in sysfs.c taking the uninterruptible `usb_lock_device` (sysfs.c:765). It joins the convoy instead of clearing it. - **`authorized` on anything wedged** — same uninterruptible lock. Driver unbind/bind (`/sys/bus/usb/drivers/usb/{unbind,bind}`) does the same unconfigure/reconfigure via `usb_generic_driver_disconnect` (generic.c) but ALSO takes the parent hub's lock (`need_parent_lock`), so it is strictly worse; it was removed from `usb_recover.sh` for that reason. - **`pci-rebind` for a wedged DEVICE.** It is the cure for a dead CONTROLLER (see below), not for a device-lock convoy: with a live D-state URB the re-bind can hang and leave the controller with **no driver** and every fixture offline (observed once). Recover that with `pci-bind <addr>`. - **Writing `/sys/bus/pci/devices/<addr>/reset`** — no rig controller has FLR, so it becomes a bus reset behind a live driver: card halted, host power cycle. - **Resetting a victim's board.** Two boards were reset innocently before anyone found the holder. Map by `devnum`, not by which board "should" be running. - **Assuming one controller.** Observed: 26 D-state processes across three xHCI controllers, all cleared by one probe reset on one device. ## Rig layout (ci.lan, bus numbers renumber every boot) `readlink -f /sys/bus/usb/devices/usb<N>` → its PCI address; `sudo uhubctl` lists the root hubs it can drive, against their PCI address. Eight Renesas uPD720201 controllers — `0000:01:00.0` to `0000:08:00.0`, on two cards — advertise per-port `ppps` on both their USB2 and USB3 root hubs, **but do not implement it**: the silicon never drops VBUS (measured on the `05`–`08` card; `01`–`04` is unmeasured, assume the same), so a cycle re-enumerates the port and nothing more (above). Do not read `uhubctl`'s `ppps` as power control on this rig. Which tree holds which probes moves with re-cabling, so derive it (`lsusb -s <bus>:`) rather than trusting a stored map. Verified 2026-09-22; `sudo` is passwordless for `hathach` here, so every rung above runs without a prompt.
GitHubで見る