From 5d66a6038208fd1e74b3f35ca04874682c1ebff3 Mon Sep 17 00:00:00 2001 From: beaucasque Date: Mon, 31 Aug 2026 21:29:11 -0400 Subject: [PATCH 1/2] driver_vive.libusb.h: survive_disconnect_device: defer close out of the 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. --- src/driver_vive.c | 10 ++++++++++ src/driver_vive.libusb.h | 19 ++++++++++++++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/driver_vive.c b/src/driver_vive.c index 005cfff8..9425797b 100755 --- a/src/driver_vive.c +++ b/src/driver_vive.c @@ -447,6 +447,9 @@ struct SurviveUSBInfo { struct survive_config_packet *cfg_user; bool request_close, request_reopen; + /* Raised from the libusb transfer-completion callback, consumed by the + poll loop off-callback. See survive_disconnect_device(). */ + bool request_disconnect; }; struct SurviveViveData { @@ -1031,6 +1034,13 @@ int survive_vive_usb_poll(SurviveContext *ctx, void *v) { survive_config_poll(usbInfo); + /* Close requested from a transfer callback: do it HERE, on the poll + thread, where no libusb lock is held. */ + if (usbInfo->request_disconnect) { + usbInfo->request_disconnect = false; + survive_close_usb_device(usbInfo); + } + if (survive_handle_close_request_flag(usbInfo)) { i--; } diff --git a/src/driver_vive.libusb.h b/src/driver_vive.libusb.h index 25007de1..37b620f7 100644 --- a/src/driver_vive.libusb.h +++ b/src/driver_vive.libusb.h @@ -204,8 +204,25 @@ static int survive_open_usb_device(SurviveViveData *sv, survive_usb_device_t d, static inline void survive_close_usb_device(struct SurviveUSBInfo *usbInfo); static void survive_disconnect_device(SurviveUSBInterface *iface) { + struct SurviveUSBInfo *usbInfo = iface->usbInfo; iface->ctx = 0; - survive_close_usb_device(iface->usbInfo); + + /* We are inside handle_transfer(), i.e. inside libusb's + transfer-completion callback: the event thread holds the current + transfer's locks. survive_close_usb_device() calls + libusb_cancel_transfer() on every interface -- including the one being + handled -- and survive_config_cancel() reaches libusb the same way. + Re-entering libusb from here makes pthread_mutex_lock() fail, and + usbi_mutex_lock() (os/threads_posix.h) asserts: abort(). + + Only mark here. The poll loop calls survive_close_usb_device() + off-callback, where no lock is held. The transfer being handled is + still cleaned up as before by the shutdown: label below. */ + for (size_t j = 0; j < usbInfo->interface_cnt; j++) { + usbInfo->interfaces[j].shutdown = 1; + usbInfo->interfaces[j].assoc_obj = 0; + } + usbInfo->request_disconnect = true; } static void handle_transfer(struct libusb_transfer *transfer) { uint64_t time = OGGetAbsoluteTimeUS(); From c8eb7caebd35de13a2f08116b8db60c0f165fae8 Mon Sep 17 00:00:00 2001 From: beaucasque Date: Tue, 1 Sep 2026 01:51:12 -0400 Subject: [PATCH 2/2] driver_vive.libusb.h: handle_transfer: do not free a resubmitted transfer 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. --- src/driver_vive.libusb.h | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/driver_vive.libusb.h b/src/driver_vive.libusb.h index 37b620f7..61bd3366 100644 --- a/src/driver_vive.libusb.h +++ b/src/driver_vive.libusb.h @@ -244,11 +244,18 @@ static void handle_transfer(struct libusb_transfer *transfer) { if (!iface->shutdown && transfer->status != LIBUSB_TRANSFER_COMPLETED) { SV_WARN("%f %s Device disconnect: %d", survive_run_time(ctx), survive_colorize_codename(iface->assoc_obj), transfer->status); - iface->error_count++; if (iface->error_count++ < 10) { if (libusb_submit_transfer(transfer)) { goto shutdown; } + + /* Resubmit succeeded: the transfer is IN FLIGHT again. Falling + through to shutdown: would call libusb_free_transfer() on it, + which destroys its mutex while libusb still holds it in the + flying-transfers list. The next event-loop pass then locks a + destroyed mutex and usbi_mutex_lock() asserts. Return and let + the retry run its course. */ + return; } goto disconnect;