Skip to content

Commit d9e1e5b

Browse files
committed
Address review: mac callback thread-safety + single-allocation ring
1: Drop register_device_error() from mac/hid.c hid_report_callback (races with user-thread hid_error() per the hidapi wiki Multi-threading Notes). Bundled cleanup of three stale linked-list/report-list comments that predate the ring buffer. 2: Coalesce the ring's two allocations into a single block with a three-pointer model — r->storage owns the allocation; r->lengths and r->data are non-owning views into the lengths region and slot-data region respectively. One calloc, overflow guards on both factors and the sum, push/pop_into index via r->data. Tests: 12 CHECKs added to assert all three pointers null post-destroy. Composite && assertions split for diagnostic precision.
1 parent c0fe4eb commit d9e1e5b

4 files changed

Lines changed: 70 additions & 42 deletions

File tree

core/hidapi_input_ring.h

Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,12 @@
33
communication with HID devices.
44
55
Internal ring buffer — a bounded FIFO of HID input reports.
6-
Storage is a single pre-allocated flat byte buffer of
7-
(capacity × slot_size) bytes, where slot_size is a backend-specific
8-
runtime upper bound on input report size, determined at open time.
6+
Storage is a single pre-allocated allocation owned by r->storage,
7+
holding a lengths array (capacity × sizeof(size_t) bytes) followed
8+
by a slot data region (capacity × slot_size bytes). r->lengths and
9+
r->data are non-owning views into that allocation.
10+
slot_size is a backend-specific runtime upper bound on input
11+
report size, determined at open time.
912
Drop-oldest-when-full semantics.
1013
1114
All helpers are defined as `static`; each translation unit
@@ -55,8 +58,9 @@
5558
#include <string.h>
5659

