Skip to content

hotplug: fix callback use-after-free races#1861

Open
smaeljaish771 wants to merge 2 commits into
libusb:masterfrom
smaeljaish771:fix-hotplug-uaf-races
Open

hotplug: fix callback use-after-free races#1861
smaeljaish771 wants to merge 2 commits into
libusb:masterfrom
smaeljaish771:fix-hotplug-uaf-races

Conversation

@smaeljaish771

Copy link
Copy Markdown

usbi_hotplug_exit() freed the hotplug callback list without holding hotplug_cbs_lock, while every other path operating on that list already takes the lock.

The LIBUSB_HOTPLUG_ENUMERATE path also kept using the registered callback object after dropping the lock. If another thread deregistered and freed that callback, the enumerate loop could read freed memory. Keep the handle locally, look up the callback under the lock for each enumerated device, copy the callback data to the stack, and invoke the user callback from that copy after unlocking.

Reformulating the hotplug iteration macros for Clang thread-safety analysis is left out, as it is an independent change.

Fixes: #1859

usbi_hotplug_exit() freed the hotplug callback list without holding
hotplug_cbs_lock, while every other path operating on that list already
takes the lock.

The LIBUSB_HOTPLUG_ENUMERATE path also kept using the registered callback
object after dropping the lock. If another thread deregistered and freed
that callback, the enumerate loop could read freed memory. Keep the handle
locally, look up the callback under the lock for each enumerated device,
copy the callback data to the stack, and invoke the user callback from that
copy after unlocking.

@sonatique sonatique left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for turning this around so quickly. It looks to me that both races are correctly addressed, and I'm happy with the approach. You even went further than the patch sketched for issue B, if I understand correctly.

I noticed that with your code, calling libusb_hotplug_deregister_callback() from within an ENUMERATE callback now stops enumeration at the next device. whereas before it kept firing for the remaining devices. I think this is more correct, but maybe it worth documenting it.

Comment thread libusb/hotplug.c Outdated
int found = 0;

usbi_mutex_lock(&ctx->hotplug_cbs_lock);
for_each_hotplug_cb(ctx, hotplug_cb) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the lookup reuses the outer hotplug_cb as its iterator. It's safe (unused afterward), but maybe not explicit enough when reading the code, and maybe not super-safe against refactoring.
What about using a dedicated value?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Documented the ENUMERATE deregistration behavior and used a dedicated iterator.

@mcuee mcuee added bug core Related to common codes labels Jun 28, 2026
@sonatique

Copy link
Copy Markdown
Member

Thanks @smaeljaish771 , all good to me.
Would be nice to have a second approval before merging.

@seanm

seanm commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Thanks @smaeljaish771 , all good to me. Would be nice to have a second approval before merging.

I should have time this week to look this over too...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug core Related to common codes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Two use-after-free races in libusb/hotplug.c on ctx->hotplug_cbs (unlocked free in usbi_hotplug_exit, unlocked access in the ENUMERATE loop)

5 participants