Skip to content
Merged
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
11 changes: 11 additions & 0 deletions core/libs/prx/libc/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -196,6 +197,16 @@ if(BUILD_TESTING)
set_tests_properties(guest_working_directory PROPERTIES ENVIRONMENT_MODIFICATION
"PATH=path_list_prepend:$<TARGET_FILE_DIR:libc>")
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:$<TARGET_FILE_DIR:libc>")
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)
Expand Down
145 changes: 145 additions & 0 deletions core/libs/prx/libc/src/RealPath.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
#include "prx/libc/include/ApplicationHeap.hpp"
#include "prx/libc/include/General.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include <cerrno>
#include <cstddef>
#include <cstring>
#include <filesystem>
#include <new>
#include <string>
#include <string_view>
#include <system_error>

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<char*>(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;
}
}
106 changes: 106 additions & 0 deletions core/libs/tests/GuestRealpath.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
#include "prx/libc/include/ApplicationHeap.hpp"
#include "prx/libc/include/General.hpp"
#include "prx/libc/include/general/VabiMacros.hpp"
#include <array>
#include <cctype>
#include <chrono>
#include <cstddef>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <string>

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<void*, 10> 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<char>(std::toupper(static_cast<unsigned char>(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;
}
1 change: 1 addition & 0 deletions docs/dev/TechnicalDebt.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Loading