diff --git a/core/libs/prx/libc/include/General.hpp b/core/libs/prx/libc/include/General.hpp index 654f5c78c3cd..ac1dde9aade7 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -26,6 +26,8 @@ extern "C" int RenameGuestPath_nid_no_patch(const char* from, const char* to); // directory instead of the run directory; the prefix matches whole path components only. extern "C" void AddPathAlias_nid_no_patch(const char* guestPrefix, const char* hostPath); extern "C" void RemovePathAlias_nid_no_patch(const char* guestPrefix); +using PathProvider = std::filesystem::path (*)(const std::filesystem::path& root); +extern "C" void AddPathProvider_nid_no_patch(const char* guestPath, PathProvider provide); extern "C" void RecordWrittenPath_nid_no_patch(const std::filesystem::path& path); extern "C" std::vector WrittenPaths_nid_no_patch(); extern "C" void SyncWrittenPaths_nid_no_patch(); diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 133196f578e3..b700dd7e8b2c 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -39,6 +39,7 @@ struct PathAliases { std::mutex mutex; std::vector> entries; std::vector blocked; + std::vector> providers; }; PathAliases& Aliases() { @@ -118,7 +119,23 @@ struct WorkingDirectory { std::filesystem::path current = root; }; WorkingDirectory& Directories() { static WorkingDirectory state; return state; } +std::optional ProvidedPath(const WorkingDirectory& state, const std::filesystem::path& guest) { + const auto relative = TrimSlashes(guest.relative_path().generic_string().c_str()); + PathProvider provide = nullptr; + { + auto& aliases = Aliases(); + std::lock_guard lock(aliases.mutex); + for (const auto& [path, provider] : aliases.providers) { + if (SameName(relative, path)) provide = provider; + } + } + if (provide == nullptr) return std::nullopt; + auto provided = provide(state.root); + if (provided.empty()) return std::nullopt; + return provided.make_preferred(); +} std::filesystem::path HostPath(WorkingDirectory& state, const std::filesystem::path& guest) { + if (auto provided = ProvidedPath(state, guest)) return *provided; if (auto aliased = ResolveAlias(guest.relative_path().generic_string())) return *aliased; return ResolveHostPath(state.root, guest.relative_path()).make_preferred(); } @@ -303,6 +320,23 @@ extern "C" void AddPathAlias_nid_no_patch(const char* guestPrefix, const char* h aliases.entries.emplace_back(prefix, hostPath); } +extern "C" void AddPathProvider_nid_no_patch(const char* guestPath, PathProvider provide) { + if (guestPath == nullptr || provide == nullptr) { + APS5_INVALID_ARG_EX; + } + const auto path = TrimSlashes(guestPath); + if (path.empty()) { APS5_INVALID_ARG_EX; } + auto& aliases = Aliases(); + std::lock_guard lock(aliases.mutex); + for (auto& entry : aliases.providers) { + if (SameName(entry.first, path)) { + entry.second = provide; + return; + } + } + aliases.providers.emplace_back(path, provide); +} + extern "C" void RemovePathAlias_nid_no_patch(const char* guestPrefix) { if (guestPrefix == nullptr) { APS5_INVALID_ARG_EX; diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 72ce92342a41..6df9987cbe7e 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -14,6 +14,7 @@ add_library(libkernel SHARED EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/File/src/OpenFileLimit.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/SharedMemory.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/RandomDevice.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/File/src/SandboxCertificates.cpp ${CMAKE_CURRENT_SOURCE_DIR}/File/src/Stdio.cpp ${CMAKE_CURRENT_SOURCE_DIR}/../libc/src/Rename.cpp ${CMAKE_CURRENT_SOURCE_DIR}/Semaphore/src/Semaphore.cpp @@ -77,7 +78,7 @@ target_include_directories(libkernel PRIVATE ${LIBS_INCLUDE_DIR}/nid/include) target_link_libraries(libkernel PRIVATE ${CMAKE_DL_LIBS}) target_link_libraries(libkernel PRIVATE libc) if(WIN32) - target_link_libraries(libkernel PRIVATE ws2_32 shell32 advapi32 wbemuuid ole32 oleaut32) + target_link_libraries(libkernel PRIVATE ws2_32 shell32 advapi32 wbemuuid ole32 oleaut32 crypt32) target_compile_definitions(libkernel PRIVATE _WIN32_WINNT=0x0602) endif() configure_windows_unwind(libkernel) @@ -270,6 +271,17 @@ if(BUILD_TESTING) set_tests_properties(guest_kernel_sync PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_sandbox_certificates_tests ${LIBS_INCLUDE_DIR}/tests/GuestSandboxCertificates.cpp) + target_include_directories(guest_sandbox_certificates_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_sandbox_certificates_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_sandbox_certificates_tests) + set(sandboxCertificatesRun ${CMAKE_CURRENT_BINARY_DIR}/guest_sandbox_certificates) + file(MAKE_DIRECTORY ${sandboxCertificatesRun}) + add_test(NAME guest_sandbox_certificates COMMAND guest_sandbox_certificates_tests WORKING_DIRECTORY ${sandboxCertificatesRun}) + if(WIN32) + set_tests_properties(guest_sandbox_certificates PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_kernel_rename_tests ${LIBS_INCLUDE_DIR}/tests/GuestKernelRename.cpp) target_include_directories(guest_kernel_rename_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_kernel_rename_tests PRIVATE libkernel libc) diff --git a/core/libs/prx/libkernel/File/src/SandboxCertificates.cpp b/core/libs/prx/libkernel/File/src/SandboxCertificates.cpp new file mode 100644 index 000000000000..16e6cd153f8f --- /dev/null +++ b/core/libs/prx/libkernel/File/src/SandboxCertificates.cpp @@ -0,0 +1,114 @@ +#include "prx/libc/include/General.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#include +#else +#include +#endif + +extern "C" const char* APS5_VABI sceKernelGetFsSandboxRandomWord(); + +namespace { +constexpr char CertificateBegin[] = "-----BEGIN CERTIFICATE-----"; + +std::optional ReadFile(const std::filesystem::path& path) { + std::ifstream file(path, std::ios::binary); + if (!file) return std::nullopt; + std::string text((std::istreambuf_iterator(file)), std::istreambuf_iterator()); + if (file.bad()) throw std::runtime_error("Reading " + path.string() + " failed"); + return text; +} + +#ifdef _WIN32 +void AppendStore(const wchar_t* name, std::string& bundle) { + const std::unique_ptr store(CertOpenSystemStoreW(0, name), [](HCERTSTORE handle) { CertCloseStore(handle, 0); }); + if (store == nullptr) throw std::system_error(static_cast(GetLastError()), std::system_category(), "Opening the host certificate store"); + for (PCCERT_CONTEXT certificate = CertEnumCertificatesInStore(store.get(), nullptr); certificate != nullptr; + certificate = CertEnumCertificatesInStore(store.get(), certificate)) { + DWORD length = 0; + std::wstring text; + bool encoded = CryptBinaryToStringW(certificate->pbCertEncoded, certificate->cbCertEncoded, CRYPT_STRING_BASE64HEADER, nullptr, &length); + if (encoded) { + text.resize(length); + encoded = CryptBinaryToStringW(certificate->pbCertEncoded, certificate->cbCertEncoded, CRYPT_STRING_BASE64HEADER, text.data(), &length); + } + if (!encoded) { + const auto error = GetLastError(); + CertFreeCertificateContext(certificate); + throw std::system_error(static_cast(error), std::system_category(), "Encoding a host certificate"); + } + text.resize(length); + for (const wchar_t character : text) bundle.push_back(static_cast(character)); + } +} + +std::string HostCertificates() { + std::string bundle; + AppendStore(L"ROOT", bundle); + AppendStore(L"CA", bundle); + return bundle; +} + +unsigned long ProcessId() { return GetCurrentProcessId(); } +#else +std::string HostCertificates() { + std::array candidates{std::getenv("SSL_CERT_FILE"), "/etc/ssl/certs/ca-certificates.crt", "/etc/pki/tls/certs/ca-bundle.crt", + "/etc/ssl/ca-bundle.pem"}; + for (const char* candidate : candidates) { + std::error_code error; + if (candidate == nullptr || *candidate == '\0' || !std::filesystem::is_regular_file(candidate, error)) continue; + if (auto text = ReadFile(candidate)) return *text; + } + return {}; +} + +unsigned long ProcessId() { return static_cast(getpid()); } +#endif + +std::filesystem::path WriteCertificateList(const std::filesystem::path& root) { + const auto bundle = HostCertificates(); + if (bundle.find(CertificateBegin) == std::string::npos) return {}; + const auto directory = root / "cert_cache"; + std::filesystem::create_directories(directory); + const auto target = directory / "CA_LIST.cer"; + if (ReadFile(target) == bundle) return target; + auto temporary = target; + temporary += "." + std::to_string(ProcessId()) + ".tmp"; + { + std::ofstream file(temporary, std::ios::binary | std::ios::trunc); + file.write(bundle.data(), static_cast(bundle.size())); + if (!file.flush()) throw std::runtime_error("Writing " + temporary.string() + " failed"); + } + using std::filesystem::perms; + std::filesystem::permissions(temporary, perms::owner_read | perms::group_read | perms::others_read); + std::error_code error; + if (std::filesystem::exists(target, error)) std::filesystem::permissions(target, perms::owner_write, std::filesystem::perm_options::add); + std::filesystem::rename(temporary, target); + return target; +} + +std::filesystem::path CertificateList(const std::filesystem::path& root) { + static const std::filesystem::path list = WriteCertificateList(root); + return list; +} + +const bool g_certificateListProvided = [] { + AddPathProvider_nid_no_patch((std::string("/") + sceKernelGetFsSandboxRandomWord() + "/common/cert/CA_LIST.cer").c_str(), &CertificateList); + return true; +}(); +} diff --git a/core/libs/tests/GuestSandboxCertificates.cpp b/core/libs/tests/GuestSandboxCertificates.cpp new file mode 100644 index 000000000000..4f99b3f312aa --- /dev/null +++ b/core/libs/tests/GuestSandboxCertificates.cpp @@ -0,0 +1,76 @@ +#include "prx/libc/include/General.hpp" +#include "prx/libkernel/KernelErrors.hpp" +#include "SceTypes.hpp" +#include +#include +#include +#include +#include +#include +extern "C" { +const char* APS5_VABI sceKernelGetFsSandboxRandomWord(); +int APS5_VABI sceKernelOpen(const char*, int, unsigned short); +int APS5_VABI sceKernelClose(int); +std::int64_t APS5_VABI sceKernelRead(int, void*, std::size_t); +int APS5_VABI sceKernelStat(const char*, FileStat*); +void* APS5_VABI fopen_nid_postfix(const char*, const char*); +std::size_t APS5_VABI fread_nid_postfix(void*, std::size_t, std::size_t, void*); +int APS5_VABI fclose_nid_postfix(void*); +} +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Sandbox certificates check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) +static std::string ReadKernel(const std::string& path) { + const int descriptor = sceKernelOpen(path.c_str(), 0, 0); + Require(descriptor >= 0); + std::string text; + char buffer[4096]; + for (std::int64_t count; (count = sceKernelRead(descriptor, buffer, sizeof(buffer))) != 0;) { + Require(count > 0); + text.append(buffer, static_cast(count)); + } + Require(sceKernelClose(descriptor) == 0); + return text; +} +static std::string ReadStream(const std::string& path) { + void* stream = fopen_nid_postfix(path.c_str(), "rb"); + Require(stream != nullptr); + std::string text; + char buffer[4096]; + for (std::size_t count; (count = fread_nid_postfix(buffer, 1, sizeof(buffer), stream)) != 0;) text.append(buffer, count); + Require(fclose_nid_postfix(stream) == 0); + return text; +} +int main() { +#ifndef _WIN32 + const auto bundle = std::filesystem::absolute("host_bundle.pem"); + const std::string expected = "-----BEGIN CERTIFICATE-----\nMIIBAA==\n-----END CERTIFICATE-----\n"; + { std::ofstream file(bundle, std::ios::binary | std::ios::trunc); file << expected; } + Require(setenv("SSL_CERT_FILE", bundle.c_str(), 1) == 0); +#endif + const std::string directory = std::string("/") + sceKernelGetFsSandboxRandomWord() + "/common/cert"; + const std::string path = directory + "/CA_LIST.cer"; + const auto text = ReadKernel(path); + const auto begin = text.find("-----BEGIN CERTIFICATE-----"); + Require(begin != std::string::npos); + Require(text.find("-----END CERTIFICATE-----", begin) != std::string::npos); +#ifndef _WIN32 + Require(text == expected); +#endif + Require(ReadStream(path) == text); + Require(ReadKernel(directory + "/./ca_list.cer") == text); + FileStat status{}; + Require(sceKernelStat(path.c_str(), &status) == 0); + Require(status.st_size == static_cast(text.size())); + const auto cached = std::filesystem::current_path() / "cert_cache" / "CA_LIST.cer"; + Require(std::filesystem::is_regular_file(cached)); + Require((std::filesystem::status(cached).permissions() & std::filesystem::perms::owner_write) == std::filesystem::perms::none); + Require(sceKernelOpen((directory + "/CA_LIST2.cer").c_str(), 0, 0) == SCE_KERNEL_ERROR_ENOENT); + Require(sceKernelOpen((directory + "/CA_LIST.cer.bak").c_str(), 0, 0) == SCE_KERNEL_ERROR_ENOENT); + Require(fopen_nid_postfix((directory + "/CA_LIST2.cer").c_str(), "rb") == nullptr); + return 0; +} diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 637ab5eb36c7..a823e625cb6a 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -113,7 +113,8 @@ - [sceKernelPread, sceKernelPwrite, sceKernelPreadv, sceKernelPwritev](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) on Windows transfer at the given offset with an overlapped `ReadFile`/`WriteFile` and restore the descriptor's file pointer afterwards, so a concurrent `read`/`write` on the same descriptor can see the moved pointer in between, unlike on the console. Handles that are not disk files return `ESPIPE`. `readv` on a pipe or console handle stops after the first iovec read, a short read the console is assumed to allow; each iovec is transferred separately, so a failure after the first transfer returns the bytes already done - [sceKernelAioSubmitReadCommands, sceKernelAioSubmitWriteCommands and their Multiple variants](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) on Windows transfer through the same positioned path as `sceKernelPread`/`sceKernelPwrite` and restore the descriptor's file pointer afterwards, so a concurrent `read`/`write` on the same descriptor can see the moved pointer in between, unlike on the console - [sceKernelAioSetParam](../../../core/libs/prx/libkernel/Aio/src/Aio.cpp) (libkernel) writes the five values into the 20-byte scheduling struct the first argument points to, following the declarations in [Orbital](https://github.com/AlexAltea/orbital/blob/cc408367da6a6ebbf5ac1ac559fefeadc73b2831/tools/ida/db_types.json), [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/Kernel/KernelAio.cs) and [ampr_emu](https://github.com/drakmor/ampr_emu/blob/cfa85df379f6eeeb165d7badf9b648e266fe77b7/ps5/include/sce/_fs.h). Whether the console checks the values is unknown, so a null struct, a scheduling window or delayed count limit outside 0..128, a split flag other than 0 or 1 and a split size or chunk size above 0x1000000 (SharpProspero's maximums) throw. The scheduling settings are not used: `sceKernelAioInitializeImpl` ignores the struct and requests run synchronously on submit -- [sceKernelGetFsSandboxRandomWord](../../../core/libs/prx/libkernel/System/src/Sandbox.cpp) (libkernel) returns a fixed directory name, `gkbHtXc3Wq`. On the console the name is the process's randomized path (syscall 602, `randomized_path`), and the sandbox exposes system files under it, such as `common/lib` and `common/cert`. Nothing is mounted under the name here, so `/gkbHtXc3Wq/common/...` resolves under the guest root like any other path and does not exist +- [sceKernelGetFsSandboxRandomWord](../../../core/libs/prx/libkernel/System/src/Sandbox.cpp) (libkernel) returns a fixed directory name, `gkbHtXc3Wq`. On the console the name is the process's randomized path (syscall 602, `randomized_path`), and the sandbox exposes system files under it, such as `common/lib` and `common/cert`. Nothing is mounted under the name here, so `/gkbHtXc3Wq/common/...` resolves under the guest root like any other path and does not exist, except `/gkbHtXc3Wq/common/cert/CA_LIST.cer`, which [SandboxCertificates.cpp](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) serves from the host trust store +- [The sandbox CA list](../../../core/libs/prx/libkernel/File/src/SandboxCertificates.cpp) (libkernel), `//common/cert/CA_LIST.cer`, is a PEM bundle of the host's trust anchors, written once per process to `cert_cache/CA_LIST.cer` in the run directory with the owner write bit cleared: on Windows the certificates of the current user's `ROOT` and `CA` system stores (Windows fetches missing roots on demand, so a root that was never fetched is absent), elsewhere `SSL_CERT_FILE` or the first of `/etc/ssl/certs/ca-certificates.crt`, `/etc/pki/tls/certs/ca-bundle.crt` and `/etc/ssl/ca-bundle.pem` that exists. Without any certificate the path does not exist. The console file's format is unverified: only that bundled OpenSSL accepts a PEM bundle there is shown (ProsperoRadio PPSA99001 and SwanStation PPSA98510, #5001). The other entries of the console's `common/cert` directory, the directory itself (it does not exist here) and the error a write open gets (`EACCES` from the host here) are unverified - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [makecontext](../../../core/libs/prx/libSceLibcInternal/UserContext.cpp) (libSceLibcInternal) use FreeBSD 11's amd64 `ucontext_t` (`mcontext` at 0x10, `mc_len` 800, `uc_link` at 0x330, `uc_stack` at 0x338, 0x370 bytes), the layout homebrew built against FreeBSD's headers passes: PPSA99039's OpenSSL `ASYNC` (`crypto/async/arch/async_posix.c`) captures into a 0x370-byte stack buffer. `getcontext` writes the 0x330 bytes before `uc_link`, as FreeBSD's `UC_COPY_SIZE`. The console's own layout for these functions is not verified; the exception handler's context is a different one (see `sceKernelRaiseException`) - [getcontext and setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) keep the 512-byte FXSAVE image in `mc_fpstate` (x87, MXCSR, XMM0-15), so the upper halves of YMM0-15 are neither saved nor restored, and `getcontext` leaves `mc_xfpustate` and `mc_xfpustate_len` 0. `getcontext` sets `mc_fpformat` and `mc_ownedfp` to FreeBSD's `_MC_FPFMT_XMM` and `_MC_FPOWNED_FPU`, `mc_cs` and `mc_ss` to the host selectors, and `mc_flags`, `mc_fsbase` and `mc_gsbase` to 0. `setcontext` loads `mc_fpstate` whatever `mc_fpformat` and `mc_ownedfp` say, and restores neither the segment selectors, `mc_fsbase` and `mc_gsbase` nor the extended state, which FreeBSD restores when `mc_flags` asks for them - [setcontext](../../../core/libs/prx/libkernel/System/src/UserContext.cpp) (libkernel) and [longjmp](../../../core/libs/prx/libc/src/JumpBuffer.cpp) (libc) on Windows set the thread's stack bounds in the TEB to the stack they switch to: a `makecontext` stack, or else the committed region around the target stack pointer. A switch to any other guest stack, such as a context built by hand, gets the bounds of that region only