Skip to content

[bug fix] driver_vive: fix double-cleanup reentrancy bug in handle_transfer disconnect - #376

Closed
rocketmark wants to merge 1 commit into
collabora:masterfrom
rocketmark:upstream-pr/driver-vive-disconnect-reentrancy
Closed

rocketmark wants to merge 1 commit into
collabora:masterfrom
rocketmark:upstream-pr/driver-vive-disconnect-reentrancy

Conversation

@rocketmark

Copy link
Copy Markdown
Contributor

Summary

survive_close_usb_device() cancels every interface's in-flight transfer, including the one whose own handle_transfer() is still on the call stack — it doesn't skip the interface that originated the disconnect. If libusb's cancellation of that interface's own transfer invokes its completion callback synchronously, handle_transfer() reenters itself, and the shutdown cleanup runs twice for one interface.

Running that cleanup twice corrupts shared state instead of crashing cleanly, which is worse: the bug can sit silent until the device's transfer accounting is already wrong.

Demonstration

Trigger: a USB transfer error path (repeated STALLs, or 3+ consecutive timeouts on an RF device) calls survive_disconnect_device() → survive_close_usb_device(), which cancels every interface's transfer — including its own. If libusb's cancellation of that self-transfer invokes the callback synchronously, handle_transfer() reenters itself mid-cleanup:

BEFORE fix:
active_transfers = 2 (two interfaces: IMU, Buttons)
Buttons' error path calls survive_disconnect_device(Buttons)
cancel loop reaches IMU's transfer: reentrant cleanup, active_transfers-- → 1
cancel loop reaches Buttons' own transfer: reentrant cleanup (inner pass)
active_transfers-- → 0
libusb_release_interface() called (1st time) for Buttons
iface->transfer = 0
outer pass resumes, redoes the same cleanup for Buttons:
libusb_release_interface() called (2nd time) for the same interface
active_transfers-- → underflows: 18446744073709551615 (SIZE_MAX)
Result: corrupted transfer count, Buttons' USB interface released twice

AFTER fix:
outer pass resumes, iface->transfer == 0 (already cleaned up by the inner pass)
guard returns immediately, no redundant work
Result: active_transfers == 0, libusb_release_interface() called exactly once
Impact

Any tracked device with multiple USB interfaces (IMU, Lightcap, Buttons — the normal case) is exposed whenever this disconnect path runs. It's reachable via the existing consecutive-timeout/RF-device path or via repeated transfer errors — not an exotic trigger, just a timing-dependent one (it depends on whether libusb's cancellation callback fires synchronously or is deferred). When it fires, active_transfers is corrupted for the rest of that device's lifetime and a USB interface claim is released twice.

Change

One file, src/driver_vive.libusb.h. The shared shutdown: cleanup block in handle_transfer() is guarded with a check for iface->transfer == 0 — set by the first pass through this cleanup, before the transfer is freed. A reentrant second pass for an already-cleaned-up interface now returns immediately instead of redoing the decrement and release. Includes a regression test (driver_vive_reentrancy_test.c) that drives the real, unmodified handle_transfer/AttachInterface code against a fake libusb (mock_libusb.c) whose libusb_cancel_transfer synchronously re-invokes the target transfer's callback, reproducing the bug deterministically without hardware.

Found via

Found while auditing handle_transfer()'s disconnect path, prompted by reviewing the call sites that reach survive_disconnect_device() from inside this function. Built a deterministic repro to check whether that path was reentrancy-safe; it wasn't — reproduced with the exact numbers above, included here as a regression test.

…connect path

survive_close_usb_device() cancels every interface's in-flight transfer,
including the one whose own handle_transfer() is still on the call stack --
it doesn't skip the interface that originated the disconnect (reachable via
the consecutive-timeout/RF-device path, or via repeated transfer errors). If
libusb's cancellation of that interface's own transfer invokes its
completion callback synchronously, handle_transfer() reenters itself: the
shutdown cleanup (active_transfers--, libusb_release_interface) runs once in
the reentrant inner call, then runs again when the outer frame resumes.
active_transfers underflows (0 - 1 wraps to SIZE_MAX on this unsigned
counter) instead of landing on 0, and libusb_release_interface is called
twice for the same interface.

Guard the shutdown cleanup with a check for iface->transfer == 0 (set by
the first pass, before it's freed) so a reentrant second pass for an
already-cleaned-up interface is a no-op instead of corrupting
active_transfers and double-releasing the interface.

Includes a regression test (driver_vive_reentrancy_test.c) that drives the
real handle_transfer/AttachInterface code against a fake libusb
(mock_libusb.c) whose libusb_cancel_transfer synchronously re-invokes the
transfer's own callback -- the worst-case reentrant ordering a real libusb
backend could produce -- reproducing the bug deterministically without
hardware.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@rocketmark

Copy link
Copy Markdown
Contributor Author

Closing in favor of #372, which fixes this same reentrancy (survive_close_usb_device() canceling the interface whose own callback is still executing) more thoroughly — it defers survive_close_usb_device() entirely out of the libusb callback via the poll loop, which also covers survive_config_cancel()'s reentrant call that this PR didn't address.

@rocketmark rocketmark closed this Oct 10, 2026
@rocketmark
rocketmark deleted the upstream-pr/driver-vive-disconnect-reentrancy branch October 10, 2026 21:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant