Hotplug (macOS): self-join deadlock in hid_internal_hotplug_cleanup #794

Open
opened 2026-04-24 09:18:37 -05:00 by Youw · 0 comments
Youw commented 2026-04-24 09:18:37 -05:00 (Migrated from github.com)

Summary

When a hotplug callback that self-deregisters runs in response to a device-removal event on macOS, `hid_internal_hotplug_cleanup` is reached from within the hotplug thread itself and calls `pthread_join(hid_hotplug_context.thread, NULL)` on its own thread — which hangs forever.

Call chain

```
hotplug_thread()
CFRunLoopRunInMode() [hotplug thread's run loop]
hid_internal_hotplug_disconnect_callback() [IOHID callback, on hotplug thread]
pthread_mutex_lock() [recursive OK]
hid_internal_invoke_callbacks()
user_callback() returns non-zero [self-deregister]
callback->events = 0; cb_list_dirty = 1
mutex_in_use = 0 [guard reset]
hid_internal_hotplug_remove_postponed()
callback freed; hotplug_cbs == NULL
hid_internal_hotplug_cleanup() [mac/hid.c ~line 1089]
mutex_in_use == 0 guard passes
hotplug_cbs == NULL so cleanup proceeds
thread_state = 2; signal run loop
pthread_join(hid_hotplug_context.thread) [*** SELF-JOIN ***]
HANG
```

The `mutex_in_use` guard in `hid_internal_invoke_callbacks` resets to 0 before returning (mac/hid.c:997), so it does not protect `hid_internal_hotplug_cleanup` from running when the path enters from an IOHID callback.

Proposed fix

In `hid_internal_hotplug_cleanup`, detect the self-thread case and use `pthread_detach` instead of `pthread_join`:

```c
if (pthread_equal(pthread_self(), hid_hotplug_context.thread)) {
pthread_detach(hid_hotplug_context.thread);
} else {
pthread_join(hid_hotplug_context.thread, NULL);
}
```

Alternatively, defer the teardown signal until the thread's next run-loop iteration so the cleanup never runs on the self thread.

Reproducer

A callback registered with `HID_API_HOTPLUG_EVENT_DEVICE_LEFT` that returns non-zero when the event fires, on a device that is then physically (or programmatically) unplugged, hangs the removal thread.

Scope

  • `mac/hid.c` only.
  • Verify analogous paths in `linux/hid.c`, `libusb/hid.c`, `windows/hid.c` don't share the same problem (`windows/hid.c` already does self-thread guards in some places).

Drafted with Claude Code.

## Summary When a hotplug callback that self-deregisters runs in response to a device-removal event on macOS, \`hid_internal_hotplug_cleanup\` is reached from within the hotplug thread itself and calls \`pthread_join(hid_hotplug_context.thread, NULL)\` on its own thread — which hangs forever. ## Call chain \`\`\` hotplug_thread() CFRunLoopRunInMode() [hotplug thread's run loop] hid_internal_hotplug_disconnect_callback() [IOHID callback, on hotplug thread] pthread_mutex_lock() [recursive OK] hid_internal_invoke_callbacks() user_callback() returns non-zero [self-deregister] callback->events = 0; cb_list_dirty = 1 mutex_in_use = 0 [guard reset] hid_internal_hotplug_remove_postponed() callback freed; hotplug_cbs == NULL hid_internal_hotplug_cleanup() [mac/hid.c ~line 1089] mutex_in_use == 0 guard passes hotplug_cbs == NULL so cleanup proceeds thread_state = 2; signal run loop pthread_join(hid_hotplug_context.thread) [*** SELF-JOIN ***] HANG \`\`\` The \`mutex_in_use\` guard in \`hid_internal_invoke_callbacks\` resets to 0 before returning (mac/hid.c:997), so it does not protect \`hid_internal_hotplug_cleanup\` from running when the path enters from an IOHID callback. ## Proposed fix In \`hid_internal_hotplug_cleanup\`, detect the self-thread case and use \`pthread_detach\` instead of \`pthread_join\`: \`\`\`c if (pthread_equal(pthread_self(), hid_hotplug_context.thread)) { pthread_detach(hid_hotplug_context.thread); } else { pthread_join(hid_hotplug_context.thread, NULL); } \`\`\` Alternatively, defer the teardown signal until the thread's next run-loop iteration so the cleanup never runs on the self thread. ## Reproducer A callback registered with \`HID_API_HOTPLUG_EVENT_DEVICE_LEFT\` that returns non-zero when the event fires, on a device that is then physically (or programmatically) unplugged, hangs the removal thread. ## Scope - \`mac/hid.c\` only. - Verify analogous paths in \`linux/hid.c\`, \`libusb/hid.c\`, \`windows/hid.c\` don't share the same problem (\`windows/hid.c\` already does self-thread guards in some places). ## Related - Original finding: https://github.com/libusb/hidapi/pull/674#discussion_r3015415035 - Documentation: #790 - Original RFC: #674 --- _Drafted with Claude Code._
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
gh-libusb/hidapi#794
No description provided.