Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions core/libs/prx/libc/include/General.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<std::filesystem::path> WrittenPaths_nid_no_patch();
extern "C" void SyncWrittenPaths_nid_no_patch();
Expand Down
34 changes: 34 additions & 0 deletions core/libs/prx/libc/src/General.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ struct PathAliases {
std::mutex mutex;
std::vector<std::pair<std::string, std::string>> entries;
std::vector<std::string> blocked;
std::vector<std::pair<std::string, PathProvider>> providers;
};

PathAliases& Aliases() {
Expand Down Expand Up @@ -118,7 +119,23 @@ struct WorkingDirectory {
std::filesystem::path current = root;
};
WorkingDirectory& Directories() { static WorkingDirectory state; return state; }
std::optional<std::filesystem::path> 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();
}
Expand Down Expand Up @@ -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;
Expand Down
14 changes: 13 additions & 1 deletion core/libs/prx/libkernel/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -270,6 +271,17 @@ if(BUILD_TESTING)
set_tests_properties(guest_kernel_sync PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libkernel>")
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:$<TARGET_FILE_DIR:libkernel>")
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)
Expand Down
114 changes: 114 additions & 0 deletions core/libs/prx/libkernel/File/src/SandboxCertificates.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
#include "prx/libc/include/General.hpp"

#include <array>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <memory>
#include <optional>
#include <stdexcept>
#include <string>
#include <system_error>

#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#include <windows.h>
#include <wincrypt.h>
#else
#include <unistd.h>
#endif

extern "C" const char* APS5_VABI sceKernelGetFsSandboxRandomWord();

namespace {
constexpr char CertificateBegin[] = "-----BEGIN CERTIFICATE-----";

std::optional<std::string> ReadFile(const std::filesystem::path& path) {
std::ifstream file(path, std::ios::binary);
if (!file) return std::nullopt;
std::string text((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
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<void, void (*)(HCERTSTORE)> store(CertOpenSystemStoreW(0, name), [](HCERTSTORE handle) { CertCloseStore(handle, 0); });
if (store == nullptr) throw std::system_error(static_cast<int>(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<int>(error), std::system_category(), "Encoding a host certificate");
}
text.resize(length);
for (const wchar_t character : text) bundle.push_back(static_cast<char>(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<const char*, 4> 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<unsigned long>(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<std::streamsize>(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;
}();
}
76 changes: 76 additions & 0 deletions core/libs/tests/GuestSandboxCertificates.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
#include "prx/libc/include/General.hpp"
#include "prx/libkernel/KernelErrors.hpp"
#include "SceTypes.hpp"
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <fstream>
#include <string>
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<std::size_t>(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<std::int64_t>(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;
}
3 changes: 2 additions & 1 deletion docs/dev/debt/libkernel.md
Original file line number Diff line number Diff line change
Expand Up @@ -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), `/<sandbox word>/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
Expand Down
Loading