usb-host: exhausted open retries prevent fast re-enumerated devices from reconnecting

Host environment

  • Deterministic source-level reproduction: macOS, arm64, Clang 21, GLib 2.90.
  • QEMU source: 615ece3c406b262996e569f29ea6eed0d81c4d8d (11.1.50).
  • The same autoscan function is present in the v11.0.0 release.
  • A full qemu-system-x86_64 build with libusb 1.0.30 was also compiled.
  • No guest image or physical USB device is required for the attached fixture.

Description of problem

usb_host_auto_check() stores an open-failure counter on the QEMU USBHostDevice, rather than on a particular enumerated host device. Successful opens leave that counter unchanged. Once it reaches three, matching devices are skipped before an open is attempted.

The counter is reset only when an inventory scan sees no matching device. A device that disconnects and re-enumerates between scans can therefore inherit the exhausted retry count of its previous incarnation, even if its USB address changed and it is now openable. The exhausted object keeps matching, so subsequent scans do not recover it.

A minimal synthetic sequence is:

  1. Device A is attached. On disconnect, an attempted open of the stale A fails. A replacement B appears without an intervening empty scan and attaches successfully; the counter remains one.
  2. Repeat with B and replacement C; the counter remains two.
  3. C disconnects and its stale open fails. Replacement D appears and is openable, but QEMU never attempts to open it because the counter is three.

Expected: the new enumerated device receives an opportunity to attach. Actual: no open attempt occurs until a scan sees no match or the QEMU USB object is recreated. Three consecutive failures followed by a new device also reproduce the problem; successful intervening connections are not required.

The original retry-limit commit 3ee886c5ba77a65d6b2c2a372a091d6796ed502b explicitly intended unplug/replug to restore the retry budget.

Steps to reproduce

Download and extract qemu-usb-reconnect-repro.zip. Prerequisites: Python 3, a C compiler named cc, pkg-config, and GLib headers/libraries. No QEMU checkout, network access, guest image, or physical USB device is needed. From the extracted directory run:

python3 run-repro.py --source upstream-host-libusb.c

The archive includes the unmodified upstream source file from the pinned revision, the fixture, the runner, expected output, and file checksums.

The runner extracts the actual autoscan and unrealize function bodies from that revision and compiles them against deterministic fake inventory, open/close, timer, and device-lifetime boundaries. It does not duplicate the retry algorithm. All device identities are synthetic (1234:5678, bus 1).

Relevant output:

TEST repeated-reconnects
  reconnect 1: replacement attempted=yes attached=yes
  reconnect 2: replacement attempted=yes attached=yes
  reconnect 3: replacement attempted=no attached=no
  expectation failed: state.dh == &devices[i]
FAIL repeated-reconnects

The full fixture exits with status 1: 15 scenarios, seven failing expectations. These include replacement after retry exhaustion, address reuse, recovery across successful connections, and independent matching devices. Existing controls cover bounded retries for a continuously failing device and recovery after an empty scan.

Additional information

Source locations:

This is a source-level reproduction of retry policy. It does not exercise physical USB, actual libusb event scheduling, or guest traffic. It does not identify why a physical device disconnected. The full binary build is a separate compile check, not a claim of physical reconnect validation.

Automated-tool disclosure: AI assistance was used for source analysis, fixture generation, and this report. An AI coding agent executed the fixture against both the unmodified source and a local experimental correction, and checked the reported results. The reporter authorized submission; we do not claim a separate manual execution by the reporter. The local generated patch is not being proposed for upstream acceptance under QEMU's current code-provenance policy.