From c510a410a65250578dfc37cc63becb5a3bd9831a Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Thu, 22 Oct 2026 06:59:31 +0200 Subject: [PATCH 1/4] fix(http): preserve context and epoll state on allocation failure --- core/libs/prx/libSceHttp/CMakeLists.txt | 20 +++ core/libs/prx/libSceHttp/Export.cpp | 20 +-- core/libs/tests/HttpStateAllocation.cpp | 159 ++++++++++++++++++++++++ docs/dev/debt/libSceHttp.md | 2 + 4 files changed, 193 insertions(+), 8 deletions(-) create mode 100644 core/libs/tests/HttpStateAllocation.cpp diff --git a/core/libs/prx/libSceHttp/CMakeLists.txt b/core/libs/prx/libSceHttp/CMakeLists.txt index 701f264762d9..4dd53767cec4 100644 --- a/core/libs/prx/libSceHttp/CMakeLists.txt +++ b/core/libs/prx/libSceHttp/CMakeLists.txt @@ -23,3 +23,23 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_http_state_allocation_tests ${LIBS_INCLUDE_DIR}/tests/HttpStateAllocation.cpp Export.cpp src/Options.cpp) + target_include_directories(guest_http_state_allocation_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_http_state_allocation_tests PRIVATE libc) + configure_windows_unwind(guest_http_state_allocation_tests) + add_test(NAME guest_http_state_init COMMAND guest_http_state_allocation_tests init) + add_test(NAME guest_http_state_init_live COMMAND guest_http_state_allocation_tests init-live) + add_test(NAME guest_http_state_create_epoll COMMAND guest_http_state_allocation_tests create) + foreach(budget RANGE 0 3) + add_test(NAME guest_http_state_term_${budget} COMMAND guest_http_state_allocation_tests term ${budget}) + endforeach() + set(httpStateTests guest_http_state_init guest_http_state_init_live guest_http_state_create_epoll guest_http_state_term_0 + guest_http_state_term_1 guest_http_state_term_2 guest_http_state_term_3) + set_tests_properties(${httpStateTests} PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(${httpStateTests} PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index 1cde24b0127e..cd066144c5a3 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -19,7 +19,6 @@ #include #include #include -#include // No network is emulated: contexts, templates and requests can be created, but any request // that would touch the network fails with the library's network error. @@ -208,8 +207,9 @@ int APS5_VABI sceHttpCreateEpoll(int http_ctx_id, HttpEpollHandle* eh) { if (!g_contexts.contains(http_ctx_id)) return ERROR_INVALID_ID; if (!eh) return ERROR_INVALID_VALUE; auto epoll = std::make_shared(); - *eh = epoll.get(); - g_epolls.emplace(epoll.get(), std::move(epoll)); + const auto handle = epoll.get(); + g_epolls.emplace(handle, std::move(epoll)); + *eh = handle; return 0; } @@ -299,7 +299,12 @@ int APS5_VABI sceHttpInit_nid_postfix(int memid, int ssl_ctx_id, uint64_t pool_s const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); std::lock_guard lock(g_stateMutex); g_contexts.insert(id); - AddHttpContext(id); + try { + AddHttpContext(id); + } catch (...) { + g_contexts.erase(id); + throw; + } return id; } @@ -419,17 +424,16 @@ int APS5_VABI sceHttpsSetSslCallback(int id, HttpsCallback cbfunc, void* user_ar } int APS5_VABI sceHttpTerm_nid_postfix(int http_ctx_id) { - std::vector> epolls; + decltype(g_epolls) epolls; { std::lock_guard lock(g_stateMutex); if (g_contexts.empty()) return ERROR_BEFORE_INIT; if (g_contexts.erase(http_ctx_id) == 0) return ERROR_INVALID_ID; RemoveHttpContext(http_ctx_id); if (!g_contexts.empty()) return 0; - for (auto& [handle, epoll] : g_epolls) epolls.push_back(std::move(epoll)); - g_epolls.clear(); + epolls.swap(g_epolls); } - for (const auto& epoll : epolls) MarkDestroyed(*epoll); + for (const auto& entry : epolls) MarkDestroyed(*entry.second); return 0; } diff --git a/core/libs/tests/HttpStateAllocation.cpp b/core/libs/tests/HttpStateAllocation.cpp new file mode 100644 index 000000000000..0d73301267e6 --- /dev/null +++ b/core/libs/tests/HttpStateAllocation.cpp @@ -0,0 +1,159 @@ +#include "SceTypes.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libSceHttp/src/HttpErrors.hpp" +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceHttpInit_nid_postfix(int, int, std::uint64_t); +int APS5_VABI sceHttpTerm_nid_postfix(int); +int APS5_VABI sceHttpAuthCacheFlush(int); +int APS5_VABI sceHttpGetNonblock(int, int*); +int APS5_VABI sceHttpCreateEpoll(int, HttpEpollHandle*); +int APS5_VABI sceHttpDestroyEpoll(int, HttpEpollHandle); +int APS5_VABI sceHttpWaitRequest(HttpEpollHandle, HttpNBEvent*, int, int); +} + +static int allocationBudget = -1; +static unsigned allocationAttempts = 0; + +void* operator new(std::size_t size) { + if (allocationBudget >= 0) { + ++allocationAttempts; + if (allocationBudget == 0) throw std::bad_alloc(); + --allocationBudget; + } + if (void* memory = std::malloc(size ? size : 1)) return memory; + throw std::bad_alloc(); +} + +void operator delete(void* memory) noexcept { std::free(memory); } +void operator delete(void* memory, std::size_t) noexcept { ::operator delete(memory); } + +static void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +static bool Inactive() { + int enabled = -1; + return sceHttpAuthCacheFlush(-1) == ERROR_BEFORE_INIT && sceHttpGetNonblock(-1, &enabled) == ERROR_BEFORE_INIT; +} + +static int InitCases(bool withExistingContext) { + unsigned rejectedCases = 0; + for (int budget = 0; budget < 8; ++budget) { + Require(Inactive(), "service was already initialized"); + const int existing = withExistingContext ? sceHttpInit_nid_postfix(0, 0, 0x10000) : 0; + Require(!withExistingContext || existing > 0, "existing context creation failed"); + allocationAttempts = 0; + allocationBudget = budget; + bool rejected = false; + int context = 0; + try { context = sceHttpInit_nid_postfix(0, 0, 0x10000); } + catch (const std::bad_alloc&) { rejected = true; } + allocationBudget = -1; + rejectedCases += rejected; + if (rejected) { + if (withExistingContext) { + Require(sceHttpAuthCacheFlush(existing) == 0, "failed init damaged an existing context"); + Require(sceHttpTerm_nid_postfix(existing) == 0, "existing context cleanup failed"); + } + if (!Inactive()) { + std::fprintf(stderr, "init budget=%d attempts=%u published a context after failure\n", budget, allocationAttempts); + return 1; + } + } else { + Require(context > 0 && sceHttpAuthCacheFlush(context) == 0, "successful context is not live"); + Require(sceHttpTerm_nid_postfix(context) == 0, "context cleanup failed"); + if (withExistingContext) { + Require(sceHttpAuthCacheFlush(existing) == 0, "new context cleanup damaged an existing context"); + Require(sceHttpTerm_nid_postfix(existing) == 0, "existing context cleanup failed"); + } + Require(Inactive(), "context remained after cleanup"); + } + } + std::printf("HTTP init existing=%d: 8 allocation budgets, %u rejected, no partial publication\n", withExistingContext, rejectedCases); + return 0; +} + +static int CreateCases() { + unsigned failures = 0, rejectedCases = 0; + for (int budget = 0; budget < 8; ++budget) { + const int context = sceHttpInit_nid_postfix(0, 0, 0x10000); + Require(context > 0, "context creation failed"); + alignas(8) std::uint64_t sentinel = 0; + const auto unchanged = reinterpret_cast(&sentinel); + HttpEpollHandle handle = unchanged; + allocationAttempts = 0; + allocationBudget = budget; + bool rejected = false; + int result = 0; + try { result = sceHttpCreateEpoll(context, &handle); } + catch (const std::bad_alloc&) { rejected = true; } + allocationBudget = -1; + rejectedCases += rejected; + if (rejected) { + if (handle != unchanged) { + std::fprintf(stderr, "epoll budget=%d attempts=%u changed output after failure\n", budget, allocationAttempts); + ++failures; + } + } else { + Require(result == 0 && handle != nullptr && handle != unchanged, "successful epoll was not published"); + HttpNBEvent event{}; + Require(sceHttpWaitRequest(handle, &event, 1, 0) == 0, "published epoll is invalid"); + Require(sceHttpDestroyEpoll(context, handle) == 0, "epoll deletion failed"); + } + Require(sceHttpAuthCacheFlush(context) == 0, "epoll creation lost its context"); + Require(sceHttpTerm_nid_postfix(context) == 0 && Inactive(), "context cleanup failed"); + } + std::printf("HTTP epoll creation: 8 allocation budgets, %u rejected, %u changed failed outputs\n", rejectedCases, failures); + return failures == 0 ? 0 : 1; +} + +static int TermCase(int budget) { + const int context = sceHttpInit_nid_postfix(0, 0, 0x10000); + Require(context > 0, "context creation failed"); + std::array epolls{}; + for (auto& handle : epolls) Require(sceHttpCreateEpoll(context, &handle) == 0, "epoll creation failed"); + allocationAttempts = 0; + allocationBudget = budget; + bool rejected = false; + int result = 0; + try { result = sceHttpTerm_nid_postfix(context); } + catch (const std::bad_alloc&) { rejected = true; } + allocationBudget = -1; + if (rejected) { + if (sceHttpAuthCacheFlush(context) != 0) { + std::fprintf(stderr, "term budget=%d attempts=%u lost the context before cleanup completed\n", budget, allocationAttempts); + return 1; + } + HttpNBEvent event{}; + for (auto handle : epolls) Require(sceHttpWaitRequest(handle, &event, 1, 0) == 0, "failed termination lost an epoll"); + Require(sceHttpTerm_nid_postfix(context) == 0, "termination retry failed"); + } else { + Require(result == 0 && Inactive(), "successful termination left a context live"); + } + const int freshContext = sceHttpInit_nid_postfix(0, 0, 0x10000); + Require(freshContext > 0, "context recreation failed"); + for (auto handle : epolls) + Require(sceHttpDestroyEpoll(freshContext, handle) == ERROR_INVALID_ID, "termination retained an epoll"); + Require(sceHttpTerm_nid_postfix(freshContext) == 0 && Inactive(), "fresh context cleanup failed"); + std::printf("HTTP termination: budget=%d rejected=%d attempts=%u, lifecycle consistent\n", budget, rejected, allocationAttempts); + return 0; +} + +int main(int argc, char** argv) { + if (argc == 2 && std::strcmp(argv[1], "init") == 0) return InitCases(false); + if (argc == 2 && std::strcmp(argv[1], "init-live") == 0) return InitCases(true); + if (argc == 2 && std::strcmp(argv[1], "create") == 0) return CreateCases(); + if (argc == 3 && std::strcmp(argv[1], "term") == 0) return TermCase(std::atoi(argv[2])); + return 1; +} diff --git a/docs/dev/debt/libSceHttp.md b/docs/dev/debt/libSceHttp.md index fce148fc5f76..e7e50e4ee164 100644 --- a/docs/dev/debt/libSceHttp.md +++ b/docs/dev/debt/libSceHttp.md @@ -30,6 +30,8 @@ ### Functional +- [Context and epoll allocation](../../../core/libs/prx/libSceHttp/Export.cpp): PS5 out-of-memory errors and failed-call output values remain unverified. Host bookkeeping uses the C++ allocator and can throw `std::bad_alloc`; the guest HTTP memory pool is not used for these allocations. + - [sceHttpUriParse, sceHttpUriBuild, sceHttpUriMerge](../../../core/libs/prx/libSceHttp/src/Uri.cpp) - console behavior for non-ASCII scheme and hostname bytes remains unmeasured. ASCII character classification and scheme case folding follow [RFC 3986 sections 3.1 and 3.2](https://www.rfc-editor.org/rfc/rfc3986#section-3.1), independently of the host locale; parsing retains the existing C-locale rejection of those bytes. - [sceHttpUriEscape](../../../core/libs/prx/libSceHttp/src/Uri.cpp) escapes every byte except the ASCII unreserved set in [RFC 3986 section 2.3](https://www.rfc-editor.org/rfc/rfc3986#section-2.3), independently of the host locale. Console behaviour for non-ASCII input remains unmeasured. From 2ba96e0e261c501b9ee13dc53890847ce172cc10 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Thu, 22 Oct 2026 07:05:17 +0200 Subject: [PATCH 2/4] docs(http): group allocation uncertainties with unknown contracts --- docs/dev/debt/libSceHttp.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/dev/debt/libSceHttp.md b/docs/dev/debt/libSceHttp.md index e7e50e4ee164..e35ad9a9b1f6 100644 --- a/docs/dev/debt/libSceHttp.md +++ b/docs/dev/debt/libSceHttp.md @@ -7,6 +7,7 @@ ### Unknown function info +- [Context and epoll allocation](../../../core/libs/prx/libSceHttp/Export.cpp): PS5 out-of-memory errors and failed-call output values remain unverified. Host bookkeeping uses the C++ allocator and can throw `std::bad_alloc`; the guest HTTP memory pool is not used for these allocations. - [HTTP object options](../../../core/libs/prx/libSceHttp/src/Options.cpp) (libSceHttp) - a template starts with redirects followed, blocking calls and no `Accept-Encoding: gzip`, and a connection or request copies its parent's options when it is created. The signatures of the option calls, these defaults, the copying and the order of the checks (`BEFORE_INIT` first; the last `sceHttpTerm` ends every object's options) follow [shadPS4](https://github.com/shadps4-emu/shadPS4/blob/a808e76/src/core/libraries/network/http.cpp) (PS4) and are not measured on a PS5. `sceHttpSetDefaultAcceptEncodingGZIPEnabled` sets the gzip value for templates created later in that context, where shadPS4 keeps one value for the library, and `sceHttpUnsetEpoll` takes a template or connection id, where shadPS4 looks only at requests. The options change no request, as none reaches the network - [sceHttpGetConnectionStat](../../../core/libs/prx/libSceHttp/Export.cpp) (libSceHttp) - connection and request diagnostic signatures are recovered; their outputs contain four and three 32-bit words, respectively. Field semantics remain incomplete. Epoll setters/getters distinguish a 32-bit epoll ID from a pointer-sized handle, and use a pointer-sized user argument. The supplied firmware getter dispatch does not handle the pointer-handle selector used by GetEpoll, so its intended successful behavior is not recoverable from this implementation. @@ -30,8 +31,6 @@ ### Functional -- [Context and epoll allocation](../../../core/libs/prx/libSceHttp/Export.cpp): PS5 out-of-memory errors and failed-call output values remain unverified. Host bookkeeping uses the C++ allocator and can throw `std::bad_alloc`; the guest HTTP memory pool is not used for these allocations. - - [sceHttpUriParse, sceHttpUriBuild, sceHttpUriMerge](../../../core/libs/prx/libSceHttp/src/Uri.cpp) - console behavior for non-ASCII scheme and hostname bytes remains unmeasured. ASCII character classification and scheme case folding follow [RFC 3986 sections 3.1 and 3.2](https://www.rfc-editor.org/rfc/rfc3986#section-3.1), independently of the host locale; parsing retains the existing C-locale rejection of those bytes. - [sceHttpUriEscape](../../../core/libs/prx/libSceHttp/src/Uri.cpp) escapes every byte except the ASCII unreserved set in [RFC 3986 section 2.3](https://www.rfc-editor.org/rfc/rfc3986#section-2.3), independently of the host locale. Console behaviour for non-ASCII input remains unmeasured. From 076daa4e361e9fb57c15c5eaa80bdaad47057f94 Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Thu, 22 Oct 2026 09:03:28 +0200 Subject: [PATCH 3/4] test(http): disarm allocation fault before throwing --- core/libs/tests/HttpStateAllocation.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/core/libs/tests/HttpStateAllocation.cpp b/core/libs/tests/HttpStateAllocation.cpp index 0d73301267e6..0c1f0cc6acb4 100644 --- a/core/libs/tests/HttpStateAllocation.cpp +++ b/core/libs/tests/HttpStateAllocation.cpp @@ -25,7 +25,10 @@ static unsigned allocationAttempts = 0; void* operator new(std::size_t size) { if (allocationBudget >= 0) { ++allocationAttempts; - if (allocationBudget == 0) throw std::bad_alloc(); + if (allocationBudget == 0) { + allocationBudget = -1; + throw std::bad_alloc(); + } --allocationBudget; } if (void* memory = std::malloc(size ? size : 1)) return memory; From 693e4bce1a304646ca7f914f7c1bee6122fe867e Mon Sep 17 00:00:00 2001 From: Ondra97 <47064087+Ondra97@users.noreply.github.com> Date: Fri, 30 Oct 2026 17:31:38 +0100 Subject: [PATCH 4/4] fix(http): roll back all context registries after allocation failure --- core/libs/prx/libSceHttp/Export.cpp | 8 +++++--- core/libs/tests/HttpStateAllocation.cpp | 7 +++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceHttp/Export.cpp b/core/libs/prx/libSceHttp/Export.cpp index e13cfb3955f7..ab2301f7c6b0 100644 --- a/core/libs/prx/libSceHttp/Export.cpp +++ b/core/libs/prx/libSceHttp/Export.cpp @@ -279,12 +279,14 @@ int APS5_VABI sceHttpGetStatusCode(int request_id, int* status_code) { int APS5_VABI sceHttpInit_nid_postfix(int memid, int ssl_ctx_id, uint64_t pool_size) { const int id = g_nextHandle.fetch_add(1, std::memory_order_relaxed); std::lock_guard lock(g_stateMutex); - g_contextPoolSizes[id] = pool_size; - g_contextRegisteredIds[id] = {memid, ssl_ctx_id}; - g_contexts.insert(id); try { + g_contextPoolSizes[id] = pool_size; + g_contextRegisteredIds[id] = {memid, ssl_ctx_id}; + g_contexts.insert(id); AddHttpContext(id); } catch (...) { + g_contextPoolSizes.erase(id); + g_contextRegisteredIds.erase(id); g_contexts.erase(id); throw; } diff --git a/core/libs/tests/HttpStateAllocation.cpp b/core/libs/tests/HttpStateAllocation.cpp index 00cd3178630d..2cf662bab278 100644 --- a/core/libs/tests/HttpStateAllocation.cpp +++ b/core/libs/tests/HttpStateAllocation.cpp @@ -57,6 +57,13 @@ static int InitCases(bool withExistingContext) { rejectedCases += rejected; if (rejected) { if (withExistingContext) { + HttpMemoryPoolStats pool{}; + int netPool = -1; + int sslContext = -1; + Require(sceHttpGetMemoryPoolStats(existing + 1, &pool) == ERROR_INVALID_ID, "failed init retained its pool-size registration"); + Require(sceHttpGetRegisteredCtxIds(existing + 1, &netPool, &sslContext) == ERROR_INVALID_ID, "failed init retained its registered context ids"); + Require(sceHttpGetMemoryPoolStats(existing, &pool) == 0 && pool.poolSize == 0x10000, "failed init damaged existing pool stats"); + Require(sceHttpGetRegisteredCtxIds(existing, &netPool, &sslContext) == 0 && netPool == 0 && sslContext == 0, "failed init damaged existing registered ids"); Require(sceHttpAuthCacheFlush(existing) == 0, "failed init damaged an existing context"); Require(sceHttpTerm_nid_postfix(existing) == 0, "existing context cleanup failed"); }