Files
AnyPS5/core/libs/tests/GuestLibreSslBio.cpp
T

565 lines
27 KiB
C++

#include "prx/libSceLibreSsl/libSceLibreSsl.hpp"
#include "prx/libc/include/Api.hpp"
#include "prx/libSceLibreSsl/ErrorQueue.hpp"
#include "prx/libc/include/ApplicationHeap.hpp"
#include <algorithm>
#include <array>
#include <atomic>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <limits>
#include <stdexcept>
#include <string>
#include <thread>
#include <vector>
#ifdef ANYPS5_BIO_DYNAMIC
#ifdef _WIN32
#include <windows.h>
#else
#include <dlfcn.h>
#endif
#endif
using LibreSsl::Bio;
using LibreSsl::BioBuffer;
using LibreSsl::BioMethod;
#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<class TFunction>
void Resolve(TFunction& function, void* module, const char* name) {
#ifdef _WIN32
function = reinterpret_cast<TFunction>(GetProcAddress(static_cast<HMODULE>(module), name));
#else
function = reinterpret_cast<TFunction>(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<int> 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<unsigned char*>(pointer)[i] == 0, "freed buffer was not wiped");
wipePointer = nullptr;
}
if (pointer) --allocations;
std::free(pointer);
}
void InitializeHeap() {
const std::array<void*, 10> api{
reinterpret_cast<void*>(Allocate), reinterpret_cast<void*>(Free), reinterpret_cast<void*>(Calloc),
reinterpret_cast<void*>(Reallocate), reinterpret_cast<void*>(Align), reinterpret_cast<void*>(Realign),
reinterpret_cast<void*>(PosixAlign)};
ApplicationHeapRegister_nid_no_patch(api.data());
}
template<class TFunction>
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<std::int64_t>(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<std::int64_t>(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) == -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);
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");
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");
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) == -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");
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<char, 8> 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<BioBuffer*>(calloc_nid_postfix(1, sizeof(BioBuffer)));
replacement->data = static_cast<char*>(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<int>((sequence >> 16) % 65);
if ((sequence & 3) != 0) {
std::array<char, 65> input{};
for (int i = 0; i < count; ++i) input[i] = static_cast<char>(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<char, 66> output;
output.fill(static_cast<char>(0xa5));
const int expected = static_cast<int>(std::min<std::size_t>(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<char>(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 WriteEvent {
int operation;
const char* source;
int size;
std::int64_t result;
int flags;
std::uint64_t bytesWritten;
};
struct WriteTrace {
std::vector<WriteEvent> 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<WriteTrace*>(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<char*>(&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<char*>(&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<int> 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<CallbackState*>(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<char*>(&state);
Require(BIO_write_nid_postfix(bio, "abc", 3) == 5 && bio->bytesWritten == 3, "callback changes result after accounting");
Require(state.events == std::vector<int>{3, 0x83}, "write callback ordering");
state.events.clear();
Require(BIO_puts_nid_postfix(bio, "d") == 3 && state.events == std::vector<int>{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<int>{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<int>{3}, "write callback veto");
Require(bio->bytesWritten == 4 && static_cast<BioBuffer*>(bio->data)->length == 4, "veto prevents transfer");
state.events.clear();
Require(BIO_free_nid_postfix(bio) == -7 && state.events == std::vector<int>{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_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);
std::array<std::thread, referenceCount> 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;
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<void>(argc);
static_cast<void>(argv);
#endif
InitializeHeap();
ERR_clear_error_nid_postfix();
TestWritable();
TestReadOnlyAndLines();
TestBufferOwnership();
TestInterleavedTransfers();
TestWriteBoundaries();
TestDispatchAndReferences();
TestUnsupported();
Require(allocations == 0, "BIO tests leaked guest heap storage");
Require(ERR_get_error_nid_postfix() == 0, "BIO tests left an unexpected error");
}