Skip to content

Commit ede7069

Browse files
committed
darwin: fix use after free during reenumeration
When one process has two contexts to libusb, it may trigger a reenumeration while the other context accesses `*dpriv->device`. Adding a mutex to access `device` can be costly because it is used in every function. Instead, we maintain an invariant: the `darwin_cached_device` must be valid at all times. In particular, this means the fields `device` and `service` must not be stale or we will have a use after free triggerable through a race condition. Previously, in `darwin_devices_detached` during reenumeration, it was possible for `old_device->device` to be freed. We now defer this free to `darwin_get_cached_device` after a new `device` is written. Note that the order is important because we do not have locks, so we must free the old device after writing the new one. The same point applies to `service` which we take care to free after the new `service` is written. A caveat here is that even if we do not crash it is still possible for one context to be using an outdated `device` and `service` leading to an error return. This should be acceptable because it would be a similar situation to the device being detached during an API call.
1 parent 7313527 commit ede7069

File tree

1 file changed

+15
-10
lines changed

1 file changed

+15
-10
lines changed

libusb/os/darwin_usb.c

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -681,12 +681,6 @@ static void darwin_devices_detached (void *ptr, io_iterator_t rem_devices) {
681681
usbi_dbg (NULL, "detected device detached due to re-enumeration. sessionID: 0x%" PRIx64
682682
", locationID: 0x%" PRIx32, session, locationID);
683683

684-
/* the device object is no longer usable so go ahead and release it */
685-
if (old_device->device) {
686-
(*old_device->device)->Release(old_device->device);
687-
old_device->device = NULL;
688-
}
689-
690684
is_reenumerating = true;
691685
} else {
692686
darwin_deref_cached_device (old_device);
@@ -1292,7 +1286,8 @@ static enum libusb_error darwin_get_cached_device(struct libusb_context *ctx, io
12921286
UInt64 sessionID = 0, parent_sessionID = 0;
12931287
UInt32 locationID = 0;
12941288
enum libusb_error ret = LIBUSB_SUCCESS;
1295-
usb_device_t device;
1289+
usb_device_t device, old_device = NULL;
1290+
io_service_t old_service = IO_OBJECT_NULL;
12961291
UInt8 port = 0;
12971292

12981293
/* assuming sessionID != 0 normally (never seen it be 0) */
@@ -1320,6 +1315,8 @@ static enum libusb_error darwin_get_cached_device(struct libusb_context *ctx, io
13201315
if (new_device->location == locationID && new_device->in_reenumerate) {
13211316
usbi_dbg (ctx, "found cached device with matching location that is being re-enumerated");
13221317
*old_session_id = new_device->session;
1318+
old_device = new_device->device;
1319+
old_service = new_device->service;
13231320
break;
13241321
}
13251322

@@ -1358,9 +1355,6 @@ static enum libusb_error darwin_get_cached_device(struct libusb_context *ctx, io
13581355
(*device)->GetLocationID (device, &new_device->location);
13591356
new_device->port = port;
13601357
new_device->parent_session = parent_sessionID;
1361-
} else {
1362-
/* release the ref to old device's service */
1363-
IOObjectRelease (new_device->service);
13641358
}
13651359

13661360
/* keep track of devices regardless of if we successfully enumerate them to
@@ -1374,6 +1368,17 @@ static enum libusb_error darwin_get_cached_device(struct libusb_context *ctx, io
13741368
/* retain the service */
13751369
IOObjectRetain (service);
13761370

1371+
/* release the old device */
1372+
if (old_device) {
1373+
(*old_device)->Release (old_device);
1374+
old_device = NULL;
1375+
}
1376+
1377+
/* release the ref to old device's service */
1378+
if (old_service) {
1379+
IOObjectRelease (old_service);
1380+
}
1381+
13771382
/* cache the device descriptor */
13781383
ret = darwin_cache_device_descriptor(ctx, new_device);
13791384
if (ret)

0 commit comments

Comments
 (0)