diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index ffe615e86..18b8ce510 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -639,6 +639,18 @@ if(BUILD_TESTING) endif() endif() +add_test_executable(guest_libc_cnd_timed_wait_tests EXCLUDE_FROM_ALL ${LIBS_INCLUDE_DIR}/tests/GuestLibcCndTimedWait.cpp) +target_include_directories(guest_libc_cnd_timed_wait_tests PRIVATE ${LIBS_INCLUDE_DIR}) +target_link_libraries(guest_libc_cnd_timed_wait_tests PRIVATE libc) +configure_windows_unwind(guest_libc_cnd_timed_wait_tests) +if(BUILD_TESTING) + set_target_properties(guest_libc_cnd_timed_wait_tests PROPERTIES EXCLUDE_FROM_ALL FALSE) + add_test(NAME guest_libc_cnd_timed_wait COMMAND guest_libc_cnd_timed_wait_tests) + if(WIN32) + set_tests_properties(guest_libc_cnd_timed_wait PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") + endif() +endif() + if(MINGW) if(BUILD_TESTING) enable_language(ASM) diff --git a/core/libs/prx/libc/src/Threads.cpp b/core/libs/prx/libc/src/Threads.cpp index fd8aa32c7..e74bcb5f9 100644 --- a/core/libs/prx/libc/src/Threads.cpp +++ b/core/libs/prx/libc/src/Threads.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -24,18 +25,18 @@ struct Xtime { constexpr std::int64_t nanosPerSecond = 1000000000; constexpr std::int64_t longestSleepSeconds = 86400; -timespec XtimeDeadline(const Xtime& time) { +timespec XtimeDeadline(const Xtime& time, const char* function) { constexpr auto largest = std::numeric_limits::max(); constexpr auto smallest = std::numeric_limits::min(); const auto carry = time.nsec / nanosPerSecond; auto nanos = time.nsec % nanosPerSecond; auto seconds = time.sec; if ((carry > 0 && seconds > largest - carry) || (carry < 0 && seconds < smallest - carry)) - throw std::overflow_error("_Thrd_sleep: xtime seconds overflow"); + throw std::overflow_error(std::string(function) + ": xtime seconds overflow"); seconds += carry; if (nanos < 0) { if (seconds == smallest) - throw std::overflow_error("_Thrd_sleep: xtime seconds overflow"); + throw std::overflow_error(std::string(function) + ": xtime seconds overflow"); nanos += nanosPerSecond; --seconds; } @@ -44,6 +45,7 @@ timespec XtimeDeadline(const Xtime& time) { constexpr int threadSuccess = 0; constexpr int threadNomem = 1; +constexpr int threadTimedout = 2; constexpr int threadBusy = 3; constexpr int threadError = 4; @@ -220,6 +222,44 @@ int APS5_VABI _Cnd_wait_nid_postfix(void** conditionHandle, void** mutexHandle) return threadSuccess; } +int APS5_VABI _Cnd_timedwait_nid_postfix(void** conditionHandle, void** mutexHandle, const Xtime* target) { + if (!target) + APS5_INVALID_ARG_EX; + const auto deadline = XtimeDeadline(*target, "_Cnd_timedwait"); + constexpr auto maxDuration = std::chrono::system_clock::duration::max(); + constexpr auto minDuration = std::chrono::system_clock::duration::min(); + constexpr auto maxSeconds = std::chrono::duration_cast(maxDuration).count(); + constexpr auto minSeconds = std::chrono::duration_cast(minDuration).count(); + constexpr auto maxNanos = std::chrono::duration_cast(maxDuration - std::chrono::seconds(maxSeconds)).count(); + if (deadline.tv_sec > maxSeconds || (deadline.tv_sec == maxSeconds && deadline.tv_nsec > maxNanos) || + deadline.tv_sec < minSeconds) + throw std::overflow_error("_Cnd_timedwait: xtime seconds overflow"); + auto* condition = ResolveCondition(conditionHandle, "_Cnd_timedwait"); + auto* mutex = ResolveMutex(mutexHandle, "_Cnd_timedwait"); + if (!IsOwnedByCaller(mutex)) + return threadSuccess; + if (mutex->count != 1) + return threadSuccess; + mutex->count = 0; + mutex->owner.store(std::thread::id{}, std::memory_order_release); + std::unique_lock lock(mutex->native, std::adopt_lock); + bool timeout = false; + try { + const auto deadlineTime = std::chrono::system_clock::time_point( + std::chrono::seconds(deadline.tv_sec) + std::chrono::nanoseconds(deadline.tv_nsec)); + timeout = (condition->native.wait_until(lock, deadlineTime) == std::cv_status::timeout); + } catch (const std::system_error&) { + lock.release(); + mutex->owner.store(std::this_thread::get_id(), std::memory_order_release); + mutex->count = 1; + throw; + } + lock.release(); + mutex->owner.store(std::this_thread::get_id(), std::memory_order_release); + mutex->count = 1; + return timeout ? threadTimedout : threadSuccess; +} + int APS5_VABI _Cnd_broadcast_nid_postfix(void** handle) { ResolveCondition(handle, "_Cnd_broadcast")->native.notify_all(); return threadSuccess; @@ -233,7 +273,7 @@ int SignalCondition_nid_no_patch(void** handle) { int APS5_VABI _Thrd_sleep_nid_postfix(const Xtime* target, Xtime*) { if (!target) APS5_INVALID_ARG_EX; - const auto deadline = XtimeDeadline(*target); + const auto deadline = XtimeDeadline(*target, "_Thrd_sleep"); for (;;) { timespec now{}; if (clock_gettime(CLOCK_REALTIME, &now) != 0) diff --git a/core/libs/tests/GuestLibcCndTimedWait.cpp b/core/libs/tests/GuestLibcCndTimedWait.cpp new file mode 100644 index 000000000..07bb8719c --- /dev/null +++ b/core/libs/tests/GuestLibcCndTimedWait.cpp @@ -0,0 +1,110 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include + +struct Xtime { + std::int64_t sec; + std::int64_t nsec; +}; + +extern "C" { +int APS5_VABI _Mtx_init_nid_postfix(void** handle, int type); +void APS5_VABI _Mtx_destroy_nid_postfix(void** handle); +int APS5_VABI _Mtx_lock_nid_postfix(void** handle); +int APS5_VABI _Mtx_unlock_nid_postfix(void** handle); +int APS5_VABI _Cnd_init_nid_postfix(void** handle); +void APS5_VABI _Cnd_destroy_nid_postfix(void** handle); +int APS5_VABI _Cnd_broadcast_nid_postfix(void** handle); +int APS5_VABI _Cnd_timedwait_nid_postfix(void** condition, void** mutex, const Xtime* target); +} + +static void Require(bool value) { if (!value) std::abort(); } + +template +static void RequireThrows(TCall call) { + try { + call(); + } catch (const TException&) { + return; + } + std::abort(); +} + +constexpr int success = 0; +constexpr int timedout = 2; +constexpr int plain = 0x01; + +int main() { + void* mutex = nullptr; + void* condition = nullptr; + Require(_Mtx_init_nid_postfix(&mutex, plain) == success); + Require(_Cnd_init_nid_postfix(&condition) == success); + + Require(_Mtx_lock_nid_postfix(&mutex) == success); + + const auto now = std::chrono::system_clock::now(); + const auto nowSec = std::chrono::duration_cast(now.time_since_epoch()).count(); + const auto nowNsec = std::chrono::duration_cast(now.time_since_epoch()).count() % 1000000000; + + Xtime pastTime{nowSec - 1, nowNsec}; + Require(_Cnd_timedwait_nid_postfix(&condition, &mutex, &pastTime) == timedout); + + Xtime unnormalizedPastTime{nowSec - 2, nowNsec + 1000000000}; + Require(_Cnd_timedwait_nid_postfix(&condition, &mutex, &unnormalizedPastTime) == timedout); + + Xtime negativePastTime{nowSec, nowNsec - 2000000000}; + Require(_Cnd_timedwait_nid_postfix(&condition, &mutex, &negativePastTime) == timedout); + + const auto timeoutDeadline = std::chrono::system_clock::now() + std::chrono::milliseconds(20); + const auto timeoutSec = std::chrono::duration_cast(timeoutDeadline.time_since_epoch()).count(); + const auto timeoutNsec = std::chrono::duration_cast(timeoutDeadline.time_since_epoch()).count() % 1000000000; + const Xtime timeoutTime{timeoutSec, timeoutNsec}; + while (true) { + const int result = _Cnd_timedwait_nid_postfix(&condition, &mutex, &timeoutTime); + if (result == timedout) { + Require(std::chrono::system_clock::now() >= timeoutDeadline); + break; + } + Require(result == success); + } + + const auto unnormalizedDeadline = std::chrono::system_clock::now() + std::chrono::milliseconds(20); + const auto unnormalizedSec = std::chrono::duration_cast(unnormalizedDeadline.time_since_epoch()).count(); + const auto unnormalizedNsec = std::chrono::duration_cast(unnormalizedDeadline.time_since_epoch()).count() % 1000000000; + const Xtime unnormalizedTimeoutTime{unnormalizedSec - 1, unnormalizedNsec + 1000000000}; + while (true) { + const int result = _Cnd_timedwait_nid_postfix(&condition, &mutex, &unnormalizedTimeoutTime); + if (result == timedout) { + Require(std::chrono::system_clock::now() >= unnormalizedDeadline); + break; + } + Require(result == success); + } + + Xtime futureTime{nowSec + 5, nowNsec}; + bool ready = false; + std::thread signaler([&] { + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + Require(_Mtx_lock_nid_postfix(&mutex) == success); + ready = true; + Require(_Cnd_broadcast_nid_postfix(&condition) == success); + Require(_Mtx_unlock_nid_postfix(&mutex) == success); + }); + while (!ready) { + Require(_Cnd_timedwait_nid_postfix(&condition, &mutex, &futureTime) == success); + } + signaler.join(); + + Require(_Mtx_unlock_nid_postfix(&mutex) == success); + RequireThrows([&] { _Cnd_timedwait_nid_postfix(&condition, &mutex, nullptr); }); + const Xtime overflowCarryTime{std::numeric_limits::max(), 1000000000}; + RequireThrows([&] { _Cnd_timedwait_nid_postfix(&condition, &mutex, &overflowCarryTime); }); + const Xtime overflowSecTime{std::numeric_limits::max(), 0}; + RequireThrows([&] { _Cnd_timedwait_nid_postfix(&condition, &mutex, &overflowSecTime); }); + _Cnd_destroy_nid_postfix(&condition); + _Mtx_destroy_nid_postfix(&mutex); +} diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index 114f60dba..c8b5c16e1 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -6,6 +6,7 @@ - [nearbyintf](../../../core/libs/prx/libc/src/Math.cpp) (libc, NID `c+4r-T-tEIc`) uses the host rounding mode, not the console's; console rounding mode state remains unverified. - [_Thrd_sleep](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `jfRI3snge3o`) follows the Dinkumware threads library shipped with [Visual C++ 2013](https://github.com/adityaramesh/concurrency_runtime/tree/master/VS2013_SP2/crt/src/thr) (`xtime.c`) and C11 Annex K, without console verification. It reads a 16-byte xtime (64-bit seconds and nanoseconds) on the guest `CLOCK_REALTIME`, normalizes the nanoseconds and sleeps through `nanosleep`, retrying on `EINTR`, so it is a cancellation point; a null xtime or one whose normalization overflows throws. The console's `_Thrd_sleep` (disassembled from a title's `libc.prx`) also takes an optional `xtime*` of remaining time and returns `int`; here the sleep is never cut short, so it returns 0 and leaves the remaining time untouched. - [_Mtx_init_with_name](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `tgioGpKtmbE`) forwards to `_Mtx_init` and ignores the mutex name parameter. +- [_Cnd_timedwait](../../../core/libs/prx/libc/src/Threads.cpp) (libc, NID `McaImWKXong`) follows the console's `_Cnd_timedwait` (disassembled from a title's `libc.prx`), where xtime is an absolute deadline converted against the current time: it returns `_Thrd_timedout` (2) on expiration and `_Thrd_success` (0) on notification. The console returns 4 for other internal pthread errors while this implementation throws. - [Guest arena](../../../core/libs/prx/libc/src/GuestArena.cpp) (libc) - the application map area is assumed to end at 0xFC_0000_0000, as `SCE_KERNEL_APP_MAP_AREA_END_ADDR` does on the PS4. On Windows, host allocations made in the arena before libc loads stay, and a fixed guest mapping over one throws - [sceLibcInternalBacktraceForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(const char* heapName)` and its return follow the `libc.prx` ASTRO BOT (PPSA21564 01.007.000) bundles: its four calls (0x6801, 0x7F46, 0xE75E, 0xEE02) are in the `malloc_stats` and `sceLibcMspaceMallocStats` paths, each after a `[%s] ...` line printed with the same heap name buffer, and the caller ignores the result and continues. The console's backtrace output is not reproduced: the heap name and the guest return address are written to the host standard error - [sceLibcInternalHeapErrorReportForGame](../../../core/libs/prx/libc/src/HeapDiagnostics.cpp) (libSceLibcInternal, implemented in libc) - `(void* heap, void* block, uint32 error)` follows the same `libc.prx` (call at 0xF634): it passes the heap, the block and the nonzero result of its block check, and then calls a function that never returns, so the report only has to return. The meaning of the error values is unknown; the three values and the guest return address are written to the host standard error