From 2bafc417cc2630e51ffc89a7469a22f4505331a7 Mon Sep 17 00:00:00 2001 From: JaceTheBlu Date: Sun, 25 Oct 2026 17:32:10 +0000 Subject: [PATCH 1/4] feat(libSceLibreSsl): implement X509_VERIFY_PARAM_set_hostflags and X509_VERIFY_PARAM_set1_ip_asc Both exports threw and were declared without parameters. They follow LibreSSL 2.5.4, unchanged in 2.7: set_hostflags stores the host matching flags in the verify parameters' identity, and set1_ip_asc parses an IPv4 or IPv6 address the way a2i_ipadd does, copies the 4 or 16 bytes into a guest malloc block that replaces the previous address, and returns 0 without touching it for text the parser rejects. IPv4 parts are read with the guest sscanf, as the module would. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceLibreSsl/CMakeLists.txt | 16 ++ core/libs/prx/libSceLibreSsl/Export.cpp | 10 -- core/libs/prx/libSceLibreSsl/include/Api.hpp | 4 +- .../libSceLibreSsl/src/X509VerifyParam.cpp | 131 +++++++++++++++ .../libSceLibreSsl/tests/X509VerifyParam.cpp | 153 ++++++++++++++++++ docs/dev/debt/libSceLibreSsl.md | 4 +- 6 files changed, 305 insertions(+), 13 deletions(-) create mode 100644 core/libs/prx/libSceLibreSsl/src/X509VerifyParam.cpp create mode 100644 core/libs/prx/libSceLibreSsl/tests/X509VerifyParam.cpp diff --git a/core/libs/prx/libSceLibreSsl/CMakeLists.txt b/core/libs/prx/libSceLibreSsl/CMakeLists.txt index 5bedd7e5e2..fdb1460e96 100644 --- a/core/libs/prx/libSceLibreSsl/CMakeLists.txt +++ b/core/libs/prx/libSceLibreSsl/CMakeLists.txt @@ -109,3 +109,19 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +target_sources(libSceLibreSsl PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/X509VerifyParam.cpp) + +if(BUILD_TESTING) + add_test_executable(guest_libressl_x509_verify_param_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/X509VerifyParam.cpp) + target_include_directories(guest_libressl_x509_verify_param_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_x509_verify_param_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_x509_verify_param_tests) + add_test(NAME guest_libressl_x509_verify_param COMMAND guest_libressl_x509_verify_param_tests) + add_test(NAME guest_libressl_x509_verify_param_allocator COMMAND guest_libressl_x509_verify_param_tests allocator) + set_tests_properties(guest_libressl_x509_verify_param guest_libressl_x509_verify_param_allocator PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_x509_verify_param guest_libressl_x509_verify_param_allocator PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceLibreSsl/Export.cpp b/core/libs/prx/libSceLibreSsl/Export.cpp index 6aec90b0ed..1b77c3339f 100644 --- a/core/libs/prx/libSceLibreSsl/Export.cpp +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -1608,16 +1608,6 @@ int APS5_VABI X509_VERIFY_PARAM_set1_host_nid_postfix(void) { return 0; } -int APS5_VABI X509_VERIFY_PARAM_set1_ip_asc_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI X509_VERIFY_PARAM_set_hostflags_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI X509_check_issued_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibreSsl/include/Api.hpp b/core/libs/prx/libSceLibreSsl/include/Api.hpp index a1ba0f9325..63d69e3afb 100644 --- a/core/libs/prx/libSceLibreSsl/include/Api.hpp +++ b/core/libs/prx/libSceLibreSsl/include/Api.hpp @@ -416,8 +416,8 @@ void APS5_VABI X509_STORE_free_nid_postfix(LibreSslX509Store* store); LibreSslX509Store* APS5_VABI X509_STORE_new_nid_postfix(void); int APS5_VABI X509_STORE_set_flags_nid_postfix(LibreSslX509Store* store, std::uint64_t flags); int APS5_VABI X509_VERIFY_PARAM_set1_host_nid_postfix(void); -int APS5_VABI X509_VERIFY_PARAM_set1_ip_asc_nid_postfix(void); -int APS5_VABI X509_VERIFY_PARAM_set_hostflags_nid_postfix(void); +int APS5_VABI X509_VERIFY_PARAM_set1_ip_asc_nid_postfix(LibreSslX509VerifyParam* param, const char* text); +void APS5_VABI X509_VERIFY_PARAM_set_hostflags_nid_postfix(LibreSslX509VerifyParam* param, std::uint32_t flags); int APS5_VABI X509_check_issued_nid_postfix(void); int APS5_VABI X509_free_nid_postfix(void); int APS5_VABI X509_get_ext_d2i_nid_postfix(void); diff --git a/core/libs/prx/libSceLibreSsl/src/X509VerifyParam.cpp b/core/libs/prx/libSceLibreSsl/src/X509VerifyParam.cpp new file mode 100644 index 0000000000..df000a04e5 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/X509VerifyParam.cpp @@ -0,0 +1,131 @@ +#include "prx/libSceLibreSsl/libSceLibreSsl.hpp" +#include "prx/libc/include/Api.hpp" +#include "prx/libc/include/General.hpp" +#include +#include +#include + +namespace { + +struct Ipv6Text { + unsigned char bytes[16]; + int total; + int zeroPosition; + int zeroCount; +}; + +bool Ipv4FromText(unsigned char* out, const char* text) { + int parts[4]; + if (sscanf_nid_postfix(text, "%d.%d.%d.%d", &parts[0], &parts[1], &parts[2], &parts[3]) != 4) return false; + for (int part : parts) { + if (part < 0 || part > 255) return false; + } + for (int index = 0; index < 4; ++index) out[index] = static_cast(parts[index]); + return true; +} + +bool Ipv6Hex(unsigned char* out, const char* text, int length) { + if (length > 4) return false; + unsigned int number = 0; + while (length--) { + const unsigned char character = static_cast(*text++); + number <<= 4; + if (character >= '0' && character <= '9') number |= character - '0'; + else if (character >= 'A' && character <= 'F') number |= character - 'A' + 10; + else if (character >= 'a' && character <= 'f') number |= character - 'a' + 10; + else return false; + } + out[0] = static_cast(number >> 8); + out[1] = static_cast(number & 0xff); + return true; +} + +bool Ipv6Element(const char* element, int length, Ipv6Text& state) { + if (state.total == 16) return false; + if (length == 0) { + if (state.zeroPosition == -1) state.zeroPosition = state.total; + else if (state.zeroPosition != state.total) return false; + ++state.zeroCount; + return true; + } + if (length > 4) { + if (state.total > 12 || element[length]) return false; + if (!Ipv4FromText(state.bytes + state.total, element)) return false; + state.total += 4; + return true; + } + if (!Ipv6Hex(state.bytes + state.total, element, length)) return false; + state.total += 2; + return true; +} + +bool Ipv6Elements(const char* text, Ipv6Text& state) { + const char* start = text; + for (;;) { + const char* separator = std::strchr(start, ':'); + bool accepted; + if (separator == start || !*start) { + accepted = Ipv6Element(nullptr, 0, state); + } else { + const char* last = separator ? separator - 1 : start + std::strlen(start) - 1; + accepted = Ipv6Element(start, static_cast(last - start + 1), state); + } + if (!accepted) return false; + if (!separator) return true; + start = separator + 1; + } +} + +bool Ipv6FromText(unsigned char* out, const char* text) { + Ipv6Text state{{}, 0, -1, 0}; + if (!Ipv6Elements(text, state)) return false; + if (state.zeroPosition == -1) { + if (state.total != 16) return false; + std::memcpy(out, state.bytes, 16); + return true; + } + if (state.total == 16 || state.zeroCount > 3) return false; + if (state.zeroCount == 3 && state.total > 0) return false; + if (state.zeroCount == 2 && state.zeroPosition != 0 && state.zeroPosition != state.total) return false; + if (state.zeroCount == 1 && (state.zeroPosition == 0 || state.zeroPosition == state.total)) return false; + std::memcpy(out, state.bytes, state.zeroPosition); + std::memset(out + state.zeroPosition, 0, 16 - state.total); + if (state.total != state.zeroPosition) + std::memcpy(out + state.zeroPosition + 16 - state.total, state.bytes + state.zeroPosition, state.total - state.zeroPosition); + return true; +} + +std::size_t AddressFromText(unsigned char* out, const char* text) { + if (std::strchr(text, ':')) return Ipv6FromText(out, text) ? 16 : 0; + return Ipv4FromText(out, text) ? 4 : 0; +} + +LibreSslX509VerifyParamId* Identity(LibreSslX509VerifyParam* param, const char* function) { + if (!param || !param->id) NotImplemented_nid_no_patch(function); + return param->id; +} + +} // namespace + +extern "C" { + +void APS5_VABI X509_VERIFY_PARAM_set_hostflags_nid_postfix(LibreSslX509VerifyParam* param, std::uint32_t flags) { + Identity(param, __func__)->hostFlags = flags; +} + +int APS5_VABI X509_VERIFY_PARAM_set1_ip_asc_nid_postfix(LibreSslX509VerifyParam* param, const char* text) { + if (!text) NotImplemented_nid_no_patch(__func__); + unsigned char address[16]; + const std::size_t length = AddressFromText(address, text); + if (length == 0) return 0; + auto* id = Identity(param, __func__); + auto* copy = static_cast(malloc_nid_postfix(length)); + if (!copy) return 0; + std::memcpy(copy, address, length); + free_nid_postfix(id->ip); + id->ip = copy; + id->ipLength = length; + return 1; +} + +} diff --git a/core/libs/prx/libSceLibreSsl/tests/X509VerifyParam.cpp b/core/libs/prx/libSceLibreSsl/tests/X509VerifyParam.cpp new file mode 100644 index 0000000000..b2d38264e1 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/tests/X509VerifyParam.cpp @@ -0,0 +1,153 @@ +#include "prx/libSceLibreSsl/libSceLibreSsl.hpp" +#include "prx/libc/include/Api.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "failed: %s\n", message); + std::abort(); +} + +template +void RequireThrows(TAction action, const char* message) { + try { + action(); + } catch (const std::exception&) { + return; + } + Require(false, message); +} + +void RequireAddress(LibreSslX509VerifyParam* param, const char* text, std::initializer_list expected) { + Require(X509_VERIFY_PARAM_set1_ip_asc_nid_postfix(param, text) == 1, text); + Require(param->id->ipLength == expected.size() && std::memcmp(param->id->ip, expected.begin(), expected.size()) == 0, text); +} + +void RequireRejected(LibreSslX509VerifyParam* param, const char* text) { + const unsigned char* before = param->id->ip; + const std::uint64_t length = param->id->ipLength; + Require(X509_VERIFY_PARAM_set1_ip_asc_nid_postfix(param, text) == 0, text); + Require(param->id->ip == before && param->id->ipLength == length, "a rejected address keeps the previous one"); +} + +void HostFlags() { + auto* store = X509_STORE_new_nid_postfix(); + auto* param = store->param; + X509_VERIFY_PARAM_set_hostflags_nid_postfix(param, 0x4); + Require(param->id->hostFlags == 0x4, "host flags are stored"); + X509_VERIFY_PARAM_set_hostflags_nid_postfix(param, 0xffffffffu); + Require(param->id->hostFlags == 0xffffffffu, "host flags are replaced, not ORed"); + X509_VERIFY_PARAM_set_hostflags_nid_postfix(param, 0); + Require(param->id->hostFlags == 0 && param->flags == 0 && param->inheritanceFlags == 0, "only the host flags change"); + RequireThrows([] { X509_VERIFY_PARAM_set_hostflags_nid_postfix(nullptr, 1); }, "null verify parameters"); + auto* id = param->id; + param->id = nullptr; + RequireThrows([&] { X509_VERIFY_PARAM_set_hostflags_nid_postfix(param, 1); }, "verify parameters without an identity"); + param->id = id; + X509_STORE_free_nid_postfix(store); +} + +void Ipv4() { + auto* store = X509_STORE_new_nid_postfix(); + auto* param = store->param; + RequireAddress(param, "192.168.0.1", {192, 168, 0, 1}); + RequireAddress(param, "0.0.0.0", {0, 0, 0, 0}); + RequireAddress(param, "255.255.255.255", {255, 255, 255, 255}); + RequireAddress(param, " 1. 2.3.4", {1, 2, 3, 4}); + RequireAddress(param, "+1.2.3.4x", {1, 2, 3, 4}); + RequireAddress(param, "010.1.1.1", {10, 1, 1, 1}); + for (const char* text : {"", "1.2.3", "256.0.0.0", "1.2.3.256", "-1.0.0.0", "1..2.3", "a.b.c.d", "1 .2.3.4"}) + RequireRejected(param, text); + X509_STORE_free_nid_postfix(store); +} + +void Ipv6() { + auto* store = X509_STORE_new_nid_postfix(); + auto* param = store->param; + RequireAddress(param, "::", {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); + RequireAddress(param, "::1", {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}); + RequireAddress(param, "1::", {0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}); + RequireAddress(param, "fe80::aB:1", {0xfe, 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xab, 0, 1}); + RequireAddress(param, "1:2:3:4:5:6:7:8", {0, 1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 0, 7, 0, 8}); + RequireAddress(param, "::ffff:192.0.2.1", {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 192, 0, 2, 1}); + RequireAddress(param, "1:2:3:4:5:6:10.0.0.1", {0, 1, 0, 2, 0, 3, 0, 4, 0, 5, 0, 6, 10, 0, 0, 1}); + for (const char* text : {":::", "1:::2", "1::2::3", ":1", "1:", "::1:", "1:2:3:4:5:6:7", "1:2:3:4:5:6:7:8:9", + "1::2:3:4:5:6:7:8", "12345::", "g::", "1.2.3.4::", "1:2:3:4:5:6:7:1.2.3.4", "::1.2.3"}) + RequireRejected(param, text); + X509_STORE_free_nid_postfix(store); +} + +void Unsupported() { + auto* store = X509_STORE_new_nid_postfix(); + RequireThrows([&] { X509_VERIFY_PARAM_set1_ip_asc_nid_postfix(store->param, nullptr); }, "null address text"); + Require(X509_VERIFY_PARAM_set1_ip_asc_nid_postfix(nullptr, "bad") == 0, "a rejected address does not read the verify parameters"); + RequireThrows([] { X509_VERIFY_PARAM_set1_ip_asc_nid_postfix(nullptr, "1.2.3.4"); }, "null verify parameters"); + X509_STORE_free_nid_postfix(store); +} + +int allocations = 0; +int frees = 0; +bool failNext = false; + +void* APS5_VABI Allocate_nid_no_patch(std::size_t size) { + if (failNext) { + failNext = false; + return nullptr; + } + ++allocations; + return std::malloc(size); +} + +void APS5_VABI Free_nid_no_patch(void* pointer) { + if (pointer) ++frees; + std::free(pointer); +} + +void* APS5_VABI Calloc_nid_no_patch(std::size_t count, std::size_t size) { return std::calloc(count, size); } +void* APS5_VABI Reallocate_nid_no_patch(void*, std::size_t) { throw std::runtime_error("unexpected realloc"); } +void* APS5_VABI Align_nid_no_patch(std::size_t, std::size_t) { throw std::runtime_error("unexpected aligned allocation"); } +void* APS5_VABI Realign_nid_no_patch(void*, std::size_t, std::size_t) { throw std::runtime_error("unexpected aligned reallocation"); } +int APS5_VABI PosixAlign_nid_no_patch(void**, std::size_t, std::size_t) { throw std::runtime_error("unexpected posix_memalign"); } + +void Allocator() { + std::array api{ + reinterpret_cast(Allocate_nid_no_patch), reinterpret_cast(Free_nid_no_patch), reinterpret_cast(Calloc_nid_no_patch), + reinterpret_cast(Reallocate_nid_no_patch), reinterpret_cast(Align_nid_no_patch), reinterpret_cast(Realign_nid_no_patch), + reinterpret_cast(PosixAlign_nid_no_patch)}; + ApplicationHeapRegister_nid_no_patch(api.data()); + auto* store = X509_STORE_new_nid_postfix(); + auto* param = store->param; + allocations = frees = 0; + Require(X509_VERIFY_PARAM_set1_ip_asc_nid_postfix(param, "10.0.0.1") == 1 && allocations == 1 && frees == 0, "the address is copied into a malloc'd block"); + Require(X509_VERIFY_PARAM_set1_ip_asc_nid_postfix(param, "::1") == 1 && allocations == 2 && frees == 1, "a new address frees the previous one"); + unsigned char* kept = param->id->ip; + failNext = true; + Require(X509_VERIFY_PARAM_set1_ip_asc_nid_postfix(param, "10.0.0.2") == 0, "a failed copy fails the call"); + Require(param->id->ip == kept && param->id->ipLength == 16 && frees == 1, "a failed copy keeps the previous address"); + X509_STORE_free_nid_postfix(store); +} + +} // namespace + +int main(int argc, char**) { + if (argc > 1) { + Allocator(); + return 0; + } + std::array api{}; + ApplicationHeapRegister_nid_no_patch(api.data()); + HostFlags(); + Ipv4(); + Ipv6(); + Unsupported(); +} diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index eca9d68a14..402f1fe142 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -2,7 +2,7 @@ ### Unknown function info -- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 342 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 340 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. - [MD4, MD5, SHA1, SHA224, SHA256, SHA384 and SHA512 Init/Update/Final and SHA256](../../../core/libs/prx/libSceLibreSsl/src/Digest.cpp) (libSceLibreSsl) - signatures and context layouts follow the [LibreSSL headers](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha/sha.h), and buffering and `Final` follow its [sources](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha); nothing is checked on the console. Null contexts, null input with a nonzero length, null digest buffers in the 32-bit `Final` functions and a buffered byte count that is not below the block size, which LibreSSL does not check, throw - [DES_set_key, DES_ecb_encrypt, DES_set_odd_parity](../../../core/libs/prx/libSceLibreSsl/Export.cpp) (libSceLibreSsl) - the 128-byte `DES_key_schedule` holds the 8-byte key followed by zeros instead of LibreSSL's precomputed subkeys, so a title reading its contents sees different bytes; `DES_ecb_encrypt` derives the subkeys through mbedtls on each call. Not compared with the console. Results follow LibreSSL [des_key.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des_key.c) and [des.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des.c): `DES_set_key` skips the parity and weak-key checks and returns 0, as it does while `DES_check_key` is 0 (that variable is not in the PS5 export inventory), a nonzero `enc` encrypts, and `DES_set_odd_parity` sets each byte's low bit for odd parity. Null pointers throw where LibreSSL faults - [X509_verify_cert_error_string](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - no console call is verified. The 64-bit argument, low-32-bit lookup, messages and shared numeric fallback follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509/x509_txt.c), the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware versions remain unverified. Unknown-code results share an overwritten static buffer and are not safe for concurrent use, as in that release; code 1 and codes above 54 use the numeric fallback. @@ -19,3 +19,5 @@ - [ASN1_INTEGER_get and ASN1_STRING_data/length/type/to_UTF8](../../../core/libs/prx/libSceLibreSsl/src/Asn1.cpp) are unverified on PS5. Signatures, the 24-byte guest string layout and conversions follow [LibreSSL 2.5.4's ASN.1 sources](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/asn1), the version reported for firmware 13.40 in [discussion #8981](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Null accessor inputs, null conversion outputs, missing nonempty data, oversized converted output and unrepresentable signed integer values throw where the source has undefined behaviour. Malformed encodings and allocation failure throw until ASN.1 error-queue reporting is implemented. Conversion buffers use the guest libc allocator and can be released with guest `free`. - [X509_STORE_new, X509_STORE_free and X509_STORE_set_flags](../../../core/libs/prx/libSceLibreSsl/src/X509Store.cpp) are unverified on PS5. The 136-byte store, 64-byte verify parameters, 56-byte parameter identity, 32-byte stacks and the results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509) `x509_lu.c` and `x509_vpm.c`, the version in [firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). No export adds a certificate, CRL, lookup method or policy object, so `X509_STORE_free` throws instead of freeing one and the object cache's comparator throws. A null store or verify parameters, and a null element array behind a positive count, throw where LibreSSL faults + +- [X509_VERIFY_PARAM_set_hostflags and X509_VERIFY_PARAM_set1_ip_asc](../../../core/libs/prx/libSceLibreSsl/src/X509VerifyParam.cpp) are unverified on PS5. They follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509/x509_vpm.c) `x509_vpm.c` and the `a2i_ipadd` parser of its [`v3_utl.c`](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509v3/v3_utl.c), unchanged in 2.7, the first release whose symbols include `X509_VERIFY_PARAM_set1_host`, which the module also exports; IPv4 parts are read with the guest `sscanf` as the module would. LibreSSL 2.8 appends a `poisoned` field to the parameter identity, which these layouts lack. A null address text, and null verify parameters or identity once an address is accepted, throw where LibreSSL faults From e8d6c3fb477fe583c554efd13b6ba88752eecb91 Mon Sep 17 00:00:00 2001 From: JaceTheBlu Date: Sun, 25 Oct 2026 17:36:02 +0000 Subject: [PATCH 2/4] feat(libSceLibreSsl): implement X509_LOOKUP_file and X509_STORE_add_lookup Both exports threw and were declared without parameters. They follow LibreSSL 2.5.4, unchanged in 2.7: X509_LOOKUP_file returns the static 80-byte file method, and X509_STORE_add_lookup returns the store's lookup for a method, or mallocs a 32-byte lookup owned by the store and pushes it on the store's methods, growing the stack the way sk_insert does. X509_STORE_free now frees the store's lookups. The file method's ctrl, which loads PEM files, throws: there is no certificate model yet. Callbacks of guest-supplied methods throw instead of being called. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceLibreSsl/CMakeLists.txt | 14 ++ core/libs/prx/libSceLibreSsl/Export.cpp | 10 - core/libs/prx/libSceLibreSsl/include/Api.hpp | 25 ++- .../libs/prx/libSceLibreSsl/src/X509Store.cpp | 74 ++++++- .../prx/libSceLibreSsl/tests/X509Lookup.cpp | 180 ++++++++++++++++++ docs/dev/debt/libSceLibreSsl.md | 6 +- 6 files changed, 294 insertions(+), 15 deletions(-) create mode 100644 core/libs/prx/libSceLibreSsl/tests/X509Lookup.cpp diff --git a/core/libs/prx/libSceLibreSsl/CMakeLists.txt b/core/libs/prx/libSceLibreSsl/CMakeLists.txt index 5bedd7e5e2..646a79c2c1 100644 --- a/core/libs/prx/libSceLibreSsl/CMakeLists.txt +++ b/core/libs/prx/libSceLibreSsl/CMakeLists.txt @@ -109,3 +109,17 @@ if(BUILD_TESTING) "PATH=path_list_prepend:$") endif() endif() + +if(BUILD_TESTING) + add_test_executable(guest_libressl_x509_lookup_tests ${CMAKE_CURRENT_SOURCE_DIR}/tests/X509Lookup.cpp) + target_include_directories(guest_libressl_x509_lookup_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_x509_lookup_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_x509_lookup_tests) + add_test(NAME guest_libressl_x509_lookup COMMAND guest_libressl_x509_lookup_tests) + add_test(NAME guest_libressl_x509_lookup_allocator COMMAND guest_libressl_x509_lookup_tests allocator) + set_tests_properties(guest_libressl_x509_lookup guest_libressl_x509_lookup_allocator PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_x509_lookup guest_libressl_x509_lookup_allocator PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() +endif() diff --git a/core/libs/prx/libSceLibreSsl/Export.cpp b/core/libs/prx/libSceLibreSsl/Export.cpp index 6aec90b0ed..be7c72e7ed 100644 --- a/core/libs/prx/libSceLibreSsl/Export.cpp +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -1518,11 +1518,6 @@ int APS5_VABI X509_INFO_free_nid_postfix(void) { return 0; } -int APS5_VABI X509_LOOKUP_file_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI X509_NAME_ENTRY_get_data_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -1598,11 +1593,6 @@ int APS5_VABI X509_STORE_add_crl_nid_postfix(void) { return 0; } -int APS5_VABI X509_STORE_add_lookup_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI X509_VERIFY_PARAM_set1_host_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibreSsl/include/Api.hpp b/core/libs/prx/libSceLibreSsl/include/Api.hpp index a1ba0f9325..e2ea7f2a17 100644 --- a/core/libs/prx/libSceLibreSsl/include/Api.hpp +++ b/core/libs/prx/libSceLibreSsl/include/Api.hpp @@ -41,6 +41,27 @@ struct LibreSslX509Store { std::int32_t references; }; +struct LibreSslX509LookupMethod { + const char* name; + void* newItem; + void* free; + void* init; + void* shutdown; + void* ctrl; + void* getBySubject; + void* getByIssuerSerial; + void* getByFingerprint; + void* getByAlias; +}; + +struct LibreSslX509Lookup { + std::int32_t init; + std::int32_t skip; + LibreSslX509LookupMethod* method; + char* methodData; + LibreSslX509Store* storeContext; +}; + inline constexpr int MaxRounds = 14; struct AesKey { @@ -395,7 +416,7 @@ int APS5_VABI X509_EXTENSION_get_critical_nid_postfix(void); int APS5_VABI X509_EXTENSION_get_data_nid_postfix(void); int APS5_VABI X509_EXTENSION_get_object_nid_postfix(void); int APS5_VABI X509_INFO_free_nid_postfix(void); -int APS5_VABI X509_LOOKUP_file_nid_postfix(void); +LibreSslX509LookupMethod* APS5_VABI X509_LOOKUP_file_nid_postfix(void); int APS5_VABI X509_NAME_ENTRY_get_data_nid_postfix(void); int APS5_VABI X509_NAME_get_entry_nid_postfix(void); int APS5_VABI X509_NAME_get_index_by_NID_nid_postfix(void); @@ -411,7 +432,7 @@ int APS5_VABI X509_STORE_CTX_get_ex_data_nid_postfix(void); int APS5_VABI X509_STORE_CTX_set_error_nid_postfix(void); int APS5_VABI X509_STORE_add_cert_nid_postfix(void); int APS5_VABI X509_STORE_add_crl_nid_postfix(void); -int APS5_VABI X509_STORE_add_lookup_nid_postfix(void); +LibreSslX509Lookup* APS5_VABI X509_STORE_add_lookup_nid_postfix(LibreSslX509Store* store, LibreSslX509LookupMethod* method); void APS5_VABI X509_STORE_free_nid_postfix(LibreSslX509Store* store); LibreSslX509Store* APS5_VABI X509_STORE_new_nid_postfix(void); int APS5_VABI X509_STORE_set_flags_nid_postfix(LibreSslX509Store* store, std::uint64_t flags); diff --git a/core/libs/prx/libSceLibreSsl/src/X509Store.cpp b/core/libs/prx/libSceLibreSsl/src/X509Store.cpp index 13e5872e05..834550d0e0 100644 --- a/core/libs/prx/libSceLibreSsl/src/X509Store.cpp +++ b/core/libs/prx/libSceLibreSsl/src/X509Store.cpp @@ -24,6 +24,13 @@ static_assert(offsetof(LibreSslX509Store, param) == 24); static_assert(offsetof(LibreSslX509Store, callbacks) == 32); static_assert(offsetof(LibreSslX509Store, exData) == 120); static_assert(offsetof(LibreSslX509Store, references) == 128); +static_assert(sizeof(LibreSslX509LookupMethod) == 80); +static_assert(offsetof(LibreSslX509LookupMethod, shutdown) == 32); +static_assert(offsetof(LibreSslX509LookupMethod, ctrl) == 40); +static_assert(sizeof(LibreSslX509Lookup) == 32); +static_assert(offsetof(LibreSslX509Lookup, method) == 8); +static_assert(offsetof(LibreSslX509Lookup, methodData) == 16); +static_assert(offsetof(LibreSslX509Lookup, storeContext) == 24); namespace { @@ -51,6 +58,14 @@ int APS5_VABI CompareObjects(const void*, const void*) { return 0; } +int APS5_VABI LoadFile(LibreSslX509Lookup*, int, const char*, std::int64_t, char**) { + NotImplemented_nid_no_patch("X509_LOOKUP_file: loading certificates and CRLs from a file"); + return 0; +} + +LibreSslX509LookupMethod fileLookup{"Load file into cache", nullptr, nullptr, nullptr, nullptr, + reinterpret_cast(LoadFile), nullptr, nullptr, nullptr, nullptr}; + Stack* NewStack(Compare compare) { auto* stack = static_cast(malloc_nid_postfix(sizeof(Stack))); if (!stack) return nullptr; @@ -78,6 +93,29 @@ char** Elements(const Stack* stack, const char* function) { return stack->data; } +bool Push(Stack* stack, void* element, const char* function) { + if (!stack) return false; + if (stack->numAlloc <= stack->num + 1) { + if (stack->numAlloc <= 0 || stack->num < 0) NotImplemented_nid_no_patch(function); + auto* grown = static_cast(realloc_nid_postfix(stack->data, static_cast(stack->numAlloc) * 2 * sizeof(char*))); + if (!grown) return false; + stack->data = grown; + stack->numAlloc *= 2; + } + stack->data[stack->num] = static_cast(element); + ++stack->num; + stack->sorted = 0; + return true; +} + +void FreeLookup(LibreSslX509Lookup* lookup, bool shutdown, const char* function) { + if (!lookup) NotImplemented_nid_no_patch(function); + const auto* method = lookup->method; + if (shutdown && method && method->shutdown) NotImplemented_nid_no_patch("X509_LOOKUP shutdown callback"); + if (method && method->free) NotImplemented_nid_no_patch("X509_LOOKUP free callback"); + free_nid_postfix(lookup); +} + bool HoldsElements(const Stack* stack, const char* function) { if (!stack || stack->num <= 0) return false; char** const elements = Elements(stack, function); @@ -150,7 +188,11 @@ void APS5_VABI X509_STORE_free_nid_postfix(LibreSslX509Store* store) { if (!store) return; if (std::atomic_ref(store->references).fetch_sub(1) - 1 > 0) return; auto* lookupMethods = static_cast(store->lookupMethods); - if (lookupMethods && lookupMethods->num > 0) NotImplemented_nid_no_patch("X509_STORE_free: lookup methods"); + if (lookupMethods && lookupMethods->num > 0) { + char** const lookups = Elements(lookupMethods, __func__); + for (int index = 0; index < lookupMethods->num; ++index) + FreeLookup(reinterpret_cast(lookups[index]), true, __func__); + } FreeStack(lookupMethods); auto* objects = static_cast(store->objects); if (HoldsElements(objects, __func__)) NotImplemented_nid_no_patch("X509_STORE_free: stored certificates and CRLs"); @@ -167,4 +209,34 @@ int APS5_VABI X509_STORE_set_flags_nid_postfix(LibreSslX509Store* store, std::ui return 1; } +LibreSslX509LookupMethod* APS5_VABI X509_LOOKUP_file_nid_postfix(void) { + return &fileLookup; +} + +LibreSslX509Lookup* APS5_VABI X509_STORE_add_lookup_nid_postfix(LibreSslX509Store* store, LibreSslX509LookupMethod* method) { + if (!store) NotImplemented_nid_no_patch(__func__); + auto* lookupMethods = static_cast(store->lookupMethods); + if (lookupMethods && lookupMethods->num > 0) { + char** const lookups = Elements(lookupMethods, __func__); + for (int index = 0; index < lookupMethods->num; ++index) { + auto* lookup = reinterpret_cast(lookups[index]); + if (!lookup) NotImplemented_nid_no_patch(__func__); + if (lookup->method == method) return lookup; + } + } + auto* lookup = static_cast(malloc_nid_postfix(sizeof(LibreSslX509Lookup))); + if (!lookup) return nullptr; + lookup->init = 0; + lookup->skip = 0; + lookup->method = method; + lookup->methodData = nullptr; + lookup->storeContext = nullptr; + if (!method) NotImplemented_nid_no_patch(__func__); + if (method->newItem) NotImplemented_nid_no_patch("X509_LOOKUP new_item callback"); + lookup->storeContext = store; + if (Push(lookupMethods, lookup, __func__)) return lookup; + FreeLookup(lookup, false, __func__); + return nullptr; +} + } diff --git a/core/libs/prx/libSceLibreSsl/tests/X509Lookup.cpp b/core/libs/prx/libSceLibreSsl/tests/X509Lookup.cpp new file mode 100644 index 0000000000..15aefe0518 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/tests/X509Lookup.cpp @@ -0,0 +1,180 @@ +#include "prx/libSceLibreSsl/libSceLibreSsl.hpp" +#include "prx/libc/include/Api.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include +#include +#include +#include +#include +#include +#include + +namespace { + +struct Stack { + std::int32_t num; + char** data; + std::int32_t sorted; + std::int32_t numAlloc; + int (APS5_VABI* comp)(const void*, const void*); +}; + +using LookupControl = int (APS5_VABI*)(LibreSslX509Lookup*, int, const char*, std::int64_t, char**); + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "failed: %s\n", message); + std::abort(); +} + +template +void RequireThrows(TAction action, const char* message) { + try { + action(); + } catch (const std::exception&) { + return; + } + Require(false, message); +} + +Stack* Lookups(LibreSslX509Store* store) { + return static_cast(store->lookupMethods); +} + +void FileMethod() { + auto* method = X509_LOOKUP_file_nid_postfix(); + Require(method && method == X509_LOOKUP_file_nid_postfix(), "the file method is one static object"); + Require(std::strcmp(method->name, "Load file into cache") == 0, "file method name"); + Require(!method->newItem && !method->free && !method->init && !method->shutdown, "the file method has no lifecycle callbacks"); + Require(!method->getBySubject && !method->getByIssuerSerial && !method->getByFingerprint && !method->getByAlias, + "the file method has no lookup callbacks"); + Require(method->ctrl, "the file method has a control callback"); + auto control = reinterpret_cast(method->ctrl); + RequireThrows([&] { control(nullptr, 1, "certs.pem", 1, nullptr); }, "loading a file needs the certificate model"); +} + +void AddLookup() { + auto* store = X509_STORE_new_nid_postfix(); + auto* method = X509_LOOKUP_file_nid_postfix(); + auto* lookup = X509_STORE_add_lookup_nid_postfix(store, method); + Require(lookup && lookup->init == 0 && lookup->skip == 0 && lookup->method == method && lookup->methodData == nullptr + && lookup->storeContext == store, "a new lookup starts as X509_LOOKUP_new leaves it, owned by the store"); + Require(Lookups(store)->num == 1 && Lookups(store)->data[0] == reinterpret_cast(lookup) && Lookups(store)->sorted == 0, + "the lookup is pushed on the store's methods"); + Require(X509_STORE_add_lookup_nid_postfix(store, method) == lookup && Lookups(store)->num == 1, "the same method returns the same lookup"); + lookup->init = 1; + Require(X509_STORE_add_lookup_nid_postfix(store, method) == lookup && lookup->init == 1, "an existing lookup is returned unchanged"); + + std::array methods{}; + std::array added{}; + for (std::size_t index = 0; index < methods.size(); ++index) { + added[index] = X509_STORE_add_lookup_nid_postfix(store, &methods[index]); + Require(added[index] && added[index]->method == &methods[index], "each method gets its own lookup"); + } + Require(Lookups(store)->num == 6 && Lookups(store)->numAlloc == 8, "the stack doubles once four slots are not enough"); + for (std::size_t index = 0; index < methods.size(); ++index) + Require(X509_STORE_add_lookup_nid_postfix(store, &methods[index]) == added[index], "lookups are found after growth"); + Require(Lookups(store)->num == 6, "finding a lookup adds nothing"); + X509_STORE_free_nid_postfix(store); +} + +void Unsupported() { + auto* store = X509_STORE_new_nid_postfix(); + RequireThrows([] { X509_STORE_add_lookup_nid_postfix(nullptr, X509_LOOKUP_file_nid_postfix()); }, "null store"); + RequireThrows([&] { X509_STORE_add_lookup_nid_postfix(store, nullptr); }, "null method"); + LibreSslX509LookupMethod withNewItem{}; + withNewItem.newItem = reinterpret_cast(&Require); + RequireThrows([&] { X509_STORE_add_lookup_nid_postfix(store, &withNewItem); }, "a guest new_item callback"); + Require(Lookups(store)->num == 0, "a refused method adds no lookup"); + X509_STORE_free_nid_postfix(store); + + store = X509_STORE_new_nid_postfix(); + LibreSslX509LookupMethod withShutdown{}; + withShutdown.shutdown = reinterpret_cast(&Require); + Require(X509_STORE_add_lookup_nid_postfix(store, &withShutdown) != nullptr, "a method with only a shutdown callback is added"); + RequireThrows([&] { X509_STORE_free_nid_postfix(store); }, "a guest shutdown callback"); + + store = X509_STORE_new_nid_postfix(); + LibreSslX509LookupMethod withFree{}; + withFree.free = reinterpret_cast(&Require); + Require(X509_STORE_add_lookup_nid_postfix(store, &withFree) != nullptr, "a method with only a free callback is added"); + RequireThrows([&] { X509_STORE_free_nid_postfix(store); }, "a guest free callback"); + + store = X509_STORE_new_nid_postfix(); + Lookups(store)->num = 1; + RequireThrows([&] { X509_STORE_add_lookup_nid_postfix(store, X509_LOOKUP_file_nid_postfix()); }, "a null listed lookup"); +} + +int allocations = 0; +int frees = 0; +int reallocations = 0; +bool failRealloc = false; + +void* APS5_VABI Allocate_nid_no_patch(std::size_t size) { + ++allocations; + return std::malloc(size); +} + +void APS5_VABI Free_nid_no_patch(void* pointer) { + if (pointer) ++frees; + std::free(pointer); +} + +void* APS5_VABI Reallocate_nid_no_patch(void* pointer, std::size_t size) { + ++reallocations; + if (failRealloc) return nullptr; + return std::realloc(pointer, size); +} + +void* APS5_VABI Calloc_nid_no_patch(std::size_t count, std::size_t size) { return std::calloc(count, size); } +void* APS5_VABI Align_nid_no_patch(std::size_t, std::size_t) { throw std::runtime_error("unexpected aligned allocation"); } +void* APS5_VABI Realign_nid_no_patch(void*, std::size_t, std::size_t) { throw std::runtime_error("unexpected aligned reallocation"); } +int APS5_VABI PosixAlign_nid_no_patch(void**, std::size_t, std::size_t) { throw std::runtime_error("unexpected posix_memalign"); } + +void Allocator() { + std::array api{ + reinterpret_cast(Allocate_nid_no_patch), reinterpret_cast(Free_nid_no_patch), reinterpret_cast(Calloc_nid_no_patch), + reinterpret_cast(Reallocate_nid_no_patch), reinterpret_cast(Align_nid_no_patch), reinterpret_cast(Realign_nid_no_patch), + reinterpret_cast(PosixAlign_nid_no_patch)}; + ApplicationHeapRegister_nid_no_patch(api.data()); + std::array methods{}; + + auto* store = X509_STORE_new_nid_postfix(); + allocations = frees = reallocations = 0; + for (auto& method : methods) X509_STORE_add_lookup_nid_postfix(store, &method); + Require(allocations == 4 && reallocations == 1 && frees == 0, "four lookups are malloc'd and the fourth push grows the array"); + X509_STORE_free_nid_postfix(store); + Require(frees == 4 + 7, "freeing the store frees its lookups"); + + store = X509_STORE_new_nid_postfix(); + for (std::size_t index = 0; index < 3; ++index) X509_STORE_add_lookup_nid_postfix(store, &methods[index]); + allocations = frees = 0; + failRealloc = true; + Require(X509_STORE_add_lookup_nid_postfix(store, &methods[3]) == nullptr, "a failed growth fails the call"); + Require(allocations == 1 && frees == 1 && Lookups(store)->num == 3 && Lookups(store)->numAlloc == 4, "a failed growth frees the new lookup and keeps the stack"); + failRealloc = false; + X509_STORE_free_nid_postfix(store); + + store = X509_STORE_new_nid_postfix(); + auto* lookupMethods = Lookups(store); + store->lookupMethods = nullptr; + allocations = frees = 0; + Require(X509_STORE_add_lookup_nid_postfix(store, &methods[0]) == nullptr && allocations == 1 && frees == 1, + "a store without a method stack frees the new lookup"); + store->lookupMethods = lookupMethods; + X509_STORE_free_nid_postfix(store); +} + +} // namespace + +int main(int argc, char**) { + if (argc > 1) { + Allocator(); + return 0; + } + std::array api{}; + ApplicationHeapRegister_nid_no_patch(api.data()); + FileMethod(); + AddLookup(); + Unsupported(); +} diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index eca9d68a14..2c6086f6d1 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -2,7 +2,7 @@ ### Unknown function info -- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 342 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. +- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 340 named callable exports are listed in the [PS5 3.20 export table](https://github.com/DNNDHH/PS5-3.20_Libs/blob/b2a77c09cd71c01f34c790cfbe98c751a13262ad/libSceLibreSsl.c) and were cross-checked against the same module in PS5 firmware 13.60. Their signatures and behaviour are unverified; the declarations use the no-parameter unknown-signature convention and throw through `NotImplemented_nid_no_patch`. - [MD4, MD5, SHA1, SHA224, SHA256, SHA384 and SHA512 Init/Update/Final and SHA256](../../../core/libs/prx/libSceLibreSsl/src/Digest.cpp) (libSceLibreSsl) - signatures and context layouts follow the [LibreSSL headers](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha/sha.h), and buffering and `Final` follow its [sources](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/sha); nothing is checked on the console. Null contexts, null input with a nonzero length, null digest buffers in the 32-bit `Final` functions and a buffered byte count that is not below the block size, which LibreSSL does not check, throw - [DES_set_key, DES_ecb_encrypt, DES_set_odd_parity](../../../core/libs/prx/libSceLibreSsl/Export.cpp) (libSceLibreSsl) - the 128-byte `DES_key_schedule` holds the 8-byte key followed by zeros instead of LibreSSL's precomputed subkeys, so a title reading its contents sees different bytes; `DES_ecb_encrypt` derives the subkeys through mbedtls on each call. Not compared with the console. Results follow LibreSSL [des_key.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des_key.c) and [des.c](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/des/des.c): `DES_set_key` skips the parity and weak-key checks and returns 0, as it does while `DES_check_key` is 0 (that variable is not in the PS5 export inventory), a nonzero `enc` encrypts, and `DES_set_odd_parity` sets each byte's low bit for odd parity. Null pointers throw where LibreSSL faults - [X509_verify_cert_error_string](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - no console call is verified. The 64-bit argument, low-32-bit lookup, messages and shared numeric fallback follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509/x509_txt.c), the version identified in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware versions remain unverified. Unknown-code results share an overwritten static buffer and are not safe for concurrent use, as in that release; code 1 and codes above 54 use the numeric fallback. @@ -18,4 +18,6 @@ - [ASN1_INTEGER_get and ASN1_STRING_data/length/type/to_UTF8](../../../core/libs/prx/libSceLibreSsl/src/Asn1.cpp) are unverified on PS5. Signatures, the 24-byte guest string layout and conversions follow [LibreSSL 2.5.4's ASN.1 sources](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/asn1), the version reported for firmware 13.40 in [discussion #8981](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Null accessor inputs, null conversion outputs, missing nonempty data, oversized converted output and unrepresentable signed integer values throw where the source has undefined behaviour. Malformed encodings and allocation failure throw until ASN.1 error-queue reporting is implemented. Conversion buffers use the guest libc allocator and can be released with guest `free`. -- [X509_STORE_new, X509_STORE_free and X509_STORE_set_flags](../../../core/libs/prx/libSceLibreSsl/src/X509Store.cpp) are unverified on PS5. The 136-byte store, 64-byte verify parameters, 56-byte parameter identity, 32-byte stacks and the results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509) `x509_lu.c` and `x509_vpm.c`, the version in [firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). No export adds a certificate, CRL, lookup method or policy object, so `X509_STORE_free` throws instead of freeing one and the object cache's comparator throws. A null store or verify parameters, and a null element array behind a positive count, throw where LibreSSL faults +- [X509_STORE_new, X509_STORE_free and X509_STORE_set_flags](../../../core/libs/prx/libSceLibreSsl/src/X509Store.cpp) are unverified on PS5. The 136-byte store, 64-byte verify parameters, 56-byte parameter identity, 32-byte stacks and the results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509) `x509_lu.c` and `x509_vpm.c`, the version in [firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). No export adds a certificate, CRL or policy object, so `X509_STORE_free` throws instead of freeing one and the object cache's comparator throws. A null store or verify parameters, and a null element array behind a positive count, throw where LibreSSL faults + +- [X509_LOOKUP_file and X509_STORE_add_lookup](../../../core/libs/prx/libSceLibreSsl/src/X509Store.cpp) are unverified on PS5. The 80-byte method, the 32-byte lookup, the stack growth and the results follow LibreSSL 2.5.4 [`by_file.c`, `x509_lu.c`](https://github.com/libressl/openbsd/tree/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/x509) and `stack.c`, unchanged in 2.7. The file method's `ctrl`, which loads PEM certificates and CRLs, throws: there is no certificate model. Callbacks of a guest-supplied method (`new_item`, `shutdown`, `free`) throw instead of being called, and a null store, method or listed lookup throws where LibreSSL faults From 36c5e3126bed8b3624815d5777fa18e961ffe02c Mon Sep 17 00:00:00 2001 From: JaceTheBlu Date: Sun, 25 Oct 2026 21:12:16 +0000 Subject: [PATCH 3/4] fix(libSceLibreSsl): refuse unsupported lookup methods before allocating X509_STORE_add_lookup allocated the lookup before it threw for a null method or a guest new_item callback, so each caught refusal left one guest heap block behind. Both checks now run before the allocation, and the allocator test checks that a refused method allocates nothing. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libSceLibreSsl/src/X509Store.cpp | 5 ++--- core/libs/prx/libSceLibreSsl/tests/X509Lookup.cpp | 9 +++++++++ 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/core/libs/prx/libSceLibreSsl/src/X509Store.cpp b/core/libs/prx/libSceLibreSsl/src/X509Store.cpp index 834550d0e0..f37553c9fa 100644 --- a/core/libs/prx/libSceLibreSsl/src/X509Store.cpp +++ b/core/libs/prx/libSceLibreSsl/src/X509Store.cpp @@ -224,15 +224,14 @@ LibreSslX509Lookup* APS5_VABI X509_STORE_add_lookup_nid_postfix(LibreSslX509Stor if (lookup->method == method) return lookup; } } + if (!method) NotImplemented_nid_no_patch(__func__); + if (method->newItem) NotImplemented_nid_no_patch("X509_LOOKUP new_item callback"); auto* lookup = static_cast(malloc_nid_postfix(sizeof(LibreSslX509Lookup))); if (!lookup) return nullptr; lookup->init = 0; lookup->skip = 0; lookup->method = method; lookup->methodData = nullptr; - lookup->storeContext = nullptr; - if (!method) NotImplemented_nid_no_patch(__func__); - if (method->newItem) NotImplemented_nid_no_patch("X509_LOOKUP new_item callback"); lookup->storeContext = store; if (Push(lookupMethods, lookup, __func__)) return lookup; FreeLookup(lookup, false, __func__); diff --git a/core/libs/prx/libSceLibreSsl/tests/X509Lookup.cpp b/core/libs/prx/libSceLibreSsl/tests/X509Lookup.cpp index 15aefe0518..a0c8580cec 100644 --- a/core/libs/prx/libSceLibreSsl/tests/X509Lookup.cpp +++ b/core/libs/prx/libSceLibreSsl/tests/X509Lookup.cpp @@ -155,6 +155,15 @@ void Allocator() { failRealloc = false; X509_STORE_free_nid_postfix(store); + store = X509_STORE_new_nid_postfix(); + LibreSslX509LookupMethod withNewItem{}; + withNewItem.newItem = reinterpret_cast(&Require); + allocations = frees = 0; + RequireThrows([&] { X509_STORE_add_lookup_nid_postfix(store, nullptr); }, "null method"); + RequireThrows([&] { X509_STORE_add_lookup_nid_postfix(store, &withNewItem); }, "a guest new_item callback"); + Require(allocations == 0 && frees == 0 && Lookups(store)->num == 0, "a refused method allocates nothing"); + X509_STORE_free_nid_postfix(store); + store = X509_STORE_new_nid_postfix(); auto* lookupMethods = Lookups(store); store->lookupMethods = nullptr; From 323a528c5263529b82f79469415308d980115d97 Mon Sep 17 00:00:00 2001 From: JaceTheBlu Date: Mon, 26 Oct 2026 07:57:38 +0000 Subject: [PATCH 4/4] test(libSceLibreSsl): stop asserting that valid X509_STORE states throw The store test asserted that comparing two cached objects and freeing a store that holds certificates, lookup methods or policy objects throw. Those states are valid on the console and the throws are placeholders until the objects are modelled, so the assertions would fail once they are. The null store, missing verify parameters and null element array cases stay. Co-Authored-By: Claude Opus 5.5 --- .../prx/libSceLibreSsl/tests/X509Store.cpp | 24 +------------------ 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/core/libs/prx/libSceLibreSsl/tests/X509Store.cpp b/core/libs/prx/libSceLibreSsl/tests/X509Store.cpp index 28c4b10edb..df70b41cf4 100644 --- a/core/libs/prx/libSceLibreSsl/tests/X509Store.cpp +++ b/core/libs/prx/libSceLibreSsl/tests/X509Store.cpp @@ -61,8 +61,6 @@ void NewStore() { const auto* id = param->id; Require(id && id->hosts == nullptr && id->hostFlags == 0 && id->peerName == nullptr && id->email == nullptr && id->emailLength == 0 && id->ip == nullptr && id->ipLength == 0, "verify parameter identity starts zeroed"); - const auto* objects = static_cast(store->objects); - RequireThrows([&] { objects->comp(nullptr, nullptr); }, "object comparison needs the certificate model"); X509_STORE_free_nid_postfix(store); } @@ -118,29 +116,9 @@ void Identity() { void Unsupported() { auto* store = X509_STORE_new_nid_postfix(); - auto* objects = static_cast(store->objects); - int entry = 0; - objects->data[0] = reinterpret_cast(&entry); - objects->num = 1; - RequireThrows([&] { X509_STORE_free_nid_postfix(store); }, "stored objects need X509_OBJECT_free"); - - store = X509_STORE_new_nid_postfix(); - objects = static_cast(store->objects); - objects->num = 2; + static_cast(store->objects)->num = 2; X509_STORE_free_nid_postfix(store); - store = X509_STORE_new_nid_postfix(); - static_cast(store->lookupMethods)->num = 1; - RequireThrows([&] { X509_STORE_free_nid_postfix(store); }, "lookup methods need X509_LOOKUP_free"); - - store = X509_STORE_new_nid_postfix(); - auto* policies = static_cast(malloc_nid_postfix(sizeof(Stack))); - policies->data = static_cast(malloc_nid_postfix(sizeof(char*) * 4)); - policies->data[0] = reinterpret_cast(&entry); - policies->num = 1; - store->param->policies = policies; - RequireThrows([&] { X509_STORE_free_nid_postfix(store); }, "policy objects need ASN1_OBJECT_free"); - store = X509_STORE_new_nid_postfix(); static_cast(store->objects)->data = nullptr; static_cast(store->objects)->num = 1;