/* * ps5-native-app-boilerplate - resident/one-shot Lapy client regression. * Copyright (C) 2026 BlackBearReloaded * SPDX-License-Identifier: GPL-3.0-or-later */ #include #include #include #include #include #include #include #include #include "../examples/sandbox-elevation/src/elevation.cpp" namespace test { struct State { bool result_open_fails{}; bool prepare_fails{}; bool request_open_fails{}; bool corrupt_read{}; bool resident_claimed{}; unsigned data_open_failures{}; unsigned data_open_calls{}; unsigned data_list_failures{}; unsigned data_list_calls{}; unsigned sleeps{}; unsigned prepare_calls{}; unsigned socket_calls{}; unsigned closes{}; bool helper_read{}; std::string request; std::string result; std::string data; std::vector replies; std::vector sent; std::size_t received{}; }; State state; void reset() { state = {}; } bool starts_with(const char *value, std::string_view prefix) { return std::string_view{value}.starts_with(prefix); } void helper_replies(elevation::Status status = elevation::Status::ok) { elevation::wire::Message prepare{}; prepare.pid = 4242; prepare.kind = elevation::wire::Kind::prepare; auto response = prepare; response.kind = elevation::wire::Kind::response; response.status = status; state.replies.resize(2 * sizeof(prepare)); std::memcpy(state.replies.data(), &prepare, sizeof(prepare)); std::memcpy(state.replies.data() + sizeof(prepare), &response, sizeof(response)); } } // namespace test extern "C" { pid_t getpid() noexcept { return 4242; } uid_t geteuid() noexcept { return 1000; } int seteuid(uid_t) noexcept { ++test::state.prepare_calls; if (test::state.prepare_fails) { errno = EPERM; return -1; } return 0; } int open(const char *path, int flags, ...) { (void)flags; if (std::strcmp(path, "/download0/lapy_owned_result") == 0) { if (test::state.result_open_fails) { errno = ENOENT; return -1; } return 10; } if (test::starts_with(path, "/download0/.elevate_proc.")) { if (test::state.request_open_fails) { errno = EIO; return -1; } return 11; } if (test::starts_with(path, "/data/.lapy_probe_")) { ++test::state.data_open_calls; if (test::state.data_open_calls <= test::state.data_open_failures) { errno = EACCES; return -1; } return 12; } if (std::strcmp(path, "/app0/lapy.elf") == 0) return 13; errno = ENOENT; return -1; } // /data that opens and writes but refuses a listing: what ShadowMountPlus 1.7 // exposes to a sandboxed app. Not elevation. DIR *opendir(const char *path) { if (std::strcmp(path, "/data") != 0) { errno = ENOENT; return nullptr; } ++test::state.data_list_calls; if (test::state.data_list_calls <= test::state.data_list_failures) { errno = EPERM; return nullptr; } return reinterpret_cast(&test::state); } int closedir(DIR *) { return 0; } ssize_t write(int descriptor, const void *buffer, size_t size) { const auto count = std::min(size, std::size_t{3}); const auto bytes = std::string_view{static_cast(buffer), count}; if (descriptor == 10) test::state.result.append(bytes); else if (descriptor == 11) test::state.request.append(bytes); else if (descriptor == 12) test::state.data.append(bytes); else { errno = EBADF; return -1; } return static_cast(count); } ssize_t read(int descriptor, void *buffer, size_t size) { if (descriptor == 13) { if (test::state.helper_read) return 0; test::state.helper_read = true; const char elf[] = "\x7f" "ELF"; const auto count = std::min(size, sizeof(elf) - 1); std::memcpy(buffer, elf, count); return static_cast(count); } if (descriptor != 12) { errno = EBADF; return -1; } const auto count = std::min(size, test::state.data.size()); std::memcpy(buffer, test::state.data.data(), count); if (test::state.corrupt_read && count != 0) static_cast(buffer)[0] = 'X'; return static_cast(count); } off_t lseek(int descriptor, off_t offset, int whence) noexcept { return descriptor == 12 && offset == 0 && whence == SEEK_SET ? 0 : -1; } int close(int) { ++test::state.closes; return 0; } int fchmod(int descriptor, mode_t mode) noexcept { return descriptor == 10 && mode == 0644 ? 0 : -1; } int rename(const char *, const char *) noexcept { return 0; } int unlink(const char *path) noexcept { if (std::strcmp(path, "/download0/elevate_proc") == 0 && test::state.resident_claimed) { errno = ENOENT; return -1; } return 0; } int usleep(useconds_t) { ++test::state.sleeps; return 0; } int sceNetSocket(const char *, int, int, int) { ++test::state.socket_calls; return 20; } int sceNetSocketClose(int) { ++test::state.closes; return 0; } int sceNetConnect(int, const void *address, std::uint32_t length) { assert(length == sizeof(NetSockaddrIn)); const auto &endpoint = *static_cast(address); assert(endpoint.address == 0x0100007f && endpoint.port == 0x3d23); return 0; } int sceNetSetsockopt(int, int, int, const void *, std::uint32_t) { return 0; } int sceNetSend(int, const void *bytes, std::size_t size, int) { const auto count = std::min(size, std::size_t{7}); const auto *start = static_cast(bytes); test::state.sent.insert(test::state.sent.end(), start, start + count); return static_cast(count); } int sceNetRecv(int, void *bytes, std::size_t size, int) { const auto remaining = test::state.replies.size() - test::state.received; if (remaining == 0) return -1; const auto count = std::min({size, std::size_t{3}, remaining}); std::memcpy(bytes, test::state.replies.data() + test::state.received, count); test::state.received += count; return static_cast(count); } } int main() { using elevation::Capability; using elevation::Status; test::reset(); assert(elevation::request(Capability::filesystem) == Status::ok); assert(std::strcmp(elevation::path(), "existing") == 0); assert(test::state.prepare_calls == 0 && test::state.socket_calls == 0); test::reset(); test::state.data_open_failures = 1; assert(elevation::request(Capability::filesystem) == Status::ok); assert(std::strcmp(elevation::path(), "resident") == 0); assert(test::state.prepare_calls == 1 && test::state.socket_calls == 0); assert(test::state.request == "{\"PID\":4242}\n"); assert(test::state.result == "DATA_OK=1 OPEN_ERRNO=0\n"); test::reset(); test::state.data_list_failures = 1; assert(elevation::request(Capability::filesystem) == Status::ok); assert(std::strcmp(elevation::path(), "resident") == 0); assert(test::state.prepare_calls == 1 && test::state.data_list_calls == 2); test::reset(); test::state.data_list_failures = 1000; test::state.resident_claimed = true; assert(elevation::request(Capability::filesystem) == Status::timeout); assert(std::strcmp(elevation::path(), "resident") == 0); test::reset(); test::state.data_open_failures = 51; test::helper_replies(); assert(elevation::request(Capability::filesystem) == Status::ok); assert(std::strcmp(elevation::path(), "helper") == 0); assert(test::state.prepare_calls == 2 && test::state.socket_calls == 1); assert(test::state.sent.size() == 4 + 2 * sizeof(elevation::wire::Message)); test::reset(); test::state.data_open_failures = 1000; test::state.resident_claimed = true; assert(elevation::request(Capability::filesystem) == Status::timeout); assert(std::strcmp(elevation::path(), "resident") == 0); assert(test::state.socket_calls == 0); test::reset(); test::state.data_open_failures = 1; test::state.result_open_fails = true; test::helper_replies(Status::target_mismatch); assert(elevation::request(Capability::filesystem) == Status::target_mismatch); assert(test::state.socket_calls == 1 && test::state.prepare_calls == 1); test::reset(); test::state.data_open_failures = 1; test::state.prepare_fails = true; assert(elevation::request(Capability::filesystem) == Status::prepare_failed); assert(test::state.socket_calls == 0); test::reset(); test::state.corrupt_read = true; assert(elevation::request(Capability::filesystem) == Status::apply_failed); assert(test::state.socket_calls == 0); test::reset(); assert(elevation::request(static_cast(2)) == Status::unsupported_capability); assert(test::state.data_open_calls == 0 && test::state.socket_calls == 0); test::reset(); test::state.data_open_failures = 1; test::state.result_open_fails = true; assert(elevation::request(Capability::filesystem, nullptr) == Status::invalid_request); elevation::wire::Message request{}; request.pid = 4242; const std::array golden{'E', 'L', 'V', '1', 1, 0, 24, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0x92, 0x10, 0, 0, 0, 0, 0, 0}; assert(std::memcmp(&request, golden.data(), golden.size()) == 0); }