driver_vive: fix two crashes in the USB transfer callback - #372
Open
beaucasque wants to merge 2 commits into
Open
driver_vive: fix two crashes in the USB transfer callback#372beaucasque wants to merge 2 commits into
beaucasque wants to merge 2 commits into
Conversation
added 2 commits
September 1, 2026 02:36
…he transfer callback
handle_transfer() is libusb's transfer-completion callback: it runs on the
event thread while libusb holds the current transfer's locks. On a
non-COMPLETED status it reached survive_disconnect_device(), which called
survive_close_usb_device() synchronously. That function calls
libusb_cancel_transfer() on every interface -- including the one whose
callback is currently executing -- and survive_config_cancel() reaches
libusb the same way.
Re-entering libusb from inside its own callback makes pthread_mutex_lock()
fail, so usbi_mutex_lock() (os/threads_posix.h) asserts and the process
aborts:
Warning: 2.703381 T23 Device disconnect: 1
python: ../../libusb/os/threads_posix.h:46: usbi_mutex_lock:
Assertion `pthread_mutex_lock(mutex) == 0' failed.
Reproduced consistently with four full-speed Vive trackers behind a
single-TT USB 2.0 hub: transaction-translator saturation makes a transfer
fail about 2.7 s after start, and the process dies every time. Three
trackers on the same hub ran for hours without an error. Moving to a
multi-TT hub removes the trigger but not the latent bug -- any failing
transfer reaches this path, including an optical dropout or a tracker
powering off.
survive_disconnect_device() now only marks the interfaces down and raises
request_disconnect. The poll loop in survive_usb_poll() consumes it and
calls survive_close_usb_device() off-callback, where no libusb lock is
held. This mirrors the existing request_close mechanism, which is already
consumed from the same loop -- the deferral scheme was there, this path
simply did not use it.
The transfer being handled is still cleaned up as before by the shutdown:
label at the end of handle_transfer(), so per-transfer accounting
(active_transfers, request_close) is unchanged.
…sfer
On a non-COMPLETED status, handle_transfer() retries by calling
libusb_submit_transfer(). When that call SUCCEEDS it fell through to
'goto disconnect', and from there into the shutdown: label, which runs
libusb_free_transfer() on the transfer that was just resubmitted and is
therefore in flight.
libusb_free_transfer() destroys the transfer's mutex, but libusb still has
the transfer in its flying-transfers list. The next event-loop pass locks
that destroyed mutex, pthread_mutex_lock() fails, and usbi_mutex_lock()
(os/threads_posix.h) asserts -- the process aborts:
Warning: 2.703381 T23 Device disconnect: 1
python: ../../libusb/os/threads_posix.h:46: usbi_mutex_lock:
Assertion `pthread_mutex_lock(mutex) == 0' failed.
Observed by unplugging one of four wired Vive trackers while running: the
process dies the instant the device goes away. Also reproducible through
transaction-translator saturation on a single-TT USB 2.0 hub, where a
transfer fails a couple of seconds after start.
Return after a successful resubmit, so the retry actually gets a chance to
run and the in-flight transfer is left alone. Only a failed resubmit still
goes to shutdown:, which is correct -- nothing is in flight then.
This also drops a duplicated increment: error_count was incremented twice
per error (once on its own line, once inside the condition), so the retry
budget was five failures rather than the ten the code reads as.
beaucasque
pushed a commit
to beaucasque/free-D
that referenced
this pull request
Sep 1, 2026
2 commits, +36/-2 sur 2 fichiers, mergeable. A surveiller ; retirer les deux patches de patches/ des qu'elle sera fusionnee. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two independent problems in handle_transfer(), libusb's transfer-completion callback, both reached the moment a transfer fails — an unplugged device, a long optical dropout, a tracker powering off, or bus saturation.
The first is the crash I actually hit, every time:
Warning: 2.703381 T23 Device disconnect: 1
python: ../../libusb/os/threads_posix.h:46: usbi_mutex_lock:
Assertion `pthread_mutex_lock(mutex) == 0' failed.
A resubmitted transfer is then freed
Commit do not free a resubmitted transfer.
if (iface->error_count++ < 10) {
if (libusb_submit_transfer(transfer)) { /* != 0 == failure /
goto shutdown;
}
} / success falls out of the if /
goto disconnect; / ... and still reaches shutdown: */
When the resubmit succeeds the transfer is in flight again, yet control falls through to the shutdown: label, which runs libusb_free_transfer() on it. That destroys the transfer's mutex while libusb still has it in the flying-transfers list; the next event-loop pass locks a destroyed mutex, pthread_mutex_lock() fails, and usbi_mutex_lock() asserts.
Return after a successful resubmit, so the retry actually gets to run and the in-flight transfer is left alone. Only a failed resubmit still goes to shutdown:, which is correct — nothing is in flight then.
The same commit drops a duplicated increment: error_count was bumped twice per error, so the retry budget was five failures rather than the ten the code reads as.
Re-entering libusb from inside its own callback
Commit defer close out of the transfer callback. This one I did not observe aborting on its own; it is a re-entrancy that reading the code makes plain, and it sits on the same failure path.
On a non-COMPLETED status, survive_disconnect_device() called survive_close_usb_device() synchronously. That function calls libusb_cancel_transfer() on every interface — including the one whose callback is executing — and survive_config_cancel() reaches libusb the same way. The event thread already holds that transfer's locks.
survive_disconnect_device() now only marks the interfaces down and raises request_disconnect. The poll loop in survive_usb_poll() consumes it and calls survive_close_usb_device() off-callback, where no libusb lock is held. This mirrors the existing request_close mechanism, already consumed from the same loop — the deferral scheme was there, this path simply did not use it.
The transfer being handled is still cleaned up as before by the shutdown: label, so per-transfer accounting (active_transfers, request_close) is unchanged.
How to reproduce
Simplest: run against wired trackers and unplug one. Before these commits the process dies instantly.
Also reproducible without touching anything: four full-speed Vive trackers (28de:2300) behind a single-TT USB 2.0 hub — here two cascaded Genesys 05e3:0608. Transaction-translator saturation makes a transfer fail about 2.7 s after start, every time. Three trackers on the same hub ran for hours without a single error, which is why this can look like a device-count problem. A multi-TT hub (Realtek 0bda:5411) removes the trigger but not the bugs.
cat /sys/bus/usb/devices//bDeviceProtocol # 02 == multi-TT
Testing
Three wired trackers on a multi-TT hub, via pysurvive: 238–242 Hz per device, zero dropouts, lighthouse geometry solved as before. Unplugging a device while running no longer aborts — the process keeps running and reports the device gone.
Worth stating plainly: the deferral commit alone does not stop the abort. I wrote it first, believing it was the cause, then unplugged a tracker and the process still died. Freeing the resubmitted transfer is what actually kills it. Both are included because both are real, but only the second is backed by an observed failure — the first is backed by reading the code.