From 0d68a3e34f8f4aa1d6e1dc3b01442f02d111d620 Mon Sep 17 00:00:00 2001 From: mrfelek <17274927+mrfelek@users.noreply.github.com> Date: Sat, 24 Oct 2026 10:53:33 +0000 Subject: [PATCH 1/3] fix(libSceGameUpdate): answer no update instead of throwing on request creation Requests use the module's 8 slots with ids from 0x20000000 and sceGameUpdateCheck answers no update, as the PS5 3.20 module does when its patch service reports 0x80412883 (prosper's reading; KytyPS5 also answers no update). Hot Wheels Infinite Rush (PPSA22408) aborted on the throw at its title screen. AI-assisted. Tested by me on my own machine. --- core/libs/prx/libSceGameUpdate/Export.cpp | 94 +++++++++++++++++++---- core/libs/tests/GuestGameUpdate.cpp | 38 ++++++++- docs/dev/debt/libSceGameUpdate.md | 6 +- 3 files changed, 122 insertions(+), 16 deletions(-) diff --git a/core/libs/prx/libSceGameUpdate/Export.cpp b/core/libs/prx/libSceGameUpdate/Export.cpp index f8527db000..bee980659d 100644 --- a/core/libs/prx/libSceGameUpdate/Export.cpp +++ b/core/libs/prx/libSceGameUpdate/Export.cpp @@ -1,6 +1,7 @@ -#include +#include #include #include +#include #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" @@ -10,20 +11,45 @@ static constexpr int SCE_GAME_UPDATE_ERROR_ALREADY_INITIALIZED = static_cast(0x80412803); static constexpr int SCE_GAME_UPDATE_ERROR_INVALID_SIZE = static_cast(0x80412804); static constexpr int SCE_GAME_UPDATE_ERROR_REQUEST_NOT_FOUND = static_cast(0x80412805); +static constexpr int SCE_GAME_UPDATE_ERROR_TOO_MANY_REQUESTS = static_cast(0x80412806); +static constexpr int SCE_GAME_UPDATE_ERROR_ABORTED = static_cast(0x80412807); +static constexpr int FIRST_REQUEST_ID = 0x20000000; +static constexpr int LAST_REQUEST_ID = 0x2FFFFFFF; static_assert(sizeof(GameUpdateCheckParam) == 0x30); static_assert(sizeof(GameUpdateCheckResult) == 0x30); -static std::atomic initialized{false}; +struct Request { + int id; + bool aborted; +}; -static int checkRequestId(int requestId) { +static std::mutex stateMutex; +static bool initialized = false; +static int nextRequestId = FIRST_REQUEST_ID; +static std::array requests{}; + +static Request* findRequest(int requestId) { + for (Request& request : requests) { + if (request.id != 0 && request.id == requestId) { + return &request; + } + } + return nullptr; +} + +static int checkRequestId(int requestId, Request** request) { if (requestId <= 0) { return SCE_GAME_UPDATE_ERROR_REQUEST_NOT_FOUND; } - if (!initialized.load()) { + if (!initialized) { return SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED; } - return SCE_GAME_UPDATE_ERROR_REQUEST_NOT_FOUND; + *request = findRequest(requestId); + if (*request == nullptr) { + return SCE_GAME_UPDATE_ERROR_REQUEST_NOT_FOUND; + } + return 0; } static bool hasNonZeroReserved(const GameUpdateCheckParam* param) { @@ -38,7 +64,13 @@ static bool hasNonZeroReserved(const GameUpdateCheckParam* param) { extern "C" { int APS5_VABI sceGameUpdateAbortRequest(int requestId) { - return checkRequestId(requestId); + std::lock_guard lock(stateMutex); + Request* request = nullptr; + if (int error = checkRequestId(requestId, &request); error != 0) { + return error; + } + request->aborted = true; + return 0; } int APS5_VABI sceGameUpdateCheck(int requestId, const GameUpdateCheckParam* param, GameUpdateCheckResult* result) { @@ -54,18 +86,44 @@ int APS5_VABI sceGameUpdateCheck(int requestId, const GameUpdateCheckParam* para if (param->option != 0 || hasNonZeroReserved(param)) { return SCE_GAME_UPDATE_ERROR_INVALID_ARGUMENT; } - return checkRequestId(requestId); + std::lock_guard lock(stateMutex); + Request* request = nullptr; + if (int error = checkRequestId(requestId, &request); error != 0) { + return error; + } + if (request->aborted) { + return SCE_GAME_UPDATE_ERROR_ABORTED; + } + result->found = false; + result->addcont_found = false; + return 0; } int APS5_VABI sceGameUpdateCreateRequest(void) { - if (!initialized.load()) { + std::lock_guard lock(stateMutex); + if (!initialized) { return SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED; } - throw std::runtime_error("sceGameUpdateCreateRequest: patch service request creation is not implemented"); + for (Request& request : requests) { + if (request.id != 0) { + continue; + } + const int requestId = nextRequestId; + nextRequestId = requestId == LAST_REQUEST_ID ? FIRST_REQUEST_ID : requestId + 1; + request = {requestId, false}; + return requestId; + } + return SCE_GAME_UPDATE_ERROR_TOO_MANY_REQUESTS; } int APS5_VABI sceGameUpdateDeleteRequest(int requestId) { - return checkRequestId(requestId); + std::lock_guard lock(stateMutex); + Request* request = nullptr; + if (int error = checkRequestId(requestId, &request); error != 0) { + return error; + } + *request = {}; + return 0; } int APS5_VABI sceGameUpdateGetAddcontLatestVersion(uint32_t serviceLabel, const NpUnifiedEntitlementLabel* entitlementLabel, GameUpdateAddcontVersionInfo* info) { @@ -76,23 +134,31 @@ int APS5_VABI sceGameUpdateGetAddcontLatestVersion(uint32_t serviceLabel, const if (entitlementLabel == nullptr) { return SCE_GAME_UPDATE_ERROR_INVALID_ARGUMENT; } - if (!initialized.load()) { - return SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED; + { + std::lock_guard lock(stateMutex); + if (!initialized) { + return SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED; + } } throw std::runtime_error("sceGameUpdateGetAddcontLatestVersion: entitlement version lookup is not implemented"); } int APS5_VABI sceGameUpdateInitialize(void) { - if (initialized.exchange(true)) { + std::lock_guard lock(stateMutex); + if (initialized) { return SCE_GAME_UPDATE_ERROR_ALREADY_INITIALIZED; } + initialized = true; return 0; } int APS5_VABI sceGameUpdateTerminate(void) { - if (!initialized.exchange(false)) { + std::lock_guard lock(stateMutex); + if (!initialized) { return SCE_GAME_UPDATE_ERROR_NOT_INITIALIZED; } + initialized = false; + requests = {}; return 0; } diff --git a/core/libs/tests/GuestGameUpdate.cpp b/core/libs/tests/GuestGameUpdate.cpp index c46d451c83..b77beb64c3 100644 --- a/core/libs/tests/GuestGameUpdate.cpp +++ b/core/libs/tests/GuestGameUpdate.cpp @@ -67,6 +67,42 @@ static void TestRequestArguments(int notInitialized, int invalidArgument, int in Require(sceGameUpdateTerminate() == 0); } +static void TestRequestLifecycle(int notFound) { + constexpr int tooManyRequests = static_cast(0x80412806); + constexpr int aborted = static_cast(0x80412807); + GameUpdateCheckParam param{}; + param.size = sizeof(param); + GameUpdateCheckResult result{}; + std::memset(&result, 0xAB, sizeof(result)); + result.size = sizeof(result); + const auto original = result; + + int ids[8]; + for (int& id : ids) { + id = sceGameUpdateCreateRequest(); + Require(id >= 0x20000000 && id <= 0x2FFFFFFF); + } + Require(ids[0] != ids[1]); + Require(sceGameUpdateCreateRequest() == tooManyRequests); + + Require(sceGameUpdateCheck(ids[0], ¶m, &result) == 0); + Require(!result.found && !result.addcont_found); + Require(std::memcmp(&result, &original, 8) == 0); + Require(std::memcmp(reinterpret_cast(&result) + 10, reinterpret_cast(&original) + 10, sizeof(result) - 10) == 0); + + Require(sceGameUpdateAbortRequest(ids[1]) == 0); + Require(sceGameUpdateCheck(ids[1], ¶m, &result) == aborted); + + Require(sceGameUpdateDeleteRequest(ids[3]) == 0); + Require(sceGameUpdateDeleteRequest(ids[3]) == notFound); + Require(sceGameUpdateCheck(ids[3], ¶m, &result) == notFound); + Require(sceGameUpdateCreateRequest() >= 0x20000000); + + Require(sceGameUpdateTerminate() == 0); + Require(sceGameUpdateInitialize() == 0); + Require(sceGameUpdateCheck(ids[0], ¶m, &result) == notFound); +} + int main() { constexpr int notInitialized = static_cast(0x80412801); constexpr int invalidArgument = static_cast(0x80412803); @@ -100,7 +136,7 @@ int main() { const auto original = info; RequireThrows([&] { sceGameUpdateGetAddcontLatestVersion(0, &label, &info); }); Require(std::memcmp(&info, &original, sizeof(info)) == 0); - RequireThrows([] { sceGameUpdateCreateRequest(); }); + TestRequestLifecycle(notFound); Require(sceGameUpdateTerminate() == 0); Require(sceGameUpdateTerminate() == notInitialized); diff --git a/docs/dev/debt/libSceGameUpdate.md b/docs/dev/debt/libSceGameUpdate.md index fd8f0f341a..88f35c8e6d 100644 --- a/docs/dev/debt/libSceGameUpdate.md +++ b/docs/dev/debt/libSceGameUpdate.md @@ -1,7 +1,11 @@ # libSceGameUpdate technical debt +### Silent stubs + +- [sceGameUpdateCreateRequest, sceGameUpdateCheck](../../../core/libs/prx/libSceGameUpdate/Export.cpp) (libSceGameUpdate) - requests are kept in the module's 8 slots with ids from `0x20000000` (a ninth returns `0x80412806`, an aborted request's Check `0x80412807`), as [prosper](https://github.com/mattias800/prosper) reads the PS5 3.20 `libSceGameUpdate.sprx`. Check never asks a patch service: it always answers no update (`found` and the byte after it cleared, the rest of the result untouched, return 0), which is what the module returns when its service reports `0x80412883`; KytyPS5 also answers no update. What the service returns on an offline console is not measured. The answer only decides whether a title offers a patch. Hot Wheels Infinite Rush (PPSA22408) aborted on the throw in sceGameUpdateCreateRequest at its title screen. Closes when: measured on a PS5 + ### Functional -- [libSceGameUpdate](../../../core/libs/prx/libSceGameUpdate/Export.cpp): initialization tracks host state but does not connect the patch service. Request creation and entitlement version lookup throw instead of fabricating an offline error or a successful no-update result. The original maintains eight service-backed requests with IDs starting at `0x20000000`; the service's offline response is still unknown. Check, Abort and Delete implement argument/state checks but have no live requests to operate on. +- [libSceGameUpdate](../../../core/libs/prx/libSceGameUpdate/Export.cpp): initialization tracks host state but does not connect the patch service. Entitlement version lookup throws. - [sceGameUpdateCheck](../../../core/libs/prx/libSceGameUpdate/Export.cpp): option zero is supported for argument validation. The original additionally permits option one when a kernel platform predicate succeeds; that platform mode is not modeled. - [sceGameUpdateGetAddcontLatestVersion](../../../core/libs/prx/libSceGameUpdate/Export.cpp): the supplied module confirms a 20-byte entitlement-label input and a 48-byte service output. Null pointers are rejected before initialization is checked. There is no local output-size validation or blanket clearing of the output; the host leaves it untouched when the missing service causes an exception. From 0870b4dc11cdabca7174e3db6e457b60a328dee4 Mon Sep 17 00:00:00 2001 From: mrfelek <17274927+mrfelek@users.noreply.github.com> Date: Sat, 24 Oct 2026 13:00:52 +0000 Subject: [PATCH 2/3] fix(libSceGameUpdate): skip request ids still held when the counter wraps AI-assisted. Tested by me on my own machine. --- core/libs/prx/libSceGameUpdate/Export.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceGameUpdate/Export.cpp b/core/libs/prx/libSceGameUpdate/Export.cpp index bee980659d..f5f27d716f 100644 --- a/core/libs/prx/libSceGameUpdate/Export.cpp +++ b/core/libs/prx/libSceGameUpdate/Export.cpp @@ -38,6 +38,19 @@ static Request* findRequest(int requestId) { return nullptr; } +static int followingRequestId(int requestId) { + return requestId == LAST_REQUEST_ID ? FIRST_REQUEST_ID : requestId + 1; +} + +static int takeRequestId() { + int requestId = nextRequestId; + while (findRequest(requestId) != nullptr) { + requestId = followingRequestId(requestId); + } + nextRequestId = followingRequestId(requestId); + return requestId; +} + static int checkRequestId(int requestId, Request** request) { if (requestId <= 0) { return SCE_GAME_UPDATE_ERROR_REQUEST_NOT_FOUND; @@ -108,8 +121,7 @@ int APS5_VABI sceGameUpdateCreateRequest(void) { if (request.id != 0) { continue; } - const int requestId = nextRequestId; - nextRequestId = requestId == LAST_REQUEST_ID ? FIRST_REQUEST_ID : requestId + 1; + const int requestId = takeRequestId(); request = {requestId, false}; return requestId; } From 57f5e4103e6e0ae81d5ff4ca2f1157b75463fef8 Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Mon, 26 Oct 2026 06:09:52 +1300 Subject: [PATCH 3/3] docs(libSceGameUpdate): name the title by its ID in the debt entry --- docs/dev/debt/libSceGameUpdate.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceGameUpdate.md b/docs/dev/debt/libSceGameUpdate.md index 88f35c8e6d..75626c8e75 100644 --- a/docs/dev/debt/libSceGameUpdate.md +++ b/docs/dev/debt/libSceGameUpdate.md @@ -2,7 +2,7 @@ ### Silent stubs -- [sceGameUpdateCreateRequest, sceGameUpdateCheck](../../../core/libs/prx/libSceGameUpdate/Export.cpp) (libSceGameUpdate) - requests are kept in the module's 8 slots with ids from `0x20000000` (a ninth returns `0x80412806`, an aborted request's Check `0x80412807`), as [prosper](https://github.com/mattias800/prosper) reads the PS5 3.20 `libSceGameUpdate.sprx`. Check never asks a patch service: it always answers no update (`found` and the byte after it cleared, the rest of the result untouched, return 0), which is what the module returns when its service reports `0x80412883`; KytyPS5 also answers no update. What the service returns on an offline console is not measured. The answer only decides whether a title offers a patch. Hot Wheels Infinite Rush (PPSA22408) aborted on the throw in sceGameUpdateCreateRequest at its title screen. Closes when: measured on a PS5 +- [sceGameUpdateCreateRequest, sceGameUpdateCheck](../../../core/libs/prx/libSceGameUpdate/Export.cpp) (libSceGameUpdate) - requests are kept in the module's 8 slots with ids from `0x20000000` (a ninth returns `0x80412806`, an aborted request's Check `0x80412807`), as [prosper](https://github.com/mattias800/prosper) reads the PS5 3.20 `libSceGameUpdate.sprx`. Check never asks a patch service: it always answers no update (`found` and the byte after it cleared, the rest of the result untouched, return 0), which is what the module returns when its service reports `0x80412883`; KytyPS5 also answers no update. What the service returns on an offline console is not measured. The answer only decides whether a title offers a patch. PPSA22408 aborted on the throw in sceGameUpdateCreateRequest at its title screen. Closes when: measured on a PS5 ### Functional