Skip to content

Commit 1889e9f

Browse files
authored
libusb: Fix high CPU usage in callback thread (#783)
Callback_thread used hidapi_thread_cond_timedwait with an absolute timespec already in the past, causing a tight spin loop. Replaced with cond_wait + predicate pattern so the thread sleeps until an event or a shutdown signal arrives. Multithreading fixes folded in along the way: - Queue mutex moved from libusb_thread to callback_thread (its natural condvar partner), with signal-under-mutex to avoid a lost-wakeup race at shutdown. - process_hotplug_event() now runs under hid_hotplug_context.mutex to match hid_hotplug_register_callback()'s HID_API_HOTPLUG_ENUMERATE traversal of hid_hotplug_context.devs — fixes a use-after-free where a concurrent disconnect could free a list node mid-traversal. - devs teardown moved to hid_internal_hotplug_cleanup() (after hidapi_thread_join) so every access to devs happens under hid_hotplug_context.mutex. - hid_hotplug_register_callback() now frees devs on the libusb_hotplug_register_callback() error path, which previously leaked. Closes: #782 Assisted-by: Copilot:claude-sonnet-4.6 Assisted-by: Copilot:claude-opus-4.6 Assisted-by: Claude:claude-opus-4.7
1 parent 8bd8ff7 commit 1889e9f

2 files changed

Lines changed: 45 additions & 28 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,6 @@ doxygen/html/
3333

3434
# Visual Studio Code + CMake
3535
.vscode/
36+
37+
# Local build directory
3638
build/

libusb/hid.c

Lines changed: 43 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -172,7 +172,10 @@ static struct hid_hotplug_context {
172172
/* Linked list of the hotplug callbacks */
173173
struct hid_hotplug_callback *hotplug_cbs;
174174

175-
/* Linked list of the device infos (mandatory when the device is disconnected) */
175+
/* Linked list of the device infos (mandatory when the device is disconnected).
176+
* Protected by `mutex`: all reads, writes and the final free during teardown
177+
* are performed while holding it. The teardown free runs in
178+
* hid_internal_hotplug_cleanup() after the hotplug threads have been joined. */
176179
struct hid_device_info *devs;
177180
} hid_hotplug_context = {
178181
.next_handle = FIRST_HOTPLUG_CALLBACK_HANDLE,
@@ -584,6 +587,11 @@ static void hid_internal_hotplug_cleanup()
584587

585588
/* Wait for both threads to stop */
586589
hidapi_thread_join(&hid_hotplug_context.libusb_thread);
590+
591+
/* Both hotplug threads have exited: we now have exclusive access to `devs`
592+
* (the caller holds `mutex` and no hotplug event can reach process_hotplug_event). */
593+
hid_free_enumeration(hid_hotplug_context.devs);
594+
hid_hotplug_context.devs = NULL;
587595
}
588596

589597
static void hid_internal_hotplug_init()
@@ -1143,8 +1151,8 @@ static int hid_libusb_hotplug_callback(libusb_context *ctx, libusb_device *devic
11431151
msg->event = event;
11441152
msg->next = NULL;
11451153

1146-
/* We use this thread's mutex to protect the queue */
1147-
hidapi_thread_mutex_lock(&hid_hotplug_context.libusb_thread);
1154+
/* Use callback_thread's mutex to protect the queue and signal it */
1155+
hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread);
11481156
struct hid_hotplug_queue* end = hid_hotplug_context.queue;
11491157
if (end) {
11501158
while (end->next) {
@@ -1154,16 +1162,21 @@ static int hid_libusb_hotplug_callback(libusb_context *ctx, libusb_device *devic
11541162
} else {
11551163
hid_hotplug_context.queue = msg;
11561164
}
1157-
hidapi_thread_mutex_unlock(&hid_hotplug_context.libusb_thread);
11581165

1159-
/* Wake up the other thread so it can react to the new message immediately */
1166+
/* Wake up the callback thread so it can react to the new message immediately */
11601167
hidapi_thread_cond_signal(&hid_hotplug_context.callback_thread);
1168+
hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread);
11611169

11621170
return 0;
11631171
}
11641172

11651173
static void process_hotplug_event(struct hid_hotplug_queue* msg)
11661174
{
1175+
/* Lock the mutex to avoid race conditions with hid_hotplug_register_callback(),
1176+
* which may iterate devs during HID_API_HOTPLUG_ENUMERATE while holding this mutex.
1177+
* The mutex is recursive, so hid_internal_invoke_callbacks() can safely re-acquire it. */
1178+
pthread_mutex_lock(&hid_hotplug_context.mutex);
1179+
11671180
if (msg->event == LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED) {
11681181
struct hid_device_info* info = hid_enumerate_from_libusb(msg->device, 0, 0);
11691182
struct hid_device_info* info_cur = info;
@@ -1204,6 +1217,8 @@ static void process_hotplug_event(struct hid_hotplug_queue* msg)
12041217
}
12051218
}
12061219

1220+
pthread_mutex_unlock(&hid_hotplug_context.mutex);
1221+
12071222
/* Release the libusb device - we are done with it */
12081223
libusb_unref_device(msg->device);
12091224

@@ -1216,40 +1231,32 @@ static void* callback_thread(void* user_data)
12161231
{
12171232
(void) user_data;
12181233

1219-
/* 5 msec timeout seems reasonable; don't set too low to avoid high CPU usage */
1220-
/* This timeout only affects how much time it takes to stop the thread */
1221-
hidapi_timespec ts;
1222-
ts.tv_sec = 0;
1223-
ts.tv_nsec = 5000000;
1224-
12251234
hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread);
1226-
1235+
12271236
/* We stop the thread if by the moment there are no events left in the queue there are no callbacks left */
12281237
while (1) {
1229-
/* Make the tread fall asleep and wait for a condition to wake it up */
1230-
hidapi_thread_cond_timedwait(&hid_hotplug_context.callback_thread, &ts);
1238+
/* Wait for events to arrive or shutdown signal */
1239+
while (!hid_hotplug_context.queue && hid_hotplug_context.hotplug_cbs) {
1240+
hidapi_thread_cond_wait(&hid_hotplug_context.callback_thread);
1241+
}
12311242

1232-
/* We use this thread's mutex to protect the queue */
1233-
hidapi_thread_mutex_lock(&hid_hotplug_context.libusb_thread);
1243+
/* Process all pending events from the queue */
12341244
while (hid_hotplug_context.queue) {
12351245
struct hid_hotplug_queue *cur_event = hid_hotplug_context.queue;
1236-
process_hotplug_event(cur_event);
1246+
hid_hotplug_context.queue = cur_event->next;
12371247

1238-
/* Empty the queue */
1239-
cur_event = cur_event->next;
1240-
free(hid_hotplug_context.queue);
1241-
hid_hotplug_context.queue = cur_event;
1248+
/* Release the lock while processing to avoid blocking event producers */
1249+
hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread);
1250+
process_hotplug_event(cur_event);
1251+
free(cur_event);
1252+
hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread);
12421253
}
1243-
hidapi_thread_mutex_unlock(&hid_hotplug_context.libusb_thread);
1254+
12441255
if (!hid_hotplug_context.hotplug_cbs) {
12451256
break;
12461257
}
12471258
}
12481259

