From dbb27b9ef1f93c6a21fe035aeb4753e6120ce0c1 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 21 Oct 2026 20:37:28 +0200 Subject: [PATCH 1/3] feat(libSceUsbd): handle events with FreeBSD libusb's event lock and handler thread sceUsbdLockEvents, sceUsbdTryLockEvents, sceUsbdUnlockEvents, sceUsbdLockEventWaiters, sceUsbdUnlockEventWaiters, sceUsbdWaitForEvent, sceUsbdEventHandlerActive, sceUsbdHandleEvents and sceUsbdHandleEventsLocked follow FreeBSD 11.4 libusb10_io.c: one lock for events and their waiters, one handler thread that releases the lock while the host libusb handles events, and waiters that are woken when the handler unlocks and take over handling when there is none. sceUsbdEventHandlingOk now reports whether the calling thread is the handler, and sceUsbdHandleEventsTimeout takes the same path. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceUsbd/Export.cpp | 173 +++++++++++++++++++++++----- core/libs/tests/GuestUsbd.cpp | 86 +++++++++++++- docs/dev/debt/libSceUsbd.md | 9 +- 3 files changed, 234 insertions(+), 34 deletions(-) diff --git a/core/libs/prx/libSceUsbd/Export.cpp b/core/libs/prx/libSceUsbd/Export.cpp index edc6a19de005..579df9557c9d 100644 --- a/core/libs/prx/libSceUsbd/Export.cpp +++ b/core/libs/prx/libSceUsbd/Export.cpp @@ -7,8 +7,10 @@ #include #include +#include #include #include +#include #include #include #include @@ -104,6 +106,108 @@ void DropDeviceReference(UsbdState& state, libusb_device* device) { if (--found->second == 0) state.devices.erase(found); } +struct UsbdEvents { + std::mutex lock; + std::condition_variable changed; + std::atomic owner{}; + std::atomic handler{}; + std::atomic completions{0}; +}; + +UsbdEvents& Events() { + static auto* events = new UsbdEvents(); + return *events; +} + +bool ValidTimeout(const UsbdTimeval* timeout) { + return timeout == nullptr || (timeout->seconds >= 0 && timeout->seconds <= INT_MAX && timeout->microseconds >= 0 && timeout->microseconds < 1000000); +} + +void AcquireEvents(UsbdEvents& events, const char* function) { + const auto self = std::this_thread::get_id(); + if (events.owner.load() == self) throw std::runtime_error(std::string(function) + ": this thread already holds the event lock"); + events.lock.lock(); + events.owner.store(self); +} + +void RequireEventLock(UsbdEvents& events, const char* function) { + if (events.owner.load() != std::this_thread::get_id()) throw std::runtime_error(std::string(function) + ": this thread does not hold the event lock"); +} + +void ReleaseEvents(UsbdEvents& events, const char* function) { + RequireEventLock(events, function); + events.owner.store(std::thread::id{}); + events.lock.unlock(); +} + +void LockEventsAndClaim(UsbdEvents& events, const char* function) { + AcquireEvents(events, function); + if (events.handler.load() == std::thread::id{}) events.handler.store(std::this_thread::get_id()); +} + +void UnlockEventsAndRelease(UsbdEvents& events, const char* function) { + RequireEventLock(events, function); + if (events.handler.load() == std::this_thread::get_id()) { + events.handler.store(std::thread::id{}); + events.changed.notify_all(); + } + ReleaseEvents(events, function); +} + +int WaitForChange(UsbdEvents& events, const UsbdTimeval* timeout) { + const auto self = std::this_thread::get_id(); + std::unique_lock guard(events.lock, std::adopt_lock); + events.owner.store(std::thread::id{}); + bool timedOut = false; + if (timeout == nullptr) { + events.changed.wait(guard); + } else { + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(timeout->seconds) + std::chrono::microseconds(timeout->microseconds); + timedOut = events.changed.wait_until(guard, deadline) == std::cv_status::timeout; + } + guard.release(); + events.owner.store(self); + if (events.handler.load() == std::thread::id{}) events.handler.store(self); + return timedOut ? 1 : 0; +} + +libusb_context* CurrentContext(); + +int HandleAsHandler(UsbdEvents& events, const UsbdTimeval* timeout) { + const auto self = std::this_thread::get_id(); + const auto before = events.completions.load(); + events.owner.store(std::thread::id{}); + events.lock.unlock(); + libusb_context* context = CurrentContext(); + int result = 0; + if (timeout != nullptr && context == nullptr) { + std::this_thread::sleep_for(std::chrono::seconds(timeout->seconds) + std::chrono::microseconds(timeout->microseconds)); + } else if (timeout != nullptr) { + timeval host{}; + host.tv_sec = static_cast(timeout->seconds); + host.tv_usec = static_cast(timeout->microseconds); + result = libusb_handle_events_timeout_completed(context, &host, nullptr); + } else { + while (result == 0 && events.completions.load() == before) { + if (context == nullptr) { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + } else { + timeval slice{}; + slice.tv_sec = 1; + result = libusb_handle_events_timeout_completed(context, &slice, nullptr); + } + } + } + events.lock.lock(); + events.owner.store(self); + return result; +} + +std::int32_t HandleEventsLocked(UsbdEvents& events, const UsbdTimeval* timeout) { + if (events.handler.load() == std::this_thread::get_id()) return ToGuestError(HandleAsHandler(events, timeout)); + return WaitForChange(events, timeout) != 0 ? ToGuestError(LIBUSB_ERROR_TIMEOUT) : 0; +} + void LIBUSB_CALL CompleteTransfer(libusb_transfer* host) { auto* guest = static_cast(host->user_data); guest->status = host->status; @@ -118,9 +222,16 @@ void LIBUSB_CALL CompleteTransfer(libusb_transfer* host) { state.inFlight.erase(guest); } libusb_free_transfer(host); + Events().completions.fetch_add(1); if (guest->callback != nullptr) guest->callback(guest); } +libusb_context* CurrentContext() { + auto& state = State(); + std::lock_guard lock(state.mutex); + return state.context; +} + int MaxPacketSize(libusb_device* device, std::uint8_t endpoint) { libusb_config_descriptor* config = nullptr; const int result = libusb_get_active_config_descriptor(device, &config); @@ -215,20 +326,12 @@ void APS5_VABI sceUsbdFreeDeviceList(void** list, std::int32_t unrefDevices) { std::int32_t APS5_VABI sceUsbdHandleEventsTimeout(const UsbdTimeval* timeout) { if (timeout == nullptr || timeout->seconds < 0 || timeout->microseconds < 0 || timeout->microseconds >= 1000000) return SCE_USBD_ERROR_INVALID_ARG; - libusb_context* context; - { - auto& state = State(); - std::lock_guard lock(state.mutex); - context = state.context; - } - if (context == nullptr) { - std::this_thread::sleep_for(std::chrono::seconds(timeout->seconds) + std::chrono::microseconds(timeout->microseconds)); - return 0; - } - timeval host{}; - host.tv_sec = static_cast(timeout->seconds); - host.tv_usec = static_cast(timeout->microseconds); - return ToGuestError(libusb_handle_events_timeout_completed(context, &host, nullptr)); + if (!ValidTimeout(timeout)) throw std::runtime_error(std::string(__func__) + ": a timeout of more than INT_MAX seconds is not supported"); + auto& events = Events(); + LockEventsAndClaim(events, __func__); + const std::int32_t result = HandleEventsLocked(events, timeout); + UnlockEventsAndRelease(events, __func__); + return result; } UsbdTransfer* APS5_VABI sceUsbdAllocTransfer(std::int32_t isoPackets) { @@ -358,7 +461,7 @@ std::int32_t APS5_VABI sceUsbdCancelTransfer(UsbdTransfer* transfer) { } std::int32_t APS5_VABI sceUsbdEventHandlingOk() { - return 1; + return Events().handler.load() == std::this_thread::get_id() ? 1 : 0; } void APS5_VABI sceUsbdFreeConfigDescriptor(void* config) { @@ -572,8 +675,7 @@ std::int32_t APS5_VABI sceUsbdDetachKernelDriver(void* deviceHandle, int interfa } std::int32_t APS5_VABI sceUsbdEventHandlerActive() { - NotImplemented_nid_no_patch(__func__); - return 0; + return Events().handler.load() != std::thread::id{} ? 1 : 0; } std::int32_t APS5_VABI sceUsbdGetConfigDescriptorByValue(void* device, std::uint8_t configurationValue, void** config) { @@ -661,21 +763,26 @@ std::int32_t APS5_VABI sceUsbdGetStringDescriptorAscii(void* deviceHandle, std:: } std::int32_t APS5_VABI sceUsbdHandleEvents() { - NotImplemented_nid_no_patch(__func__); - return 0; + auto& events = Events(); + LockEventsAndClaim(events, __func__); + const std::int32_t result = HandleEventsLocked(events, nullptr); + UnlockEventsAndRelease(events, __func__); + return result; } std::int32_t APS5_VABI sceUsbdHandleEventsLocked(UsbdTimeval* timeout) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (!ValidTimeout(timeout)) throw std::runtime_error(std::string(__func__) + ": a negative or oversized timeout has no known result"); + auto& events = Events(); + RequireEventLock(events, __func__); + return HandleEventsLocked(events, timeout); } void APS5_VABI sceUsbdLockEventWaiters() { - NotImplemented_nid_no_patch(__func__); + AcquireEvents(Events(), __func__); } void APS5_VABI sceUsbdLockEvents() { - NotImplemented_nid_no_patch(__func__); + LockEventsAndClaim(Events(), __func__); } void* APS5_VABI sceUsbdOpenDeviceWithVidPid(std::uint16_t vendorId, std::uint16_t productId) { @@ -715,21 +822,31 @@ std::int32_t APS5_VABI sceUsbdSetInterfaceAltSetting(void* deviceHandle, int int } std::int32_t APS5_VABI sceUsbdTryLockEvents() { - NotImplemented_nid_no_patch(__func__); + auto& events = Events(); + const auto self = std::this_thread::get_id(); + if (events.owner.load() == self || !events.lock.try_lock()) return 1; + if (events.handler.load() != std::thread::id{}) { + events.lock.unlock(); + return 1; + } + events.owner.store(self); + events.handler.store(self); return 0; } void APS5_VABI sceUsbdUnlockEventWaiters() { - NotImplemented_nid_no_patch(__func__); + ReleaseEvents(Events(), __func__); } void APS5_VABI sceUsbdUnlockEvents() { - NotImplemented_nid_no_patch(__func__); + UnlockEventsAndRelease(Events(), __func__); } std::int32_t APS5_VABI sceUsbdWaitForEvent(UsbdTimeval* timeout) { - NotImplemented_nid_no_patch(__func__); - return 0; + if (!ValidTimeout(timeout)) throw std::runtime_error(std::string(__func__) + ": a negative or oversized timeout has no known result"); + auto& events = Events(); + RequireEventLock(events, __func__); + return WaitForChange(events, timeout); } } diff --git a/core/libs/tests/GuestUsbd.cpp b/core/libs/tests/GuestUsbd.cpp index 0e8f5ecee60c..db6d0ce42c11 100644 --- a/core/libs/tests/GuestUsbd.cpp +++ b/core/libs/tests/GuestUsbd.cpp @@ -23,6 +23,7 @@ constexpr std::int32_t noDevice = static_cast(0x80240004); constexpr std::int32_t notFound = static_cast(0x80240005); constexpr std::int32_t otherError = static_cast(0x802400FF); constexpr std::uint8_t absentEndpoint = 0x70; +constexpr std::int32_t timedOut = static_cast(0x80240007); void Require(bool condition, const char* message) { if (!condition) { @@ -50,6 +51,74 @@ void APS5_VABI MarkDoneThenSignal(UsbdTransfer* transfer) { notified->store(true, std::memory_order_release); } +void EventLocks() { + Require(sceUsbdEventHandlerActive() == 0 && sceUsbdEventHandlingOk() == 0, "an event handler was active before any lock"); + sceUsbdLockEvents(); + Require(sceUsbdEventHandlingOk() == 1 && sceUsbdEventHandlerActive() == 1, "the thread that locked events is not the handler"); + Require(sceUsbdTryLockEvents() == 1, "the holder of the event lock took it again"); + Require(Throws([] { sceUsbdLockEvents(); }), "one thread took the event lock twice"); + Require(Throws([] { sceUsbdLockEventWaiters(); }), "the holder of the event lock took it again as a waiter"); + std::thread([] { + Require(sceUsbdTryLockEvents() == 1, "the event lock was taken while another thread held it"); + Require(sceUsbdEventHandlingOk() == 0 && sceUsbdEventHandlerActive() == 1, "a thread without the lock saw itself as the handler"); + }).join(); + sceUsbdUnlockEvents(); + Require(sceUsbdEventHandlerActive() == 0 && sceUsbdEventHandlingOk() == 0, "the handler stayed active after unlocking events"); + UsbdTimeval brief{0, 20000}; + Require(Throws([] { sceUsbdUnlockEvents(); }) && Throws([] { sceUsbdUnlockEventWaiters(); }), "an event lock that was not held was released"); + Require(Throws([&] { sceUsbdWaitForEvent(&brief); }) && Throws([&] { sceUsbdHandleEventsLocked(&brief); }), "waited or handled events without the event lock"); + Require(sceUsbdTryLockEvents() == 0 && sceUsbdEventHandlingOk() == 1, "a free event lock was not taken"); + UsbdTimeval negative{-1, 0}; + Require(Throws([&] { sceUsbdHandleEventsLocked(&negative); }) && Throws([&] { sceUsbdWaitForEvent(&negative); }), "a negative timeout was accepted"); + const auto start = std::chrono::steady_clock::now(); + Require(sceUsbdHandleEventsLocked(&brief) == 0, "the handler failed to handle events"); + Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(15), "the handler returned before the timeout"); + sceUsbdUnlockEvents(); + + sceUsbdLockEventWaiters(); + Require(sceUsbdEventHandlingOk() == 0, "locking the event waiters made this thread the handler"); + Require(sceUsbdWaitForEvent(&brief) == 1, "waiting without a handler did not time out"); + Require(sceUsbdEventHandlingOk() == 1, "a waiter did not take over handling after a timeout with no handler"); + sceUsbdUnlockEvents(); + Require(sceUsbdEventHandlerActive() == 0, "the waiter kept handling after unlocking events"); + + sceUsbdLockEvents(); + std::atomic waiting{false}; + std::atomic woken{-1}; + std::atomic handling{-1}; + std::thread waiter([&] { + sceUsbdLockEventWaiters(); + UsbdTimeval patience{5, 0}; + waiting.store(true); + woken.store(sceUsbdWaitForEvent(&patience)); + handling.store(sceUsbdEventHandlingOk()); + sceUsbdUnlockEvents(); + }); + while (!waiting.load()) Require(sceUsbdHandleEventsLocked(&brief) == 0, "the handler failed while a waiter queued"); + sceUsbdUnlockEvents(); + waiter.join(); + Require(woken.load() == 0 && handling.load() == 1, "a waiter was not woken and handed the events when the handler unlocked"); + Require(sceUsbdEventHandlerActive() == 0, "the handler stayed active after the woken waiter unlocked"); + + sceUsbdLockEvents(); + std::atomic entered{false}; + std::atomic waiterResult{0}; + std::thread follower([&] { + sceUsbdLockEventWaiters(); + entered.store(true); + UsbdTimeval shortWait{0, 30000}; + waiterResult.store(sceUsbdHandleEventsLocked(&shortWait)); + sceUsbdUnlockEventWaiters(); + }); + while (!entered.load()) Require(sceUsbdHandleEventsLocked(&brief) == 0, "the handler failed while another thread queued"); + UsbdTimeval longer{0, 100000}; + Require(sceUsbdHandleEventsLocked(&longer) == 0, "the handler failed while another thread waited"); + follower.join(); + Require(waiterResult.load() == timedOut, "a thread that is not the handler did not time out in sceUsbdHandleEventsLocked"); + sceUsbdUnlockEvents(); + Require(sceUsbdEventHandlerActive() == 0, "the handler stayed active at the end"); +} + void Pump(const int& done) { const UsbdTimeval slice{0, 100000}; for (int round = 0; round < 50 && done == 0; ++round) Require(sceUsbdHandleEventsTimeout(&slice) == 0, "event handling failed while a transfer was in flight"); @@ -192,6 +261,20 @@ void Device() { Require(Throws([&] { sceUsbdFreeTransfer(reinterpret_cast(transfer)); }), "an in-flight transfer was freed"); Pump(done); Require(done == 1 && transfer->status == 0 && transfer->actualLength == 18, "asynchronous control transfer did not complete"); + done = 0; + Require(sceUsbdSubmitTransfer(transfer) == 0, "resubmitting the control transfer failed"); + while (done == 0) Require(sceUsbdHandleEvents() == 0, "sceUsbdHandleEvents failed while a transfer was in flight"); + Require(transfer->status == 0 && transfer->actualLength == 18, "the control transfer did not complete through sceUsbdHandleEvents"); + done = 0; + sceUsbdLockEvents(); + Require(sceUsbdEventHandlingOk() == 1, "the thread that locked events cannot handle them"); + Require(sceUsbdSubmitTransfer(transfer) == 0, "submitting the control transfer under the event lock failed"); + { + UsbdTimeval slice{0, 100000}; + for (int round = 0; round < 50 && done == 0; ++round) Require(sceUsbdHandleEventsLocked(&slice) == 0, "sceUsbdHandleEventsLocked failed while a transfer was in flight"); + } + sceUsbdUnlockEvents(); + Require(done == 1 && transfer->status == 0 && transfer->actualLength == 18, "the control transfer did not complete through sceUsbdHandleEventsLocked"); Require(std::memcmp(setup + 8, descriptor, 18) == 0, "asynchronous device descriptor differs from the cached one"); bool broken = false; { @@ -286,6 +369,7 @@ int main(int argc, char** argv) { sceUsbdFreeDeviceList(list, 1); Require(Throws([&] { sceUsbdFreeDeviceList(list, 1); }), "a device list was freed twice"); + EventLocks(); Require(sceUsbdGetDeviceList(nullptr) == invalidArgument, "null list accepted"); Require(sceUsbdHandleEventsTimeout(nullptr) == invalidArgument, "null timeout accepted"); const UsbdTimeval invalid{0, 1000000}; @@ -376,7 +460,7 @@ int main(int argc, char** argv) { Require(sceUsbdOpenDeviceWithVidPid(0, 0) == nullptr, "a device with vendor and product 0 was opened"); sceUsbdFreeTransfer(nullptr); - Require(sceUsbdEventHandlingOk() == 1, "event handling not allowed"); + Require(sceUsbdEventHandlingOk() == 0, "a thread without the event lock may handle events"); sceUsbdFreeConfigDescriptor(nullptr); Require(Throws([] { sceUsbdFillControlSetup(nullptr, 0x80, 6, 0x0100, 0, 18); }), "null control setup buffer accepted"); diff --git a/docs/dev/debt/libSceUsbd.md b/docs/dev/debt/libSceUsbd.md index e7e535f68b69..a8c36d799158 100644 --- a/docs/dev/debt/libSceUsbd.md +++ b/docs/dev/debt/libSceUsbd.md @@ -2,14 +2,13 @@ ### Silent stubs -- [libSceUsbd](../../../core/libs/prx/libSceUsbd/Export.cpp) passes the calls through to the host with [libusb](https://github.com/libusb/libusb) built from the `3rdparty/libusb-cmake` submodule. Devices, handles and descriptors are libusb's own objects: the device, config, interface and endpoint descriptor layouts of [FreeBSD 11.4 libusb](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb.h) match libusb's field for field, which `static_assert` pins. Each one handed to the title is tracked, and a pointer this library did not return throws. Errors are libusb's codes mapped to `0x80240000 - code`, with `LIBUSB_ERROR_OTHER` at `0x802400FF`, following [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/usbd/usbd.cpp); the null arguments FreeBSD libusb rejects ([libusb10.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10.c), [libusb10_desc.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10_desc.c), [libusb10_io.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10_io.c)) return `0x80240002`, and `sceUsbdOpen` and `sceUsbdGetActiveConfigDescriptor` throw on a null device, which it dereferences. A submitted transfer is copied into a host transfer whose completion copies the status, actual length and iso packet results back and then calls the title's callback from `sceUsbdHandleEventsTimeout`; transfer flags other than short-not-ok and add-zero-packet throw. `sceUsbdAttachKernelDriver` returns 0 as FreeBSD's stub does, `sceUsbdCheckConnected` reports a device gone when reading its configuration fails with `LIBUSB_ERROR_NO_DEVICE` (FreeBSD asks the kernel directly), and `sceUsbdEventHandlingOk` returns 1. When the host has no USB access, initialization keeps the empty device list and event handling waits for its timeout. Opening, descriptors, string and control requests, interface claims and an asynchronous control transfer were checked against an Xbox 360 controller on Windows; the transfer layout, the argument lists, the error codes and isochronous transfers are unverified on PS5 - -### Unknown function info - -- [sceUsbdEventHandlerActive, sceUsbdHandleEvents, sceUsbdHandleEventsLocked, sceUsbdLockEventWaiters, sceUsbdLockEvents, sceUsbdTryLockEvents, sceUsbdUnlockEventWaiters, sceUsbdUnlockEvents, sceUsbdWaitForEvent](../../../core/libs/prx/libSceUsbd/Export.cpp) (libSceUsbd) - signatures follow the console type database in [orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json#L2268-L2322), but their behavior is unknown and each call throws; not measured on a console +- [libSceUsbd](../../../core/libs/prx/libSceUsbd/Export.cpp) passes the calls through to the host with [libusb](https://github.com/libusb/libusb) built from the `3rdparty/libusb-cmake` submodule. Devices, handles and descriptors are libusb's own objects: the device, config, interface and endpoint descriptor layouts of [FreeBSD 11.4 libusb](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb.h) match libusb's field for field, which `static_assert` pins. Each one handed to the title is tracked, and a pointer this library did not return throws. +- [libSceUsbd errors](../../../core/libs/prx/libSceUsbd/Export.cpp) (libSceUsbd) - errors are libusb's codes mapped to `0x80240000 - code`, with `LIBUSB_ERROR_OTHER` at `0x802400FF`, following [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/main/src/core/libraries/usbd/usbd.cpp); the null arguments FreeBSD libusb rejects ([libusb10.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10.c), [libusb10_desc.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10_desc.c), [libusb10_io.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10_io.c)) return `0x80240002`, and `sceUsbdOpen` and `sceUsbdGetActiveConfigDescriptor` throw on a null device, which it dereferences. +- [libSceUsbd transfers, kernel drivers and host access](../../../core/libs/prx/libSceUsbd/Export.cpp) (libSceUsbd) - a submitted transfer is copied into a host transfer whose completion copies the status, actual length and iso packet results back and then calls the title's callback from event handling; transfer flags other than short-not-ok and add-zero-packet throw. `sceUsbdAttachKernelDriver` returns 0 as FreeBSD's stub does, `sceUsbdCheckConnected` reports a device gone when reading its configuration fails with `LIBUSB_ERROR_NO_DEVICE` (FreeBSD asks the kernel directly). When the host has no USB access, initialization keeps the empty device list and event handling waits for its timeout. Opening, descriptors, string and control requests, interface claims and an asynchronous control transfer were checked against an Xbox 360 controller on Windows; the transfer layout, the argument lists, the error codes and isochronous transfers are unverified on PS5 ### Functional - [sceUsbdGetConfiguration, sceUsbdGetConfigDescriptorByValue, sceUsbdGetDescriptor, sceUsbdGetStringDescriptorAscii](../../../core/libs/prx/libSceUsbd/Export.cpp) (libSceUsbd) - not measured on a console; they follow FreeBSD 11.4 ([libusb10_desc.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10_desc.c), [libusb20.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb20.c)). The configuration and descriptor queries return host libusb errors. `sceUsbdGetStringDescriptorAscii` rebuilds `libusb20_dev_req_string_simple_sync` with only its 255-byte read: libusb's Windows HID backend reports the requested length as `bLength`, cutting a 4-byte header read to one character. Unlike the host `libusb_get_string_descriptor_ascii` it accepts index 0, drops non-printable characters and `<>` instead of writing `?`, and fails with `0x802400FF`. Closes when: measured on a PS5 - [sceUsbdGetDevice, sceUsbdGetDeviceSpeed, sceUsbdGetMaxPacketSize, sceUsbdGetMaxIsoPacketSize, sceUsbdOpenDeviceWithVidPid](../../../core/libs/prx/libSceUsbd/Export.cpp) (libSceUsbd) - not measured on a console; they follow FreeBSD 11.4 [libusb10.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10.c) over the host libusb. The packet sizes are searched in the active configuration and the iso size is scaled as FreeBSD does, not through the host's versions, which differ for an unreadable configuration and for SuperSpeed endpoints. A host SuperSpeed+ device reports SuperSpeed, the fastest speed FreeBSD 11.4 has. A null device in `sceUsbdGetMaxIsoPacketSize` throws, as FreeBSD dereferences it. A device stays known while a handle to it is open, so the device of a handle from `sceUsbdOpenDeviceWithVidPid` can be used until that handle is closed. Closes when: measured on a PS5 - [sceUsbdSetInterfaceAltSetting, sceUsbdClearHalt, sceUsbdDetachKernelDriver](../../../core/libs/prx/libSceUsbd/Export.cpp) (libSceUsbd) - not measured on a console; they follow FreeBSD 11.4 [libusb10.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10.c) over the host libusb. `sceUsbdSetInterfaceAltSetting` checks the interface range and this library's record of claimed interfaces, and returns `0x802400FF` for any other failure; it throws while the device has transfers in flight, which FreeBSD cancels first. `sceUsbdClearHalt` returns 0, or `0x802400FF` for an endpoint the host does not find; FreeBSD ignores the result of the clear itself, so the host's other errors throw, and so does a null handle, which FreeBSD dereferences. `sceUsbdDetachKernelDriver` returns `0x802400FF` for any failure, which on Windows is every call, as libusb cannot detach drivers there. Closes when: measured on a PS5 +- [sceUsbdLockEvents, sceUsbdTryLockEvents, sceUsbdUnlockEvents, sceUsbdLockEventWaiters, sceUsbdUnlockEventWaiters, sceUsbdWaitForEvent, sceUsbdEventHandlingOk, sceUsbdEventHandlerActive, sceUsbdHandleEvents, sceUsbdHandleEventsLocked, sceUsbdHandleEventsTimeout](../../../core/libs/prx/libSceUsbd/Export.cpp) (libSceUsbd) - not measured on a console; they follow FreeBSD 11.4 [libusb10_io.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10_io.c): one lock for events and waiters and one handler thread, reported by `sceUsbdEventHandlingOk`. Waiters wake when the handler unlocks and take over when there is none. The lock is released while the host libusb handles events. Taking the lock twice in one thread, releasing it or waiting without it, and a negative or oversized timeout throw. With no timeout, event handling returns once a transfer completes; FreeBSD also returns on its other poll events. Closes when: measured on a PS5 From c0d8f0a67046acdc47a313f10a1c7b4772a1af9b Mon Sep 17 00:00:00 2001 From: Max Date: Sat, 24 Oct 2026 00:11:08 +0200 Subject: [PATCH 2/3] test(libSceUsbd): release the event lock while a timed-out waiter finishes The timed-out follower case joined the follower while the main thread held the event lock, so a follower whose timeout had expired but which had not yet reacquired the lock could not return. Keep handling events in slices, which release the lock, until the follower has returned, and run the case a second time with the lock held past the follower's timeout. Co-Authored-By: Claude Opus 5.5 --- core/libs/tests/GuestUsbd.cpp | 44 +++++++++++++++++++++-------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/core/libs/tests/GuestUsbd.cpp b/core/libs/tests/GuestUsbd.cpp index db6d0ce42c11..3c0320c75f05 100644 --- a/core/libs/tests/GuestUsbd.cpp +++ b/core/libs/tests/GuestUsbd.cpp @@ -51,6 +51,31 @@ void APS5_VABI MarkDoneThenSignal(UsbdTransfer* transfer) { notified->store(true, std::memory_order_release); } +void FollowerTimesOut(bool holdPastTimeout) { + UsbdTimeval brief{0, 20000}; + sceUsbdLockEvents(); + std::atomic entered{false}; + std::atomic finished{false}; + std::atomic waiterResult{0}; + std::thread follower([&] { + sceUsbdLockEventWaiters(); + entered.store(true); + UsbdTimeval shortWait{0, 30000}; + waiterResult.store(sceUsbdHandleEventsLocked(&shortWait)); + sceUsbdUnlockEventWaiters(); + finished.store(true); + }); + while (!entered.load()) Require(sceUsbdHandleEventsLocked(&brief) == 0, "the handler failed while another thread queued"); + if (holdPastTimeout) std::this_thread::sleep_for(std::chrono::milliseconds(100)); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (!finished.load() && std::chrono::steady_clock::now() < deadline) Require(sceUsbdHandleEventsLocked(&brief) == 0, "the handler failed while another thread waited"); + Require(finished.load(), "a thread that is not the handler did not return from sceUsbdHandleEventsLocked while the handler released the event lock"); + follower.join(); + Require(waiterResult.load() == timedOut, "a thread that is not the handler did not time out in sceUsbdHandleEventsLocked"); + sceUsbdUnlockEvents(); + Require(sceUsbdEventHandlerActive() == 0, "the handler stayed active at the end"); +} + void EventLocks() { Require(sceUsbdEventHandlerActive() == 0 && sceUsbdEventHandlingOk() == 0, "an event handler was active before any lock"); sceUsbdLockEvents(); @@ -100,23 +125,8 @@ void EventLocks() { Require(woken.load() == 0 && handling.load() == 1, "a waiter was not woken and handed the events when the handler unlocked"); Require(sceUsbdEventHandlerActive() == 0, "the handler stayed active after the woken waiter unlocked"); - sceUsbdLockEvents(); - std::atomic entered{false}; - std::atomic waiterResult{0}; - std::thread follower([&] { - sceUsbdLockEventWaiters(); - entered.store(true); - UsbdTimeval shortWait{0, 30000}; - waiterResult.store(sceUsbdHandleEventsLocked(&shortWait)); - sceUsbdUnlockEventWaiters(); - }); - while (!entered.load()) Require(sceUsbdHandleEventsLocked(&brief) == 0, "the handler failed while another thread queued"); - UsbdTimeval longer{0, 100000}; - Require(sceUsbdHandleEventsLocked(&longer) == 0, "the handler failed while another thread waited"); - follower.join(); - Require(waiterResult.load() == timedOut, "a thread that is not the handler did not time out in sceUsbdHandleEventsLocked"); - sceUsbdUnlockEvents(); - Require(sceUsbdEventHandlerActive() == 0, "the handler stayed active at the end"); + FollowerTimesOut(false); + FollowerTimesOut(true); } void Pump(const int& done) { From 9f3799cfaadcedffae53b0dea63ddb7ee57fe768 Mon Sep 17 00:00:00 2001 From: Max Date: Wed, 28 Oct 2026 15:27:07 +0100 Subject: [PATCH 3/3] fix(libSceUsbd): wake event waiters after each pass of event handling FreeBSD 11.4's libusb10_handle_events_sub ends every pass, a timed-out one included, with pthread_cond_broadcast on the context's condition variable, so a thread in libusb_wait_for_event wakes after each pass of the handler. Here waiters woke only when the handler unlocked, so a waiter could sleep through the handler's whole loop. Notify them when the handler takes the event lock back after a pass. The follower test that expected a timeout while the handler ran 20 ms passes now races that wake-up, so guest_usbd checks three cases instead: a follower with a long timeout returns 0 after a pass, a follower times out during a longer pass, and a follower returns after the handler held the event lock past its timeout. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceUsbd/Export.cpp | 1 + core/libs/tests/GuestUsbd.cpp | 60 +++++++++++++++++++++++++---- docs/dev/debt/libSceUsbd.md | 2 +- 3 files changed, 54 insertions(+), 9 deletions(-) diff --git a/core/libs/prx/libSceUsbd/Export.cpp b/core/libs/prx/libSceUsbd/Export.cpp index 579df9557c9d..ea86f3130fe3 100644 --- a/core/libs/prx/libSceUsbd/Export.cpp +++ b/core/libs/prx/libSceUsbd/Export.cpp @@ -200,6 +200,7 @@ int HandleAsHandler(UsbdEvents& events, const UsbdTimeval* timeout) { } events.lock.lock(); events.owner.store(self); + events.changed.notify_all(); return result; } diff --git a/core/libs/tests/GuestUsbd.cpp b/core/libs/tests/GuestUsbd.cpp index 3c0320c75f05..5fc00c074998 100644 --- a/core/libs/tests/GuestUsbd.cpp +++ b/core/libs/tests/GuestUsbd.cpp @@ -51,27 +51,70 @@ void APS5_VABI MarkDoneThenSignal(UsbdTransfer* transfer) { notified->store(true, std::memory_order_release); } -void FollowerTimesOut(bool holdPastTimeout) { +void FollowerWakesAfterPass() { UsbdTimeval brief{0, 20000}; sceUsbdLockEvents(); - std::atomic entered{false}; + std::atomic finished{false}; + std::atomic waiterResult{-1}; + std::atomic waited{0}; + std::thread follower([&] { + sceUsbdLockEventWaiters(); + UsbdTimeval patience{5, 0}; + const auto start = std::chrono::steady_clock::now(); + waiterResult.store(sceUsbdHandleEventsLocked(&patience)); + waited.store(std::chrono::duration_cast(std::chrono::steady_clock::now() - start).count()); + sceUsbdUnlockEventWaiters(); + finished.store(true); + }); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (!finished.load() && std::chrono::steady_clock::now() < deadline) Require(sceUsbdHandleEventsLocked(&brief) == 0, "the handler failed while another thread waited"); + Require(finished.load(), "a thread that is not the handler did not return from sceUsbdHandleEventsLocked while the handler released the event lock"); + follower.join(); + Require(waiterResult.load() == 0 && waited.load() < 2000, "a thread that is not the handler was not woken when a pass of event handling ended"); + sceUsbdUnlockEvents(); + Require(sceUsbdEventHandlerActive() == 0, "the handler stayed active at the end"); +} + +void FollowerTimesOutDuringPass() { + UsbdTimeval longPass{2, 0}; + sceUsbdLockEvents(); std::atomic finished{false}; std::atomic waiterResult{0}; std::thread follower([&] { sceUsbdLockEventWaiters(); - entered.store(true); UsbdTimeval shortWait{0, 30000}; waiterResult.store(sceUsbdHandleEventsLocked(&shortWait)); sceUsbdUnlockEventWaiters(); finished.store(true); }); + const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); + while (!finished.load() && std::chrono::steady_clock::now() < deadline) Require(sceUsbdHandleEventsLocked(&longPass) == 0, "the handler failed while another thread waited"); + Require(finished.load(), "a thread that is not the handler did not return from sceUsbdHandleEventsLocked during a pass of event handling"); + follower.join(); + Require(waiterResult.load() == timedOut, "a thread that is not the handler did not time out in sceUsbdHandleEventsLocked during a longer pass of event handling"); + sceUsbdUnlockEvents(); + Require(sceUsbdEventHandlerActive() == 0, "the handler stayed active at the end"); +} + +void FollowerReturnsAfterHeldLock() { + UsbdTimeval brief{0, 20000}; + sceUsbdLockEvents(); + std::atomic entered{false}; + std::atomic finished{false}; + std::thread follower([&] { + sceUsbdLockEventWaiters(); + entered.store(true); + UsbdTimeval shortWait{0, 30000}; + sceUsbdHandleEventsLocked(&shortWait); + sceUsbdUnlockEventWaiters(); + finished.store(true); + }); while (!entered.load()) Require(sceUsbdHandleEventsLocked(&brief) == 0, "the handler failed while another thread queued"); - if (holdPastTimeout) std::this_thread::sleep_for(std::chrono::milliseconds(100)); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(10); while (!finished.load() && std::chrono::steady_clock::now() < deadline) Require(sceUsbdHandleEventsLocked(&brief) == 0, "the handler failed while another thread waited"); - Require(finished.load(), "a thread that is not the handler did not return from sceUsbdHandleEventsLocked while the handler released the event lock"); + Require(finished.load(), "a thread that is not the handler did not return from sceUsbdHandleEventsLocked after the handler held the event lock past its timeout"); follower.join(); - Require(waiterResult.load() == timedOut, "a thread that is not the handler did not time out in sceUsbdHandleEventsLocked"); sceUsbdUnlockEvents(); Require(sceUsbdEventHandlerActive() == 0, "the handler stayed active at the end"); } @@ -125,8 +168,9 @@ void EventLocks() { Require(woken.load() == 0 && handling.load() == 1, "a waiter was not woken and handed the events when the handler unlocked"); Require(sceUsbdEventHandlerActive() == 0, "the handler stayed active after the woken waiter unlocked"); - FollowerTimesOut(false); - FollowerTimesOut(true); + FollowerWakesAfterPass(); + FollowerTimesOutDuringPass(); + FollowerReturnsAfterHeldLock(); } void Pump(const int& done) { diff --git a/docs/dev/debt/libSceUsbd.md b/docs/dev/debt/libSceUsbd.md index a8c36d799158..2d81cf7b5343 100644 --- a/docs/dev/debt/libSceUsbd.md +++ b/docs/dev/debt/libSceUsbd.md @@ -11,4 +11,4 @@ - [sceUsbdGetConfiguration, sceUsbdGetConfigDescriptorByValue, sceUsbdGetDescriptor, sceUsbdGetStringDescriptorAscii](../../../core/libs/prx/libSceUsbd/Export.cpp) (libSceUsbd) - not measured on a console; they follow FreeBSD 11.4 ([libusb10_desc.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10_desc.c), [libusb20.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb20.c)). The configuration and descriptor queries return host libusb errors. `sceUsbdGetStringDescriptorAscii` rebuilds `libusb20_dev_req_string_simple_sync` with only its 255-byte read: libusb's Windows HID backend reports the requested length as `bLength`, cutting a 4-byte header read to one character. Unlike the host `libusb_get_string_descriptor_ascii` it accepts index 0, drops non-printable characters and `<>` instead of writing `?`, and fails with `0x802400FF`. Closes when: measured on a PS5 - [sceUsbdGetDevice, sceUsbdGetDeviceSpeed, sceUsbdGetMaxPacketSize, sceUsbdGetMaxIsoPacketSize, sceUsbdOpenDeviceWithVidPid](../../../core/libs/prx/libSceUsbd/Export.cpp) (libSceUsbd) - not measured on a console; they follow FreeBSD 11.4 [libusb10.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10.c) over the host libusb. The packet sizes are searched in the active configuration and the iso size is scaled as FreeBSD does, not through the host's versions, which differ for an unreadable configuration and for SuperSpeed endpoints. A host SuperSpeed+ device reports SuperSpeed, the fastest speed FreeBSD 11.4 has. A null device in `sceUsbdGetMaxIsoPacketSize` throws, as FreeBSD dereferences it. A device stays known while a handle to it is open, so the device of a handle from `sceUsbdOpenDeviceWithVidPid` can be used until that handle is closed. Closes when: measured on a PS5 - [sceUsbdSetInterfaceAltSetting, sceUsbdClearHalt, sceUsbdDetachKernelDriver](../../../core/libs/prx/libSceUsbd/Export.cpp) (libSceUsbd) - not measured on a console; they follow FreeBSD 11.4 [libusb10.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10.c) over the host libusb. `sceUsbdSetInterfaceAltSetting` checks the interface range and this library's record of claimed interfaces, and returns `0x802400FF` for any other failure; it throws while the device has transfers in flight, which FreeBSD cancels first. `sceUsbdClearHalt` returns 0, or `0x802400FF` for an endpoint the host does not find; FreeBSD ignores the result of the clear itself, so the host's other errors throw, and so does a null handle, which FreeBSD dereferences. `sceUsbdDetachKernelDriver` returns `0x802400FF` for any failure, which on Windows is every call, as libusb cannot detach drivers there. Closes when: measured on a PS5 -- [sceUsbdLockEvents, sceUsbdTryLockEvents, sceUsbdUnlockEvents, sceUsbdLockEventWaiters, sceUsbdUnlockEventWaiters, sceUsbdWaitForEvent, sceUsbdEventHandlingOk, sceUsbdEventHandlerActive, sceUsbdHandleEvents, sceUsbdHandleEventsLocked, sceUsbdHandleEventsTimeout](../../../core/libs/prx/libSceUsbd/Export.cpp) (libSceUsbd) - not measured on a console; they follow FreeBSD 11.4 [libusb10_io.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10_io.c): one lock for events and waiters and one handler thread, reported by `sceUsbdEventHandlingOk`. Waiters wake when the handler unlocks and take over when there is none. The lock is released while the host libusb handles events. Taking the lock twice in one thread, releasing it or waiting without it, and a negative or oversized timeout throw. With no timeout, event handling returns once a transfer completes; FreeBSD also returns on its other poll events. Closes when: measured on a PS5 +- [sceUsbdLockEvents, sceUsbdTryLockEvents, sceUsbdUnlockEvents, sceUsbdLockEventWaiters, sceUsbdUnlockEventWaiters, sceUsbdWaitForEvent, sceUsbdEventHandlingOk, sceUsbdEventHandlerActive, sceUsbdHandleEvents, sceUsbdHandleEventsLocked, sceUsbdHandleEventsTimeout](../../../core/libs/prx/libSceUsbd/Export.cpp) (libSceUsbd) - not measured on a console; they follow FreeBSD 11.4 [libusb10_io.c](https://github.com/freebsd/freebsd-src/blob/d106002967eff712894ab8b5337bbbff106ce5e0/lib/libusb/libusb10_io.c): one lock for events and waiters and one handler thread, reported by `sceUsbdEventHandlingOk`. Waiters wake after each handling pass and when the handler unlocks, and take over when there is none. The host libusb handles events without the lock. Taking the lock twice in one thread, releasing or waiting without it, and a negative or oversized timeout throw. With no timeout, handling returns once a transfer completes; FreeBSD also returns on other poll events. Closes when: measured on a PS5