Skip to content

Commit da500c6

Browse files
authored
Fix possible deadlock in Connection-callback feature (#676)
Fix the possible issues that happen when a register or de-register call is made from inside a callback. The way it works is the same for all platforms and is described below. Resolves: #673 1) The mutex is made recursive, so it can be locked by the same thread multiple times 2) `mutex_ready` kept intact, added 2 more flags, `mutex_in_use` and `cb_list_dirty`. 3) When the `mitex_in_use` flag is set, the Deregister call is no longer allowed to immediately remove any callbacks from the list. Instead, the `events` field in the callback is set to 0, which makes it "invalid", as it will no longer match any events, and the `cb_list_dirty` flag is set to 1 to indicate that the list needs to be checked for invalid events later. 4) When a callback returns a non-zero value, indicating that the callback is to be disarmed and removed from the list, it is marked in the same manner until the processing finishes (unless the callback was called directly by the Register call, in which case it's return value is ignored on purpose) 5) After all the callbacks are processed, if `cb_list_dirty` flag is set, the list of callbacks is checked for any callbacks marked for removal (`events` field set to 0), and those are only removed after all the processing is finished. 6) The Register call is allowed to register callbacks, as it causes no issues so long as the mutex it locks is recursive 7) Since the Register call can also call the new callback if `HID_API_HOTPLUG_ENUMERATE` is specified, `mutex_in_use` flag is set to prevent callback removal in that new callback. 8) The return value of any callbacks called for pre-existing devices is still ignored as per documentation and does not mark them invalid.
1 parent b6606ca commit da500c6

File tree

5 files changed

+694
-218
lines changed

5 files changed

+694
-218
lines changed

hidtest/test.c

Lines changed: 268 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,17 @@
1919
#include <wchar.h>
2020
#include <string.h>
2121
#include <stdlib.h>
22+
#include <ctype.h> // for "tolower()"
2223

2324
#include <hidapi.h>
2425

25-
// Headers needed for sleeping.
26+
// Headers needed for sleeping and console management (wait for a keypress)
2627
#ifdef _WIN32
2728
#include <windows.h>
29+
#include <conio.h>
2830
#else
31+
#include <fcntl.h>
32+
#include <termios.h>
2933
#include <unistd.h>
3034
#endif
3135

@@ -50,8 +54,46 @@
5054
#if defined(USING_HIDAPI_LIBUSB) && HID_API_VERSION >= HID_API_MAKE_VERSION(0, 12, 0)
5155
#include <hidapi_libusb.h>
5256
#endif
57+
5358
//
59+
// A function that waits for a key to be pressed and reports it's code
60+
// Used for immediate response in interactive subroutines
61+
// Taken from:
62+
// https://cboard.cprogramming.com/c-programming/63166-kbhit-linux.html
63+
int waitkey(void)
64+
{
65+
#ifdef _WIN32
66+
return _getch();
67+
#else
68+
struct termios oldt, newt;
69+
int ch;
70+
int oldf;
71+
72+
tcgetattr(STDIN_FILENO, &oldt);
73+
newt = oldt;
74+
newt.c_lflag &= ~(ICANON | ECHO);
75+
tcsetattr(STDIN_FILENO, TCSANOW, &newt);
76+
oldf = fcntl(STDIN_FILENO, F_GETFL, 0);
77+
fcntl(STDIN_FILENO, F_SETFL, oldf | O_NONBLOCK);
78+
79+
do
80+
{
81+
usleep(1);
82+
ch = getchar();
83+
}
84+
while (EOF == ch);
85+
86+
tcsetattr(STDIN_FILENO, TCSANOW, &oldt);
87+
fcntl(STDIN_FILENO, F_SETFL, oldf);
5488

89+
return ch;
90+
#endif
91+
}
92+
93+
//
94+
95+
//
96+
// Report Device info
5597
const char *hid_bus_name(hid_bus_type bus_type) {
5698
static const char *const HidBusTypeName[] = {
5799
"Unknown",
@@ -126,80 +168,28 @@ void print_devices_with_descriptor(struct hid_device_info *cur_dev) {
126168
}
127169
}
128170

129-
int device_callback(
130-
hid_hotplug_callback_handle callback_handle,
131-
struct hid_device_info* device,
132-
hid_hotplug_event event,
133-
void* user_data)
171+
//
172+
// Default static testing
173+
void test_static(void)
134174
{
135-
(void)user_data;
136-
137-
if (event & HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED)
138-
printf("Handle %d: New device is connected: %s.\n", callback_handle, device->path);
139-
else
140-
printf("Handle %d: Device was disconnected: %s.\n", callback_handle, device->path);
141-
142-
printf("type: %04hx %04hx\n serial_number: %ls", device->vendor_id, device->product_id, device->serial_number);
143-
printf("\n");
144-
printf(" Manufacturer: %ls\n", device->manufacturer_string);
145-
printf(" Product: %ls\n", device->product_string);
146-
printf(" Release: %hx\n", device->release_number);
147-
printf(" Interface: %d\n", device->interface_number);
148-
printf(" Usage (page): 0x%hx (0x%hx)\n", device->usage, device->usage_page);
149-
printf("\n");
150-
151-
/* Printed data might not show on the screen - force it out */
152-
fflush(stdout);
175+
struct hid_device_info *devs;
153176

154-
return 0;
177+
devs = hid_enumerate(0x0, 0x0);
178+
print_devices_with_descriptor(devs);
179+
hid_free_enumeration(devs);
155180
}
156181

157-
int main(int argc, char* argv[])
158-
{
159-
(void)argc;
160-
(void)argv;
161182

183+
//
184+
// Fixed device testing
185+
void test_device(void)
186+
{
162187
int res;
163188
unsigned char buf[256];
164-
#define MAX_STR 255
189+
#define MAX_STR 255
165190
wchar_t wstr[MAX_STR];
166191
hid_device *handle;
167192
int i;
168-
hid_hotplug_callback_handle token1, token2;
169-
170-
struct hid_device_info *devs;
171-
172-
printf("hidapi test/example tool. Compiled with hidapi version %s, runtime version %s.\n", HID_API_VERSION_STR, hid_version_str());
173-
if (HID_API_VERSION == HID_API_MAKE_VERSION(hid_version()->major, hid_version()->minor, hid_version()->patch)) {
174-
printf("Compile-time version matches runtime version of hidapi.\n\n");
175-
}
176-
else {
177-
printf("Compile-time version is different than runtime version of hidapi.\n]n");
178-
}
179-
180-
if (hid_init())
181-
return -1;
182-
183-
#if defined(__APPLE__) && HID_API_VERSION >= HID_API_MAKE_VERSION(0, 12, 0)
184-
// To work properly needs to be called before hid_open/hid_open_path after hid_init.
185-
// Best/recommended option - call it right after hid_init.
186-
hid_darwin_set_open_exclusive(0);
187-
#endif
188-
189-
devs = hid_enumerate(0x0, 0x0);
190-
print_devices_with_descriptor(devs);
191-
hid_free_enumeration(devs);
192-
193-
hid_hotplug_register_callback(0, 0, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED | HID_API_HOTPLUG_EVENT_DEVICE_LEFT, HID_API_HOTPLUG_ENUMERATE, device_callback, NULL, &token1);
194-
hid_hotplug_register_callback(0x054c, 0x0ce6, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED | HID_API_HOTPLUG_EVENT_DEVICE_LEFT, HID_API_HOTPLUG_ENUMERATE, device_callback, NULL, &token2);
195-
196-
while (1)
197-
{
198-
199-
}
200-
201-
hid_hotplug_deregister_callback(token2);
202-
hid_hotplug_deregister_callback(token1);
203193

204194
// Set up the command buffer.
205195
memset(buf,0x00,sizeof(buf));
@@ -213,8 +203,7 @@ int main(int argc, char* argv[])
213203
handle = hid_open(0x4d8, 0x3f, NULL);
214204
if (!handle) {
215205
printf("unable to open device\n");
216-
hid_exit();
217-
return 1;
206+
return;
218207
}
219208

220209
// Read the Manufacturer String
@@ -344,13 +333,221 @@ int main(int argc, char* argv[])
344333
}
345334

346335
hid_close(handle);
336+
}
347337

348-
/* Free static HIDAPI objects. */
349-
hid_exit();
338+
//
339+
// Normal hotplug testing
340+
int device_callback(
341+
hid_hotplug_callback_handle callback_handle,
342+
struct hid_device_info* device,
343+
hid_hotplug_event event,
344+
void* user_data)
345+
{
346+
(void)user_data;
350347

351-
#ifdef _WIN32
352-
system("pause");
348+
if (event & HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED)
349+
printf("Handle %d: New device is connected: %s.\n", callback_handle, device->path);
350+
else
351+
printf("Handle %d: Device was disconnected: %s.\n", callback_handle, device->path);
352+
353+
printf("type: %04hx %04hx\n serial_number: %ls", device->vendor_id, device->product_id, device->serial_number);
354+
printf("\n");
355+
printf(" Manufacturer: %ls\n", device->manufacturer_string);
356+
printf(" Product: %ls\n", device->product_string);
357+
printf(" Release: %hx\n", device->release_number);
358+
printf(" Interface: %d\n", device->interface_number);
359+
printf(" Usage (page): 0x%hx (0x%hx)\n", device->usage, device->usage_page);
360+
printf("(Press Q to exit the test)\n");
361+
printf("\n");
362+
363+
return 0;
364+
}
365+
366+
367+
void test_hotplug(void)
368+
{
369+
printf("Starting the Hotplug test\n");
370+
printf("(Press Q to exit the test)\n");
371+
372+
hid_hotplug_callback_handle token1, token2;
373+
374+
hid_hotplug_register_callback(0, 0, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED | HID_API_HOTPLUG_EVENT_DEVICE_LEFT, HID_API_HOTPLUG_ENUMERATE, device_callback, NULL, &token1);
375+
hid_hotplug_register_callback(0x054c, 0x0ce6, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED | HID_API_HOTPLUG_EVENT_DEVICE_LEFT, HID_API_HOTPLUG_ENUMERATE, device_callback, NULL, &token2);
376+
377+
while (1)
378+
{
379+
int command = tolower(waitkey());
380+
if ('q' == command)
381+
{
382+
break;
383+
}
384+
}
385+
386+
hid_hotplug_deregister_callback(token2);
387+
hid_hotplug_deregister_callback(token1);
388+
389+
printf("\n\nHotplug test stopped\n");
390+
}
391+
392+
//
393+
// Stress-testing weird edge cases in hotplugs
394+
int cb1_handle;
395+
int cb2_handle;
396+
int cb_test1_triggered;
397+
398+
int cb2_func(hid_hotplug_callback_handle callback_handle,
399+
struct hid_device_info *device,
400+
hid_hotplug_event event,
401+
void *user_data)
402+
{
403+
(void) callback_handle;
404+
(void) device;
405+
(void) event;
406+
(void) user_data;
407+
// TIP: only perform the test once
408+
if(cb_test1_triggered)
409+
{
410+
return 1;
411+
}
412+
413+
printf("Callback 2 fired\n");
414+
415+
// Deregister the first callback
416+
// It should be placed in the list at an index prior to the current one, which will make the pointer to the current one invalid on some implementations
417+
hid_hotplug_deregister_callback(cb1_handle);
418+
419+
cb_test1_triggered = 1;
420+
421+
// As long as we are inside this callback, nothing goes wrong; however, returning from here will cause a use-after-free error on flawed implementations
422+
// as to retrieve the next element (or to check for it's presence) it will look those dereference a pointer located in an already freed area
423+
// Undefined behavior
424+
return 1;
425+
}
426+
427+
int cb1_func(hid_hotplug_callback_handle callback_handle,
428+
struct hid_device_info *device,
429+
hid_hotplug_event event,
430+
void *user_data)
431+
{
432+
(void) callback_handle;
433+
(void) device;
434+
(void) event;
435+
(void) user_data;
436+
437+
// TIP: only perform the test once
438+
if(cb_test1_triggered)
439+
{
440+
return 1;
441+
}
442+
443+
printf("Callback 1 fired\n");
444+
445+
// Register the second callback and make it be called immediately by enumeration attempt
446+
// Will cause a deadlock on Linux immediately
447+
hid_hotplug_register_callback(0, 0, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED | HID_API_HOTPLUG_EVENT_DEVICE_LEFT, HID_API_HOTPLUG_ENUMERATE, cb2_func, NULL, &cb2_handle);
448+
return 1;
449+
}
450+
451+
void test_hotplug_deadlocks(void)
452+
{
453+
cb_test1_triggered = 0;
454+
printf("Starting the Hotplug callbacks deadlocks test\n");
455+
printf("TIP: if you don't see a message that it succeeded, it means the test failed and the system is now deadlocked\n");
456+
// Register the first callback and make it be called immediately by enumeration attempt (if at least 1 device is present)
457+
hid_hotplug_register_callback(0, 0, HID_API_HOTPLUG_EVENT_DEVICE_ARRIVED | HID_API_HOTPLUG_EVENT_DEVICE_LEFT, HID_API_HOTPLUG_ENUMERATE, cb1_func, NULL, &cb1_handle);
458+
459+
printf("Test finished successfully (at least no deadlocks were found)\n");
460+
461+
// Intentionally leave a callback registered to test how hid_exit handles it
462+
//hid_hotplug_deregister_callback(cb2_handle);
463+
}
464+
465+
466+
//
467+
// CLI
468+
469+
void print_version_check(void)
470+
{
471+
printf("hidapi test/example tool. Compiled with hidapi version %s, runtime version %s.\n", HID_API_VERSION_STR, hid_version_str());
472+
if (HID_API_VERSION == HID_API_MAKE_VERSION(hid_version()->major, hid_version()->minor, hid_version()->patch)) {
473+
printf("Compile-time version matches runtime version of hidapi.\n\n");
474+
}
475+
else {
476+
printf("Compile-time version is different than runtime version of hidapi.\n]n");
477+
}
478+
}
479+
480+
void interactive_loop(void)
481+
{
482+
int command = 0;
483+
484+
print_version_check();
485+
486+
do {
487+
printf("Interactive HIDAPI testing utility\n");
488+
printf(" 1: List connected devices\n");
489+
printf(" 2: Dynamic hotplug test\n");
490+
printf(" 3: Test specific device [04d8:003f]\n");
491+
printf(" 4: Test hotplug callback management deadlocking scenario\n");
492+
printf(" Q: Quit\n");
493+
printf("Please enter command:");
494+
495+
/* Printed data might not show on the screen when the command is done - force it out */
496+
fflush(stdout);
497+
498+
command = toupper(waitkey());
499+
500+
printf("%c\n\n========================================\n\n", command);
501+
502+
// GET COMMAND
503+
switch (command) {
504+
case '1':
505+
test_static();
506+
break;
507+
case '2':
508+
test_hotplug();
509+
break;
510+
case '3':
511+
test_device();
512+
break;
513+
case '4':
514+
test_hotplug_deadlocks();
515+
break;
516+
case 'Q':
517+
printf("Quitting.\n");
518+
return;
519+
default:
520+
printf("Command not recognized\n");
521+
break;
522+
}
523+
524+
/* Printed data might not show on the screen when the command is done - force it out */
525+
fflush(stdout);
526+
527+
printf("\n\n========================================\n\n");
528+
} while(command != 'Q');
529+
}
530+
531+
//
532+
// Main
533+
int main(int argc, char* argv[])
534+
{
535+
(void)argc;
536+
(void)argv;
537+
538+
if (hid_init())
539+
return -1;
540+
541+
#if defined(__APPLE__) && HID_API_VERSION >= HID_API_MAKE_VERSION(0, 12, 0)
542+
// To work properly needs to be called before hid_open/hid_open_path after hid_init.
543+
// Best/recommended option - call it right after hid_init.
544+
hid_darwin_set_open_exclusive(0);
353545
#endif
354546

547+
interactive_loop();
548+
549+
/* Free static HIDAPI objects. */
550+
hid_exit();
551+
355552
return 0;
356553
}

0 commit comments

Comments
 (0)