Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions core/libs/prx/libSceHttp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,23 @@ if(BUILD_TESTING)
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libSceHttp>")
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:$<TARGET_FILE_DIR:libc>")
endif()
endif()
28 changes: 17 additions & 11 deletions core/libs/prx/libSceHttp/Export.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
#include <string_view>
#include <unordered_map>
#include <unordered_set>
#include <vector>

// 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.
Expand Down Expand Up @@ -192,8 +191,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<HttpEpoll>();
*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;
}

Expand Down Expand Up @@ -279,10 +279,17 @@ 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);
AddHttpContext(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;
}
return id;
}

Expand Down Expand Up @@ -400,7 +407,7 @@ int APS5_VABI sceHttpsSetSslCallback(int id, HttpsCallback cbfunc, void* user_ar
}

int APS5_VABI sceHttpTerm_nid_postfix(int http_ctx_id) {
std::vector<std::shared_ptr<HttpEpoll>> epolls;
decltype(g_epolls) epolls;
{
std::lock_guard lock(g_stateMutex);
g_contextPoolSizes.erase(http_ctx_id);
Expand All @@ -409,10 +416,9 @@ int APS5_VABI sceHttpTerm_nid_postfix(int http_ctx_id) {
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;
}

Expand Down
160 changes: 160 additions & 0 deletions core/libs/tests/HttpStateAllocation.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
#include "SceTypes.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include "prx/libSceHttp/include/Http.hpp"
#include "prx/libSceHttp/src/HttpErrors.hpp"
#include <array>
#include <cstddef>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <new>

static int allocationBudget = -1;
static unsigned allocationAttempts = 0;

void* operator new(std::size_t size) {
if (allocationBudget >= 0) {
++allocationAttempts;
if (allocationBudget == 0) {
allocationBudget = -1;
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) {
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");
}
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<HttpEpollHandle>(&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<HttpEpollHandle, 3> 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;
}
1 change: 1 addition & 0 deletions docs/dev/debt/libSceHttp.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,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.

Expand Down
Loading