From e1f168a24d15ae194ec8f0149bba405b70afdae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20=C5=A0l=C3=A9gl?= Date: Thu, 8 Oct 2026 18:30:23 +0200 Subject: [PATCH] feat(libc): implement realpath Resolves a guest path the way FreeBSD 11's realpath does: a relative path starts from the guest working directory, "." and empty components are skipped, ".." drops the last component and never climbs above "/", every other component must exist, and one that is not a directory with anything after it is ENOTDIR. Backslashes separate components as in the rest of the path layer. The result is the guest path; the host is consulted only for existence and type, so host symlinks are followed but not expanded, and ".." is lexical, as in the rest of the path layer. A null buffer is PATH_MAX bytes from the application heap, a null path is EINVAL, an empty one ENOENT, and 1024 bytes or more in the input or the result is ENAMETOOLONG. On failure the caller's buffer holds the path that failed. Co-Authored-By: Claude Fable 5.1 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Fnrk5Dazjb2tLooVSpdRQq --- core/libs/prx/libc/CMakeLists.txt | 11 +++ core/libs/prx/libc/src/RealPath.cpp | 145 ++++++++++++++++++++++++++++ core/libs/tests/GuestRealpath.cpp | 106 ++++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 4 files changed, 263 insertions(+) create mode 100644 core/libs/prx/libc/src/RealPath.cpp create mode 100644 core/libs/tests/GuestRealpath.cpp diff --git a/core/libs/prx/libc/CMakeLists.txt b/core/libs/prx/libc/CMakeLists.txt index 7e732496af5..9538fce00eb 100644 --- a/core/libs/prx/libc/CMakeLists.txt +++ b/core/libs/prx/libc/CMakeLists.txt @@ -39,6 +39,7 @@ target_sources(libc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src/Rename.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Errors.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Syslog.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/RealPath.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/CxxAbiSupport.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Threads.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/exception/Exports.cpp @@ -196,6 +197,16 @@ if(BUILD_TESTING) set_tests_properties(guest_working_directory PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(guest_realpath_tests ${LIBS_INCLUDE_DIR}/tests/GuestRealpath.cpp) + target_include_directories(guest_realpath_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_realpath_tests PRIVATE libc) + configure_windows_unwind(guest_realpath_tests) + add_test(NAME guest_realpath COMMAND guest_realpath_tests) + set_tests_properties(guest_realpath PROPERTIES TIMEOUT 20) + if(WIN32) + set_tests_properties(guest_realpath PROPERTIES ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() add_test_executable(guest_path_case_tests ${LIBS_INCLUDE_DIR}/tests/GuestPathCase.cpp) target_include_directories(guest_path_case_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_path_case_tests PRIVATE libc libkernel) diff --git a/core/libs/prx/libc/src/RealPath.cpp b/core/libs/prx/libc/src/RealPath.cpp new file mode 100644 index 00000000000..10a0193b940 --- /dev/null +++ b/core/libs/prx/libc/src/RealPath.cpp @@ -0,0 +1,145 @@ +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" char* APS5_VABI getcwd_nid_postfix(char* buffer, std::size_t size); + +namespace { +constexpr std::size_t PathMax = 1024; +constexpr std::string_view Separators = "/\\"; +constexpr int NoEntry = 2; +constexpr int InputOutput = 5; +constexpr int NoMemory = 12; +constexpr int PermissionDenied = 13; +constexpr int NotDirectory = 20; +constexpr int InvalidArgument = 22; +constexpr int TooManyLinks = 62; +constexpr int NameTooLong = 63; + +int StatusError(const std::error_code& error) { + if (error == std::errc::no_such_file_or_directory) return NoEntry; + if (error == std::errc::not_a_directory) return NotDirectory; + if (error == std::errc::permission_denied) return PermissionDenied; + if (error == std::errc::filename_too_long) return NameTooLong; + if (error == std::errc::too_many_symbolic_link_levels) return TooManyLinks; + if (error == std::errc::not_enough_memory) return NoMemory; + return InputOutput; +} + +void LeaveTrouble(char* resolved, const std::string& trouble) { + if (resolved == nullptr) return; + const auto length = trouble.size() < PathMax ? trouble.size() : PathMax - 1; + std::memcpy(resolved, trouble.data(), length); + resolved[length] = '\0'; +} + +void DropLastComponent(std::string& result) { + if (result.size() <= 1) return; + const auto slash = result.rfind('/'); + result.erase(slash == 0 ? 1 : slash); +} + +bool IsSeparator(char character) { return Separators.find(character) != std::string_view::npos; } + +bool ResolveGuest(const char* path, std::string& result, int& error) { + std::string_view left(path); + if (IsSeparator(left.front())) { + result = "/"; + left.remove_prefix(1); + } else { + char current[PathMax]; + if (getcwd_nid_postfix(current, sizeof(current)) == nullptr) { + result = "."; + error = errno; + return false; + } + result = current; + } + if (left.size() >= PathMax || result.size() >= PathMax) { + error = NameTooLong; + return false; + } + while (!left.empty()) { + const auto separator = left.find_first_of(Separators); + const auto token = left.substr(0, separator); + const bool moreFollows = separator != std::string_view::npos; + left = moreFollows ? left.substr(separator + 1) : std::string_view(); + if (result.size() > 1 && result.size() + 1 >= PathMax) { + error = NameTooLong; + return false; + } + if (token.empty() || token == ".") continue; + if (token == "..") { + DropLastComponent(result); + continue; + } + std::string candidate = result; + if (candidate.size() > 1) candidate += '/'; + candidate += token; + result = candidate; + if (candidate.size() >= PathMax) { + error = NameTooLong; + return false; + } + std::error_code code; + const auto status = std::filesystem::status(ResolvePath_nid_no_patch(candidate.c_str()), code); + if (code && code != std::errc::no_such_file_or_directory && code != std::errc::not_a_directory) { + error = StatusError(code); + return false; + } + if (!std::filesystem::exists(status)) { + error = NoEntry; + return false; + } + if (moreFollows && !std::filesystem::is_directory(status)) { + error = NotDirectory; + return false; + } + } + return true; +} +} + +extern "C" char* APS5_VABI realpath_nid_postfix(const char* path, char* resolved) { + if (path == nullptr) { + errno = InvalidArgument; + return nullptr; + } + if (*path == '\0') { + errno = NoEntry; + return nullptr; + } + std::string result; + int error = 0; + try { + if (!ResolveGuest(path, result, error)) { + LeaveTrouble(resolved, result); + errno = error; + return nullptr; + } + if (resolved == nullptr) { + resolved = static_cast(ApplicationHeapAllocate_nid_no_patch(PathMax)); + if (resolved == nullptr) { + errno = NoMemory; + return nullptr; + } + } + std::memcpy(resolved, result.c_str(), result.size() + 1); + return resolved; + } catch (const std::bad_alloc&) { + errno = NoMemory; + return nullptr; + } catch (const std::filesystem::filesystem_error& failure) { + LeaveTrouble(resolved, result); + errno = StatusError(failure.code()); + return nullptr; + } +} diff --git a/core/libs/tests/GuestRealpath.cpp b/core/libs/tests/GuestRealpath.cpp new file mode 100644 index 00000000000..faae73050fe --- /dev/null +++ b/core/libs/tests/GuestRealpath.cpp @@ -0,0 +1,106 @@ +#include "prx/libc/include/ApplicationHeap.hpp" +#include "prx/libc/include/General.hpp" +#include "prx/libc/include/general/VabiMacros.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +char* APS5_VABI realpath_nid_postfix(const char*, char*); +int APS5_VABI chdir_nid_postfix(const char*); +void APS5_VABI free_nid_postfix(void*); +int* APS5_VABI __error_nid_postfix(); +} + +static void Require(bool value) { if (!value) std::abort(); } + +static bool Resolves(const std::string& path, const std::string& expected) { + char out[1024]; + std::memset(out, 'x', sizeof(out)); + return realpath_nid_postfix(path.c_str(), out) == out && expected == out; +} + +static bool Fails(const char* path, int error, const std::string& trouble = {}) { + char out[1024]; + std::memset(out, 'x', sizeof(out)); + *__error_nid_postfix() = 0; + if (realpath_nid_postfix(path, out) != nullptr || *__error_nid_postfix() != error) return false; + return trouble.empty() || trouble == out; +} + +int main() { + std::array api{}; + ApplicationHeapRegister_nid_no_patch(api.data()); + const auto host = std::filesystem::canonical(std::filesystem::current_path()); + const auto name = "anyps5-realpath-test-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()); + const auto directory = host / name; + Require(std::filesystem::create_directories(directory / "sub")); + { std::ofstream file(directory / "file.txt"); file << "file"; } + { std::ofstream file(directory / "sub" / "inner.txt"); file << "inner"; } + const std::string guest = "/" + name; + + Require(Resolves("/", "/")); + Require(Resolves(guest, guest)); + Require(Resolves(guest + "/", guest)); + Require(Resolves(guest + "/sub/../file.txt", guest + "/file.txt")); + Require(Resolves(guest + "/./sub//inner.txt", guest + "/sub/inner.txt")); + Require(Resolves("/../.." + guest + "/sub/", guest + "/sub")); + Require(Resolves(guest + "/sub/..", guest)); + Require(Resolves("/..", "/")); + Require(Resolves(guest + "\\sub\\..\\file.txt", guest + "/file.txt")); + Require(Resolves("\\" + name + "\\sub\\", guest + "/sub")); + + std::string upper = guest; + for (auto& character : upper) character = static_cast(std::toupper(static_cast(character))); + Require(Resolves(upper + "/file.txt", upper + "/file.txt")); + + Require(chdir_nid_postfix(guest.c_str()) == 0); + Require(Resolves("sub/inner.txt", guest + "/sub/inner.txt")); + Require(Resolves("../" + name + "/file.txt", guest + "/file.txt")); + Require(Resolves(".", guest)); + Require(Resolves("sub/./../sub", guest + "/sub")); + Require(Resolves("..", "/")); + Require(chdir_nid_postfix("/") == 0); + + AddPathAlias_nid_no_patch("realpath-alias", directory.string().c_str()); + Require(Resolves("/realpath-alias/sub/../file.txt", "/realpath-alias/file.txt")); + Require(Fails("/realpath-alias/missing", 2, "/realpath-alias/missing")); + RemovePathAlias_nid_no_patch("realpath-alias"); + BlockPathAlias_nid_no_patch("realpath-blocked"); + Require(Fails("/realpath-blocked/x", 2, "/realpath-blocked")); + + char* allocated = realpath_nid_postfix((guest + "/file.txt").c_str(), nullptr); + Require(allocated != nullptr && guest + "/file.txt" == allocated); + free_nid_postfix(allocated); + + Require(Fails(nullptr, 22)); + Require(Fails("", 2)); + Require(Fails((guest + "/missing").c_str(), 2, guest + "/missing")); + Require(Fails((guest + "/missing/../file.txt").c_str(), 2, guest + "/missing")); + Require(Fails((guest + "/file.txt/").c_str(), 20, guest + "/file.txt")); + Require(Fails((guest + "/file.txt/x").c_str(), 20, guest + "/file.txt")); + Require(Fails((guest + "/file.txt/.").c_str(), 20)); + Require(Fails((guest + "/file.txt/..").c_str(), 20)); + + const std::string overflowing = "/" + std::string(1023, 'a'); + Require(Fails(overflowing.c_str(), 63, overflowing.substr(0, 1023))); + const std::string longInput = guest + "/sub/" + std::string(1024, '.') + "/inner.txt"; + Require(Fails(longInput.c_str(), 63)); + std::string boundary = guest; + while (boundary.size() + 8 <= 1023) boundary += "/missing"; + boundary += "/" + std::string(1023 - boundary.size() - 1, 'm'); + Require(boundary.size() == 1023); + Require(Fails(boundary.c_str(), 2, guest + "/missing")); + + *__error_nid_postfix() = 0; + Require(realpath_nid_postfix((guest + "/missing").c_str(), nullptr) == nullptr && *__error_nid_postfix() == 2); + + std::filesystem::remove_all(directory); + return 0; +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 73ac7832e70..c8bfcb48cf9 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -317,6 +317,7 @@ Throughout the project, every function at every stage either **does exactly what - [sceKernelWriteThrottlingStatus](../../core/libs/prx/libkernel/File/src/Stdio.cpp) (libkernel) reports no write throttling: the host SSD has no write budget, so the first qword of the 32-byte status is set to `UINT32_MAX` and the rest is zeroed. PPSA17221 reads only that qword: it multiplies it by 64 KiB and compares the product with byte thresholds up to 0x4AF00000 to pick one of five tiers. When the call fails the title assumes 0x4B000001 bytes, which selects the same top tier as `UINT32_MAX`. What the value measures on hardware (a remaining budget or a rate) and the meaning of the other three qwords are unknown - [/dev/random and /dev/urandom](../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) (libkernel) opened for reading return a descriptor backed by the host null device whose reads are filled from `std::random_device`; both paths behave the same, never block, and positioned reads ignore the offset (FreeBSD `random(4)`). Only the exact paths are recognised, a `dup`ed descriptor reads as an empty file, `fstat`, `lseek`, `mmap` and `select` see the null device, and opening them for writing throws - [Wide-character conversions](../../core/libs/prx/libc/src/Multibyte.cpp) (libc) - The wide `wcsto*` functions parse the ASCII prefix of the guest string, as the C locale accepts only ASCII white space and digits. [`wcsrtombs`](../../core/libs/prx/libc/src/Multibyte.cpp) uses the guest's single-byte C locale through `wcrtomb`; other encodings and shift states are not supported +- [realpath](../../core/libs/prx/libc/src/RealPath.cpp) (libc) follows FreeBSD 11's [`realpath.c`](https://github.com/freebsd/freebsd-src/blob/stable/11/lib/libc/stdlib/realpath.c) except for symlinks. The console's guest filesystem has none, so a host symlink under the guest root is followed for existence and type but not expanded, and `..` drops the previous component lexically, as `Resolve` does for every guest path. Through a host link `linkdir -> sub/deep`, `linkdir/../x` gives `x` beside the link, not beside its target. - [SPIRV-Tools optimization](../../core/shader/recompiler/SpirvBackend/src/SpirvOptimizer.cpp) skips CCP and Simplification for modules with storage-only 8- or 16-bit types because the pinned folder produces illegal narrow constants. This also disables unrelated constant folding; the restricted full pipeline mirrors the pinned performance recipe. - [SRT signed comparison](../../core/shader/recompiler/Optimization/src/SrtWalker/SrtEvaluator.cpp) evaluates `SLessThan32` as signed 32-bit less-than, matching the existing SPIR-V backend and [SPIR-V `OpSLessThan`](https://registry.khronos.org/SPIR-V/specs/unified1/SPIRV.html#OpSLessThan). Signed boundary descriptor expressions are tested on the host; native RDNA2/PS5 execution and title behavior remain unverified. - On macOS the [write watch](../../core/libs/prx/libc/src/GuestWriteWatch.cpp) write-protects watched pages and records the first write to each from a SIGBUS/SIGSEGV handler, because macOS has no counterpart to `userfaultfd` write-protect or `MEM_WRITE_WATCH`. The protection is real, so: