From 6a5e3b5ea8d9c3f5a4c8662347e82fc7ad23e0dd Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:27:08 +0200 Subject: [PATCH 01/11] feat(libSceLibreSsl): implement the BN_ BIGNUM, BN_CTX, conversion, word, BN_mul and BN_mod_inverse functions --- core/libs/prx/libSceLibreSsl/CMakeLists.txt | 16 + core/libs/prx/libSceLibreSsl/Export.cpp | 75 ---- core/libs/prx/libSceLibreSsl/src/Bignum.cpp | 393 ++++++++++++++++++++ 3 files changed, 409 insertions(+), 75 deletions(-) create mode 100644 core/libs/prx/libSceLibreSsl/CMakeLists.txt create mode 100644 core/libs/prx/libSceLibreSsl/src/Bignum.cpp diff --git a/core/libs/prx/libSceLibreSsl/CMakeLists.txt b/core/libs/prx/libSceLibreSsl/CMakeLists.txt new file mode 100644 index 0000000000..93602f14d8 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/CMakeLists.txt @@ -0,0 +1,16 @@ +add_library(libSceLibreSsl SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/Export.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Bignum.cpp) +target_include_directories(libSceLibreSsl PRIVATE ${LIBS_INCLUDE_DIR}) +configure_windows_unwind(libSceLibreSsl) + +if(BUILD_TESTING) + add_test_executable(guest_libressl_bn_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslBn.cpp) + target_include_directories(guest_libressl_bn_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_bn_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_bn_tests) + add_test(NAME guest_libressl_bn COMMAND guest_libressl_bn_tests) + set_tests_properties(guest_libressl_bn PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_bn 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 93b05e0fec..207d5d0753 100644 --- a/core/libs/prx/libSceLibreSsl/Export.cpp +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -141,86 +141,11 @@ int APS5_VABI BIO_write_nid_postfix(void) { return 0; } -int APS5_VABI BN_CTX_end_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI BN_CTX_free_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI BN_CTX_new_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI BN_CTX_start_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI BN_add_word_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI BN_bin2bn_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI BN_bn2bin_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI BN_free_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI BN_hex2bn_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI BN_mod_inverse_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI BN_mul_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI BN_new_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI BN_num_bits_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI BN_print_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI BN_set_word_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI BN_sub_word_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI CMAC_CTX_free_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibreSsl/src/Bignum.cpp b/core/libs/prx/libSceLibreSsl/src/Bignum.cpp new file mode 100644 index 0000000000..a23d2e20d5 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/Bignum.cpp @@ -0,0 +1,393 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include "prx/libc/include/General.hpp" + +extern "C" { +void* APS5_VABI malloc_nid_postfix(std::size_t size); +void* APS5_VABI calloc_nid_postfix(std::size_t count, std::size_t size); +void APS5_VABI free_nid_postfix(void* pointer); +} + +namespace { +struct Bignum { + std::uint64_t* d; + int top; + int dmax; + int neg; + int flags; +}; + +struct BignumContext { + std::vector marks; +}; + +static_assert(sizeof(Bignum) == 24); +static_assert(offsetof(Bignum, d) == 0); +static_assert(offsetof(Bignum, top) == 8); +static_assert(offsetof(Bignum, dmax) == 12); +static_assert(offsetof(Bignum, neg) == 16); +static_assert(offsetof(Bignum, flags) == 20); + +constexpr int FlagMalloced = 1; +constexpr int FlagStaticData = 2; + +using Mag = std::vector; + +struct Int { + Mag mag; + bool neg = false; +}; + +void Trim(Mag& m) { + while (!m.empty() && m.back() == 0) m.pop_back(); +} + +int Compare(const Mag& a, const Mag& b) { + if (a.size() != b.size()) return a.size() < b.size() ? -1 : 1; + for (std::size_t i = a.size(); i-- > 0;) { + if (a[i] != b[i]) return a[i] < b[i] ? -1 : 1; + } + return 0; +} + +Mag AddMag(const Mag& a, const Mag& b) { + const std::size_t count = a.size() > b.size() ? a.size() : b.size(); + Mag r; + r.reserve(count + 1); + std::uint64_t carry = 0; + for (std::size_t i = 0; i < count; i++) { + const unsigned __int128 sum = static_cast(i < a.size() ? a[i] : 0) + (i < b.size() ? b[i] : 0) + carry; + r.push_back(static_cast(sum)); + carry = static_cast(sum >> 64); + } + if (carry) r.push_back(carry); + return r; +} + +Mag SubMag(const Mag& a, const Mag& b) { + Mag r(a.size()); + std::uint64_t borrow = 0; + for (std::size_t i = 0; i < a.size(); i++) { + const std::uint64_t x = a[i]; + const std::uint64_t y = i < b.size() ? b[i] : 0; + const std::uint64_t diff = x - y; + const std::uint64_t first = x < y; + r[i] = diff - borrow; + borrow = first | (diff < borrow); + } + Trim(r); + return r; +} + +Mag MulMag(const Mag& a, const Mag& b) { + if (a.empty() || b.empty()) return {}; + Mag r(a.size() + b.size(), 0); + for (std::size_t i = 0; i < a.size(); i++) { + std::uint64_t carry = 0; + for (std::size_t j = 0; j < b.size(); j++) { + const unsigned __int128 product = static_cast(a[i]) * b[j] + r[i + j] + carry; + r[i + j] = static_cast(product); + carry = static_cast(product >> 64); + } + r[i + b.size()] = carry; + } + Trim(r); + return r; +} + +std::size_t BitLength(const Mag& m) { + if (m.empty()) return 0; + return (m.size() - 1) * 64 + std::bit_width(m.back()); +} + +Mag ShiftLeft(const Mag& m, std::size_t bits) { + if (m.empty()) return {}; + const std::size_t words = bits / 64; + const unsigned rest = static_cast(bits % 64); + Mag r(m.size() + words + 1, 0); + for (std::size_t i = 0; i < m.size(); i++) { + r[i + words] |= m[i] << rest; + if (rest) r[i + words + 1] |= m[i] >> (64 - rest); + } + Trim(r); + return r; +} + +void ShiftRightOne(Mag& m) { + for (std::size_t i = 0; i < m.size(); i++) { + m[i] >>= 1; + if (i + 1 < m.size()) m[i] |= m[i + 1] << 63; + } + Trim(m); +} + +void DivMod(const Mag& a, const Mag& b, Mag& quotient, Mag& remainder) { + remainder = a; + quotient.clear(); + const std::size_t aBits = BitLength(a); + const std::size_t bBits = BitLength(b); + if (aBits < bBits) return; + const std::size_t shift = aBits - bBits; + Mag divisor = ShiftLeft(b, shift); + quotient.assign(shift / 64 + 1, 0); + for (std::size_t s = shift + 1; s-- > 0;) { + if (Compare(remainder, divisor) >= 0) { + remainder = SubMag(remainder, divisor); + quotient[s / 64] |= std::uint64_t(1) << (s % 64); + } + ShiftRightOne(divisor); + } + Trim(quotient); +} + +Int AddSigned(const Int& x, const Int& y) { + Int r; + if (x.neg == y.neg) { + r.mag = AddMag(x.mag, y.mag); + r.neg = x.neg; + } else { + const int order = Compare(x.mag, y.mag); + if (order > 0) { + r.mag = SubMag(x.mag, y.mag); + r.neg = x.neg; + } else if (order < 0) { + r.mag = SubMag(y.mag, x.mag); + r.neg = y.neg; + } + } + if (r.mag.empty()) r.neg = false; + return r; +} + +bool InverseMod(const Int& a, const Mag& n, Mag& out) { + if (n.size() == 1 && n[0] == 1) { + out.clear(); + return true; + } + Mag quotient; + Mag r1; + DivMod(a.mag, n, quotient, r1); + if (a.neg && !r1.empty()) r1 = SubMag(n, r1); + Mag r0 = n; + Int t0; + Int t1; + t1.mag = {1}; + while (!r1.empty()) { + Mag remainder; + DivMod(r0, r1, quotient, remainder); + Int scaled; + scaled.mag = MulMag(quotient, t1.mag); + scaled.neg = !t1.neg && !scaled.mag.empty(); + Int t2 = AddSigned(t0, scaled); + r0 = std::move(r1); + r1 = std::move(remainder); + t0 = std::move(t1); + t1 = std::move(t2); + } + if (r0.size() != 1 || r0[0] != 1) return false; + out = t0.neg ? SubMag(n, t0.mag) : t0.mag; + return true; +} + +Bignum* Checked(Bignum* value, const char* function) { + if (!value) throw std::invalid_argument(std::string(function) + ": null BIGNUM"); + return value; +} + +const Bignum* Checked(const Bignum* value, const char* function) { + if (!value) throw std::invalid_argument(std::string(function) + ": null BIGNUM"); + return value; +} + +Int Load(const Bignum* bn) { + Int r; + if (bn->top > 0) r.mag.assign(bn->d, bn->d + bn->top); + Trim(r.mag); + r.neg = bn->neg && !r.mag.empty(); + return r; +} + +bool Store(Bignum* bn, const Mag& m, bool neg) { + if (m.empty()) { + bn->top = 0; + bn->neg = 0; + return true; + } + const std::size_t count = m.size(); + if (!bn->d || static_cast(bn->dmax) < count) { + if (bn->flags & FlagStaticData) return false; + if (count > 0x7FFFFFFF) return false; + auto* grown = static_cast(calloc_nid_postfix(count, sizeof(std::uint64_t))); + if (!grown) return false; + if (bn->d) free_nid_postfix(bn->d); + bn->d = grown; + bn->dmax = static_cast(count); + } + for (std::size_t i = 0; i < count; i++) bn->d[i] = m[i]; + for (std::size_t i = count; i < static_cast(bn->dmax); i++) bn->d[i] = 0; + bn->top = static_cast(count); + bn->neg = neg ? 1 : 0; + return true; +} + +Bignum* NewBignum() { + auto* bn = static_cast(calloc_nid_postfix(1, sizeof(Bignum))); + if (bn) bn->flags = FlagMalloced; + return bn; +} + +void FreeBignum(Bignum* bn) { + if (!bn) return; + if (bn->d && !(bn->flags & FlagStaticData)) free_nid_postfix(bn->d); + if (bn->flags & FlagMalloced) { + free_nid_postfix(bn); + } else { + bn->d = nullptr; + } +} + +int HexValue(char c) { + if (c >= '0' && c <= '9') return c - '0'; + if (c >= 'a' && c <= 'f') return c - 'a' + 10; + if (c >= 'A' && c <= 'F') return c - 'A' + 10; + return -1; +} + +bool ApplyWord(Bignum* a, std::uint64_t w, bool subtract) { + Int word; + if (w) word.mag = {w}; + word.neg = subtract && w; + const Int sum = AddSigned(Load(a), word); + return Store(a, sum.mag, sum.neg); +} +} + +extern "C" { +Bignum* APS5_VABI BN_new_nid_postfix(void) { + return NewBignum(); +} + +void APS5_VABI BN_free_nid_postfix(Bignum* a) { + FreeBignum(a); +} + +BignumContext* APS5_VABI BN_CTX_new_nid_postfix(void) { + return new (std::nothrow) BignumContext(); +} + +void APS5_VABI BN_CTX_free_nid_postfix(BignumContext* ctx) { + delete ctx; +} + +void APS5_VABI BN_CTX_start_nid_postfix(BignumContext* ctx) { + if (!ctx) throw std::invalid_argument(std::string(__func__) + ": null context"); + ctx->marks.push_back(0); +} + +void APS5_VABI BN_CTX_end_nid_postfix(BignumContext* ctx) { + if (!ctx) throw std::invalid_argument(std::string(__func__) + ": null context"); + if (!ctx->marks.empty()) ctx->marks.pop_back(); +} + +Bignum* APS5_VABI BN_bin2bn_nid_postfix(const unsigned char* s, int len, Bignum* ret) { + if (len < 0) return nullptr; + if (len > 0 && !s) throw std::invalid_argument(std::string(__func__) + ": null input"); + Bignum* target = ret ? ret : NewBignum(); + if (!target) return nullptr; + Mag m((static_cast(len) + 7) / 8, 0); + for (int i = 0; i < len; i++) { + const std::size_t bit = static_cast(len - 1 - i); + m[bit / 8] |= static_cast(s[i]) << (bit % 8 * 8); + } + Trim(m); + if (!Store(target, m, false)) { + if (!ret) FreeBignum(target); + return nullptr; + } + return target; +} + +int APS5_VABI BN_num_bits_nid_postfix(const Bignum* a) { + return static_cast(BitLength(Load(Checked(a, __func__)).mag)); +} + +int APS5_VABI BN_bn2bin_nid_postfix(const Bignum* a, unsigned char* to) { + const Mag m = Load(Checked(a, __func__)).mag; + const std::size_t length = (BitLength(m) + 7) / 8; + if (length && !to) throw std::invalid_argument(std::string(__func__) + ": null output"); + for (std::size_t i = 0; i < length; i++) { + const std::size_t bit = length - 1 - i; + to[i] = static_cast(m[bit / 8] >> (bit % 8 * 8)); + } + return static_cast(length); +} + +int APS5_VABI BN_hex2bn_nid_postfix(Bignum** a, const char* str) { + if (!str || !*str) return 0; + const bool negative = *str == '-'; + const char* digits = str + negative; + std::size_t count = 0; + while (HexValue(digits[count]) >= 0) count++; + if (count == 0 || count > 0x7FFFFFFF / 4) return 0; + const int consumed = static_cast(count + negative); + if (!a) return consumed; + Mag m((count + 15) / 16, 0); + for (std::size_t i = 0; i < count; i++) { + const std::size_t nibble = count - 1 - i; + m[nibble / 16] |= static_cast(HexValue(digits[i])) << (nibble % 16 * 4); + } + Trim(m); + Bignum* target = *a ? *a : NewBignum(); + if (!target) return 0; + if (!Store(target, m, negative)) { + if (!*a) FreeBignum(target); + return 0; + } + *a = target; + return consumed; +} + +int APS5_VABI BN_set_word_nid_postfix(Bignum* a, std::uint64_t w) { + Checked(a, __func__); + Mag m; + if (w) m = {w}; + return Store(a, m, false); +} + +int APS5_VABI BN_add_word_nid_postfix(Bignum* a, std::uint64_t w) { + return ApplyWord(Checked(a, __func__), w, false); +} + +int APS5_VABI BN_sub_word_nid_postfix(Bignum* a, std::uint64_t w) { + return ApplyWord(Checked(a, __func__), w, true); +} + +int APS5_VABI BN_mul_nid_postfix(Bignum* r, const Bignum* a, const Bignum* b, BignumContext*) { + Checked(r, __func__); + const Int x = Load(Checked(a, __func__)); + const Int y = Load(Checked(b, __func__)); + const Mag product = MulMag(x.mag, y.mag); + return Store(r, product, x.neg != y.neg); +} + +Bignum* APS5_VABI BN_mod_inverse_nid_postfix(Bignum* r, const Bignum* a, const Bignum* n, BignumContext*) { + const Int value = Load(Checked(a, __func__)); + const Int modulus = Load(Checked(n, __func__)); + if (modulus.mag.empty()) return nullptr; + Mag inverse; + if (!InverseMod(value, modulus.mag, inverse)) return nullptr; + Bignum* target = r ? r : NewBignum(); + if (!target) return nullptr; + if (!Store(target, inverse, false)) { + if (!r) FreeBignum(target); + return nullptr; + } + return target; +} +} From fd3853d5c36856a0ea865c94ecdeca2dffd8bbba Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:27:08 +0200 Subject: [PATCH 02/11] test(libSceLibreSsl): cover the BN_ functions with known-answer vectors --- core/libs/tests/GuestLibreSslBn.cpp | 215 ++++++++++++++++++++++++++++ 1 file changed, 215 insertions(+) create mode 100644 core/libs/tests/GuestLibreSslBn.cpp diff --git a/core/libs/tests/GuestLibreSslBn.cpp b/core/libs/tests/GuestLibreSslBn.cpp new file mode 100644 index 0000000000..88bd8b96fa --- /dev/null +++ b/core/libs/tests/GuestLibreSslBn.cpp @@ -0,0 +1,215 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include + +struct Bn { + std::uint64_t* d; + int top; + int dmax; + int neg; + int flags; +}; + +struct BnCtx; + +extern "C" { +Bn* APS5_VABI BN_new_nid_postfix(void); +void APS5_VABI BN_free_nid_postfix(Bn*); +BnCtx* APS5_VABI BN_CTX_new_nid_postfix(void); +void APS5_VABI BN_CTX_free_nid_postfix(BnCtx*); +void APS5_VABI BN_CTX_start_nid_postfix(BnCtx*); +void APS5_VABI BN_CTX_end_nid_postfix(BnCtx*); +Bn* APS5_VABI BN_bin2bn_nid_postfix(const unsigned char*, int, Bn*); +int APS5_VABI BN_bn2bin_nid_postfix(const Bn*, unsigned char*); +int APS5_VABI BN_hex2bn_nid_postfix(Bn**, const char*); +int APS5_VABI BN_num_bits_nid_postfix(const Bn*); +int APS5_VABI BN_set_word_nid_postfix(Bn*, std::uint64_t); +int APS5_VABI BN_add_word_nid_postfix(Bn*, std::uint64_t); +int APS5_VABI BN_sub_word_nid_postfix(Bn*, std::uint64_t); +int APS5_VABI BN_mul_nid_postfix(Bn*, const Bn*, const Bn*, BnCtx*); +Bn* APS5_VABI BN_mod_inverse_nid_postfix(Bn*, const Bn*, const Bn*, BnCtx*); +} + +static_assert(sizeof(Bn) == 24); +static_assert(offsetof(Bn, top) == 8); +static_assert(offsetof(Bn, neg) == 16); + +static void Require(bool value) { if (!value) std::abort(); } + +static Bn* FromHex(const char* text) { + Bn* value = nullptr; + Require(BN_hex2bn_nid_postfix(&value, text) > 0 && value); + return value; +} + +static std::string Hex(const Bn* value) { + unsigned char bytes[64]; + const int length = BN_bn2bin_nid_postfix(value, bytes); + Require(length >= 0 && length <= 64); + static constexpr char digits[] = "0123456789ABCDEF"; + std::string text; + for (int i = 0; i < length; i++) { + text += digits[bytes[i] >> 4]; + text += digits[bytes[i] & 15]; + } + while (text.size() > 1 && text[0] == '0') text.erase(0, 1); + if (text.empty()) text = "0"; + return value->neg ? "-" + text : text; +} + +int main() { + Bn* fresh = BN_new_nid_postfix(); + Require(fresh && fresh->top == 0 && fresh->neg == 0 && BN_num_bits_nid_postfix(fresh) == 0); + BN_free_nid_postfix(fresh); + BN_free_nid_postfix(nullptr); + + BnCtx* ctx = BN_CTX_new_nid_postfix(); + Require(ctx); + BN_CTX_start_nid_postfix(ctx); + BN_CTX_end_nid_postfix(ctx); + BN_CTX_free_nid_postfix(ctx); + BN_CTX_free_nid_postfix(nullptr); + ctx = BN_CTX_new_nid_postfix(); + Require(ctx); + + { + Bn* a = FromHex("FFFFFFFFFFFFFFFF"); + Bn* b = FromHex("FFFFFFFFFFFFFFFF"); + Bn* r = BN_new_nid_postfix(); + Require(BN_mul_nid_postfix(r, a, b, ctx) == 1); + Require(Hex(r) == "FFFFFFFFFFFFFFFE0000000000000001"); + Require(r->top == 2 && r->neg == 0); + BN_free_nid_postfix(a); + BN_free_nid_postfix(b); + BN_free_nid_postfix(r); + } + { + Bn* a = FromHex("0123456789ABCDEFFEDCBA98765432100F1E2D3C4B5A6978"); + Bn* b = FromHex("00FF00FF00FF00FF00FF00FF00FF00FF00FF"); + Bn* r = BN_new_nid_postfix(); + Require(BN_mul_nid_postfix(r, a, b, ctx) == 1); + Require(Hex(r) == "12223444566678876665444322210000F0F1CFC09E8F6D55CB34DD56FF79219B43BB52CA61D970E88"); + Require(BN_mul_nid_postfix(a, a, a, ctx) == 1); + Require(Hex(a) == "14B66DC33F6ACDEE9D609EBE253A6424AD884E9A04269A9706601BB096443363E2CE44634913DF9C3B9FAA3D3A840"); + Bn* c = FromHex("-3"); + Bn* d = FromHex("5"); + Require(BN_mul_nid_postfix(d, c, d, ctx) == 1 && Hex(d) == "-F" && d->neg == 1); + Require(BN_mul_nid_postfix(c, c, c, ctx) == 1 && Hex(c) == "9" && c->neg == 0); + Bn* zero = FromHex("0"); + Require(BN_mul_nid_postfix(d, d, zero, ctx) == 1 && d->top == 0 && d->neg == 0); + BN_free_nid_postfix(a); + BN_free_nid_postfix(b); + BN_free_nid_postfix(c); + BN_free_nid_postfix(d); + BN_free_nid_postfix(r); + BN_free_nid_postfix(zero); + } + { + const unsigned char bytes[] = {0x00, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09}; + Bn* value = BN_bin2bn_nid_postfix(bytes, sizeof(bytes), nullptr); + Require(value && value->neg == 0 && value->top == 2); + Require(Hex(value) == "10203040506070809"); + unsigned char out[16]; + std::memset(out, 0xEE, sizeof(out)); + Require(BN_bn2bin_nid_postfix(value, out) == 9); + Require(std::memcmp(out, bytes + 2, 9) == 0 && out[9] == 0xEE); + Require(BN_bin2bn_nid_postfix(bytes, 3, value) == value && Hex(value) == "102" && value->top == 1); + const unsigned char zeros[] = {0, 0, 0}; + Require(BN_bin2bn_nid_postfix(zeros, 3, value) == value && value->top == 0); + std::memset(out, 0xEE, sizeof(out)); + Require(BN_bn2bin_nid_postfix(value, out) == 0 && out[0] == 0xEE); + Require(BN_bin2bn_nid_postfix(bytes, 0, value) == value && value->top == 0); + Require(BN_bin2bn_nid_postfix(bytes, -1, value) == nullptr); + BN_free_nid_postfix(value); + } + { + Bn* value = BN_new_nid_postfix(); + Require(BN_num_bits_nid_postfix(value) == 0); + Require(BN_set_word_nid_postfix(value, 1) == 1 && BN_num_bits_nid_postfix(value) == 1); + Require(BN_set_word_nid_postfix(value, 0x100) == 1 && BN_num_bits_nid_postfix(value) == 9); + Bn* big = FromHex("10000000000000000"); + Require(BN_num_bits_nid_postfix(big) == 65 && big->top == 2); + Require(BN_set_word_nid_postfix(big, 0) == 1 && BN_num_bits_nid_postfix(big) == 0 && big->top == 0 && big->neg == 0); + Require(BN_set_word_nid_postfix(value, 0xFFFFFFFFFFFFFFFFull) == 1 && BN_num_bits_nid_postfix(value) == 64); + BN_free_nid_postfix(value); + BN_free_nid_postfix(big); + } + { + Bn* value = nullptr; + Require(BN_hex2bn_nid_postfix(&value, "-1F") == 3 && value); + Require(value->neg == 1 && value->top == 1 && value->d[0] == 0x1F); + Bn* same = value; + Require(BN_hex2bn_nid_postfix(&value, "ABC") == 3 && value == same); + Require(value->neg == 0 && value->d[0] == 0xABC); + Require(BN_hex2bn_nid_postfix(&value, "FFzz") == 2 && value->d[0] == 0xFF); + Require(BN_hex2bn_nid_postfix(&value, "") == 0 && value->d[0] == 0xFF); + Require(BN_hex2bn_nid_postfix(&value, "zz") == 0 && BN_hex2bn_nid_postfix(&value, "-") == 0); + Require(BN_hex2bn_nid_postfix(&value, "-0") == 2 && value->top == 0 && value->neg == 0); + Require(BN_hex2bn_nid_postfix(&value, "deadBEEF0123456789abcdef") == 24); + Require(Hex(value) == "DEADBEEF0123456789ABCDEF" && value->top == 2); + Require(BN_hex2bn_nid_postfix(nullptr, "12G") == 2); + BN_free_nid_postfix(value); + } + { + Bn* value = FromHex("FFFFFFFFFFFFFFFF"); + Require(BN_add_word_nid_postfix(value, 1) == 1); + Require(value->top == 2 && value->d[0] == 0 && value->d[1] == 1 && Hex(value) == "10000000000000000"); + Require(BN_sub_word_nid_postfix(value, 1) == 1); + Require(value->top == 1 && value->d[0] == 0xFFFFFFFFFFFFFFFFull && value->neg == 0); + Require(BN_set_word_nid_postfix(value, 3) == 1); + Require(BN_sub_word_nid_postfix(value, 5) == 1 && value->neg == 1 && value->top == 1 && value->d[0] == 2); + Require(BN_add_word_nid_postfix(value, 2) == 1 && value->top == 0 && value->neg == 0); + Require(BN_sub_word_nid_postfix(value, 7) == 1 && value->neg == 1 && Hex(value) == "-7"); + Require(BN_add_word_nid_postfix(value, 10) == 1 && value->neg == 0 && Hex(value) == "3"); + Require(BN_sub_word_nid_postfix(value, 0) == 1 && Hex(value) == "3"); + Bn* wide = FromHex("-10000000000000000"); + Require(BN_sub_word_nid_postfix(wide, 1) == 1 && wide->neg == 1 && Hex(wide) == "-10000000000000001"); + Require(BN_add_word_nid_postfix(wide, 2) == 1 && wide->neg == 1 && Hex(wide) == "-FFFFFFFFFFFFFFFF" && wide->top == 1); + Bn* chain = FromHex("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"); + Require(BN_add_word_nid_postfix(chain, 1) == 1 && chain->top == 3 && Hex(chain) == "100000000000000000000000000000000"); + Require(BN_sub_word_nid_postfix(chain, 1) == 1 && chain->top == 2 && Hex(chain) == "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"); + BN_free_nid_postfix(value); + BN_free_nid_postfix(wide); + BN_free_nid_postfix(chain); + } + { + Bn* r = BN_new_nid_postfix(); + Bn* three = FromHex("3"); + Bn* eleven = FromHex("B"); + Require(BN_mod_inverse_nid_postfix(r, three, eleven, ctx) == r && Hex(r) == "4"); + Bn* seven = FromHex("7"); + Bn* forty = FromHex("28"); + Require(BN_mod_inverse_nid_postfix(r, seven, forty, ctx) == r && Hex(r) == "17"); + Bn* four = FromHex("4"); + Bn* eight = FromHex("8"); + Require(BN_mod_inverse_nid_postfix(r, four, eight, ctx) == nullptr); + Bn* allocated = BN_mod_inverse_nid_postfix(nullptr, three, eleven, ctx); + Require(allocated && Hex(allocated) == "4"); + Bn* minusThree = FromHex("-3"); + Require(BN_mod_inverse_nid_postfix(r, minusThree, eleven, ctx) == r && Hex(r) == "7"); + Bn* fifteen = FromHex("F"); + Require(BN_mod_inverse_nid_postfix(r, fifteen, eleven, ctx) == r && Hex(r) == "3"); + Bn* inPlace = FromHex("3"); + Require(BN_mod_inverse_nid_postfix(inPlace, inPlace, eleven, ctx) == inPlace && Hex(inPlace) == "4"); + + Bn* prime = FromHex("7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF"); + Bn* a = FromHex("123456789ABCDEF0FEDCBA9876543210"); + Bn* inverse = BN_mod_inverse_nid_postfix(nullptr, a, prime, ctx); + Require(inverse && Hex(inverse) == "62CFEFE6EB41053969967BD711C5169F"); + Bn* product = BN_new_nid_postfix(); + Require(BN_mul_nid_postfix(product, a, inverse, ctx) == 1); + Bn* quotient = FromHex("E0DA4F6BE78BFD74B8C9F95AF178811"); + Bn* check = BN_new_nid_postfix(); + Require(BN_mul_nid_postfix(check, quotient, prime, ctx) == 1); + Require(BN_add_word_nid_postfix(check, 1) == 1); + Require(Hex(check) == Hex(product)); + + Bn* all[] = {r, three, eleven, seven, forty, four, eight, allocated, minusThree, fifteen, inPlace, prime, a, inverse, product, quotient, check}; + for (Bn* value : all) BN_free_nid_postfix(value); + } + BN_CTX_free_nid_postfix(ctx); + return 0; +} From d7369cbcb411531ed332efb26e2ce6a28f3b0f04 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:27:08 +0200 Subject: [PATCH 03/11] docs(libSceLibreSsl): record the BN_ functions and their limits --- docs/dev/debt/libSceLibreSsl.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index b8330484a5..4562c741ef 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -2,4 +2,5 @@ ### Unknown function info -- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 399 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) - 384 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`. +- [BN_new, BN_free, BN_CTX_new, BN_CTX_free, BN_CTX_start, BN_CTX_end, BN_bin2bn, BN_bn2bin, BN_hex2bn, BN_num_bits, BN_set_word, BN_add_word, BN_sub_word, BN_mul and BN_mod_inverse](../../../core/libs/prx/libSceLibreSsl/src/Bignum.cpp) (libSceLibreSsl) - the `BIGNUM` layout (24 bytes, 64-bit limbs, `top`, `dmax`, `neg`, `flags`) and the documented semantics follow the public LibreSSL 2.x API; the module's LibreSSL version is unknown and nothing is checked on the console. `BN_CTX` is opaque and only counts `BN_CTX_start` and `BN_CTX_end`, since `BN_CTX_get` is not implemented, and `BN_mul` and `BN_mod_inverse` ignore it. `BN_mod_inverse` treats a negative modulus by its magnitude and returns 0 for a modulus of 1, which is not compared with LibreSSL. `BN_hex2bn` rejects a string without a hex digit, such as `-`, which older LibreSSL versions accept. Null `BIGNUM` pointers, which LibreSSL does not check, throw. `BN_print` still throws because it needs a `BIO`. From 2710c1218f51c048d9b4d7c0df144b4af5f5082b Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:54:36 +0200 Subject: [PATCH 04/11] test(libSceLibreSsl): fix the BN_bin2bn reuse case and report the failing line --- core/libs/tests/GuestLibreSslBn.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/core/libs/tests/GuestLibreSslBn.cpp b/core/libs/tests/GuestLibreSslBn.cpp index 88bd8b96fa..20e711cb89 100644 --- a/core/libs/tests/GuestLibreSslBn.cpp +++ b/core/libs/tests/GuestLibreSslBn.cpp @@ -1,6 +1,7 @@ #include "prx/libc/include/general/VabiMacros.hpp" #include #include +#include #include #include #include @@ -37,7 +38,12 @@ static_assert(sizeof(Bn) == 24); static_assert(offsetof(Bn, top) == 8); static_assert(offsetof(Bn, neg) == 16); -static void Require(bool value) { if (!value) std::abort(); } +static void Require(bool value, int line = __builtin_LINE()) { + if (value) return; + std::fprintf(stderr, "check failed at line %d +", line); + std::abort(); +} static Bn* FromHex(const char* text) { Bn* value = nullptr; @@ -116,7 +122,7 @@ int main() { std::memset(out, 0xEE, sizeof(out)); Require(BN_bn2bin_nid_postfix(value, out) == 9); Require(std::memcmp(out, bytes + 2, 9) == 0 && out[9] == 0xEE); - Require(BN_bin2bn_nid_postfix(bytes, 3, value) == value && Hex(value) == "102" && value->top == 1); + Require(BN_bin2bn_nid_postfix(bytes, 4, value) == value && Hex(value) == "102" && value->top == 1); const unsigned char zeros[] = {0, 0, 0}; Require(BN_bin2bn_nid_postfix(zeros, 3, value) == value && value->top == 0); std::memset(out, 0xEE, sizeof(out)); From 8d01bfd4a55a7285c546db4f4f0a60a2521069d9 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:57:53 +0200 Subject: [PATCH 05/11] test(libSceLibreSsl): terminate the failing-line message in the BN_ test --- core/libs/tests/GuestLibreSslBn.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/core/libs/tests/GuestLibreSslBn.cpp b/core/libs/tests/GuestLibreSslBn.cpp index 20e711cb89..bad5c24e86 100644 --- a/core/libs/tests/GuestLibreSslBn.cpp +++ b/core/libs/tests/GuestLibreSslBn.cpp @@ -40,8 +40,7 @@ static_assert(offsetof(Bn, neg) == 16); static void Require(bool value, int line = __builtin_LINE()) { if (value) return; - std::fprintf(stderr, "check failed at line %d -", line); + std::fprintf(stderr, "check failed at line %d\n", line); std::abort(); } From 5c3dc05f0555b066beab1184f666829d3f4af780 Mon Sep 17 00:00:00 2001 From: AlxcBTW <187763517+AlxcBTW@users.noreply.github.com> Date: Thu, 22 Oct 2026 00:02:23 +0200 Subject: [PATCH 06/11] test(libSceLibreSsl): register the application heap before the BN_ test allocates --- core/libs/tests/GuestLibreSslBn.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/core/libs/tests/GuestLibreSslBn.cpp b/core/libs/tests/GuestLibreSslBn.cpp index bad5c24e86..fbfce42040 100644 --- a/core/libs/tests/GuestLibreSslBn.cpp +++ b/core/libs/tests/GuestLibreSslBn.cpp @@ -1,9 +1,11 @@ +#include "prx/libc/include/ApplicationHeap.hpp" #include "prx/libc/include/general/VabiMacros.hpp" #include #include #include #include #include +#include #include struct Bn { @@ -65,7 +67,13 @@ static std::string Hex(const Bn* value) { return value->neg ? "-" + text : text; } +static void InitializeHeap() { + void* const api[10] {}; + ApplicationHeapRegister_nid_no_patch(api); +} + int main() { + InitializeHeap(); Bn* fresh = BN_new_nid_postfix(); Require(fresh && fresh->top == 0 && fresh->neg == 0 && BN_num_bits_nid_postfix(fresh) == 0); BN_free_nid_postfix(fresh); From 0b6880e4d07b01e007f33d7d103eb0ce0c09176f Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Wed, 21 Oct 2026 21:12:10 -0300 Subject: [PATCH 07/11] feat(libSceLibreSsl): implement memory BIOs --- core/libs/CMakeLists.txt | 1 + core/libs/prx/libSceLibreSsl/Export.cpp | 36 +- core/libs/prx/libSceLibreSsl/MemoryBio.cmake | 30 ++ core/libs/prx/libSceLibreSsl/include/Bio.hpp | 72 +++ core/libs/prx/libSceLibreSsl/src/Bio.cpp | 287 +++++++++++ core/libs/tests/GuestLibreSslBio.cpp | 483 +++++++++++++++++++ docs/dev/debt/libSceLibreSsl.md | 6 +- 7 files changed, 881 insertions(+), 34 deletions(-) create mode 100644 core/libs/prx/libSceLibreSsl/MemoryBio.cmake create mode 100644 core/libs/prx/libSceLibreSsl/include/Bio.hpp create mode 100644 core/libs/prx/libSceLibreSsl/src/Bio.cpp create mode 100644 core/libs/tests/GuestLibreSslBio.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 9bbf0a021f..af89974813 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -231,6 +231,7 @@ endif() add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) include(prx/libSceLibreSsl/ErrorQueue.cmake) +include(prx/libSceLibreSsl/MemoryBio.cmake) if(BUILD_TESTING) add_test_executable(guest_posix_scalar_io_tests tests/GuestPosixScalarIo.cpp) diff --git a/core/libs/prx/libSceLibreSsl/Export.cpp b/core/libs/prx/libSceLibreSsl/Export.cpp index 1b7c846324..fee76aa01c 100644 --- a/core/libs/prx/libSceLibreSsl/Export.cpp +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -1,6 +1,7 @@ #include #include #include "SceTypes.hpp" +#include "prx/libSceLibreSsl/include/Bio.hpp" #include "prx/libc/include/General.hpp" extern "C" { @@ -81,36 +82,16 @@ int APS5_VABI ASN1_TIME_print_nid_postfix(void) { return 0; } -int APS5_VABI BIO_ctrl_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI BIO_f_ssl_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI BIO_free_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI BIO_new_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI BIO_new_fp_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI BIO_new_mem_buf_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI BIO_new_socket_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -121,24 +102,13 @@ int APS5_VABI BIO_printf_nid_postfix(void) { return 0; } -int APS5_VABI BIO_puts_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI BIO_s_file_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; } -int APS5_VABI BIO_s_mem_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI BIO_write_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; +int APS5_VABI BIO_write_nid_postfix(LibreSsl::Bio* bio, const void* source, int size) { + return LibreSslBioWrite_nid_no_patch(bio, source, size); } int APS5_VABI BN_CTX_end_nid_postfix(void) { diff --git a/core/libs/prx/libSceLibreSsl/MemoryBio.cmake b/core/libs/prx/libSceLibreSsl/MemoryBio.cmake new file mode 100644 index 0000000000..26b6a3ce35 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/MemoryBio.cmake @@ -0,0 +1,30 @@ +target_sources(libSceLibreSsl PRIVATE ${CMAKE_CURRENT_LIST_DIR}/src/Bio.cpp) + +if(BUILD_TESTING) + add_test_executable(guest_libressl_bio_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslBio.cpp) + target_include_directories(guest_libressl_bio_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_bio_tests PRIVATE libSceLibreSsl libc) + configure_windows_unwind(guest_libressl_bio_tests) + add_test(NAME guest_libressl_bio COMMAND guest_libressl_bio_tests) + set_tests_properties(guest_libressl_bio PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_bio PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() + + add_test_executable(guest_libressl_bio_patched_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslBio.cpp) + target_include_directories(guest_libressl_bio_patched_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_compile_definitions(guest_libressl_bio_patched_tests PRIVATE ANYPS5_BIO_DYNAMIC) + target_link_libraries(guest_libressl_bio_patched_tests PRIVATE ${CMAKE_DL_LIBS}) + configure_windows_unwind(guest_libressl_bio_patched_tests) + if(WIN32) + target_link_libraries(guest_libressl_bio_patched_tests PRIVATE libc) + endif() + add_dependencies(guest_libressl_bio_patched_tests libs) + add_test(NAME guest_libressl_bio_patched COMMAND guest_libressl_bio_patched_tests ${PATCHED_DIR}) + set_tests_properties(guest_libressl_bio_patched PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_bio_patched PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:${PATCHED_DIR}") + endif() +endif() diff --git a/core/libs/prx/libSceLibreSsl/include/Bio.hpp b/core/libs/prx/libSceLibreSsl/include/Bio.hpp new file mode 100644 index 0000000000..63613ce1d7 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/include/Bio.hpp @@ -0,0 +1,72 @@ +#ifndef CORE_LIBS_PRX_LIBSCELIBRESSL_INCLUDE_BIO_HPP +#define CORE_LIBS_PRX_LIBSCELIBRESSL_INCLUDE_BIO_HPP + +#include +#include +#include "prx/libc/include/general/VabiMacros.hpp" + +namespace LibreSsl { + +struct Bio; +using BioCallback = std::int64_t (APS5_VABI *)(Bio*, int, const char*, int, std::int64_t, std::int64_t); +using BioInfoCallback = void (APS5_VABI *)(Bio*, int, const char*, int, std::int64_t, std::int64_t); + +struct BioMethod { + int type; + const char* name; + int (APS5_VABI *write)(Bio*, const char*, int); + int (APS5_VABI *read)(Bio*, char*, int); + int (APS5_VABI *puts)(Bio*, const char*); + int (APS5_VABI *gets)(Bio*, char*, int); + std::int64_t (APS5_VABI *control)(Bio*, int, std::int64_t, void*); + int (APS5_VABI *create)(Bio*); + int (APS5_VABI *destroy)(Bio*); + std::int64_t (APS5_VABI *callbackControl)(Bio*, int, BioInfoCallback); +}; + +struct Bio { + const BioMethod* method; + BioCallback callback; + char* callbackArgument; + int initialized; + int shutdown; + int flags; + int retryReason; + int number; + void* data; + Bio* next; + Bio* previous; + int references; + std::uint64_t bytesRead; + std::uint64_t bytesWritten; + void* extraData; +}; + +struct BioBuffer { + std::size_t length; + char* data; + std::size_t capacity; +}; + +static_assert(sizeof(BioMethod) == 80); +static_assert(offsetof(BioMethod, write) == 16); +static_assert(offsetof(BioMethod, control) == 48); +static_assert(offsetof(BioMethod, callbackControl) == 72); +static_assert(sizeof(Bio) == 104); +static_assert(offsetof(Bio, initialized) == 24); +static_assert(offsetof(Bio, flags) == 32); +static_assert(offsetof(Bio, number) == 40); +static_assert(offsetof(Bio, data) == 48); +static_assert(offsetof(Bio, references) == 72); +static_assert(offsetof(Bio, bytesRead) == 80); +static_assert(offsetof(Bio, bytesWritten) == 88); +static_assert(offsetof(Bio, extraData) == 96); +static_assert(sizeof(BioBuffer) == 24); +static_assert(offsetof(BioBuffer, data) == 8); +static_assert(offsetof(BioBuffer, capacity) == 16); + +} // namespace LibreSsl + +extern "C" int LibreSslBioWrite_nid_no_patch(LibreSsl::Bio* bio, const void* source, int size); + +#endif diff --git a/core/libs/prx/libSceLibreSsl/src/Bio.cpp b/core/libs/prx/libSceLibreSsl/src/Bio.cpp new file mode 100644 index 0000000000..5bffff2ac6 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/Bio.cpp @@ -0,0 +1,287 @@ +#include "prx/libSceLibreSsl/include/Bio.hpp" +#include "prx/libSceLibreSsl/ErrorQueue.hpp" +#include "prx/libc/include/General.hpp" +#include +#include +#include +#include +#include + +extern "C" { +void* APS5_VABI calloc_nid_postfix(std::size_t, std::size_t); +void APS5_VABI free_nid_postfix(void*); +} + +namespace { + +using LibreSsl::Bio; +using LibreSsl::BioBuffer; +using LibreSsl::BioMethod; + +constexpr int ReadOnly = 0x200; +constexpr int RetryFlags = 0xf; +constexpr int RetryRead = 0x9; +constexpr std::size_t MaximumGrowth = 0x5ffffffc; +constexpr int BioLibrary = 32; +constexpr int BufferLibrary = 7; +constexpr int AllocationFailure = 65; +constexpr int NullParameter = 115; +constexpr int Uninitialized = 120; +constexpr int UnsupportedMethod = 121; +constexpr int ReadOnlyWrite = 126; + +int queueError(int library, int reason, int result) { + LibreSslPutError_nid_no_patch(library, 0xfff, reason, __FILE__, __LINE__); + return result; +} + +void clearBuffer(BioBuffer& buffer) { + auto* bytes = reinterpret_cast(buffer.data); + for (std::size_t i = 0; i < buffer.capacity; ++i) bytes[i] = 0; +} + +BioBuffer& memoryBuffer(Bio* bio) { + if (!bio || !bio->data) throw std::invalid_argument("BIO: missing memory buffer"); + return *static_cast(bio->data); +} + +int APS5_VABI createMemory(Bio* bio) { + auto* buffer = static_cast(calloc_nid_postfix(1, sizeof(BioBuffer))); + if (!buffer) return queueError(BufferLibrary, AllocationFailure, 0); + bio->data = buffer; + bio->initialized = 1; + bio->shutdown = 1; + bio->number = -1; + return 1; +} + +int APS5_VABI destroyMemory(Bio* bio) { + if (!bio) return 0; + if (bio->shutdown && bio->initialized && bio->data) { + auto* buffer = static_cast(bio->data); + if (!(bio->flags & ReadOnly)) { + clearBuffer(*buffer); + free_nid_postfix(buffer->data); + } + free_nid_postfix(buffer); + bio->data = nullptr; + } + return 1; +} + +int APS5_VABI readMemory(Bio* bio, char* destination, int size) { + auto& buffer = memoryBuffer(bio); + bio->flags &= ~RetryFlags; + const int count = size >= 0 ? static_cast(std::min(size, buffer.length)) : size; + if (destination && count > 0) { + std::memcpy(destination, buffer.data, count); + buffer.length -= count; + if (bio->flags & ReadOnly) buffer.data += count; + else std::memmove(buffer.data, buffer.data + count, buffer.length); + return count; + } + if (buffer.length != 0) return count; + if (bio->number != 0) bio->flags |= RetryRead; + return bio->number; +} + +int APS5_VABI writeMemory(Bio* bio, const char* source, int size) { + auto& buffer = memoryBuffer(bio); + if (!source) return queueError(BioLibrary, NullParameter, -1); + if (bio->flags & ReadOnly) return queueError(BioLibrary, ReadOnlyWrite, -1); + if (size < 0) throw std::invalid_argument("BIO: negative memory write size"); + bio->flags &= ~RetryFlags; + const auto count = static_cast(size); + if (buffer.length > std::numeric_limits::max() - count) NotImplemented_nid_no_patch(__func__); + const std::size_t length = buffer.length + count; + if (length > buffer.capacity) { + if (length > MaximumGrowth) return queueError(BufferLibrary, AllocationFailure, -1); + const std::size_t capacity = (length + 3) / 3 * 4; + auto* data = static_cast(calloc_nid_postfix(capacity, 1)); + if (!data) return queueError(BufferLibrary, AllocationFailure, -1); + if (buffer.length != 0) std::memcpy(data, buffer.data, buffer.length); + if (count != 0) std::memcpy(data + buffer.length, source, count); + clearBuffer(buffer); + free_nid_postfix(buffer.data); + buffer.data = data; + buffer.capacity = capacity; + } else if (count != 0) { + std::memmove(buffer.data + buffer.length, source, count); + } + buffer.length = length; + return size; +} + +int APS5_VABI putsMemory(Bio* bio, const char* source) { + if (!source) throw std::invalid_argument("BIO_puts: null string"); + const std::size_t size = std::strlen(source); + if (size > static_cast(std::numeric_limits::max())) NotImplemented_nid_no_patch(__func__); + return writeMemory(bio, source, static_cast(size)); +} + +int APS5_VABI getsMemory(Bio* bio, char* destination, int size) { + auto& buffer = memoryBuffer(bio); + if (!destination || size == std::numeric_limits::min()) throw std::invalid_argument("BIO: invalid line buffer"); + bio->flags &= ~RetryFlags; + if (size <= 1 || buffer.length == 0) { + *destination = '\0'; + return 0; + } + const auto limit = std::min(size - 1, buffer.length); + const void* newline = std::memchr(buffer.data, '\n', limit); + const std::size_t count = newline ? static_cast(newline) - buffer.data + 1 : limit; + const int result = readMemory(bio, destination, static_cast(count)); + destination[result] = '\0'; + return result; +} + +std::int64_t APS5_VABI controlMemory(Bio* bio, int command, std::int64_t argument, void* pointer) { + if (!bio) throw std::invalid_argument("BIO: null memory BIO"); + switch (command) { + case 1: { + auto& buffer = memoryBuffer(bio); + if (buffer.data) { + if (bio->flags & ReadOnly) { + if (buffer.capacity < buffer.length) throw std::invalid_argument("BIO: invalid read-only buffer length"); + buffer.data -= buffer.capacity - buffer.length; + buffer.length = buffer.capacity; + } else { + clearBuffer(buffer); + buffer.length = 0; + } + } + return 1; + } + case 2: + return memoryBuffer(bio).length == 0; + case 3: + if (pointer) *static_cast(pointer) = memoryBuffer(bio).data; + return static_cast(memoryBuffer(bio).length); + case 8: + return bio->shutdown; + case 9: + bio->shutdown = static_cast(argument); + return 1; + case 10: + return static_cast(memoryBuffer(bio).length); + case 11: + case 12: + return 1; + case 13: + return 0; + case 114: + destroyMemory(bio); + bio->data = pointer; + bio->shutdown = static_cast(argument); + return 1; + case 115: + if (pointer) std::memcpy(pointer, &bio->data, sizeof(bio->data)); + return 1; + case 130: + bio->number = static_cast(argument); + return 1; + default: + return 0; + } +} + +const BioMethod MemoryMethod{0x401, "memory buffer", writeMemory, readMemory, putsMemory, getsMemory, controlMemory, createMemory, destroyMemory, nullptr}; + +} // namespace + +extern "C" { + +const LibreSsl::BioMethod* APS5_VABI BIO_s_mem_nid_postfix() { + return &MemoryMethod; +} + +LibreSsl::Bio* APS5_VABI BIO_new_nid_postfix(const LibreSsl::BioMethod* method) { + if (!method) throw std::invalid_argument("BIO_new: null method"); + auto* bio = static_cast(calloc_nid_postfix(1, sizeof(Bio))); + if (!bio) { + queueError(BioLibrary, AllocationFailure, 0); + return nullptr; + } + bio->method = method; + bio->shutdown = 1; + bio->references = 1; + try { + if (!method->create || method->create(bio)) return bio; + } catch (...) { + free_nid_postfix(bio); + throw; + } + free_nid_postfix(bio); + return nullptr; +} + +LibreSsl::Bio* APS5_VABI BIO_new_mem_buf_nid_postfix(const void* source, int size) { + if (!source) { + queueError(BioLibrary, NullParameter, 0); + return nullptr; + } + auto* bio = BIO_new_nid_postfix(&MemoryMethod); + if (!bio) return nullptr; + auto& buffer = memoryBuffer(bio); + buffer.data = const_cast(static_cast(source)); + buffer.length = size < 0 ? std::strlen(buffer.data) : static_cast(size); + buffer.capacity = buffer.length; + bio->flags |= ReadOnly; + bio->number = 0; + return bio; +} + +int APS5_VABI BIO_free_nid_postfix(LibreSsl::Bio* bio) { + if (!bio) return 0; + if (bio->extraData) NotImplemented_nid_no_patch(__func__); + if (std::atomic_ref(bio->references).fetch_sub(1) > 1) return 1; + if (bio->callback) { + const int result = static_cast(bio->callback(bio, 1, nullptr, 0, 0, 1)); + if (result <= 0) return result; + } + if (bio->extraData) NotImplemented_nid_no_patch(__func__); + if (bio->method && bio->method->destroy) bio->method->destroy(bio); + free_nid_postfix(bio); + return 1; +} + +std::int64_t APS5_VABI BIO_ctrl_nid_postfix(LibreSsl::Bio* bio, int command, std::int64_t argument, void* pointer) { + if (!bio) return 0; + if (!bio->method || !bio->method->control) return queueError(BioLibrary, UnsupportedMethod, -2); + const auto callback = bio->callback; + if (callback) { + const auto result = callback(bio, 6, static_cast(pointer), command, argument, 1); + if (result <= 0) return result; + } + const auto result = bio->method->control(bio, command, argument, pointer); + return callback ? callback(bio, 0x86, static_cast(pointer), command, argument, result) : result; +} + +int LibreSslBioWrite_nid_no_patch(LibreSsl::Bio* bio, const void* source, int size) { + if (!bio || !source || size <= 0) return 0; + if (!bio->method || !bio->method->write) return queueError(BioLibrary, UnsupportedMethod, -2); + const auto callback = bio->callback; + if (callback) { + const int result = static_cast(callback(bio, 3, static_cast(source), size, 0, 1)); + if (result <= 0) return result; + } + if (!bio->initialized) return queueError(BioLibrary, Uninitialized, -2); + const int result = bio->method->write(bio, static_cast(source), size); + if (result > 0) bio->bytesWritten += static_cast(result); + return callback ? static_cast(callback(bio, 0x83, static_cast(source), size, 0, result)) : result; +} + +int APS5_VABI BIO_puts_nid_postfix(LibreSsl::Bio* bio, const char* source) { + if (!bio || !bio->method || !bio->method->puts) return queueError(BioLibrary, UnsupportedMethod, -2); + const auto callback = bio->callback; + if (callback) { + const int result = static_cast(callback(bio, 4, source, 0, 0, 1)); + if (result <= 0) return result; + } + if (!bio->initialized) return queueError(BioLibrary, Uninitialized, -2); + const int result = bio->method->puts(bio, source); + if (result > 0) bio->bytesWritten += static_cast(result); + return callback ? static_cast(callback(bio, 0x84, source, 0, 0, result)) : result; +} + +} diff --git a/core/libs/tests/GuestLibreSslBio.cpp b/core/libs/tests/GuestLibreSslBio.cpp new file mode 100644 index 0000000000..dee7f5bb75 --- /dev/null +++ b/core/libs/tests/GuestLibreSslBio.cpp @@ -0,0 +1,483 @@ +#include "prx/libSceLibreSsl/include/Bio.hpp" +#include "prx/libSceLibreSsl/ErrorQueue.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef ANYPS5_BIO_DYNAMIC +#ifdef _WIN32 +#include +#else +#include +#endif +#endif + +using LibreSsl::Bio; +using LibreSsl::BioBuffer; +using LibreSsl::BioMethod; + +extern "C" { +const BioMethod* APS5_VABI BIO_s_mem_nid_postfix(); +Bio* APS5_VABI BIO_new_nid_postfix(const BioMethod*); +Bio* APS5_VABI BIO_new_mem_buf_nid_postfix(const void*, int); +int APS5_VABI BIO_free_nid_postfix(Bio*); +std::int64_t APS5_VABI BIO_ctrl_nid_postfix(Bio*, int, std::int64_t, void*); +int APS5_VABI BIO_write_nid_postfix(Bio*, const void*, int); +int APS5_VABI BIO_puts_nid_postfix(Bio*, const char*); +void* APS5_VABI calloc_nid_postfix(std::size_t, std::size_t); +void APS5_VABI free_nid_postfix(void*); +} + +#ifdef ANYPS5_BIO_DYNAMIC +namespace { +decltype(&BIO_s_mem_nid_postfix) bioMemoryMethod; +decltype(&BIO_new_nid_postfix) bioNew; +decltype(&BIO_new_mem_buf_nid_postfix) bioNewBuffer; +decltype(&BIO_free_nid_postfix) bioFree; +decltype(&BIO_ctrl_nid_postfix) bioControl; +decltype(&BIO_write_nid_postfix) bioWrite; +decltype(&BIO_puts_nid_postfix) bioPuts; +decltype(&calloc_nid_postfix) guestCalloc; +decltype(&free_nid_postfix) guestFree; +decltype(&ApplicationHeapRegister_nid_no_patch) registerHeap; +decltype(&ERR_clear_error_nid_postfix) clearError; +decltype(&ERR_peek_error_nid_postfix) peekError; +decltype(&ERR_get_error_nid_postfix) getError; + +void* Load(const std::filesystem::path& path) { +#ifdef _WIN32 + auto* module = LoadLibraryExW(path.c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH); +#else + auto* module = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); +#endif + if (!module) throw std::runtime_error("failed to load patched BIO dependency: " + path.string()); + return module; +} + +template +void Resolve(TFunction& function, void* module, const char* name) { +#ifdef _WIN32 + function = reinterpret_cast(GetProcAddress(static_cast(module), name)); +#else + function = reinterpret_cast(dlsym(module, name)); +#endif + if (!function) throw std::runtime_error(std::string("missing patched BIO dependency: ") + name); +} + +void InitializePatched(const std::filesystem::path& directory) { + void* libc = Load(directory / "libc.prx"); + void* bio = Load(directory / "libSceLibreSsl.prx"); + Resolve(bioMemoryMethod, bio, "R86sXhW699g"); + Resolve(bioNew, bio, "ARlY6zhSacE"); + Resolve(bioNewBuffer, bio, "GMzjS9jvyDQ"); + Resolve(bioFree, bio, "lpJ3zhgftCg"); + Resolve(bioControl, bio, "RHpq9hQfTj8"); + Resolve(bioWrite, bio, "nH7XM+HhXWc"); + Resolve(bioPuts, bio, "eVcWmVlimPA"); + Resolve(guestCalloc, libc, "2X5agFjKxMc"); + Resolve(guestFree, libc, "tIhsqj0qsFE"); + Resolve(registerHeap, libc, "ApplicationHeapRegister_nid_no_patch"); + Resolve(clearError, bio, "sieQ--PINWg"); + Resolve(peekError, bio, "Aytjey6FPTU"); + Resolve(getError, bio, "DJg+qWX1rwY"); +} +} // namespace + +#define BIO_s_mem_nid_postfix bioMemoryMethod +#define BIO_new_nid_postfix bioNew +#define BIO_new_mem_buf_nid_postfix bioNewBuffer +#define BIO_free_nid_postfix bioFree +#define BIO_ctrl_nid_postfix bioControl +#define BIO_write_nid_postfix bioWrite +#define BIO_puts_nid_postfix bioPuts +#define calloc_nid_postfix guestCalloc +#define free_nid_postfix guestFree +#define ApplicationHeapRegister_nid_no_patch registerHeap +#define ERR_clear_error_nid_postfix clearError +#define ERR_peek_error_nid_postfix peekError +#define ERR_get_error_nid_postfix getError +#endif + +namespace { + +void Require(bool condition, const char* message) { + if (!condition) { + std::fprintf(stderr, "%s\n", message); + std::abort(); + } +} + +int allocationsUntilFailure = -1; +std::atomic allocations{0}; +void* wipePointer = nullptr; +std::size_t wipeSize = 0; + +void* APS5_VABI Allocate(std::size_t) { throw std::logic_error("unexpected nonzeroed BIO allocation"); } +void* APS5_VABI Reallocate(void*, std::size_t) { throw std::logic_error("unexpected BIO reallocation"); } +void* APS5_VABI Align(std::size_t, std::size_t) { throw std::logic_error("unexpected BIO aligned allocation"); } +void* APS5_VABI Realign(void*, std::size_t, std::size_t) { throw std::logic_error("unexpected BIO realignment"); } +int APS5_VABI PosixAlign(void**, std::size_t, std::size_t) { throw std::logic_error("unexpected BIO aligned allocation"); } + +void* APS5_VABI Calloc(std::size_t count, std::size_t size) { + if (allocationsUntilFailure == 0) return nullptr; + if (allocationsUntilFailure > 0) --allocationsUntilFailure; + void* result = std::calloc(count, size); + if (result) ++allocations; + return result; +} + +void APS5_VABI Free(void* pointer) { + if (pointer && pointer == wipePointer) { + for (std::size_t i = 0; i < wipeSize; ++i) Require(static_cast(pointer)[i] == 0, "freed buffer was not wiped"); + wipePointer = nullptr; + } + if (pointer) --allocations; + std::free(pointer); +} + +void InitializeHeap() { + const std::array api{ + reinterpret_cast(Allocate), reinterpret_cast(Free), reinterpret_cast(Calloc), + reinterpret_cast(Reallocate), reinterpret_cast(Align), reinterpret_cast(Realign), + reinterpret_cast(PosixAlign)}; + ApplicationHeapRegister_nid_no_patch(api.data()); +} + +template +void RequireUnsupported(TFunction function) { + bool threw = false; + try { + function(); + } catch (const std::runtime_error&) { + threw = true; + } + Require(threw, "unsupported BIO path did not throw"); +} + +void RequireError(std::uint64_t expected) { + Require(ERR_peek_error_nid_postfix() == expected, "BIO error code or ordering"); + Require(ERR_get_error_nid_postfix() == expected, "consume BIO error"); + Require(ERR_get_error_nid_postfix() == 0, "unexpected extra BIO error"); +} + +BioBuffer* Buffer(Bio* bio) { + BioBuffer* buffer = nullptr; + Require(BIO_ctrl_nid_postfix(bio, 115, 0, &buffer) == 1 && buffer, "get buffer"); + return buffer; +} + +void RequireData(Bio* bio, const std::string& expected) { + char* bytes = nullptr; + Require(BIO_ctrl_nid_postfix(bio, 3, 0, &bytes) == static_cast(expected.size()), "buffer data length"); + Require(expected.empty() || (bytes && std::memcmp(bytes, expected.data(), expected.size()) == 0), "buffer data bytes"); + Require(BIO_ctrl_nid_postfix(bio, 10, 0, nullptr) == static_cast(expected.size()), "pending bytes"); + Require(BIO_ctrl_nid_postfix(bio, 2, 0, nullptr) == expected.empty(), "EOF flag"); +} + +void TestWritable() { + const auto* method = BIO_s_mem_nid_postfix(); + Require(method && method == BIO_s_mem_nid_postfix(), "stable method pointer"); + Require(method->type == 0x401 && std::strcmp(method->name, "memory buffer") == 0, "method identity"); + Require(method->write && method->read && method->puts && method->gets && method->control && method->create && method->destroy, "method callbacks"); + Require(method->callbackControl == nullptr, "no callback control method"); + Bio* bio = BIO_new_nid_postfix(method); + Require(bio && bio->method == method && bio->initialized == 1 && bio->references == 1, "new BIO state"); + Require(!bio->callback && !bio->callbackArgument && !bio->flags && !bio->retryReason && !bio->next && !bio->previous && !bio->extraData, "new BIO zero fields"); + Require(bio->number == -1 && bio->bytesRead == 0 && bio->bytesWritten == 0, "new BIO counters"); + Require(BIO_write_nid_postfix(bio, nullptr, 7) == 0 && BIO_write_nid_postfix(bio, "x", 0) == 0, "empty write guard"); + RequireError(0); + RequireData(bio, ""); + auto* buffer = Buffer(bio); + Require(buffer->capacity == 0 && buffer->data == nullptr, "new buffer storage"); + char output[16]{}; + Require(method->read(bio, output, sizeof(output)) == -1 && (bio->flags & 0xf) == 9, "empty writable BIO retry"); + Require(BIO_write_nid_postfix(bio, "abc", 3) == 3 && (bio->flags & 0xf) == 0, "write clears retry flags"); + RequireData(bio, "abc"); + Require(buffer->capacity == 8 && buffer->data[3] == 0, "first buffer expansion"); + Require(BIO_write_nid_postfix(bio, "0123456789", 10) == 10, "second write"); + Require(buffer->capacity == 20 && buffer->data[13] == 0, "second buffer expansion"); + RequireData(bio, "abc0123456789"); + Require(method->read(bio, output, 4) == 4 && std::memcmp(output, "abc0", 4) == 0, "partial memory read"); + RequireData(bio, "123456789"); + Require(BIO_puts_nid_postfix(bio, "xy") == 2 && bio->bytesWritten == 15, "puts and byte accounting"); + RequireData(bio, "123456789xy"); + Require(method->read(bio, nullptr, 3) == 3, "null raw read reports available count"); + RequireData(bio, "123456789xy"); + Require(method->read(bio, output, -3) == -3, "negative raw read"); + Require(BIO_write_nid_postfix(bio, nullptr, 5) == 0 && BIO_write_nid_postfix(bio, "z", -3) == 0, "ignored public writes"); + RequireData(bio, "123456789xy"); + Require(BIO_ctrl_nid_postfix(bio, 11, 0, nullptr) == 1 && BIO_ctrl_nid_postfix(bio, 12, 0, nullptr) == 1, "flush and duplicate controls"); + Require(BIO_ctrl_nid_postfix(bio, 13, 0, nullptr) == 0, "no pending output"); + Require(BIO_ctrl_nid_postfix(bio, 6, 0, nullptr) == 0 && BIO_ctrl_nid_postfix(bio, 7, 0, nullptr) == 0, "push and pop controls"); + Require(BIO_ctrl_nid_postfix(bio, 0x7fffffff, 0, nullptr) == 0, "unknown memory control"); + Require(BIO_ctrl_nid_postfix(bio, 1, 0, nullptr) == 1, "reset writable BIO"); + RequireData(bio, ""); + for (std::size_t i = 0; i < buffer->capacity; ++i) Require(buffer->data[i] == 0, "reset wipes full allocation"); + Require(BIO_ctrl_nid_postfix(bio, 130, 7, nullptr) == 1, "set EOF return"); + Require(method->read(bio, output, 1) == 7 && (bio->flags & 0xf) == 9, "configured EOF return and retry"); + Require(BIO_ctrl_nid_postfix(bio, 130, 0, nullptr) == 1, "set terminal EOF"); + Require(method->read(bio, output, 1) == 0 && (bio->flags & 0xf) == 0, "terminal EOF clears retry"); + Require(BIO_puts_nid_postfix(bio, "secret") == 6, "write before destruction"); + wipePointer = buffer->data; + wipeSize = buffer->capacity; + Require(BIO_free_nid_postfix(bio) == 1, "free writable BIO"); + Require(wipePointer == nullptr, "destruction frees wiped storage"); +} + +void TestReadOnlyAndLines() { + const std::array source{'a', '\0', 'b', '\n', 'c', 'd', '\n', 'e'}; + Bio* bio = BIO_new_mem_buf_nid_postfix(source.data(), source.size()); + Require(bio && (bio->flags & 0x200) && bio->number == 0, "read-only BIO state"); + auto* buffer = Buffer(bio); + Require(buffer->data == source.data() && buffer->capacity == source.size(), "read-only buffer is borrowed"); + char output[16]; + std::memset(output, 0x55, sizeof(output)); + Require(bio->method->gets(bio, output, sizeof(output)) == 4 && std::memcmp(output, source.data(), 4) == 0 && output[4] == 0, "line includes embedded NUL and newline"); + Require(buffer->data == source.data() + 4 && buffer->length == 4, "borrowed buffer advances"); + Require(bio->method->gets(bio, output, 3) == 2 && std::strcmp(output, "cd") == 0, "line length bound"); + Require(bio->method->gets(bio, output, 1) == 0 && output[0] == 0 && buffer->length == 2, "one-byte line buffer"); + Require(bio->method->gets(bio, output, sizeof(output)) == 1 && std::strcmp(output, "\n") == 0, "newline on next read"); + Require(bio->method->read(bio, output, sizeof(output)) == 1 && output[0] == 'e', "last read-only byte"); + Require(bio->method->read(bio, output, 1) == 0 && (bio->flags & 0xf) == 0, "read-only EOF"); + Require(BIO_ctrl_nid_postfix(bio, 1, 0, nullptr) == 1, "read-only reset"); + Require(buffer->data == source.data() && buffer->length == source.size(), "read-only reset restores original range"); + Require(BIO_write_nid_postfix(bio, "x", 1) == -1, "reject read-only write"); + RequireError(0x20fff07e); + Require(buffer->data == source.data() && buffer->length == source.size(), "rejected write preserves input"); + Require(BIO_free_nid_postfix(bio) == 1 && source[0] == 'a' && source[7] == 'e', "borrowed bytes survive free"); + for (const int size : {-1, -99}) { + bio = BIO_new_mem_buf_nid_postfix("ab\0cd", size); + RequireData(bio, "ab"); + Require(BIO_free_nid_postfix(bio) == 1, "free string BIO"); + } + bio = BIO_new_mem_buf_nid_postfix("", 0); + RequireData(bio, ""); + output[0] = 'x'; + Require(bio->method->gets(bio, output, 8) == 0 && output[0] == 0, "empty gets"); + Require(BIO_free_nid_postfix(bio) == 1, "free empty BIO"); +} + +void TestBufferOwnership() { + Bio* bio = BIO_new_nid_postfix(BIO_s_mem_nid_postfix()); + Require(BIO_puts_nid_postfix(bio, "retained") == 8, "write retained buffer"); + auto* retained = Buffer(bio); + Require(BIO_ctrl_nid_postfix(bio, 8, 0, nullptr) == 1, "default close flag"); + Require(BIO_ctrl_nid_postfix(bio, 9, 0, nullptr) == 1 && BIO_ctrl_nid_postfix(bio, 8, 0, nullptr) == 0, "disable close"); + Require(BIO_free_nid_postfix(bio) == 1, "free BIO without its buffer"); + Require(retained->length == 8 && std::memcmp(retained->data, "retained", 8) == 0, "detached storage remains valid"); + free_nid_postfix(retained->data); + free_nid_postfix(retained); + + bio = BIO_new_nid_postfix(BIO_s_mem_nid_postfix()); + Require(BIO_puts_nid_postfix(bio, "old") == 3, "write old buffer"); + auto* replacement = static_cast(calloc_nid_postfix(1, sizeof(BioBuffer))); + replacement->data = static_cast(calloc_nid_postfix(8, 1)); + replacement->length = 3; + replacement->capacity = 8; + std::memcpy(replacement->data, "new", 3); + Require(BIO_ctrl_nid_postfix(bio, 114, 0, replacement) == 1 && Buffer(bio) == replacement, "replace buffer"); + RequireData(bio, "new"); + Require(BIO_write_nid_postfix(bio, "!", 1) == 1, "write replacement buffer"); + RequireData(bio, "new!"); + Require(BIO_free_nid_postfix(bio) == 1, "free BIO retaining replacement"); + Require(replacement->length == 4 && std::memcmp(replacement->data, "new!", 4) == 0, "caller owns replacement"); + free_nid_postfix(replacement->data); + free_nid_postfix(replacement); + + bio = BIO_new_nid_postfix(BIO_s_mem_nid_postfix()); + Require(BIO_ctrl_nid_postfix(bio, 114, 1, nullptr) == 1, "detach without replacement"); + Require(BIO_ctrl_nid_postfix(bio, 8, 0, nullptr) == 1, "close control without storage"); + Require(BIO_ctrl_nid_postfix(bio, 11, 0, nullptr) == 1, "flush without storage"); + void* missing = bio; + Require(BIO_ctrl_nid_postfix(bio, 115, 0, &missing) == 1 && missing == nullptr, "query detached storage"); + Require(BIO_free_nid_postfix(bio) == 1, "free detached BIO"); +} + +void TestInterleavedTransfers() { + Bio* bio = BIO_new_nid_postfix(BIO_s_mem_nid_postfix()); + std::string pending; + std::uint32_t sequence = 0x346de821; + std::uint64_t written = 0; + for (int step = 0; step < 2000; ++step) { + sequence = sequence * 1664525 + 1013904223; + const int count = static_cast((sequence >> 16) % 65); + if ((sequence & 3) != 0) { + std::array input{}; + for (int i = 0; i < count; ++i) input[i] = static_cast(sequence + i * 37); + Require(BIO_write_nid_postfix(bio, input.data(), count) == count, "interleaved write count"); + pending.append(input.data(), count); + written += count; + } else { + std::array output; + output.fill(static_cast(0xa5)); + const int expected = static_cast(std::min(count, pending.size())); + const int result = bio->method->read(bio, output.data(), count); + Require(result == (pending.empty() ? -1 : expected), "interleaved read count"); + if (expected != 0) Require(std::memcmp(output.data(), pending.data(), expected) == 0, "interleaved read bytes"); + Require(output[expected] == static_cast(0xa5), "read preserves destination tail"); + pending.erase(0, expected); + } + RequireData(bio, pending); + Require(bio->bytesWritten == written && bio->bytesRead == 0, "interleaved transfer counters"); + if (step % 127 == 0) { + Require(BIO_ctrl_nid_postfix(bio, 1, 0, nullptr) == 1, "interleaved reset"); + pending.clear(); + } + } + Require(BIO_free_nid_postfix(bio) == 1, "free interleaved BIO"); +} + +struct CallbackState { + std::vector events; + std::int64_t argument = 0; + int veto = 0; +}; + +std::int64_t APS5_VABI Callback(Bio* bio, int operation, const char*, int, std::int64_t argument, std::int64_t result) { + auto& state = *reinterpret_cast(bio->callbackArgument); + state.events.push_back(operation); + state.argument = argument; + if (state.veto && !(operation & 0x80)) return state.veto; + return (operation & 0x80) ? result + 2 : 1; +} + +int APS5_VABI CreateCustom(Bio* bio) { + bio->initialized = 1; + return 1; +} + +int APS5_VABI WriteCustom(Bio*, const char*, int count) { + return count - 1; +} + +std::int64_t APS5_VABI ControlCustom(Bio*, int command, std::int64_t argument, void*) { + return argument + command; +} + +int APS5_VABI RejectCreate(Bio*) { + return 0; +} + +void TestDispatchAndReferences() { + Bio* bio = BIO_new_nid_postfix(BIO_s_mem_nid_postfix()); + CallbackState state; + bio->callback = Callback; + bio->callbackArgument = reinterpret_cast(&state); + Require(BIO_write_nid_postfix(bio, "abc", 3) == 5 && bio->bytesWritten == 3, "callback changes result after accounting"); + Require(state.events == std::vector{3, 0x83}, "write callback ordering"); + state.events.clear(); + Require(BIO_puts_nid_postfix(bio, "d") == 3 && state.events == std::vector{4, 0x84}, "puts callbacks"); + state.events.clear(); + Require(BIO_ctrl_nid_postfix(bio, 10, 0x1234567887654321LL, nullptr) == 6, "control callback result"); + Require(state.argument == 0x1234567887654321LL && state.events == std::vector{6, 0x86}, "64-bit control argument"); + state.events.clear(); + state.veto = -7; + Require(BIO_write_nid_postfix(bio, "x", 1) == -7 && state.events == std::vector{3}, "write callback veto"); + Require(bio->bytesWritten == 4 && static_cast(bio->data)->length == 4, "veto prevents transfer"); + state.events.clear(); + Require(BIO_free_nid_postfix(bio) == -7 && state.events == std::vector{1} && bio->references == 0, "free callback veto"); + bio->callback = nullptr; + Require(BIO_free_nid_postfix(bio) == 1, "free after veto"); + + BioMethod custom{0x480, "test", WriteCustom, nullptr, nullptr, nullptr, ControlCustom, CreateCustom, nullptr, nullptr}; + bio = BIO_new_nid_postfix(&custom); + Require(bio && bio->method == &custom, "title-defined method"); + Require(BIO_write_nid_postfix(bio, "abc", 3) == 2 && bio->bytesWritten == 2, "title-defined writer"); + Require(BIO_ctrl_nid_postfix(bio, 17, 0x1234567800000000LL, nullptr) == 0x1234567800000011LL, "64-bit method result"); + constexpr int referenceCount = 8; + std::atomic_ref(bio->references).store(referenceCount + 1); + std::array threads; + for (auto& thread : threads) thread = std::thread([bio] { Require(BIO_free_nid_postfix(bio) == 1, "release shared reference"); }); + for (auto& thread : threads) thread.join(); + Require(bio->references == 1 && BIO_free_nid_postfix(bio) == 1, "last reference release"); + custom.create = RejectCreate; + Require(BIO_new_nid_postfix(&custom) == nullptr, "custom create can reject allocation"); +} + +void TestUnsupported() { + Require(BIO_free_nid_postfix(nullptr) == 0, "free null"); + Require(BIO_ctrl_nid_postfix(nullptr, 1, 0, nullptr) == 0, "control null"); + Require(BIO_write_nid_postfix(nullptr, "a", 1) == 0, "write null BIO"); + Require(BIO_new_mem_buf_nid_postfix(nullptr, 0) == nullptr, "reject null borrowed input"); + RequireError(0x20fff073); + Require(BIO_puts_nid_postfix(nullptr, "a") == -2, "reject puts without a method"); + RequireError(0x20fff079); + Bio* bio = BIO_new_nid_postfix(BIO_s_mem_nid_postfix()); + bio->initialized = 0; + Require(BIO_write_nid_postfix(bio, "a", 1) == -2, "reject uninitialized write"); + RequireError(0x20fff078); + bio->initialized = 1; + bio->extraData = bio; + RequireUnsupported([&] { BIO_free_nid_postfix(bio); }); + Require(bio->references == 1, "unsupported ex-data preserves reference"); + bio->extraData = nullptr; + const BioMethod* method = bio->method; + bio->method = nullptr; + Require(BIO_ctrl_nid_postfix(bio, 1, 0, nullptr) == -2, "reject control without a method"); + RequireError(0x20fff079); + Require(BIO_write_nid_postfix(bio, "a", 1) == -2, "reject write without a method"); + RequireError(0x20fff079); + bio->method = method; + auto* buffer = Buffer(bio); + buffer->length = 0x5ffffffd; + Require(BIO_write_nid_postfix(bio, "a", 1) == -1, "reject oversized growth"); + RequireError(0x07fff041); + buffer->length = 0; + Require(BIO_free_nid_postfix(bio) == 1, "free after unsupported cases"); + for (const int failAfter : {0, 1}) { + allocationsUntilFailure = failAfter; + Require(BIO_new_nid_postfix(BIO_s_mem_nid_postfix()) == nullptr, "failed BIO allocation"); + RequireError(failAfter == 0 ? 0x20fff041 : 0x07fff041); + Require(allocations == 0, "failed creation leaked guest heap storage"); + } + allocationsUntilFailure = -1; + bio = BIO_new_nid_postfix(BIO_s_mem_nid_postfix()); + Require(BIO_puts_nid_postfix(bio, "abc") == 3, "write before growth failure"); + allocationsUntilFailure = 0; + Require(BIO_puts_nid_postfix(bio, "0123456789") == -1, "failed buffer expansion"); + RequireError(0x07fff041); + RequireData(bio, "abc"); + allocationsUntilFailure = -1; + Require(BIO_free_nid_postfix(bio) == 1, "free after growth failure"); + + bio = BIO_new_nid_postfix(BIO_s_mem_nid_postfix()); + bio->initialized = 0; + Require(BIO_puts_nid_postfix(bio, "a") == -2, "reject uninitialized puts"); + RequireError(0x20fff078); + bio->initialized = 1; + Require(bio->method->write(bio, nullptr, 1) == -1, "reject null raw write source"); + RequireError(0x20fff073); + Require(BIO_free_nid_postfix(bio) == 1, "free after method errors"); +} + +} // namespace + +int main(int argc, char** argv) { +#ifdef ANYPS5_BIO_DYNAMIC + Require(argc == 2, "patched BIO test needs the libraries directory"); + InitializePatched(std::filesystem::absolute(argv[1])); +#else + static_cast(argc); + static_cast(argv); +#endif + InitializeHeap(); + ERR_clear_error_nid_postfix(); + TestWritable(); + TestReadOnlyAndLines(); + TestBufferOwnership(); + TestInterleavedTransfers(); + TestDispatchAndReferences(); + TestUnsupported(); + Require(allocations == 0, "BIO tests leaked guest heap storage"); + Require(ERR_get_error_nid_postfix() == 0, "BIO tests left an unexpected error"); +} diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index c30d2c0338..2b0242bde4 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -3,7 +3,11 @@ ### Functional - [ERR queues and strings](../../../core/libs/prx/libSceLibreSsl/ErrorQueue.cpp) have no PS5 equivalence verification. Contracts follow LibreSSL 3.4.1's [err.c](https://github.com/libressl/openbsd/blob/078b3d1df8ca2011c1605cd40418c58fa0f5b143/src/lib/libcrypto/err/err.c), err.h, crypto.h and err_all.c at that revision: LP64 error arguments/results, 15 queued errors and historical crypto strings. System reasons use guest libc's FreeBSD errno messages, truncated to 31 bytes; console wording is unverified. Explicit thread IDs in `ERR_remove_thread_state` throw because guest pthread identity mapping is unverified. Null IDs clear the calling thread. SSL-specific strings are not loaded; SSL initialization remains unimplemented. -- [Error publication](../../../core/libs/prx/libSceLibreSsl/ErrorQueue.hpp) has no implemented production callers on this branch. BIOs and the digest, AES, allocator/RAND, DES, BIGNUM and EVP/HMAC work in pending branches require separate integration. `LibreSslPutError_nid_no_patch(int library, int function, int reason, const char* file, int line)` has C linkage and the native host calling convention, accepts upstream numeric fields and publishes to the calling thread's queue. Three-argument source calls default file/line to null/zero. It stores file/line without taking ownership; producers must supply a filename with static lifetime. Guest ERR entry points use `APS5_VABI`. Unsupported producer exports continue throwing; the local tests publish through this helper. +- [Error publication](../../../core/libs/prx/libSceLibreSsl/ErrorQueue.hpp) is integrated with memory BIO errors. The digest, AES, allocator/RAND, DES, BIGNUM and EVP/HMAC work in pending branches still require integration. `LibreSslPutError_nid_no_patch(int library, int function, int reason, const char* file, int line)` has C linkage and the native host calling convention, accepts upstream numeric fields and publishes to the calling thread's queue. Three-argument source calls default file/line to null/zero. It stores file/line without taking ownership; producers must supply a filename with static lifetime. Guest ERR entry points use `APS5_VABI`. Unsupported producer exports continue throwing; the local tests publish through this helper. + +- [Memory BIOs](../../../core/libs/prx/libSceLibreSsl/src/Bio.cpp) have not been compared with a PS5 module or title. The 104-byte BIO, 80-byte method table, 24-byte buffer and callbacks use the LP64 layouts and behavior of [LibreSSL 3.4.1](https://github.com/libressl/openbsd/tree/078b3d1df8ca2011c1605cd40418c58fa0f5b143/src/lib/libcrypto/bio). The console's LibreSSL version is unknown. Storage uses the guest heap. Registered CRYPTO ex-data is unsupported; freeing a BIO with nonnull ex-data throws. File, socket and SSL BIO constructors remain unsupported. + +- [Memory BIO unchecked inputs](../../../core/libs/prx/libSceLibreSsl/src/Bio.cpp) throw for null method or raw line-buffer pointers, negative raw method write sizes, an overflowing raw line-buffer size, inconsistent read-only buffer lengths, and memory write lengths that overflow size_t. Upstream does not validate these inputs. An unrepresentable string length throws instead of narrowing into an unchecked write. ### Unknown function info From 0c3d3ae1484015fd0b5b83c6a98455c78f521495 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Wed, 21 Oct 2026 23:33:54 -0300 Subject: [PATCH 08/11] feat(libSceLibreSsl): implement CMAC with AES-CBC methods --- core/libs/CMakeLists.txt | 1 + core/libs/prx/libSceLibreSsl/Cmac.cmake | 32 ++ core/libs/prx/libSceLibreSsl/Export.cpp | 40 -- core/libs/prx/libSceLibreSsl/include/Cmac.hpp | 57 +++ core/libs/prx/libSceLibreSsl/src/Cmac.cpp | 275 +++++++++++ core/libs/tests/GuestLibreSslCmac.cpp | 444 ++++++++++++++++++ docs/dev/debt/libSceLibreSsl.md | 6 +- 7 files changed, 813 insertions(+), 42 deletions(-) create mode 100644 core/libs/prx/libSceLibreSsl/Cmac.cmake create mode 100644 core/libs/prx/libSceLibreSsl/include/Cmac.hpp create mode 100644 core/libs/prx/libSceLibreSsl/src/Cmac.cpp create mode 100644 core/libs/tests/GuestLibreSslCmac.cpp diff --git a/core/libs/CMakeLists.txt b/core/libs/CMakeLists.txt index 9bbf0a021f..6dc41545e9 100644 --- a/core/libs/CMakeLists.txt +++ b/core/libs/CMakeLists.txt @@ -231,6 +231,7 @@ endif() add_custom_target(libs DEPENDS ${PATCHED_OUTPUTS}) include(prx/libSceLibreSsl/ErrorQueue.cmake) +include(prx/libSceLibreSsl/Cmac.cmake) if(BUILD_TESTING) add_test_executable(guest_posix_scalar_io_tests tests/GuestPosixScalarIo.cpp) diff --git a/core/libs/prx/libSceLibreSsl/Cmac.cmake b/core/libs/prx/libSceLibreSsl/Cmac.cmake new file mode 100644 index 0000000000..e7b8cf205c --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/Cmac.cmake @@ -0,0 +1,32 @@ +target_sources(libSceLibreSsl PRIVATE ${CMAKE_CURRENT_LIST_DIR}/src/Cmac.cpp) +target_link_libraries(libSceLibreSsl PRIVATE mbedcrypto) +if(MINGW OR CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_link_options(libSceLibreSsl PRIVATE LINKER:--exclude-libs,ALL) +endif() + +if(BUILD_TESTING) + add_test_executable(guest_libressl_cmac_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslCmac.cpp) + target_include_directories(guest_libressl_cmac_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_libressl_cmac_tests PRIVATE libSceLibreSsl libc mbedcrypto) + configure_windows_unwind(guest_libressl_cmac_tests) + add_test(NAME guest_libressl_cmac COMMAND guest_libressl_cmac_tests) + + add_test_executable(guest_libressl_cmac_patched_tests ${LIBS_INCLUDE_DIR}/tests/GuestLibreSslCmac.cpp) + target_include_directories(guest_libressl_cmac_patched_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_compile_definitions(guest_libressl_cmac_patched_tests PRIVATE ANYPS5_CMAC_DYNAMIC) + target_link_libraries(guest_libressl_cmac_patched_tests PRIVATE mbedcrypto ${CMAKE_DL_LIBS}) + configure_windows_unwind(guest_libressl_cmac_patched_tests) + if(WIN32) + target_link_libraries(guest_libressl_cmac_patched_tests PRIVATE libc) + endif() + add_custom_target(guest_libressl_cmac_patch DEPENDS ${PATCHED_DIR}/libSceLibreSsl.prx ${PATCHED_DIR}/libc.prx) + add_dependencies(guest_libressl_cmac_patched_tests guest_libressl_cmac_patch) + add_test(NAME guest_libressl_cmac_patched COMMAND guest_libressl_cmac_patched_tests ${PATCHED_DIR}) + set_tests_properties(guest_libressl_cmac guest_libressl_cmac_patched PROPERTIES TIMEOUT 30) + if(WIN32) + set_tests_properties(guest_libressl_cmac PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + set_tests_properties(guest_libressl_cmac_patched PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:${PATCHED_DIR}") + endif() +endif() diff --git a/core/libs/prx/libSceLibreSsl/Export.cpp b/core/libs/prx/libSceLibreSsl/Export.cpp index 1b7c846324..bd52c4010f 100644 --- a/core/libs/prx/libSceLibreSsl/Export.cpp +++ b/core/libs/prx/libSceLibreSsl/Export.cpp @@ -221,31 +221,6 @@ int APS5_VABI BN_sub_word_nid_postfix(void) { return 0; } -int APS5_VABI CMAC_CTX_free_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI CMAC_CTX_new_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI CMAC_Final_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI CMAC_Init_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - -int APS5_VABI CMAC_Update_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI CONF_modules_free_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -681,11 +656,6 @@ int APS5_VABI EVP_PKEY_type_nid_postfix(void) { return 0; } -int APS5_VABI EVP_aes_128_cbc_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI EVP_aes_128_cfb8_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -701,11 +671,6 @@ int APS5_VABI EVP_aes_128_gcm_nid_postfix(void) { return 0; } -int APS5_VABI EVP_aes_192_cbc_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI EVP_aes_192_cfb8_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; @@ -721,11 +686,6 @@ int APS5_VABI EVP_aes_192_gcm_nid_postfix(void) { return 0; } -int APS5_VABI EVP_aes_256_cbc_nid_postfix(void) { - NotImplemented_nid_no_patch(__func__); - return 0; -} - int APS5_VABI EVP_aes_256_cfb8_nid_postfix(void) { NotImplemented_nid_no_patch(__func__); return 0; diff --git a/core/libs/prx/libSceLibreSsl/include/Cmac.hpp b/core/libs/prx/libSceLibreSsl/include/Cmac.hpp new file mode 100644 index 0000000000..8a3fd1a614 --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/include/Cmac.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include "prx/libc/include/general/VabiMacros.hpp" + +struct LibreSslCipherContext; + +struct LibreSslCipher { + std::int32_t nid; + std::int32_t blockSize; + std::int32_t keyLength; + std::int32_t ivLength; + std::uint64_t flags; + int (APS5_VABI* init)(LibreSslCipherContext*, const unsigned char*, const unsigned char*, int); + int (APS5_VABI* cipher)(LibreSslCipherContext*, unsigned char*, const unsigned char*, std::size_t); + int (APS5_VABI* cleanup)(LibreSslCipherContext*); + std::int32_t contextSize; + int (APS5_VABI* setAsn1Parameters)(LibreSslCipherContext*, void*); + int (APS5_VABI* getAsn1Parameters)(LibreSslCipherContext*, void*); + int (APS5_VABI* control)(LibreSslCipherContext*, int, int, void*); + void* applicationData; +}; + +struct LibreSslCipherContext { + const LibreSslCipher* cipher; + void* engine; + std::int32_t encrypt; + std::int32_t bufferLength; + unsigned char originalIv[16]; + unsigned char iv[16]; + unsigned char buffer[32]; + std::int32_t position; + void* applicationData; + std::int32_t keyLength; + std::uint64_t flags; + void* cipherData; + std::int32_t finalUsed; + std::int32_t blockMask; + unsigned char final[32]; +}; + +static_assert(sizeof(LibreSslCipher) == 88 && offsetof(LibreSslCipher, init) == 24 && offsetof(LibreSslCipher, contextSize) == 48); +static_assert(sizeof(LibreSslCipherContext) == 168 && offsetof(LibreSslCipherContext, iv) == 40 && offsetof(LibreSslCipherContext, flags) == 112 && offsetof(LibreSslCipherContext, cipherData) == 120); + +struct LibreSslCmac; + +extern "C" { +LibreSslCmac* APS5_VABI CMAC_CTX_new_nid_postfix(); +void APS5_VABI CMAC_CTX_free_nid_postfix(LibreSslCmac* context); +int APS5_VABI CMAC_Init_nid_postfix(LibreSslCmac* context, const void* key, std::size_t keyLength, const LibreSslCipher* cipher, void* engine); +int APS5_VABI CMAC_Update_nid_postfix(LibreSslCmac* context, const void* data, std::size_t length); +int APS5_VABI CMAC_Final_nid_postfix(LibreSslCmac* context, unsigned char* output, std::size_t* length); +const LibreSslCipher* APS5_VABI EVP_aes_128_cbc_nid_postfix(); +const LibreSslCipher* APS5_VABI EVP_aes_192_cbc_nid_postfix(); +const LibreSslCipher* APS5_VABI EVP_aes_256_cbc_nid_postfix(); +} diff --git a/core/libs/prx/libSceLibreSsl/src/Cmac.cpp b/core/libs/prx/libSceLibreSsl/src/Cmac.cpp new file mode 100644 index 0000000000..7c881fdace --- /dev/null +++ b/core/libs/prx/libSceLibreSsl/src/Cmac.cpp @@ -0,0 +1,275 @@ +#include "prx/libSceLibreSsl/include/Cmac.hpp" +#include "prx/libSceLibreSsl/include/Error.hpp" +#include "prx/libc/include/General.hpp" +#include +#include +#include +#include +#include + +extern "C" { +void* APS5_VABI malloc_nid_postfix(std::size_t); +void* APS5_VABI calloc_nid_postfix(std::size_t, std::size_t); +void APS5_VABI free_nid_postfix(void*); +} + +struct LibreSslCmac { + LibreSslCipherContext cipherContext; + unsigned char firstSubkey[32]; + unsigned char secondSubkey[32]; + unsigned char previousBlock[32]; + unsigned char lastBlock[32]; + std::int32_t lastLength; +}; + +static_assert(sizeof(LibreSslCmac) == 304); + +namespace { + +void Cleanse(void* data, std::size_t length) { + auto* bytes = static_cast(data); + while (length-- != 0) *bytes++ = 0; +} + +int Error(int reason) { + LibreSslPutError_nid_no_patch(6, 0xfff, reason, __FILE__, __LINE__); + return 0; +} + +void RequireContext(const LibreSslCmac* context) { + if (context == nullptr) throw std::invalid_argument("CMAC: null context"); +} + +int Cleanup(LibreSslCipherContext& context) { + if (context.engine != nullptr) NotImplemented_nid_no_patch(__func__); + if (context.cipher != nullptr) { + if (context.cipher->cleanup != nullptr && !context.cipher->cleanup(&context)) return 0; + if (context.cipherData != nullptr) Cleanse(context.cipherData, context.cipher->contextSize); + } + free_nid_postfix(context.cipherData); + Cleanse(&context, sizeof(context)); + return 1; +} + +int Control(LibreSslCipherContext& context, int command, int argument) { + if (context.cipher == nullptr) return Error(131); + if (context.cipher->control == nullptr) return Error(132); + const int result = context.cipher->control(&context, command, argument, nullptr); + return result == -1 ? Error(133) : result; +} + +int InitializeCipher(LibreSslCipherContext& context, const LibreSslCipher* cipher, const unsigned char* key, const unsigned char* iv, void* engine) { + context.encrypt = 1; + if (context.engine != nullptr || (cipher != nullptr && engine != nullptr)) NotImplemented_nid_no_patch(__func__); + if (cipher != nullptr) { + if (cipher->contextSize < 0 || cipher->ivLength < 0) throw std::invalid_argument("CMAC: invalid cipher dimensions"); + if (context.cipher != nullptr) { + const auto flags = context.flags; + Cleanup(context); + context.encrypt = 1; + context.flags = flags; + } + context.cipher = cipher; + context.cipherData = nullptr; + if (cipher->contextSize != 0) { + context.cipherData = calloc_nid_postfix(1, cipher->contextSize); + if (context.cipherData == nullptr) return Error(65); + } + context.keyLength = cipher->keyLength; + context.flags &= 1; + if ((cipher->flags & 0x40) != 0 && !Control(context, 0, 0)) return Error(134); + } + if (context.cipher == nullptr) return Error(131); + const auto& method = *context.cipher; + if (method.blockSize != 1 && method.blockSize != 8 && method.blockSize != 16) return Error(136); + const auto mode = method.flags & 0xf0007; + if ((context.flags & 1) == 0 && mode == 0x10002) return Error(170); + if ((method.flags & 0x10) == 0) { + if (mode == 2 || mode == 3 || mode == 4 || mode == 5) { + if (method.ivLength > 16) return Error(102); + if (mode != 2) context.position = 0; + if (mode == 5) { + if (iv != nullptr) std::memcpy(context.iv, iv, method.ivLength); + } else { + if (iv != nullptr) std::memcpy(context.originalIv, iv, method.ivLength); + std::memcpy(context.iv, context.originalIv, method.ivLength); + } + } else if (mode != 0 && mode != 1) { + return 0; + } + } + if (key != nullptr || (method.flags & 0x20) != 0) { + if (method.init == nullptr) NotImplemented_nid_no_patch(__func__); + if (!method.init(&context, key, iv, 1)) return 0; + } + context.bufferLength = 0; + context.finalUsed = 0; + context.blockMask = method.blockSize - 1; + return 1; +} + +int SetKeyLength(LibreSslCipherContext& context, std::size_t length) { + const int narrowed = std::bit_cast(static_cast(length)); + if ((context.cipher->flags & 0x80) != 0) return Control(context, 1, narrowed); + if (context.keyLength == narrowed) return 1; + if (narrowed > 0 && (context.cipher->flags & 8) != 0) { + context.keyLength = narrowed; + return 1; + } + return Error(130); +} + +int EncryptBlock(LibreSslCmac& context, unsigned char* output, const unsigned char* input) { + if (context.cipherContext.cipher->cipher == nullptr) NotImplemented_nid_no_patch(__func__); + return context.cipherContext.cipher->cipher(&context.cipherContext, output, input, context.cipherContext.cipher->blockSize); +} + +void DoubleBlock(unsigned char* output, const unsigned char* input, int length) { + unsigned char carry = 0; + for (int index = length; index-- > 0;) { + const auto value = input[index]; + output[index] = static_cast((value << 1) | carry); + carry = value >> 7; + } + output[length - 1] ^= static_cast((length == 16 ? 0x87 : 0x1b) & -static_cast(carry)); +} + +int APS5_VABI AesInit(LibreSslCipherContext* context, const unsigned char* key, const unsigned char*, int encrypt) { + if (key == nullptr || context->cipherData == nullptr) throw std::invalid_argument("CMAC AES: missing key storage"); + if (context->keyLength != 16 && context->keyLength != 24 && context->keyLength != 32) return Error(143); + auto* aes = static_cast(context->cipherData); + mbedtls_aes_init(aes); + const int result = encrypt ? mbedtls_aes_setkey_enc(aes, key, context->keyLength * 8) : mbedtls_aes_setkey_dec(aes, key, context->keyLength * 8); + if (result != 0) throw std::runtime_error("CMAC AES: backend key setup failed"); + return 1; +} + +int APS5_VABI AesCipher(LibreSslCipherContext* context, unsigned char* output, const unsigned char* input, std::size_t length) { + if (context->cipherData == nullptr || (length != 0 && (output == nullptr || input == nullptr))) throw std::invalid_argument("CMAC AES: null data"); + if (length == 0) return 1; + auto* aes = static_cast(context->cipherData); + const int mode = context->encrypt ? MBEDTLS_AES_ENCRYPT : MBEDTLS_AES_DECRYPT; + const auto complete = length - length % 16; + if (complete != 0 && mbedtls_aes_crypt_cbc(aes, mode, complete, context->iv, input, output) != 0) + throw std::runtime_error("CMAC AES: backend encryption failed"); + const auto remaining = length - complete; + if (remaining == 0) return 1; + input += complete; + output += complete; + unsigned char block[16]; + unsigned char nextIv[16]; + if (context->encrypt) { + std::memcpy(block, context->iv, 16); + for (std::size_t index = 0; index < remaining; ++index) block[index] ^= input[index]; + if (mbedtls_aes_crypt_ecb(aes, mode, block, output) != 0) throw std::runtime_error("CMAC AES: backend encryption failed"); + std::memcpy(context->iv, output, 16); + } else { + std::memcpy(nextIv, input, 16); + if (mbedtls_aes_crypt_ecb(aes, mode, input, block) != 0) throw std::runtime_error("CMAC AES: backend decryption failed"); + for (std::size_t index = 0; index < remaining; ++index) output[index] = block[index] ^ context->iv[index]; + std::memcpy(context->iv, nextIv, 16); + } + Cleanse(block, sizeof(block)); + Cleanse(nextIv, sizeof(nextIv)); + return 1; +} + +const LibreSslCipher aes128{419, 16, 16, 16, 0x5002, AesInit, AesCipher, nullptr, sizeof(mbedtls_aes_context), nullptr, nullptr, nullptr, nullptr}; +const LibreSslCipher aes192{423, 16, 24, 16, 0x5002, AesInit, AesCipher, nullptr, sizeof(mbedtls_aes_context), nullptr, nullptr, nullptr, nullptr}; +const LibreSslCipher aes256{427, 16, 32, 16, 0x5002, AesInit, AesCipher, nullptr, sizeof(mbedtls_aes_context), nullptr, nullptr, nullptr, nullptr}; + +} + +extern "C" { + +LibreSslCmac* APS5_VABI CMAC_CTX_new_nid_postfix() { + auto* context = static_cast(malloc_nid_postfix(sizeof(LibreSslCmac))); + if (context != nullptr) { + context->cipherContext = {}; + context->lastLength = -1; + } + return context; +} + +void APS5_VABI CMAC_CTX_free_nid_postfix(LibreSslCmac* context) { + if (context == nullptr) return; + Cleanup(context->cipherContext); + Cleanse(context, sizeof(*context)); + free_nid_postfix(context); +} + +int APS5_VABI CMAC_Init_nid_postfix(LibreSslCmac* context, const void* key, std::size_t keyLength, const LibreSslCipher* cipher, void* engine) { + RequireContext(context); + const unsigned char zero[32]{}; + if (key == nullptr && keyLength == 0 && cipher == nullptr && engine == nullptr) { + if (context->lastLength == -1) return 0; + if (!InitializeCipher(context->cipherContext, nullptr, nullptr, zero, nullptr)) return 0; + std::memset(context->previousBlock, 0, context->cipherContext.cipher->blockSize); + context->lastLength = 0; + return 1; + } + if (cipher != nullptr && !InitializeCipher(context->cipherContext, cipher, nullptr, nullptr, engine)) return 0; + if (key == nullptr) return 1; + if (context->cipherContext.cipher == nullptr) return 0; + if (!SetKeyLength(context->cipherContext, keyLength)) return 0; + if (!InitializeCipher(context->cipherContext, nullptr, static_cast(key), zero, nullptr)) return 0; + if (!EncryptBlock(*context, context->previousBlock, zero)) return 0; + const auto blockSize = context->cipherContext.cipher->blockSize; + DoubleBlock(context->firstSubkey, context->previousBlock, blockSize); + DoubleBlock(context->secondSubkey, context->firstSubkey, blockSize); + Cleanse(context->previousBlock, blockSize); + if (!InitializeCipher(context->cipherContext, nullptr, nullptr, zero, nullptr)) return 0; + context->lastLength = 0; + return 1; +} + +int APS5_VABI CMAC_Update_nid_postfix(LibreSslCmac* context, const void* data, std::size_t length) { + RequireContext(context); + if (context->lastLength == -1) return 0; + if (length == 0) return 1; + if (data == nullptr) throw std::invalid_argument("CMAC_Update: null input"); + const auto* bytes = static_cast(data); + const auto blockSize = context->cipherContext.cipher->blockSize; + if (context->lastLength != 0) { + const auto take = std::min(length, static_cast(blockSize - context->lastLength)); + std::memcpy(context->lastBlock + context->lastLength, bytes, take); + context->lastLength += static_cast(take); + bytes += take; + length -= take; + if (length == 0) return 1; + if (!EncryptBlock(*context, context->previousBlock, context->lastBlock)) return 0; + } + while (length > static_cast(blockSize)) { + if (!EncryptBlock(*context, context->previousBlock, bytes)) return 0; + bytes += blockSize; + length -= blockSize; + } + std::memcpy(context->lastBlock, bytes, length); + context->lastLength = static_cast(length); + return 1; +} + +int APS5_VABI CMAC_Final_nid_postfix(LibreSslCmac* context, unsigned char* output, std::size_t* length) { + RequireContext(context); + if (context->lastLength == -1) return 0; + if (length == nullptr) throw std::invalid_argument("CMAC_Final: null output length"); + const auto blockSize = context->cipherContext.cipher->blockSize; + *length = blockSize; + if (output == nullptr) return 1; + const bool full = context->lastLength == blockSize; + if (!full) { + std::memset(context->lastBlock + context->lastLength, 0, blockSize - context->lastLength); + context->lastBlock[context->lastLength] = 0x80; + } + const auto* subkey = full ? context->firstSubkey : context->secondSubkey; + for (int index = 0; index < blockSize; ++index) output[index] = context->lastBlock[index] ^ subkey[index]; + if (EncryptBlock(*context, output, output)) return 1; + Cleanse(output, blockSize); + return 0; +} + +const LibreSslCipher* APS5_VABI EVP_aes_128_cbc_nid_postfix() { return &aes128; } +const LibreSslCipher* APS5_VABI EVP_aes_192_cbc_nid_postfix() { return &aes192; } +const LibreSslCipher* APS5_VABI EVP_aes_256_cbc_nid_postfix() { return &aes256; } + +} diff --git a/core/libs/tests/GuestLibreSslCmac.cpp b/core/libs/tests/GuestLibreSslCmac.cpp new file mode 100644 index 0000000000..975f0148a8 --- /dev/null +++ b/core/libs/tests/GuestLibreSslCmac.cpp @@ -0,0 +1,444 @@ +#include "prx/libSceLibreSsl/include/Cmac.hpp" +#include "prx/libSceLibreSsl/ErrorQueue.hpp" +#include "prx/libc/include/ApplicationHeap.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +#ifdef ANYPS5_CMAC_DYNAMIC +namespace { +decltype(&CMAC_CTX_new_nid_postfix) cmacNew; +decltype(&CMAC_CTX_free_nid_postfix) cmacFree; +decltype(&CMAC_Init_nid_postfix) cmacInit; +decltype(&CMAC_Update_nid_postfix) cmacUpdate; +decltype(&CMAC_Final_nid_postfix) cmacFinal; +decltype(&EVP_aes_128_cbc_nid_postfix) aes128Method; +decltype(&EVP_aes_192_cbc_nid_postfix) aes192Method; +decltype(&EVP_aes_256_cbc_nid_postfix) aes256Method; +decltype(&ERR_clear_error_nid_postfix) clearError; +decltype(&ERR_get_error_nid_postfix) getError; +decltype(&ApplicationHeapRegister_nid_no_patch) registerHeap; + +void* Load(const std::string& path) { +#ifdef _WIN32 + auto* library = LoadLibraryExA(path.c_str(), nullptr, LOAD_WITH_ALTERED_SEARCH_PATH); +#else + auto* library = dlopen(path.c_str(), RTLD_NOW | RTLD_LOCAL); +#endif + if (library == nullptr) throw std::runtime_error("failed to load patched CMAC dependency: " + path); + return library; +} + +template void Resolve(TFunction& destination, void* library, const char* name) { +#ifdef _WIN32 + destination = reinterpret_cast(GetProcAddress(static_cast(library), name)); +#else + destination = reinterpret_cast(dlsym(library, name)); +#endif + if (destination == nullptr) throw std::runtime_error(std::string("missing patched CMAC dependency: ") + name); +} + +void InitializePatched(const char* directory) { + const auto root = std::string(directory) + "/"; + auto* libc = Load(root + "libc.prx"); + auto* library = Load(root + "libSceLibreSsl.prx"); + Resolve(cmacNew, library, "mwJYST4Bb3U"); + Resolve(cmacFree, library, "cBNGAG1DkXc"); + Resolve(cmacInit, library, "LATrF+dIZYI"); + Resolve(cmacUpdate, library, "XpAJ1Ckg-q4"); + Resolve(cmacFinal, library, "6MM7ng9P+18"); + Resolve(aes128Method, library, "rV07jlO-+6Q"); + Resolve(aes192Method, library, "0X5DnA3olsk"); + Resolve(aes256Method, library, "J44dsZUcK4I"); + Resolve(clearError, library, "sieQ--PINWg"); + Resolve(getError, library, "DJg+qWX1rwY"); + Resolve(registerHeap, libc, "ApplicationHeapRegister_nid_no_patch"); +} +} + +#define CMAC_CTX_new_nid_postfix cmacNew +#define CMAC_CTX_free_nid_postfix cmacFree +#define CMAC_Init_nid_postfix cmacInit +#define CMAC_Update_nid_postfix cmacUpdate +#define CMAC_Final_nid_postfix cmacFinal +#define EVP_aes_128_cbc_nid_postfix aes128Method +#define EVP_aes_192_cbc_nid_postfix aes192Method +#define EVP_aes_256_cbc_nid_postfix aes256Method +#define ERR_clear_error_nid_postfix clearError +#define ERR_get_error_nid_postfix getError +#define ApplicationHeapRegister_nid_no_patch registerHeap +#endif + +namespace { + +void Require(bool condition, const char* message) { + if (condition) return; + std::fprintf(stderr, "failed: %s\n", message); + std::abort(); +} + +int allocationsUntilFailure = -1; +std::vector> allocations; + +void* APS5_VABI Allocate(std::size_t size) { + if (allocationsUntilFailure == 0) return nullptr; + if (allocationsUntilFailure > 0) --allocationsUntilFailure; + auto* pointer = std::malloc(size); + if (pointer != nullptr) allocations.emplace_back(pointer, size); + return pointer; +} +void* APS5_VABI Reallocate(void*, std::size_t) { throw std::logic_error("unexpected CMAC reallocation"); } +void* APS5_VABI Align(std::size_t, std::size_t) { throw std::logic_error("unexpected CMAC aligned allocation"); } +void* APS5_VABI Realign(void*, std::size_t, std::size_t) { throw std::logic_error("unexpected CMAC realignment"); } +int APS5_VABI PosixAlign(void**, std::size_t, std::size_t) { throw std::logic_error("unexpected CMAC aligned allocation"); } + +void* APS5_VABI Calloc(std::size_t count, std::size_t size) { + if (allocationsUntilFailure == 0) return nullptr; + if (allocationsUntilFailure > 0) --allocationsUntilFailure; + auto* pointer = std::calloc(count, size); + if (pointer != nullptr) allocations.emplace_back(pointer, count * size); + return pointer; +} + +void APS5_VABI Free(void* pointer) { + if (pointer == nullptr) return; + const auto found = std::find_if(allocations.begin(), allocations.end(), [&](const auto& entry) { return entry.first == pointer; }); + Require(found != allocations.end(), "free a guest-owned allocation"); + const auto* bytes = static_cast(pointer); + Require(std::all_of(bytes, bytes + found->second, [](auto value) { return value == 0; }), "wipe contexts and key storage before freeing"); + allocations.erase(found); + std::free(pointer); +} + +void InitializeHeap() { + const std::array api{ + reinterpret_cast(Allocate), reinterpret_cast(Free), reinterpret_cast(Calloc), + reinterpret_cast(Reallocate), reinterpret_cast(Align), reinterpret_cast(Realign), + reinterpret_cast(PosixAlign)}; + ApplicationHeapRegister_nid_no_patch(api.data()); +} + +template void RequireThrows(TAction action) { + try { action(); } catch (const std::exception&) { return; } + Require(false, "unsupported input must throw"); +} + +std::vector Hex(std::string_view text) { + Require(text.size() % 2 == 0, "hex fixture has complete bytes"); + std::vector bytes; + for (std::size_t index = 0; index < text.size(); index += 2) { + const auto nibble = [](char value) { return value <= '9' ? value - '0' : value - 'a' + 10; }; + bytes.push_back(static_cast((nibble(text[index]) << 4) | nibble(text[index + 1]))); + } + return bytes; +} + +using CmacFree = void (APS5_VABI*)(LibreSslCmac*); +using Context = std::unique_ptr; + +Context NewContext() { + Context context(CMAC_CTX_new_nid_postfix(), CMAC_CTX_free_nid_postfix); + Require(context != nullptr, "allocate CMAC context"); + return context; +} + +std::array Final(LibreSslCmac* context) { + std::array output; + output.fill(0xa5); + std::size_t length = 99; + Require(CMAC_Final_nid_postfix(context, output.data() + 1, &length) == 1, "finalize CMAC"); + Require(length == 16 && output.front() == 0xa5 && output.back() == 0xa5, "tag size and output bounds"); + std::array result; + std::copy_n(output.data() + 1, 16, result.data()); + return result; +} + +const LibreSslCipher* Methods[3]; + +void RfcVectors() { + const auto key = Hex("2b7e151628aed2a6abf7158809cf4f3c"); + const auto message = Hex("6bc1bee22e409f96e93d7e117393172aae2d8a571e03ac9c9eb76fac45af8e5130c81c46a35ce411e5fbc1191a0a52eff69f2445df4f9b17ad2b417be66c3710"); + const std::size_t lengths[] = {0, 16, 40, 64}; + const char* tags[] = {"bb1d6929e95937287fa37d129b756746", "070a16b46b4d4144f79bdd9dd04a287c", "dfa66747de9ae63030ca32611497c827", "51f0bebf7e3b9d92fc49741779363cfe"}; + auto context = NewContext(); + for (std::size_t vector = 0; vector < 4; ++vector) { + const auto expected = Hex(tags[vector]); + for (std::size_t split = 0; split <= lengths[vector]; ++split) { + Require(CMAC_Init_nid_postfix(context.get(), key.data(), key.size(), Methods[0], nullptr) == 1, "RFC key initialization"); + Require(CMAC_Update_nid_postfix(context.get(), message.data(), split) == 1, "RFC first update"); + Require(CMAC_Update_nid_postfix(context.get(), nullptr, 0) == 1, "empty update"); + Require(CMAC_Update_nid_postfix(context.get(), message.data() + split, lengths[vector] - split) == 1, "RFC second update"); + std::size_t length = 99; + Require(CMAC_Final_nid_postfix(context.get(), nullptr, &length) == 1 && length == 16, "length-only final"); + Require(CMAC_Final_nid_postfix(context.get(), nullptr, &length) == 1 && length == 16, "repeat length query"); + const auto actual = Final(context.get()); + if (!std::equal(actual.begin(), actual.end(), expected.begin())) { + std::fprintf(stderr, "RFC vector %zu split %zu: ", vector, split); + for (auto byte : actual) std::fprintf(stderr, "%02x", byte); + std::fputc('\n', stderr); + } + Require(std::equal(actual.begin(), actual.end(), expected.begin()), "RFC 4493 tag"); + } + } +} + +void Differential() { + const mbedtls_cipher_type_t types[] = {MBEDTLS_CIPHER_AES_128_ECB, MBEDTLS_CIPHER_AES_192_ECB, MBEDTLS_CIPHER_AES_256_ECB}; + std::array key; + std::array message; + for (std::size_t index = 0; index < key.size(); ++index) key[index] = static_cast(index * 73 + 41); + for (std::size_t index = 0; index < message.size(); ++index) message[index] = static_cast(index * 197 + 17); + auto context = NewContext(); + for (std::size_t method = 0; method < 3; ++method) { + const auto* cipher = Methods[method]; + Require(cipher->nid == 419 + static_cast(method) * 4 && cipher->blockSize == 16 && cipher->keyLength == 16 + static_cast(method) * 8 && cipher->ivLength == 16 && cipher->flags == 0x5002, "AES descriptor metadata"); + const auto* reference = mbedtls_cipher_info_from_type(types[method]); + Require(reference != nullptr, "reference cipher available"); + for (std::size_t length = 0; length <= message.size(); ++length) { + std::array expected; + Require(mbedtls_cipher_cmac(reference, key.data(), cipher->keyLength * 8, message.data(), length, expected.data()) == 0, "reference CMAC"); + for (const auto chunk : {1u, 7u, 15u, 16u, 17u, 64u, 257u}) { + Require(CMAC_Init_nid_postfix(context.get(), key.data(), cipher->keyLength, cipher, nullptr) == 1, "differential initialization"); + for (std::size_t offset = 0; offset < length;) { + const auto take = std::min(static_cast(chunk), length - offset); + Require(CMAC_Update_nid_postfix(context.get(), message.data() + offset, take) == 1, "differential update"); + offset += take; + } + Require(Final(context.get()) == expected, "CMAC agrees with independent backend"); + } + } + } +} + +void Lifecycle() { + auto context = NewContext(); + const std::array key{}; + const std::array message{}; + std::array output; + output.fill(0xa5); + std::size_t length = 99; + Require(CMAC_Update_nid_postfix(context.get(), nullptr, 1) == 0, "uninitialized update"); + Require(CMAC_Final_nid_postfix(context.get(), output.data(), &length) == 0 && length == 99 && output[0] == 0xa5, "uninitialized final preserves outputs"); + Require(CMAC_Final_nid_postfix(context.get(), nullptr, nullptr) == 0, "uninitialized final precedes null output length"); + Require(CMAC_Init_nid_postfix(context.get(), nullptr, 0, nullptr, nullptr) == 0, "uninitialized restart"); + Require(CMAC_Init_nid_postfix(context.get(), key.data(), 16, nullptr, nullptr) == 0, "key without cipher"); + Require(CMAC_Init_nid_postfix(context.get(), nullptr, 1, nullptr, nullptr) == 1, "nonzero key length without a key is not restart"); + Require(CMAC_Init_nid_postfix(context.get(), nullptr, 0, Methods[0], nullptr) == 1, "stage cipher selection"); + Require(CMAC_Update_nid_postfix(context.get(), message.data(), 1) == 0, "cipher-only context has no key"); + ERR_clear_error_nid_postfix(); + Require(CMAC_Init_nid_postfix(context.get(), key.data(), 15, nullptr, nullptr) == 0, "reject invalid key length"); + Require(ERR_get_error_nid_postfix() == 0x06fff082, "invalid key length publishes EVP error"); + Require(CMAC_Init_nid_postfix(context.get(), key.data(), 16, nullptr, nullptr) == 1, "finish staged initialization"); + Require(CMAC_Update_nid_postfix(context.get(), message.data(), message.size()) == 1, "first message"); + const auto expected = Final(context.get()); + const auto repeated = Final(context.get()); + Require(repeated != expected, "final advances the CBC state"); + Require(CMAC_Init_nid_postfix(context.get(), nullptr, 0, nullptr, nullptr) == 1, "restart reuses the key"); + Require(CMAC_Update_nid_postfix(context.get(), message.data(), message.size()) == 1 && Final(context.get()) == expected, "restart resets state"); + Require(CMAC_Init_nid_postfix(context.get(), key.data(), (std::size_t{1} << 32) + 16, nullptr, nullptr) == 1, "upstream key length narrows to int"); + Require(CMAC_Update_nid_postfix(context.get(), message.data(), message.size()) == 1 && Final(context.get()) == expected, "narrowed key length tag"); + Require(CMAC_Init_nid_postfix(context.get(), key.data(), 24, Methods[1], nullptr) == 1, "switch AES key size"); + Require(CMAC_Init_nid_postfix(context.get(), key.data(), 32, Methods[2], nullptr) == 1, "switch AES key size again"); + RequireThrows([&] { CMAC_Init_nid_postfix(nullptr, key.data(), 16, Methods[0], nullptr); }); + RequireThrows([&] { CMAC_Update_nid_postfix(nullptr, nullptr, 0); }); + RequireThrows([&] { CMAC_Final_nid_postfix(nullptr, output.data(), &length); }); + RequireThrows([&] { CMAC_Update_nid_postfix(context.get(), nullptr, 1); }); + RequireThrows([&] { CMAC_Final_nid_postfix(context.get(), output.data(), nullptr); }); + RequireThrows([&] { CMAC_Init_nid_postfix(context.get(), key.data(), 16, Methods[0], reinterpret_cast(1)); }); + CMAC_CTX_free_nid_postfix(nullptr); +} + +void AllocationFailures() { + allocationsUntilFailure = 0; + Require(CMAC_CTX_new_nid_postfix() == nullptr, "context allocation failure"); + allocationsUntilFailure = 1; + auto context = NewContext(); + const std::array key{}; + ERR_clear_error_nid_postfix(); + Require(CMAC_Init_nid_postfix(context.get(), key.data(), key.size(), Methods[0], nullptr) == 0, "cipher allocation failure"); + Require(ERR_get_error_nid_postfix() == 0x06fff041, "cipher allocation failure publishes EVP error"); + allocationsUntilFailure = -1; + Require(CMAC_Init_nid_postfix(context.get(), key.data(), key.size(), Methods[0], nullptr) == 1, "recover from failed allocation"); +} + +void CipherCallbacks() { + std::array key{}; + std::array iv; + for (std::size_t index = 0; index < iv.size(); ++index) iv[index] = static_cast(index + 1); + for (const auto* method : Methods) { + for (std::size_t length = 0; length <= 48; ++length) { + const auto padded = (length + 15) / 16 * 16; + std::vector input(padded); + for (std::size_t index = 0; index < length; ++index) input[index] = static_cast(index * 31 + 17); + std::vector expected(padded); + auto referenceIv = iv; + mbedtls_aes_context reference; + mbedtls_aes_init(&reference); + Require(mbedtls_aes_setkey_enc(&reference, key.data(), method->keyLength * 8) == 0, "reference AES key"); + Require(mbedtls_aes_crypt_cbc(&reference, MBEDTLS_AES_ENCRYPT, padded, referenceIv.data(), input.data(), expected.data()) == 0, "reference CBC"); + mbedtls_aes_free(&reference); + std::vector storage(method->contextSize); + LibreSslCipherContext context{}; + context.cipher = method; + context.cipherData = storage.data(); + context.keyLength = method->keyLength; + for (const bool inPlace : {false, true}) { + context.encrypt = 1; + std::copy(iv.begin(), iv.end(), context.iv); + Require(method->init(&context, key.data(), nullptr, 1) == 1, "AES descriptor encryption setup"); + auto output = input; + output.push_back(0xa5); + Require(method->cipher(&context, output.data(), inPlace ? output.data() : input.data(), length) == 1, "AES descriptor partial encryption"); + Require(output.back() == 0xa5 && std::equal(expected.begin(), expected.end(), output.begin()), "CBC partial encryption and bounds"); + Require(std::equal(referenceIv.begin(), referenceIv.end(), context.iv), "CBC encryption IV"); + context.encrypt = 0; + std::copy(iv.begin(), iv.end(), context.iv); + Require(method->init(&context, key.data(), nullptr, 0) == 1, "AES descriptor decryption setup"); + if (!inPlace) output.assign(padded + 1, 0xa5); + const auto before = output; + Require(method->cipher(&context, output.data(), inPlace ? output.data() : expected.data(), length) == 1, "AES descriptor partial decryption"); + Require(std::equal(input.begin(), input.begin() + length, output.begin()), "CBC decrypted prefix"); + Require(std::equal(before.begin() + length, before.end(), output.begin() + length), "partial decryption preserves trailing bytes"); + Require(std::equal(referenceIv.begin(), referenceIv.end(), context.iv), "CBC decryption IV"); + } + } + } +} + +int APS5_VABI DesInit(LibreSslCipherContext* context, const unsigned char* key, const unsigned char*, int) { + auto* des = static_cast(context->cipherData); + mbedtls_des3_init(des); + return mbedtls_des3_set3key_enc(des, key) == 0; +} + +int APS5_VABI DesCipher(LibreSslCipherContext* context, unsigned char* output, const unsigned char* input, std::size_t length) { + return mbedtls_des3_crypt_cbc(static_cast(context->cipherData), MBEDTLS_DES_ENCRYPT, length, context->iv, input, output) == 0; +} + +void GuestDes() { + const LibreSslCipher method{44, 8, 24, 8, 2, DesInit, DesCipher, nullptr, sizeof(mbedtls_des3_context), nullptr, nullptr, nullptr, nullptr}; + std::array key; + std::array message; + for (std::size_t index = 0; index < key.size(); ++index) key[index] = static_cast(index * 31 + 7); + for (std::size_t index = 0; index < message.size(); ++index) message[index] = static_cast(index * 71 + 3); + const auto* reference = mbedtls_cipher_info_from_type(MBEDTLS_CIPHER_DES_EDE3_ECB); + Require(reference != nullptr, "reference 64-bit cipher"); + auto context = NewContext(); + for (std::size_t length = 0; length <= message.size(); ++length) { + std::array expected; + Require(mbedtls_cipher_cmac(reference, key.data(), 192, message.data(), length, expected.data()) == 0, "reference 64-bit CMAC"); + Require(CMAC_Init_nid_postfix(context.get(), key.data(), key.size(), &method, nullptr) == 1, "guest 64-bit cipher initialization"); + for (std::size_t index = 0; index < length; ++index) Require(CMAC_Update_nid_postfix(context.get(), message.data() + index, 1) == 1, "guest 64-bit cipher update"); + std::array actual; + actual.fill(0xa5); + std::size_t outputLength = 99; + Require(CMAC_Final_nid_postfix(context.get(), actual.data() + 1, &outputLength) == 1, "guest 64-bit cipher final"); + Require(outputLength == 8 && actual.front() == 0xa5 && actual.back() == 0xa5 && std::equal(expected.begin(), expected.end(), actual.begin() + 1), "64-bit CMAC tag and output bounds"); + } +} + +const LibreSslCipher* delegated; +int initCalls; +int cipherCalls; +int cleanupCalls; +int failAt; + +int APS5_VABI GuestInit(LibreSslCipherContext* context, const unsigned char* key, const unsigned char* iv, int encrypt) { + ++initCalls; + Require(context->encrypt == 1 && context->keyLength == 16 && context->cipherData != nullptr, "guest callback context"); + return delegated->init(context, key, iv, encrypt); +} + +int APS5_VABI GuestCipher(LibreSslCipherContext* context, unsigned char* output, const unsigned char* input, std::size_t length) { + ++cipherCalls; + if (failAt == cipherCalls) { + std::memset(output, 0x5a, length); + return 0; + } + return delegated->cipher(context, output, input, length); +} + +int APS5_VABI GuestCleanup(LibreSslCipherContext*) { + ++cleanupCalls; + return 1; +} + +int APS5_VABI GuestControl(LibreSslCipherContext* context, int command, int argument, void*) { + if (command == 0) return 1; + if (command == 1 && argument == 16) { + context->keyLength = argument; + return 1; + } + return -1; +} + +void Callbacks() { + delegated = Methods[0]; + auto method = *delegated; + method.init = GuestInit; + method.cipher = GuestCipher; + method.cleanup = GuestCleanup; + method.control = GuestControl; + method.flags |= 0xc0; + std::array key{}; + std::array message{}; + { + auto context = NewContext(); + Require(CMAC_Init_nid_postfix(context.get(), key.data(), key.size(), &method, nullptr) == 1 && initCalls == 1, "guest initialization callback"); + Require(CMAC_Update_nid_postfix(context.get(), message.data(), message.size()) == 1 && cipherCalls == 3, "guest cipher callbacks"); + const auto tag = Final(context.get()); + Require(CMAC_Init_nid_postfix(context.get(), nullptr, 0, nullptr, nullptr) == 1, "guest callback restart"); + Require(CMAC_Update_nid_postfix(context.get(), message.data(), message.size()) == 1 && Final(context.get()) == tag, "guest-defined cipher matches builtin"); + failAt = cipherCalls + 1; + std::array output; + output.fill(0xa5); + std::size_t length = 99; + Require(CMAC_Final_nid_postfix(context.get(), output.data(), &length) == 0 && length == 16, "cipher failure propagates"); + Require(std::all_of(output.begin(), output.end(), [](auto value) { return value == 0; }), "failed final clears output"); + failAt = 0; + } + Require(cleanupCalls == 1, "guest cleanup callback"); + auto context = NewContext(); + method.blockSize = 32; + ERR_clear_error_nid_postfix(); + Require(CMAC_Init_nid_postfix(context.get(), key.data(), key.size(), &method, nullptr) == 0, "reject unsupported block size"); + Require(ERR_get_error_nid_postfix() == 0x06fff088, "bad block length publishes EVP error"); +} +} + +int main(int argc, char** argv) { +#ifdef ANYPS5_CMAC_DYNAMIC + Require(argc == 2, "patched test needs the library directory"); + InitializePatched(argv[1]); +#endif + InitializeHeap(); + Methods[0] = EVP_aes_128_cbc_nid_postfix(); + Methods[1] = EVP_aes_192_cbc_nid_postfix(); + Methods[2] = EVP_aes_256_cbc_nid_postfix(); + RfcVectors(); + Differential(); + Lifecycle(); + Callbacks(); + AllocationFailures(); + CipherCallbacks(); + GuestDes(); + Require(allocations.empty(), "all guest allocations released"); + std::puts("CMAC vectors, streaming, lifecycle and callback checks passed"); +} diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index efd40cc7e9..f7d7e4ea1c 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -3,8 +3,10 @@ ### Functional - [ERR queues and strings](../../../core/libs/prx/libSceLibreSsl/ErrorQueue.cpp) have no PS5 equivalence verification. Contracts follow LibreSSL 3.4.1's [err.c](https://github.com/libressl/openbsd/blob/078b3d1df8ca2011c1605cd40418c58fa0f5b143/src/lib/libcrypto/err/err.c), err.h, crypto.h and err_all.c at that revision: LP64 error arguments/results, 15 queued errors and historical crypto strings. The [one-time crypto string loader](https://github.com/libressl/openbsd/blob/078b3d1df8ca2011c1605cd40418c58fa0f5b143/src/lib/libcrypto/err/err_all.c) does not reload strings after `ERR_free_strings`; subsequent formatting uses numeric fallbacks, including after an explicit `ERR_load_crypto_strings` call. System reasons use guest libc's FreeBSD errno messages, truncated to 31 bytes; console wording is unverified. Explicit thread IDs in `ERR_remove_thread_state` throw because guest pthread identity mapping is unverified. Null IDs clear the calling thread. SSL-specific strings are not loaded; SSL initialization remains unimplemented. -- [Error publication](../../../core/libs/prx/libSceLibreSsl/ErrorQueue.hpp) has no implemented production callers on this branch. BIOs and the digest, AES, allocator/RAND, DES, BIGNUM and EVP/HMAC work in pending branches require separate integration. `LibreSslPutError_nid_no_patch(int library, int function, int reason, const char* file, int line)` has C linkage and the native host calling convention, accepts upstream numeric fields and publishes to the calling thread's queue. Three-argument source calls default file/line to null/zero. It stores file/line without taking ownership; producers must supply a filename with static lifetime. Guest ERR entry points use `APS5_VABI`. Unsupported producer exports continue throwing; the local tests publish through this helper. +- [Error publication](../../../core/libs/prx/libSceLibreSsl/ErrorQueue.hpp) is integrated with CMAC cipher errors. BIOs and the digest, low-level AES, allocator/RAND, DES, BIGNUM and EVP/HMAC work in pending branches require separate integration. `LibreSslPutError_nid_no_patch(int library, int function, int reason, const char* file, int line)` has C linkage and the native host calling convention, accepts upstream numeric fields and publishes to the calling thread's queue. Three-argument source calls default file/line to null/zero. It stores file/line without taking ownership; producers must supply a filename with static lifetime. Guest ERR entry points use `APS5_VABI`. Unsupported producer exports continue throwing; the local tests publish through this helper. + +- [CMAC and AES-CBC descriptors](../../../core/libs/prx/libSceLibreSsl/src/Cmac.cpp) have no PS5 equivalence verification. Signatures, the 88-byte cipher method and 168-byte cipher context follow [LibreSSL 3.4.1](https://github.com/libressl/openbsd/blob/078b3d1df8ca2011c1605cd40418c58fa0f5b143/src/lib/libcrypto/cmac/cmac.c); the console version remains unknown. AES cipher data uses Mbed TLS storage instead of LibreSSL's private key schedule. Guest-defined cipher methods run through guest-ABI callbacks. Other built-in cipher selectors and the general EVP cipher exports still throw. ENGINE-backed methods throw. Null contexts, null nonempty update buffers, null initialized-final length pointers, negative cipher dimensions and missing required callbacks throw where upstream dereferences unchecked pointers. Error metadata names host source locations. ### Unknown function info -- [libSceLibreSsl pending exports](../../../core/libs/prx/libSceLibreSsl/Export.cpp) - 399 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) - 391 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`. From 8fd07a82a847e78ded937d9dd76e6062e53f229f Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Fri, 23 Oct 2026 19:22:13 -0300 Subject: [PATCH 09/11] fix(libSceLibreSsl): generate ERR strings before compilation --- core/libs/prx/libSceLibreSsl/ErrorQueue.cmake | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/libs/prx/libSceLibreSsl/ErrorQueue.cmake b/core/libs/prx/libSceLibreSsl/ErrorQueue.cmake index d97fcd55cf..4b254515a4 100644 --- a/core/libs/prx/libSceLibreSsl/ErrorQueue.cmake +++ b/core/libs/prx/libSceLibreSsl/ErrorQueue.cmake @@ -9,6 +9,8 @@ add_custom_command(OUTPUT ${libreSslStrings} COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_LIST_DIR}/GenerateErrorStrings.py ${libreSslSource} ${libreSslStrings} DEPENDS ${CMAKE_CURRENT_LIST_DIR}/GenerateErrorStrings.py ${libreSslStringSources} VERBATIM) +add_custom_target(libressl_error_strings DEPENDS ${libreSslStrings}) +add_dependencies(libSceLibreSsl libressl_error_strings) target_sources(libSceLibreSsl PRIVATE ${CMAKE_CURRENT_LIST_DIR}/src/Error.cpp ${CMAKE_CURRENT_LIST_DIR}/ErrorQueue.cpp ${libreSslStrings}) target_include_directories(libSceLibreSsl PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/prx/libSceLibreSsl) From 2a6133dffc4e2782721d87f7b13fcbc5b2897b84 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Sat, 24 Oct 2026 21:53:02 -0300 Subject: [PATCH 10/11] test(libSceLibreSsl): avoid pinning unsupported BIO ex-data --- core/libs/tests/GuestLibreSslBio.cpp | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/core/libs/tests/GuestLibreSslBio.cpp b/core/libs/tests/GuestLibreSslBio.cpp index 801f589aa5..408a4f0506 100644 --- a/core/libs/tests/GuestLibreSslBio.cpp +++ b/core/libs/tests/GuestLibreSslBio.cpp @@ -143,17 +143,6 @@ void InitializeHeap() { ApplicationHeapRegister_nid_no_patch(api.data()); } -template -void RequireUnsupported(TFunction function) { - bool threw = false; - try { - function(); - } catch (const std::runtime_error&) { - threw = true; - } - Require(threw, "unsupported BIO path did not throw"); -} - void RequireError(std::uint64_t expected) { Require(ERR_peek_error_nid_postfix() == expected, "BIO error code or ordering"); Require(ERR_get_error_nid_postfix() == expected, "consume BIO error"); @@ -407,10 +396,6 @@ void TestUnsupported() { Require(BIO_write_nid_postfix(bio, "a", 1) == -2, "reject uninitialized write"); RequireError(0x20fff078); bio->initialized = 1; - bio->extraData = bio; - RequireUnsupported([&] { BIO_free_nid_postfix(bio); }); - Require(bio->references == 1, "unsupported ex-data preserves reference"); - bio->extraData = nullptr; const BioMethod* method = bio->method; bio->method = nullptr; Require(BIO_ctrl_nid_postfix(bio, 1, 0, nullptr) == -2, "reject control without a method"); From a3860a08ec0c4d3e245ab8f23a3d610c3d67b340 Mon Sep 17 00:00:00 2001 From: Felipebmaitan Date: Tue, 27 Oct 2026 01:33:44 -0300 Subject: [PATCH 11/11] fix(libSceLibreSsl): dispatch boundary writes and verify growth wiping --- core/libs/prx/libSceLibreSsl/MemoryBio.cmake | 4 +- core/libs/prx/libSceLibreSsl/include/Api.hpp | 15 +-- core/libs/prx/libSceLibreSsl/src/Bio.cpp | 9 +- core/libs/tests/GuestLibreSslBio.cpp | 113 ++++++++++++++++--- docs/dev/debt/libSceLibreSsl.md | 4 +- 5 files changed, 115 insertions(+), 30 deletions(-) diff --git a/core/libs/prx/libSceLibreSsl/MemoryBio.cmake b/core/libs/prx/libSceLibreSsl/MemoryBio.cmake index 26b6a3ce35..a9ed5d36c8 100644 --- a/core/libs/prx/libSceLibreSsl/MemoryBio.cmake +++ b/core/libs/prx/libSceLibreSsl/MemoryBio.cmake @@ -20,7 +20,9 @@ if(BUILD_TESTING) if(WIN32) target_link_libraries(guest_libressl_bio_patched_tests PRIVATE libc) endif() - add_dependencies(guest_libressl_bio_patched_tests libs) + add_custom_target(libressl_bio_patched_libraries DEPENDS + ${PATCHED_DIR}/libSceLibreSsl.prx ${PATCHED_DIR}/libc.prx ${PATCHED_DIR}/libkernel.prx) + add_dependencies(guest_libressl_bio_patched_tests libressl_bio_patched_libraries) add_test(NAME guest_libressl_bio_patched COMMAND guest_libressl_bio_patched_tests ${PATCHED_DIR}) set_tests_properties(guest_libressl_bio_patched PROPERTIES TIMEOUT 30) if(WIN32) diff --git a/core/libs/prx/libSceLibreSsl/include/Api.hpp b/core/libs/prx/libSceLibreSsl/include/Api.hpp index 2941e63b51..3c4bccfa32 100644 --- a/core/libs/prx/libSceLibreSsl/include/Api.hpp +++ b/core/libs/prx/libSceLibreSsl/include/Api.hpp @@ -1,6 +1,7 @@ #pragma once #include "SceTypes.hpp" +#include "prx/libSceLibreSsl/include/Bio.hpp" struct Asn1String { std::int32_t length; @@ -123,18 +124,18 @@ int APS5_VABI ASN1_STRING_print_nid_postfix(void); int APS5_VABI ASN1_STRING_to_UTF8_nid_postfix(unsigned char** output, Asn1String* value); int APS5_VABI ASN1_STRING_type_nid_postfix(Asn1String* value); int APS5_VABI ASN1_TIME_print_nid_postfix(void); -int APS5_VABI BIO_ctrl_nid_postfix(void); +std::int64_t APS5_VABI BIO_ctrl_nid_postfix(LibreSsl::Bio* bio, int command, std::int64_t argument, void* pointer); int APS5_VABI BIO_f_ssl_nid_postfix(void); -int APS5_VABI BIO_free_nid_postfix(void); -int APS5_VABI BIO_new_nid_postfix(void); +int APS5_VABI BIO_free_nid_postfix(LibreSsl::Bio* bio); +LibreSsl::Bio* APS5_VABI BIO_new_nid_postfix(const LibreSsl::BioMethod* method); int APS5_VABI BIO_new_fp_nid_postfix(void); -int APS5_VABI BIO_new_mem_buf_nid_postfix(void); +LibreSsl::Bio* APS5_VABI BIO_new_mem_buf_nid_postfix(const void* source, int size); int APS5_VABI BIO_new_socket_nid_postfix(void); int APS5_VABI BIO_printf_nid_postfix(void); -int APS5_VABI BIO_puts_nid_postfix(void); +int APS5_VABI BIO_puts_nid_postfix(LibreSsl::Bio* bio, const char* source); int APS5_VABI BIO_s_file_nid_postfix(void); -int APS5_VABI BIO_s_mem_nid_postfix(void); -int APS5_VABI BIO_write_nid_postfix(void); +const LibreSsl::BioMethod* APS5_VABI BIO_s_mem_nid_postfix(); +int APS5_VABI BIO_write_nid_postfix(LibreSsl::Bio* bio, const void* source, int size); int APS5_VABI BN_CTX_end_nid_postfix(void); int APS5_VABI BN_CTX_free_nid_postfix(void); int APS5_VABI BN_CTX_new_nid_postfix(void); diff --git a/core/libs/prx/libSceLibreSsl/src/Bio.cpp b/core/libs/prx/libSceLibreSsl/src/Bio.cpp index 5bffff2ac6..98f88e4985 100644 --- a/core/libs/prx/libSceLibreSsl/src/Bio.cpp +++ b/core/libs/prx/libSceLibreSsl/src/Bio.cpp @@ -1,4 +1,5 @@ -#include "prx/libSceLibreSsl/include/Bio.hpp" +#include "prx/libSceLibreSsl/libSceLibreSsl.hpp" +#include "prx/libc/include/Api.hpp" #include "prx/libSceLibreSsl/ErrorQueue.hpp" #include "prx/libc/include/General.hpp" #include @@ -7,10 +8,6 @@ #include #include -extern "C" { -void* APS5_VABI calloc_nid_postfix(std::size_t, std::size_t); -void APS5_VABI free_nid_postfix(void*); -} namespace { @@ -258,7 +255,7 @@ std::int64_t APS5_VABI BIO_ctrl_nid_postfix(LibreSsl::Bio* bio, int command, std } int LibreSslBioWrite_nid_no_patch(LibreSsl::Bio* bio, const void* source, int size) { - if (!bio || !source || size <= 0) return 0; + if (!bio || size < 0) return 0; if (!bio->method || !bio->method->write) return queueError(BioLibrary, UnsupportedMethod, -2); const auto callback = bio->callback; if (callback) { diff --git a/core/libs/tests/GuestLibreSslBio.cpp b/core/libs/tests/GuestLibreSslBio.cpp index dee7f5bb75..0fbade7a4e 100644 --- a/core/libs/tests/GuestLibreSslBio.cpp +++ b/core/libs/tests/GuestLibreSslBio.cpp @@ -1,4 +1,5 @@ -#include "prx/libSceLibreSsl/include/Bio.hpp" +#include "prx/libSceLibreSsl/libSceLibreSsl.hpp" +#include "prx/libc/include/Api.hpp" #include "prx/libSceLibreSsl/ErrorQueue.hpp" #include "prx/libc/include/ApplicationHeap.hpp" #include @@ -26,17 +27,6 @@ using LibreSsl::Bio; using LibreSsl::BioBuffer; using LibreSsl::BioMethod; -extern "C" { -const BioMethod* APS5_VABI BIO_s_mem_nid_postfix(); -Bio* APS5_VABI BIO_new_nid_postfix(const BioMethod*); -Bio* APS5_VABI BIO_new_mem_buf_nid_postfix(const void*, int); -int APS5_VABI BIO_free_nid_postfix(Bio*); -std::int64_t APS5_VABI BIO_ctrl_nid_postfix(Bio*, int, std::int64_t, void*); -int APS5_VABI BIO_write_nid_postfix(Bio*, const void*, int); -int APS5_VABI BIO_puts_nid_postfix(Bio*, const char*); -void* APS5_VABI calloc_nid_postfix(std::size_t, std::size_t); -void APS5_VABI free_nid_postfix(void*); -} #ifdef ANYPS5_BIO_DYNAMIC namespace { @@ -194,7 +184,9 @@ void TestWritable() { Require(bio && bio->method == method && bio->initialized == 1 && bio->references == 1, "new BIO state"); Require(!bio->callback && !bio->callbackArgument && !bio->flags && !bio->retryReason && !bio->next && !bio->previous && !bio->extraData, "new BIO zero fields"); Require(bio->number == -1 && bio->bytesRead == 0 && bio->bytesWritten == 0, "new BIO counters"); - Require(BIO_write_nid_postfix(bio, nullptr, 7) == 0 && BIO_write_nid_postfix(bio, "x", 0) == 0, "empty write guard"); + Require(BIO_write_nid_postfix(bio, nullptr, 7) == -1, "null write reaches memory method"); + RequireError(0x20fff073); + Require(BIO_write_nid_postfix(bio, "x", 0) == 0, "zero-length owned write"); RequireError(0); RequireData(bio, ""); auto* buffer = Buffer(bio); @@ -204,7 +196,11 @@ void TestWritable() { Require(BIO_write_nid_postfix(bio, "abc", 3) == 3 && (bio->flags & 0xf) == 0, "write clears retry flags"); RequireData(bio, "abc"); Require(buffer->capacity == 8 && buffer->data[3] == 0, "first buffer expansion"); + std::memset(buffer->data + buffer->length, 0xa5, buffer->capacity - buffer->length); + wipePointer = buffer->data; + wipeSize = buffer->capacity; Require(BIO_write_nid_postfix(bio, "0123456789", 10) == 10, "second write"); + Require(wipePointer == nullptr, "growth frees the wiped allocation"); Require(buffer->capacity == 20 && buffer->data[13] == 0, "second buffer expansion"); RequireData(bio, "abc0123456789"); Require(method->read(bio, output, 4) == 4 && std::memcmp(output, "abc0", 4) == 0, "partial memory read"); @@ -214,7 +210,8 @@ void TestWritable() { Require(method->read(bio, nullptr, 3) == 3, "null raw read reports available count"); RequireData(bio, "123456789xy"); Require(method->read(bio, output, -3) == -3, "negative raw read"); - Require(BIO_write_nid_postfix(bio, nullptr, 5) == 0 && BIO_write_nid_postfix(bio, "z", -3) == 0, "ignored public writes"); + Require(BIO_write_nid_postfix(bio, nullptr, 5) == -1, "reject null public source"); + RequireError(0x20fff073); RequireData(bio, "123456789xy"); Require(BIO_ctrl_nid_postfix(bio, 11, 0, nullptr) == 1 && BIO_ctrl_nid_postfix(bio, 12, 0, nullptr) == 1, "flush and duplicate controls"); Require(BIO_ctrl_nid_postfix(bio, 13, 0, nullptr) == 0, "no pending output"); @@ -337,6 +334,90 @@ void TestInterleavedTransfers() { Require(BIO_free_nid_postfix(bio) == 1, "free interleaved BIO"); } +struct WriteEvent { + int operation; + const char* source; + int size; + std::int64_t result; + int flags; + std::uint64_t bytesWritten; +}; + +struct WriteTrace { + std::vector events; + int veto = 0; + int postAdjustment = 0; +}; + +std::int64_t APS5_VABI TraceWrite(Bio* bio, int operation, const char* source, int size, std::int64_t, std::int64_t result) { + auto& trace = *reinterpret_cast(bio->callbackArgument); + trace.events.push_back({operation, source, size, result, bio->flags, bio->bytesWritten}); + if (operation == 3 && trace.veto) return trace.veto; + return (operation & 0x80) ? result + trace.postAdjustment : result; +} + +void TestWriteBoundaries() { + const char source[] = "abc"; + for (const bool readOnly : {false, true}) { + Bio* bio = readOnly ? BIO_new_mem_buf_nid_postfix(source, 3) : BIO_new_nid_postfix(BIO_s_mem_nid_postfix()); + WriteTrace trace; + bio->callback = TraceWrite; + bio->callbackArgument = reinterpret_cast(&trace); + for (const int size : {0, 1}) { + bio->flags |= 0x4009; + const int flags = bio->flags; + trace.events.clear(); + Require(BIO_write_nid_postfix(bio, nullptr, size) == -1, "null source dispatch result"); + RequireError(0x20fff073); + Require(trace.events.size() == 2, "null source has pre/post callbacks"); + Require(trace.events[0].operation == 3 && trace.events[1].operation == 0x83, "null source callback operations"); + Require(trace.events[0].source == nullptr && trace.events[1].source == nullptr && + trace.events[0].size == size && trace.events[1].size == size, "null source callback arguments"); + Require(trace.events[0].result == 1 && trace.events[1].result == -1, "null source callback results"); + Require(trace.events[0].flags == flags && trace.events[1].flags == flags && bio->flags == flags, "null source preserves retry flags"); + Require(trace.events[0].bytesWritten == 0 && trace.events[1].bytesWritten == 0 && bio->bytesWritten == 0, "failed write does not count bytes"); + } + bio->flags |= 0x4009; + const int flags = bio->flags; + trace.events.clear(); + Require(BIO_write_nid_postfix(bio, source, 0) == (readOnly ? -1 : 0), "zero-length method result"); + RequireError(readOnly ? 0x20fff07e : 0); + Require(trace.events.size() == 2 && trace.events[0].operation == 3 && trace.events[1].operation == 0x83, "zero-length write callback operations"); + Require(trace.events[0].source == source && trace.events[1].source == source && + trace.events[0].size == 0 && trace.events[1].size == 0, "zero-length callback arguments"); + Require(trace.events[0].result == 1 && trace.events[1].result == (readOnly ? -1 : 0), "zero-length callback results"); + Require(trace.events[0].flags == flags && trace.events[1].flags == (readOnly ? flags : flags & ~0xf), "retry flags change during the method"); + Require(bio->flags == (readOnly ? flags : flags & ~0xf) && bio->bytesWritten == 0, "zero-length retry flags and byte count"); + RequireData(bio, readOnly ? "abc" : ""); + trace.events.clear(); + trace.veto = -7; + Require(BIO_write_nid_postfix(bio, nullptr, 0) == -7 && trace.events.size() == 1, "null write callback veto prevents method"); + RequireError(0); + trace.veto = 0; + trace.postAdjustment = 5; + trace.events.clear(); + Require(BIO_write_nid_postfix(bio, nullptr, 0) == 4 && bio->bytesWritten == 0, "post callback can override failure without counting bytes"); + RequireError(0x20fff073); + bio->callback = nullptr; + Require(BIO_free_nid_postfix(bio) == 1, "free boundary BIO"); + } + Bio* bio = BIO_new_nid_postfix(BIO_s_mem_nid_postfix()); + WriteTrace trace; + bio->callback = TraceWrite; + bio->callbackArgument = reinterpret_cast(&trace); + const auto* method = bio->method; + bio->method = nullptr; + Require(BIO_write_nid_postfix(bio, nullptr, 0) == -2 && trace.events.empty(), "method validation precedes callback"); + RequireError(0x20fff079); + bio->method = method; + bio->initialized = 0; + Require(BIO_write_nid_postfix(bio, nullptr, 0) == -2 && trace.events.size() == 1 && trace.events[0].operation == 3, "initialization validation follows pre-callback"); + RequireError(0x20fff078); + bio->initialized = 1; + bio->callback = nullptr; + Require(BIO_free_nid_postfix(bio) == 1, "free dispatch-order BIO"); +} + struct CallbackState { std::vector events; std::int64_t argument = 0; @@ -393,6 +474,9 @@ void TestDispatchAndReferences() { bio = BIO_new_nid_postfix(&custom); Require(bio && bio->method == &custom, "title-defined method"); Require(BIO_write_nid_postfix(bio, "abc", 3) == 2 && bio->bytesWritten == 2, "title-defined writer"); + Require(BIO_write_nid_postfix(bio, nullptr, 3) == 2 && bio->bytesWritten == 4, "custom method receives null source"); + Require(BIO_write_nid_postfix(bio, "", 0) == -1 && bio->bytesWritten == 4, "custom method receives zero length"); + RequireError(0); Require(BIO_ctrl_nid_postfix(bio, 17, 0x1234567800000000LL, nullptr) == 0x1234567800000011LL, "64-bit method result"); constexpr int referenceCount = 8; std::atomic_ref(bio->references).store(referenceCount + 1); @@ -476,6 +560,7 @@ int main(int argc, char** argv) { TestReadOnlyAndLines(); TestBufferOwnership(); TestInterleavedTransfers(); + TestWriteBoundaries(); TestDispatchAndReferences(); TestUnsupported(); Require(allocations == 0, "BIO tests leaked guest heap storage"); diff --git a/docs/dev/debt/libSceLibreSsl.md b/docs/dev/debt/libSceLibreSsl.md index 651ed82cff..919c18585e 100644 --- a/docs/dev/debt/libSceLibreSsl.md +++ b/docs/dev/debt/libSceLibreSsl.md @@ -9,7 +9,7 @@ - [CRYPTO_num_locks, CRYPTO_set_locking_callback, CRYPTO_set_add_lock_callback, CRYPTO_THREADID_set_callback, CRYPTO_THREADID_set_numeric](../../../core/libs/prx/libSceLibreSsl/Export.cpp) (libSceLibreSsl) - the locking, add-lock and thread id callbacks are stored but never called: LibreSSL calls them from `CRYPTO_lock`, `CRYPTO_add_lock` and `CRYPTO_THREADID_current`, which the module does not export, around its own shared state. Nothing is checked on the console. Results follow [LibreSSL 2.5.4](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/cryptlib.c), the version in the [firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214): 41 locks, the first non-null thread id callback is kept and later ones return 0, and `CRYPTO_THREADID_set_numeric` clears the 16-byte `{ptr, val}` structure; a null structure throws, as LibreSSL writes through it unchecked - [RAND_add, RAND_bytes, RAND_status, RAND_load_file, RAND_file_name, CRYPTO_malloc, CRYPTO_free, CRYPTO_set_mem_functions](../../../core/libs/prx/libSceLibreSsl/src/Runtime.cpp) (libSceLibreSsl) - follow LibreSSL's [rand_lib.c, randfile.c](https://github.com/libressl/openbsd/tree/0f0b16dc977f6ca499f0f4540078d992c96da597/src/lib/libcrypto/rand) and [malloc-wrapper.c](https://github.com/libressl/openbsd/blob/0f0b16dc977f6ca499f0f4540078d992c96da597/src/lib/libcrypto/malloc-wrapper.c), which are the same in 2.6.5 (3.4.3 linked); nothing is checked on the console. `RAND_bytes` reads host entropy and `CRYPTO_malloc` and `CRYPTO_free` use the guest heap. `CRYPTO_malloc` returns null for a size of zero or less, as LibreSSL 2.5.4 to 3.4 do; later versions take a `size_t` size and call `malloc`. A null buffer with a positive length throws - [AES_set_encrypt_key, AES_set_decrypt_key, AES_encrypt, AES_decrypt, AES_ecb_encrypt, AES_cbc_encrypt, AES_cfb128_encrypt, AES_ctr128_encrypt](../../../core/libs/prx/libSceLibreSsl/src/Aes.cpp) (libSceLibreSsl) - follow LibreSSL's [aes.h](https://github.com/libressl/openbsd/blob/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/aes/aes.h) and [mode code](https://github.com/libressl/openbsd/tree/bc9483725c5815efb64b6d9acc52fc5fba139212/src/lib/libcrypto/modes). Nothing is checked on the console. Key setup follows [LibreSSL 2.5.4's aes_core.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/aes/aes_core.c), the version the `X509_verify_cert_error_string` entry names: a null pointer returns -1 and a key length other than 128, 192 or 256 bits returns -2 before the key is written, and only the 44, 52 or 60 words the key length needs are written. The `AES_KEY` schedule uses that file's generic C layout (big-endian words, `rounds` 10, 12 or 14). The 2.5.4 amd64 build uses [aes-x86_64](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/aes/asm/aes-x86_64.pl) instead, which copies the key bytes without swapping, so each word is byte-reversed compared with the C layout (OpenSSL 3.0.13's x86-64 code gives the same schedules byte-reversed); the console's build options are unknown, so a schedule the title reads or fills is unverified. CTR leaves the last keystream block in `ecount_buf` even at a block boundary. Null pointers in the cipher and mode functions, other `rounds` values and a CFB or CTR position outside 0-15 throw -- [ERR_get_error, ERR_peek_error, ERR_clear_error, ERR_remove_thread_state](../../../core/libs/prx/libSceLibreSsl/src/Error.cpp) have no console execution verification or production error producers. The 16-slot calling-thread queue retains 15 codes following LibreSSL err.c and err.h. [Firmware 13.42 module comparisons on a PC](https://github.com/boykopovar/AnyPS5/pull/9535#issuecomment-6407060914) matched libSceLibreSsl3 queue ordering and overflow for 0-20 publications; whether the two firmware libraries share a queue was not tested. Explicit thread IDs throw because guest thread identity mapping is unverified; null clears the calling thread. File/line metadata is stored by the native producer helper but no guest export exposes it. +- [ERR_get_error, ERR_peek_error, ERR_clear_error, ERR_remove_thread_state](../../../core/libs/prx/libSceLibreSsl/src/Error.cpp) have no console execution verification. Memory BIO failures publish to this queue. The 16-slot calling-thread queue retains 15 codes following LibreSSL err.c and err.h. [Firmware 13.42 module comparisons on a PC](https://github.com/boykopovar/AnyPS5/pull/9535#issuecomment-6407060914) matched libSceLibreSsl3 queue ordering and overflow for 0-20 publications; whether the two firmware libraries share a queue was not tested. Explicit thread IDs throw because guest thread identity mapping is unverified; null clears the calling thread. File/line metadata is stored by the native producer helper but no guest export exposes it. - [OCSP_response_status_str, OCSP_cert_status_str and OCSP_crl_reason_str](../../../core/libs/prx/libSceLibreSsl/src/Ocsp.cpp) (libSceLibreSsl) - the module's strings are not measured on the console. The tables follow LibreSSL [ocsp_prn.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libcrypto/ocsp/ocsp_prn.c), unchanged from revision 1.8 (2015) to 1.12 (2025), with codes from `ocsp.h`; the argument is a 64-bit `long` and every value not in a table returns `(UNKNOWN)` - [SSL_CIPHER_get_bits, SSL_CIPHER_get_name and SSL_CIPHER_get_version](../../../core/libs/prx/libSceLibreSsl/src/Cipher.cpp) - the 88-byte guest cipher layout and these calls are not console-verified. They follow [LibreSSL 2.5.4 ssl.h](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl.h) and [ssl_ciph.c](https://github.com/libressl/openbsd/blob/2e2c612066aa993717528a0249c15c51128d61cb/src/lib/libssl/ssl_ciph.c), matching the version in [published firmware 13.40 measurements](https://github.com/boykopovar/AnyPS5/discussions/8981#discussioncomment-19111214). Console modifications and other firmware layouts remain unverified. Protocol labels use the full 64-bit cipher ID, not the protocol flags; TLS 1.2 ciphers still report `TLSv1/SSLv3`, as in that release. @@ -18,7 +18,7 @@ ### Functional -- [Memory BIOs](../../../core/libs/prx/libSceLibreSsl/src/Bio.cpp) have not been compared with a PS5 module or title. The 104-byte BIO, 80-byte method table, 24-byte buffer and callbacks use the LP64 layouts and behavior of [LibreSSL 3.4.1](https://github.com/libressl/openbsd/tree/078b3d1df8ca2011c1605cd40418c58fa0f5b143/src/lib/libcrypto/bio). The console's LibreSSL version is unknown. Storage uses the guest heap. Registered CRYPTO ex-data is unsupported; freeing a BIO with nonnull ex-data throws. File, socket and SSL BIO constructors remain unsupported. +- [Memory BIOs](../../../core/libs/prx/libSceLibreSsl/src/Bio.cpp) have no console execution verification. [Firmware 13.42 module comparisons on a PC](https://github.com/boykopovar/AnyPS5/pull/9536#issuecomment-6407192928) cover 1,048 sequences with stand-in allocators, including null/zero writes, callbacks and growth wiping. They support dispatching nonnegative writes to the method even with null source or zero length; LibreSSL 3.4.1 instead guards those calls. Negative public lengths retain that source's early return because the module report did not measure them. The 104-byte BIO, 80-byte method and 24-byte buffer use LP64 layouts. Allocator padding is zeroed rather than retaining the module allocator's fill. Other firmware and console imports remain unverified. CRYPTO ex-data, file, socket and SSL BIOs remain unsupported. - [Memory BIO unchecked inputs](../../../core/libs/prx/libSceLibreSsl/src/Bio.cpp) throw for null method or raw line-buffer pointers, negative raw method write sizes, an overflowing raw line-buffer size, inconsistent read-only buffer lengths, and memory write lengths that overflow size_t. Upstream does not validate these inputs. An unrepresentable string length throws instead of narrowing into an unchecked write.