feat(libkernel): implement _umtx_op wait and wake operations
Titles that ship their own libc.prx or Unity's PS5Util.prx import _umtx_op (04AjkP0jO9U) for atomic waits, call_once and futex-style waits. Implement UMTX_OP_WAIT, UMTX_OP_WAKE, UMTX_OP_WAIT_UINT, UMTX_OP_WAIT_UINT_PRIVATE and UMTX_OP_WAKE_PRIVATE as FreeBSD 11's kern_umtx.c does, on the sceKernelSyncOnAddressWait waiter queue. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HeC9bA3n74TzJRYv9AgBE8
This commit is contained in:
parent
90c152e840
commit
79f90f3d9b
|
|
@ -597,6 +597,16 @@ if(BUILD_TESTING)
|
|||
set_tests_properties(guest_sync_on_address PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
|
||||
endif()
|
||||
add_test_executable(guest_umtx_op_tests ${LIBS_INCLUDE_DIR}/tests/GuestUmtxOp.cpp)
|
||||
target_include_directories(guest_umtx_op_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_umtx_op_tests PRIVATE libkernel libc)
|
||||
configure_windows_unwind(guest_umtx_op_tests)
|
||||
add_test(NAME guest_umtx_op COMMAND guest_umtx_op_tests)
|
||||
set_tests_properties(guest_umtx_op PROPERTIES TIMEOUT 20)
|
||||
if(WIN32)
|
||||
set_tests_properties(guest_umtx_op PROPERTIES ENVIRONMENT_MODIFICATION
|
||||
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
|
||||
endif()
|
||||
add_test_executable(guest_available_cpumask_tests ${LIBS_INCLUDE_DIR}/tests/GuestAvailableCpumask.cpp)
|
||||
target_include_directories(guest_available_cpumask_tests PRIVATE ${LIBS_INCLUDE_DIR})
|
||||
target_link_libraries(guest_available_cpumask_tests PRIVATE libkernel libc)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
#include <list>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
|
|
@ -13,10 +15,36 @@
|
|||
#include "prx/libkernel/Time/include/Time.hpp"
|
||||
#include "prx/libkernel/Time/include/TimedWait.hpp"
|
||||
|
||||
extern "C" int* APS5_VABI __error_nid_postfix();
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int SYNC_ON_ADDRESS_OK = 0;
|
||||
|
||||
constexpr int UMTX_OP_RESERVED1 = 1;
|
||||
constexpr int UMTX_OP_WAIT = 2;
|
||||
constexpr int UMTX_OP_WAKE = 3;
|
||||
constexpr int UMTX_OP_WAIT_UINT = 11;
|
||||
constexpr int UMTX_OP_WAIT_UINT_PRIVATE = 15;
|
||||
constexpr int UMTX_OP_WAKE_PRIVATE = 16;
|
||||
constexpr int UMTX_OP_MAX = 27;
|
||||
constexpr std::uint32_t UMTX_ABSTIME = 1;
|
||||
constexpr int GUEST_CLOCK_REALTIME = 0;
|
||||
|
||||
constexpr int GUEST_EFAULT = 14;
|
||||
constexpr int GUEST_EINVAL = 22;
|
||||
constexpr int GUEST_EOPNOTSUPP = 45;
|
||||
constexpr int GUEST_ETIMEDOUT = 60;
|
||||
|
||||
constexpr std::int64_t NANOS_PER_SECOND = 1000000000;
|
||||
constexpr std::uint64_t UNBOUNDED_WAIT_NANOS = 1ULL << 62;
|
||||
|
||||
struct UmtxTime {
|
||||
KernelTimespec timeout;
|
||||
std::uint32_t flags;
|
||||
std::uint32_t clockId;
|
||||
};
|
||||
|
||||
struct AddressWaiter {
|
||||
TimedWait::Condition condition;
|
||||
bool woken = false;
|
||||
|
|
@ -31,11 +59,11 @@ bool IsAlignedAddress(std::uintptr_t address) {
|
|||
}
|
||||
|
||||
template <class TValue>
|
||||
int WaitOnAddress(TValue* address, TValue expected, const KernelUseconds* timeout, const void* caller) {
|
||||
bool WaitOnAddressUntil(TValue* address, TValue expected, std::optional<std::uint64_t> deadlineNanos, const void* caller) {
|
||||
const auto key = reinterpret_cast<std::uintptr_t>(address);
|
||||
std::unique_lock<std::mutex> lock(g_waitersLock);
|
||||
if (std::atomic_ref<TValue>(*address).load() != expected) {
|
||||
return SYNC_ON_ADDRESS_OK;
|
||||
return true;
|
||||
}
|
||||
|
||||
AddressWaiter waiter;
|
||||
|
|
@ -43,10 +71,10 @@ int WaitOnAddress(TValue* address, TValue expected, const KernelUseconds* timeou
|
|||
const auto position = queue.insert(queue.end(), &waiter);
|
||||
const auto isWoken = [&] { return waiter.woken; };
|
||||
const auto waitStart = std::chrono::steady_clock::now();
|
||||
if (timeout == nullptr) {
|
||||
if (!deadlineNanos) {
|
||||
waiter.condition.Wait(lock, isWoken);
|
||||
} else {
|
||||
waiter.condition.WaitUntil(lock, TimedWait::DeadlineNanos(*timeout), isWoken);
|
||||
waiter.condition.WaitUntil(lock, *deadlineNanos, isWoken);
|
||||
}
|
||||
const auto waited = std::chrono::duration_cast<std::chrono::nanoseconds>(std::chrono::steady_clock::now() - waitStart);
|
||||
|
||||
|
|
@ -59,7 +87,94 @@ int WaitOnAddress(TValue* address, TValue expected, const KernelUseconds* timeou
|
|||
}
|
||||
lock.unlock();
|
||||
KernelTraceWait_nid_postfix("addr", caller, static_cast<std::uint64_t>(waited.count()), !woken);
|
||||
return woken ? SYNC_ON_ADDRESS_OK : SCE_KERNEL_ERROR_ETIMEDOUT;
|
||||
return woken;
|
||||
}
|
||||
|
||||
template <class TValue>
|
||||
int WaitOnAddress(TValue* address, TValue expected, const KernelUseconds* timeout, const void* caller) {
|
||||
const auto deadline = timeout == nullptr ? std::nullopt : std::optional<std::uint64_t>(TimedWait::DeadlineNanos(*timeout));
|
||||
return WaitOnAddressUntil(address, expected, deadline, caller) ? SYNC_ON_ADDRESS_OK : SCE_KERNEL_ERROR_ETIMEDOUT;
|
||||
}
|
||||
|
||||
void WakeAddress(std::uintptr_t key, std::int32_t count, bool wakesAtLeastOne) {
|
||||
std::lock_guard<std::mutex> lock(g_waitersLock);
|
||||
const auto entry = g_waiters.find(key);
|
||||
if (entry == g_waiters.end()) {
|
||||
return;
|
||||
}
|
||||
auto& queue = entry->second;
|
||||
for (std::int32_t woken = 0; !queue.empty() && (woken < count || (wakesAtLeastOne && woken == 0)); ++woken) {
|
||||
AddressWaiter* waiter = queue.front();
|
||||
queue.pop_front();
|
||||
waiter->woken = true;
|
||||
waiter->condition.NotifyOne();
|
||||
}
|
||||
if (queue.empty()) {
|
||||
g_waiters.erase(entry);
|
||||
}
|
||||
}
|
||||
|
||||
std::uint64_t SaturatingNanos(std::int64_t seconds, std::int64_t nanos) {
|
||||
if (seconds < 0) {
|
||||
return 0;
|
||||
}
|
||||
if (seconds >= static_cast<std::int64_t>(UNBOUNDED_WAIT_NANOS / NANOS_PER_SECOND)) {
|
||||
return UNBOUNDED_WAIT_NANOS;
|
||||
}
|
||||
const std::int64_t total = seconds * NANOS_PER_SECOND + nanos;
|
||||
return total <= 0 ? 0 : static_cast<std::uint64_t>(total);
|
||||
}
|
||||
|
||||
int UmtxDeadline(const void* sizeArgument, const void* timeArgument, std::optional<std::uint64_t>& deadlineNanos) {
|
||||
deadlineNanos.reset();
|
||||
if (timeArgument == nullptr) {
|
||||
return 0;
|
||||
}
|
||||
UmtxTime time{{0, 0}, 0, GUEST_CLOCK_REALTIME};
|
||||
if (reinterpret_cast<std::uintptr_t>(sizeArgument) <= sizeof(KernelTimespec)) {
|
||||
time.timeout = *static_cast<const KernelTimespec*>(timeArgument);
|
||||
} else {
|
||||
time = *static_cast<const UmtxTime*>(timeArgument);
|
||||
}
|
||||
if (time.timeout.tv_sec < 0 || time.timeout.tv_nsec < 0 || time.timeout.tv_nsec >= NANOS_PER_SECOND) {
|
||||
return GUEST_EINVAL;
|
||||
}
|
||||
|
||||
std::uint64_t remaining = 0;
|
||||
if ((time.flags & UMTX_ABSTIME) == 0) {
|
||||
remaining = SaturatingNanos(time.timeout.tv_sec, time.timeout.tv_nsec);
|
||||
} else {
|
||||
KernelTimespec now{};
|
||||
clock_gettime_nid_postfix(static_cast<int>(time.clockId), &now);
|
||||
remaining = SaturatingNanos(time.timeout.tv_sec - now.tv_sec, time.timeout.tv_nsec - now.tv_nsec);
|
||||
}
|
||||
if (remaining < UNBOUNDED_WAIT_NANOS) {
|
||||
deadlineNanos = TimedWait::NowNanos() + remaining;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
template <class TValue>
|
||||
int UmtxWait(void* object, TValue expected, const void* sizeArgument, const void* timeArgument, const void* caller) {
|
||||
std::optional<std::uint64_t> deadline;
|
||||
if (const int error = UmtxDeadline(sizeArgument, timeArgument, deadline); error != 0) {
|
||||
return error;
|
||||
}
|
||||
if (object == nullptr) {
|
||||
return GUEST_EFAULT;
|
||||
}
|
||||
if (!IsAlignedAddress<TValue>(reinterpret_cast<std::uintptr_t>(object))) {
|
||||
APS5_INVALID_ARG_EX;
|
||||
}
|
||||
return WaitOnAddressUntil(static_cast<TValue*>(object), expected, deadline, caller) ? 0 : GUEST_ETIMEDOUT;
|
||||
}
|
||||
|
||||
int UmtxWake(void* object, std::uint64_t count, bool isPrivate) {
|
||||
if (object == nullptr) {
|
||||
return isPrivate ? 0 : GUEST_EFAULT;
|
||||
}
|
||||
WakeAddress(reinterpret_cast<std::uintptr_t>(object), static_cast<std::int32_t>(count), true);
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
|
@ -112,22 +227,40 @@ int APS5_VABI sceKernelSyncOnAddressWake(void* address, std::int32_t count) {
|
|||
APS5_INVALID_ARG_EX;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(g_waitersLock);
|
||||
const auto entry = g_waiters.find(key);
|
||||
if (entry == g_waiters.end()) {
|
||||
return SYNC_ON_ADDRESS_OK;
|
||||
}
|
||||
auto& queue = entry->second;
|
||||
for (; count > 0 && !queue.empty(); --count) {
|
||||
AddressWaiter* waiter = queue.front();
|
||||
queue.pop_front();
|
||||
waiter->woken = true;
|
||||
waiter->condition.NotifyOne();
|
||||
}
|
||||
if (queue.empty()) {
|
||||
g_waiters.erase(entry);
|
||||
}
|
||||
WakeAddress(key, count, false);
|
||||
return SYNC_ON_ADDRESS_OK;
|
||||
}
|
||||
|
||||
int APS5_VABI _umtx_op_nid_postfix(void* object, int operation, std::uint64_t value, void* sizeArgument, void* timeArgument) {
|
||||
const void* caller = __builtin_return_address(0);
|
||||
int error = 0;
|
||||
switch (operation) {
|
||||
case UMTX_OP_WAIT:
|
||||
error = UmtxWait<std::uint64_t>(object, value, sizeArgument, timeArgument, caller);
|
||||
break;
|
||||
case UMTX_OP_WAIT_UINT:
|
||||
case UMTX_OP_WAIT_UINT_PRIVATE:
|
||||
error = UmtxWait<std::uint32_t>(object, static_cast<std::uint32_t>(value), sizeArgument, timeArgument, caller);
|
||||
break;
|
||||
case UMTX_OP_WAKE:
|
||||
case UMTX_OP_WAKE_PRIVATE:
|
||||
error = UmtxWake(object, value, operation == UMTX_OP_WAKE_PRIVATE);
|
||||
break;
|
||||
default:
|
||||
if (operation < 0 || operation >= UMTX_OP_MAX) {
|
||||
error = GUEST_EINVAL;
|
||||
} else if (operation <= UMTX_OP_RESERVED1) {
|
||||
error = GUEST_EOPNOTSUPP;
|
||||
} else {
|
||||
NotImplemented_nid_no_patch(("_umtx_op operation " + std::to_string(operation)).c_str());
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (error == 0) {
|
||||
return 0;
|
||||
}
|
||||
*__error_nid_postfix() = error;
|
||||
return -1;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,231 @@
|
|||
#include "SceTypes.hpp"
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <climits>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
extern "C" {
|
||||
int APS5_VABI _umtx_op_nid_postfix(void* object, int operation, std::uint64_t value, void* sizeArgument, void* timeArgument);
|
||||
int* APS5_VABI __error_nid_postfix();
|
||||
int APS5_VABI clock_gettime_nid_postfix(int clockId, KernelTimespec* tp);
|
||||
}
|
||||
|
||||
static constexpr int UMTX_OP_WAIT = 2;
|
||||
static constexpr int UMTX_OP_WAKE = 3;
|
||||
static constexpr int UMTX_OP_MUTEX_LOCK = 5;
|
||||
static constexpr int UMTX_OP_WAIT_UINT = 11;
|
||||
static constexpr int UMTX_OP_WAIT_UINT_PRIVATE = 15;
|
||||
static constexpr int UMTX_OP_WAKE_PRIVATE = 16;
|
||||
static constexpr int UMTX_OP_SEM2_WAIT = 23;
|
||||
static constexpr int UMTX_OP_MAX = 27;
|
||||
static constexpr std::uint32_t UMTX_ABSTIME = 1;
|
||||
static constexpr int CLOCK_MONOTONIC_ID = 4;
|
||||
|
||||
static constexpr int GUEST_EFAULT = 14;
|
||||
static constexpr int GUEST_EINVAL = 22;
|
||||
static constexpr int GUEST_EOPNOTSUPP = 45;
|
||||
static constexpr int GUEST_ETIMEDOUT = 60;
|
||||
|
||||
static constexpr auto SETTLE = std::chrono::milliseconds(100);
|
||||
|
||||
struct UmtxTime {
|
||||
KernelTimespec timeout;
|
||||
std::uint32_t flags;
|
||||
std::uint32_t clockId;
|
||||
};
|
||||
|
||||
static void Require(bool value) { if (!value) std::abort(); }
|
||||
|
||||
static KernelTimespec FailsafeTimeout() { return {10, 0}; }
|
||||
|
||||
static int Umtx(void* object, int operation, std::uint64_t value, void* sizeArgument = nullptr, void* timeArgument = nullptr) {
|
||||
*__error_nid_postfix() = 0;
|
||||
return _umtx_op_nid_postfix(object, operation, value, sizeArgument, timeArgument);
|
||||
}
|
||||
|
||||
static bool FailsWith(int result, int error) {
|
||||
return result == -1 && *__error_nid_postfix() == error;
|
||||
}
|
||||
|
||||
static void* SizeOf(std::size_t size) { return reinterpret_cast<void*>(size); }
|
||||
|
||||
static void AwaitCount(const std::atomic<int>& counter, int value) {
|
||||
const auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
||||
while (counter.load() != value) {
|
||||
Require(std::chrono::steady_clock::now() < deadline);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
}
|
||||
|
||||
static void ReturnsWhenTheValueDiffers() {
|
||||
std::uint64_t value = 2;
|
||||
Require(Umtx(&value, UMTX_OP_WAIT, 1) == 0);
|
||||
KernelTimespec none{0, 0};
|
||||
Require(Umtx(&value, UMTX_OP_WAIT, 1, nullptr, &none) == 0);
|
||||
|
||||
std::uint32_t word = 2;
|
||||
Require(Umtx(&word, UMTX_OP_WAIT_UINT, 1) == 0);
|
||||
Require(Umtx(&word, UMTX_OP_WAIT_UINT_PRIVATE, 1, nullptr, &none) == 0);
|
||||
}
|
||||
|
||||
static void WaitComparesALongAndWaitUintAnInt() {
|
||||
std::uint64_t value = 0x100000001ULL;
|
||||
KernelTimespec none{0, 0};
|
||||
Require(Umtx(&value, UMTX_OP_WAIT, 1, nullptr, &none) == 0);
|
||||
Require(FailsWith(Umtx(&value, UMTX_OP_WAIT, 0x100000001ULL, nullptr, &none), GUEST_ETIMEDOUT));
|
||||
|
||||
auto* low = reinterpret_cast<std::uint32_t*>(&value);
|
||||
Require(FailsWith(Umtx(low, UMTX_OP_WAIT_UINT, 1, nullptr, &none), GUEST_ETIMEDOUT));
|
||||
Require(FailsWith(Umtx(low, UMTX_OP_WAIT_UINT_PRIVATE, 0x700000001ULL, nullptr, &none), GUEST_ETIMEDOUT));
|
||||
}
|
||||
|
||||
static void TimesOutWhileTheValueMatches() {
|
||||
std::uint32_t word = 1;
|
||||
KernelTimespec none{0, 0};
|
||||
Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, nullptr, &none), GUEST_ETIMEDOUT));
|
||||
|
||||
KernelTimespec relative{0, 20000000};
|
||||
auto start = std::chrono::steady_clock::now();
|
||||
Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, SizeOf(sizeof(relative)), &relative), GUEST_ETIMEDOUT));
|
||||
Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(15));
|
||||
|
||||
UmtxTime time{{0, 20000000}, 0, CLOCK_MONOTONIC_ID};
|
||||
start = std::chrono::steady_clock::now();
|
||||
Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, SizeOf(sizeof(time)), &time), GUEST_ETIMEDOUT));
|
||||
Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(15));
|
||||
|
||||
KernelTimespec now{};
|
||||
Require(clock_gettime_nid_postfix(CLOCK_MONOTONIC_ID, &now) == 0);
|
||||
UmtxTime absolute{{now.tv_sec, now.tv_nsec + 20000000}, UMTX_ABSTIME, CLOCK_MONOTONIC_ID};
|
||||
if (absolute.timeout.tv_nsec >= 1000000000) {
|
||||
absolute.timeout.tv_sec += 1;
|
||||
absolute.timeout.tv_nsec -= 1000000000;
|
||||
}
|
||||
start = std::chrono::steady_clock::now();
|
||||
Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, SizeOf(sizeof(absolute)), &absolute), GUEST_ETIMEDOUT));
|
||||
Require(std::chrono::steady_clock::now() - start >= std::chrono::milliseconds(15));
|
||||
|
||||
UmtxTime past{{now.tv_sec, now.tv_nsec}, UMTX_ABSTIME, CLOCK_MONOTONIC_ID};
|
||||
start = std::chrono::steady_clock::now();
|
||||
Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, SizeOf(sizeof(past)), &past), GUEST_ETIMEDOUT));
|
||||
Require(std::chrono::steady_clock::now() - start < std::chrono::seconds(1));
|
||||
}
|
||||
|
||||
static void RejectsAnInvalidTimeout() {
|
||||
std::uint32_t word = 1;
|
||||
KernelTimespec negative{-1, 0};
|
||||
Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, nullptr, &negative), GUEST_EINVAL));
|
||||
KernelTimespec tooManyNanos{0, 1000000000};
|
||||
Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, nullptr, &tooManyNanos), GUEST_EINVAL));
|
||||
UmtxTime negativeNanos{{0, -1}, 0, CLOCK_MONOTONIC_ID};
|
||||
Require(FailsWith(Umtx(&word, UMTX_OP_WAIT_UINT, 1, SizeOf(sizeof(negativeNanos)), &negativeNanos), GUEST_EINVAL));
|
||||
}
|
||||
|
||||
static void WakeReleasesAWaiterWithoutAValueChange() {
|
||||
std::uint64_t value = 1;
|
||||
std::atomic<int> finished{0};
|
||||
int result = -2;
|
||||
std::thread waiter([&] {
|
||||
KernelTimespec failsafe = FailsafeTimeout();
|
||||
result = Umtx(&value, UMTX_OP_WAIT, 1, nullptr, &failsafe);
|
||||
++finished;
|
||||
});
|
||||
while (finished.load() == 0) {
|
||||
Require(Umtx(&value, UMTX_OP_WAKE, 1) == 0);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
waiter.join();
|
||||
Require(result == 0);
|
||||
}
|
||||
|
||||
static void WakeHonoursTheCountAndTheAddress() {
|
||||
std::uint32_t word = 1;
|
||||
std::uint32_t other = 1;
|
||||
std::atomic<int> started{0};
|
||||
std::atomic<int> finished{0};
|
||||
std::atomic<int> otherFinished{0};
|
||||
std::vector<int> results(4, -2);
|
||||
int otherResult = -2;
|
||||
|
||||
std::vector<std::thread> waiters;
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
waiters.emplace_back([&, i] {
|
||||
KernelTimespec failsafe = FailsafeTimeout();
|
||||
++started;
|
||||
results[i] = Umtx(&word, UMTX_OP_WAIT_UINT_PRIVATE, 1, nullptr, &failsafe);
|
||||
++finished;
|
||||
});
|
||||
}
|
||||
std::thread otherWaiter([&] {
|
||||
KernelTimespec failsafe = FailsafeTimeout();
|
||||
++started;
|
||||
otherResult = Umtx(&other, UMTX_OP_WAIT_UINT, 1, nullptr, &failsafe);
|
||||
++otherFinished;
|
||||
});
|
||||
AwaitCount(started, 5);
|
||||
std::this_thread::sleep_for(SETTLE);
|
||||
|
||||
Require(Umtx(&word, UMTX_OP_WAKE_PRIVATE, 1) == 0);
|
||||
AwaitCount(finished, 1);
|
||||
std::this_thread::sleep_for(SETTLE);
|
||||
Require(finished.load() == 1);
|
||||
|
||||
Require(Umtx(&word, UMTX_OP_WAKE, 0) == 0);
|
||||
AwaitCount(finished, 2);
|
||||
std::this_thread::sleep_for(SETTLE);
|
||||
Require(finished.load() == 2);
|
||||
|
||||
Require(Umtx(&word, UMTX_OP_WAKE, INT_MAX) == 0);
|
||||
AwaitCount(finished, 4);
|
||||
std::this_thread::sleep_for(SETTLE);
|
||||
Require(otherFinished.load() == 0);
|
||||
|
||||
Require(Umtx(&other, UMTX_OP_WAKE_PRIVATE, 1) == 0);
|
||||
for (auto& waiter : waiters) waiter.join();
|
||||
otherWaiter.join();
|
||||
for (const int result : results) Require(result == 0);
|
||||
Require(otherResult == 0);
|
||||
}
|
||||
|
||||
static void WakeWithoutWaitersSucceeds() {
|
||||
std::uint32_t word = 0;
|
||||
Require(Umtx(&word, UMTX_OP_WAKE, 1) == 0);
|
||||
Require(Umtx(&word, UMTX_OP_WAKE_PRIVATE, INT_MAX) == 0);
|
||||
}
|
||||
|
||||
static void RejectsBadObjectsAndOperations() {
|
||||
Require(FailsWith(Umtx(nullptr, UMTX_OP_WAIT, 0), GUEST_EFAULT));
|
||||
Require(FailsWith(Umtx(nullptr, UMTX_OP_WAIT_UINT, 0), GUEST_EFAULT));
|
||||
Require(FailsWith(Umtx(nullptr, UMTX_OP_WAKE, 1), GUEST_EFAULT));
|
||||
Require(Umtx(nullptr, UMTX_OP_WAKE_PRIVATE, 1) == 0);
|
||||
|
||||
std::uint64_t value = 0;
|
||||
Require(FailsWith(Umtx(&value, 0, 0), GUEST_EOPNOTSUPP));
|
||||
Require(FailsWith(Umtx(&value, 1, 0), GUEST_EOPNOTSUPP));
|
||||
Require(FailsWith(Umtx(&value, UMTX_OP_MAX, 0), GUEST_EINVAL));
|
||||
Require(FailsWith(Umtx(&value, -1, 0), GUEST_EINVAL));
|
||||
|
||||
bool threw = false;
|
||||
try { Umtx(&value, UMTX_OP_MUTEX_LOCK, 0); }
|
||||
catch (const std::runtime_error&) { threw = true; }
|
||||
Require(threw);
|
||||
threw = false;
|
||||
try { Umtx(&value, UMTX_OP_SEM2_WAIT, 0); }
|
||||
catch (const std::runtime_error&) { threw = true; }
|
||||
Require(threw);
|
||||
}
|
||||
|
||||
int main() {
|
||||
ReturnsWhenTheValueDiffers();
|
||||
WaitComparesALongAndWaitUintAnInt();
|
||||
TimesOutWhileTheValueMatches();
|
||||
RejectsAnInvalidTimeout();
|
||||
WakeReleasesAWaiterWithoutAValueChange();
|
||||
WakeHonoursTheCountAndTheAddress();
|
||||
WakeWithoutWaitersSucceeds();
|
||||
RejectsBadObjectsAndOperations();
|
||||
}
|
||||
|
|
@ -166,6 +166,7 @@ Throughout the project, every function at every stage either **does exactly what
|
|||
- [sceKernelGetModuleInfo](../../core/libs/prx/libkernel/Module/src/ModuleInfo.cpp) (libkernel) - the 0x160-byte layout (`st_size`, `name[256]`, 4 segments, `segment_count`, `fingerprint[20]`) follows [fpPS4](https://github.com/red-prig/fpPS4/blob/04cefd43e6fddd1ab033e7980cd356d14c964905/sys/sys_types.pas#L319-L337) and [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/0fe263a4760dfbfa973366890061749b4af0de97/src/core/module.h#L15-L31). Only that `st_size` is accepted, as in shadPS4; other sizes return `EINVAL`, a null pointer `EFAULT` and a handle `sceKernelGetModuleList` does not list `ESRCH`. Name and segments are the ones `sceKernelGetModuleInfoFromAddr` reports, on Windows from the same PE sections, so `prot` uses the read/write/execute bits 1/2/4, while fpPS4 and shadPS4 store the ELF `p_flags`. The fingerprint is left zero, as the host image does not carry it
|
||||
- [sceKernelReleaseFlexibleMemory](../../core/libs/prx/libkernel/DirectMemory/Export.cpp) (libkernel) - unmaps the range like sceKernelMunmap, as fpPS4 and prosper do; whether the console keeps the virtual range reserved after releasing its pages is unverified
|
||||
- [Memory protection bits 0x100 and 0x200](../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) (libkernel) - taken as audio coprocessor (ACP) read and write, as in the [KytyPS5](https://github.com/KytyPS5/KytyPS5) and [etaHEN](https://github.com/etaHEN/etaHEN) headers; the coprocessor work runs on the host CPU, so they grant host read and read/write access like the GPU bits
|
||||
- [_umtx_op](../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - `UMTX_OP_WAIT`, `UMTX_OP_WAKE`, `UMTX_OP_WAIT_UINT`, `UMTX_OP_WAIT_UINT_PRIVATE` and `UMTX_OP_WAKE_PRIVATE` follow FreeBSD 11's [kern_umtx.c](https://github.com/freebsd/freebsd-src/blob/stable/11/sys/kern/kern_umtx.c): a timeout whose size argument is at most 16 is a relative `timespec`, a larger one an `_umtx_time`, and a wake of 0 or fewer wakes one waiter. Waiters share the queue of `sceKernelSyncOnAddressWait`. Operations 0 and 1 fail with `EOPNOTSUPP` and operations above 26 with `EINVAL`, as in FreeBSD 11; the other operations throw, and so does a misaligned object. PPSA12544's `libc.prx` passes `UMTX_OP_WAIT` and `UMTX_OP_WAIT_UINT` a pointer to a 4-byte microsecond timeout with size argument 0, which FreeBSD reads as a `timespec`, and waits for a running call_once with operation 1; its `PS5Util.prx` passes a `timespec`. Whether the console reads a microsecond timeout there, and what operation 1 does on it, is unverified
|
||||
- [sceKernelSyncOnAddressWait](../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - the only known caller passes a null timeout and a name string as the fourth argument; the timeout is assumed to point to microseconds like the other kernel waits, and the name is ignored
|
||||
- [sceKernelSyncOnAddressWait8 / sceKernelSyncOnAddressWait16 / sceKernelSyncOnAddressWait32 / sceKernelSyncOnAddressWait64](../../core/libs/prx/libkernel/SyncOnAddress/src/SyncOnAddress.cpp) (libkernel) - libc's `__std_atomic_wait_direct_8/16/32/64` pass a microsecond timeout or null and ignore the result; the by-value comparand, return values and the 2- and 8-byte alignment of Wait16 and Wait64 are assumed
|
||||
- [scePthreadRwlockDestroy, pthread_rwlock_destroy](../../core/libs/prx/libkernel/Pthread/src/Rwlock.cpp) (libkernel) - a rwlock held for reading or writing returns `EBUSY` and keeps its handle, as [KytyPS5](https://github.com/KytyPS5/KytyPS5) does; FreeBSD's [libthr](https://github.com/freebsd/freebsd-src/blob/main/lib/libthr/thread/thr_rwlock.c) and shadPS4 free it without checking, and the console's answer is unverified. Destroying a rwlock while another thread is acquiring it is not guarded
|
||||
|
|
|
|||
Loading…
Reference in New Issue