1249-
/* Cleanup connected device list */
1250-
hid_free_enumeration(hid_hotplug_context.devs);
1251-
hid_hotplug_context.devs = NULL;
1252-
12531260
hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread);
12541261

12551262
return NULL;
@@ -1276,8 +1283,12 @@ static void* hotplug_thread(void* user_data)
12761283
libusb_hotplug_deregister_callback(hid_hotplug_context.context, hid_hotplug_context.callback_handle);
12771284
libusb_exit(hid_hotplug_context.context);
12781285

1279-
/* Forcibly wake up the thread so it can shut down immediately and wait for it to stop */
1286+
/* hotplug_cbs is already NULL here (the loop above exited because of that).
1287+
* Signal callback_thread under the mutex so it can observe the NULL hotplug_cbs
1288+
* and exit cleanly, rather than waiting indefinitely in cond_wait. */
1289+
hidapi_thread_mutex_lock(&hid_hotplug_context.callback_thread);
12801290
hidapi_thread_cond_signal(&hid_hotplug_context.callback_thread);
1291+
hidapi_thread_mutex_unlock(&hid_hotplug_context.callback_thread);
12811292

12821293
hidapi_thread_join(&hid_hotplug_context.callback_thread);
12831294

@@ -1354,8 +1365,12 @@ int HID_API_EXPORT HID_API_CALL hid_hotplug_register_callback(unsigned short ven
13541365
LIBUSB_HOTPLUG_EVENT_DEVICE_ARRIVED | LIBUSB_HOTPLUG_EVENT_DEVICE_LEFT,
13551366
0, LIBUSB_HOTPLUG_MATCH_ANY, LIBUSB_HOTPLUG_MATCH_ANY, LIBUSB_HOTPLUG_MATCH_ANY, &hid_libusb_hotplug_callback, NULL,
13561367
&hid_hotplug_context.callback_handle)) {
1357-
/* Major malfunction, failed to register a callback */
1368+
/* Major malfunction, failed to register a callback.
1369+
* No hotplug thread was started, so we must unwind `devs` ourselves
1370+
* (hid_internal_hotplug_cleanup() would try to join a non-existent thread). */
13581371
libusb_exit(hid_hotplug_context.context);
1372+
hid_free_enumeration(hid_hotplug_context.devs);
1373+
hid_hotplug_context.devs = NULL;
13591374
free(hotplug_cb);
13601375
hid_hotplug_context.hotplug_cbs = NULL;
13611376
pthread_mutex_unlock(&hid_hotplug_context.mutex);

0 commit comments

Comments
 (0)