5760
struct hidapi_input_ring {
58-
uint8_t *storage; /* capacity × slot_size bytes; one allocation */
59-
size_t *lengths; /* actual len of each queued report; capacity entries */
61+
uint8_t *storage; /* owns the single allocation */
62+
size_t *lengths; /* non-owning view: lengths region (capacity × sizeof(size_t)) */
63+
uint8_t *data; /* non-owning view: slot data region (capacity × slot_size) */
6064
int capacity; /* slot count; changed only by resize() */
6165
size_t slot_size; /* bytes per slot; fixed at init */
6266
int head; /* oldest report index (dequeue side) */
@@ -76,19 +80,24 @@ static int hidapi_input_ring_init(struct hidapi_input_ring *r,
7680
return -1;
7781
if (r->storage) /* double-init guard */
7882
return -1;
79-
/* Reject capacity × slot_size that would overflow size_t. */
83+
/* Reject overflow on lengths_bytes + data_bytes. */
84+
if ((size_t)capacity > SIZE_MAX / sizeof(size_t))
85+
return -1;
8086
if ((size_t)capacity > SIZE_MAX / slot_size)
8187
return -1;
88+
size_t lengths_bytes = (size_t)capacity * sizeof(size_t);
89+
size_t data_bytes = (size_t)capacity * slot_size;
90+
if (lengths_bytes > SIZE_MAX - data_bytes)
91+
return -1;
8292

83-
r->storage = (uint8_t *)malloc((size_t)capacity * slot_size);
84-
r->lengths = (size_t *)calloc((size_t)capacity, sizeof(size_t));
85-
if (!r->storage || !r->lengths) {
86-
free(r->storage);
87-
free(r->lengths);
88-
r->storage = NULL;
93+
r->storage = (uint8_t *)calloc(1, lengths_bytes + data_bytes);
94+
if (!r->storage) {
8995
r->lengths = NULL;
96+
r->data = NULL;
9097
return -1;
9198
}
99+
r->lengths = (size_t *)r->storage; /* view: lengths region at offset 0 */
100+
r->data = r->storage + lengths_bytes; /* view: data region after lengths */
92101
r->capacity = capacity;
93102
r->slot_size = slot_size;
94103
r->head = 0;
@@ -99,15 +108,15 @@ static int hidapi_input_ring_init(struct hidapi_input_ring *r,
99108
}
100109

101110
/* PRECONDITION: caller holds r's mutex, or is tearing down.
102-
* Frees storage and lengths; zeros r. Safe on zero-init or
103-
* previously-destroyed rings. */
111+
* Frees the single allocation (owned by r->storage); zeros r.
112+
* Safe on zero-init or previously-destroyed rings. */
104113
static void hidapi_input_ring_destroy(struct hidapi_input_ring *r)
105114
{
106115
if (!r) return;
107116
free(r->storage);
108-
free(r->lengths);
109117
r->storage = NULL;
110-
r->lengths = NULL;
118+
r->lengths = NULL; /* non-owning view; null for consistency */
119+
r->data = NULL; /* non-owning view; null for consistency */
111120
r->capacity = 0;
112121
r->slot_size = 0;
113122
r->head = 0;
@@ -140,7 +149,7 @@ static int hidapi_input_ring_push(struct hidapi_input_ring *r,
140149
}
141150

142151
if (len > 0) {
143-
memcpy(r->storage + (size_t)r->tail * r->slot_size, data, len);
152+
memcpy(r->data + (size_t)r->tail * r->slot_size, data, len);
144153
}
145154
r->lengths[r->tail] = len;
146155
r->tail = (r->tail + 1) % r->capacity;
@@ -182,16 +191,17 @@ static int hidapi_input_ring_pop_into(struct hidapi_input_ring *r,
182191
size_t payload_len = r->lengths[r->head];
183192
size_t to_copy = (payload_len < dst_len) ? payload_len : dst_len;
184193
if (to_copy > 0) {
185-
memcpy(dst, r->storage + (size_t)r->head * r->slot_size, to_copy);
194+
memcpy(dst, r->data + (size_t)r->head * r->slot_size, to_copy);
186195
}
187196
r->head = (r->head + 1) % r->capacity;
188197
r->count--;
189198
return (int)to_copy;
190199
}
191200

192-
/* Resize the slot array to new_cap. Allocates a new flat storage buffer
193-
* and lengths array, memcpy's surviving reports' bytes into the new
194-
* storage preserving FIFO order, then frees the old buffers.
201+
/* Resize the slot array to new_cap. Allocates a new combined block
202+
* (lengths region followed by data region), memcpy's both the surviving
203+
* lengths and their payload bytes into FIFO-ordered positions in the
204+
* new block, then frees the old block.
195205
*
196206
* On shrink below count, the oldest (count - new_cap) reports are
197207
* evicted — matching the push-time drop-oldest policy.
@@ -208,28 +218,33 @@ static int hidapi_input_ring_resize(struct hidapi_input_ring *r, int new_cap)
208218
return -1;
209219
if (new_cap == r->capacity)
210220
return 0;
211-
/* Reject new_cap × slot_size that would overflow size_t. */
221+
/* Reject overflow on the new combined block size. */
222+
if ((size_t)new_cap > SIZE_MAX / sizeof(size_t))
223+
return -1;
212224
if ((size_t)new_cap > SIZE_MAX / r->slot_size)
213225
return -1;
226+
size_t new_lengths_bytes = (size_t)new_cap * sizeof(size_t);
227+
size_t new_data_bytes = (size_t)new_cap * r->slot_size;
228+
if (new_lengths_bytes > SIZE_MAX - new_data_bytes)
229+
return -1;
214230

215-
uint8_t *new_storage = (uint8_t *)malloc((size_t)new_cap * r->slot_size);
216-
size_t *new_lengths = (size_t *)calloc((size_t)new_cap, sizeof(size_t));
217-
if (!new_storage || !new_lengths) {
218-
free(new_storage);
219-
free(new_lengths);
231+
uint8_t *new_storage = (uint8_t *)calloc(1, new_lengths_bytes + new_data_bytes);
232+
if (!new_storage) {
220233
return -1;
221234
}
235+
size_t *new_lengths = (size_t *)new_storage;
236+
uint8_t *new_data = new_storage + new_lengths_bytes;
222237

223238
int keep = (r->count > new_cap) ? new_cap : r->count;
224239
int dropped = r->count - keep;
225240

226-
/* Copy surviving reports' payload bytes into new storage, packed
241+
/* Copy surviving reports' payload bytes into new data region, packed
227242
* starting at slot 0. Oldest dropped entries are simply not copied. */
228243
int src = (r->head + dropped) % r->capacity;
229244
for (int dst = 0; dst < keep; dst++) {
230245
if (r->lengths[src] > 0) {
231-
memcpy(new_storage + (size_t)dst * r->slot_size,
232-
r->storage + (size_t)src * r->slot_size,
246+
memcpy(new_data + (size_t)dst * r->slot_size,
247+
r->data + (size_t)src * r->slot_size,
233248
r->lengths[src]);
234249
}
235250
new_lengths[dst] = r->lengths[src];
@@ -238,11 +253,11 @@ static int hidapi_input_ring_resize(struct hidapi_input_ring *r, int new_cap)
238253

239254
r->dropped += dropped;
240255

241-
free(r->storage);
242-
free(r->lengths);
256+
free(r->storage); /* frees the entire old block */
243257

244258
r->storage = new_storage;
245259
r->lengths = new_lengths;
260+
r->data = new_data;
246261
r->capacity = new_cap;
247262
r->head = 0;
248263
r->count = keep;

libusb/hid.c

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,10 @@ struct hid_device_ {
113113
int transfer_loop_finished;
114114
struct libusb_transfer *transfer;
115115

116-
/* Input report ring buffer. Flat inline-slot storage sized at
117-
(dev->num_input_buffers × max(wMaxPacketSize, 64)) bytes,
118-
allocated in hidapi_initialize_device() once the interrupt-IN
116+
/* Input report ring buffer. Single allocation owned by
117+
input_ring.storage, holding the lengths region followed by
118+
(dev->num_input_buffers × max(wMaxPacketSize, 64)) bytes of slot
119+
data, made in hidapi_initialize_device() once the interrupt-IN
119120
endpoint is known. The max(., 64) clamp is a defensive fallback
120121
for devices with no interrupt-IN endpoint. Resized under the
121122
device mutex by hid_set_num_input_buffers(). */

mac/hid.c

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ static int pthread_barrier_wait(pthread_barrier_t *barrier)
102102
}
103103

104104

105-
/* Linked List of input reports received from the device. */
105+
/* Input report ring state and supporting declarations. */
106106

107107
static struct hid_api_version api_version = {
108108
.major = HID_API_VERSION_MAJOR,
@@ -856,8 +856,8 @@ static void hid_device_removal_callback(void *context, IOReturn result,
856856
}
857857

858858
/* The Run Loop calls this function for each input report received.
859-
This function puts the data into a linked list to be picked up by
860-
hid_read(). */
859+
This function pushes the data into the input report ring buffer
860+
to be picked up by hid_read(). */
861861
static void hid_report_callback(void *context, IOReturn result, void *sender,
862862
IOHIDReportType report_type, uint32_t report_id,
863863
uint8_t *report, CFIndex report_length)
@@ -877,7 +877,9 @@ static void hid_report_callback(void *context, IOReturn result, void *sender,
877877
if (push_rc == 0) {
878878
pthread_cond_signal(&dev->condition);
879879
} else {
880-
register_device_error(dev, "input queue allocation failed");
880+
/* Push rejected. Do not call register_device_error() from the
881+
* callback thread as it would race with user-thread hid_error().
882+
* Drop silently. */
881883
}
882884

883885
pthread_mutex_unlock(&dev->mutex);
@@ -1225,7 +1227,7 @@ int HID_API_EXPORT hid_read_timeout(hid_device *dev, unsigned char *data, size_t
12251227

12261228
register_error_str(&dev->last_read_error_str, NULL);
12271229

1228-
/* Lock the access to the report list. */
1230+
/* Lock the access to the input report ring. */
12291231
pthread_mutex_lock(&dev->mutex);
12301232

12311233
/* There's an input report queued up. Return it. */

tests/test_hidapi_input_ring.c

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ static int test_init_destroy(void) {
2222
CHECK(r.capacity == 4 && r.slot_size == SLOT_SZ && r.count == 0);
2323
CHECK(r.head == 0 && r.tail == 0);
2424
hidapi_input_ring_destroy(&r);
25-
CHECK(r.storage == NULL && r.capacity == 0 && r.slot_size == 0);
25+
CHECK(r.storage == NULL);
26+
CHECK(r.lengths == NULL);
27+
CHECK(r.data == NULL);
28+
CHECK(r.capacity == 0);
29+
CHECK(r.slot_size == 0);
2630
return 0;
2731
}
2832

@@ -43,13 +47,18 @@ static int test_destroy_idempotent(void) {
4347

4448
struct hidapi_input_ring zero = {0};
4549
hidapi_input_ring_destroy(&zero);
46-
CHECK(zero.storage == NULL && zero.capacity == 0);
50+
CHECK(zero.storage == NULL);
51+
CHECK(zero.lengths == NULL);
52+
CHECK(zero.data == NULL);
53+
CHECK(zero.capacity == 0);
4754

4855
struct hidapi_input_ring r = {0};
4956
CHECK(hidapi_input_ring_init(&r, 4, SLOT_SZ) == 0);
5057
hidapi_input_ring_destroy(&r);
5158
hidapi_input_ring_destroy(&r);
5259
CHECK(r.storage == NULL);
60+
CHECK(r.lengths == NULL);
61+
CHECK(r.data == NULL);
5362
return 0;
5463
}
5564

@@ -107,6 +116,7 @@ static int test_init_overflow_reject(void) {
107116
/* r left untouched on failure — all fields still zero-initialized. */
108117
CHECK(r.storage == NULL);
109118
CHECK(r.lengths == NULL);
119+
CHECK(r.data == NULL);
110120
CHECK(r.capacity == 0);
111121
CHECK(r.slot_size == 0);
112122
CHECK(r.count == 0 && r.head == 0 && r.tail == 0 && r.dropped == 0);

0 commit comments

Comments
 (0)