From eac8bfcfc012d57aaf2bf4572967c3e40990d9de Mon Sep 17 00:00:00 2001 From: Dean Galvin Date: Thu, 15 Oct 2026 13:59:12 +1300 Subject: [PATCH] fix(libc): list the directory once for a missing path that may be a relinked module A read-only open, stat or reachability check of a missing path listed its directory twice: once for a case-insensitive match of the name and again for a case-insensitive match of the relinked .guest.prx (#5434). Both names are now matched in the same listing. Precedence and the ambiguous-name errors are unchanged. --- .../libs/prx/libc/include/FilesystemError.hpp | 4 +- core/libs/prx/libc/include/General.hpp | 1 + core/libs/prx/libc/src/General.cpp | 115 ++++++++++++------ .../prx/libkernel/File/include/NativeStat.hpp | 1 - .../prx/libkernel/File/src/NativeStat.cpp | 6 - core/libs/prx/libkernel/File/src/Open.cpp | 7 +- core/libs/prx/libkernel/File/src/Stdio.cpp | 3 +- core/libs/tests/GuestFilesystem.cpp | 43 +++++++ 8 files changed, 130 insertions(+), 50 deletions(-) diff --git a/core/libs/prx/libc/include/FilesystemError.hpp b/core/libs/prx/libc/include/FilesystemError.hpp index 8962c34b1632..c24d52e44eb8 100644 --- a/core/libs/prx/libc/include/FilesystemError.hpp +++ b/core/libs/prx/libc/include/FilesystemError.hpp @@ -25,9 +25,9 @@ inline int FilesystemError(const std::error_code& error) { std::error_code RenameNative_nid_no_patch(const std::filesystem::path& source, const std::filesystem::path& destination, bool replacesFile); -inline int ResolvePathError_nid_no_patch(const char* path, std::filesystem::path& resolved) { +inline int ResolvePathError_nid_no_patch(const char* path, std::filesystem::path& resolved, bool module = false) { try { - resolved = ResolvePath_nid_no_patch(path); + resolved = module ? ResolvePathOrRelinkedModule_nid_no_patch(path) : ResolvePath_nid_no_patch(path); return 0; } catch (const std::filesystem::filesystem_error& error) { if (error.code() == std::errc::illegal_byte_sequence) return 2; diff --git a/core/libs/prx/libc/include/General.hpp b/core/libs/prx/libc/include/General.hpp index 654f5c78c3cd..37788afcfbac 100644 --- a/core/libs/prx/libc/include/General.hpp +++ b/core/libs/prx/libc/include/General.hpp @@ -18,6 +18,7 @@ extern "C" int HostStandardError_nid_no_patch(); #endif extern "C" std::filesystem::path ResolvePath_nid_no_patch(const char* path); +extern "C" std::filesystem::path ResolvePathOrRelinkedModule_nid_no_patch(const char* path); extern "C" std::filesystem::path RelinkedModulePath_nid_no_patch(const std::filesystem::path& path); extern "C" int SetWorkingDirectory_nid_no_patch(const char* resolved); extern "C" bool FinalPathComponentIsDot_nid_no_patch(const char* path); diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index 9e5a938d36d4..69c32c15134f 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #ifdef _WIN32 #ifndef NOMINMAX @@ -71,31 +72,69 @@ bool HasPrefix(const std::string& path, const std::string& prefix) { (path.size() == prefix.size() || path[prefix.size()] == '/'); } -std::filesystem::path ResolveHostPath(std::filesystem::path root, const std::filesystem::path& relative) { +std::filesystem::path RelinkedModuleName(std::filesystem::path path) { + path += ".guest.prx"; + return path; +} + +struct NameMatch { + std::string name; + std::optional path; + bool ambiguous = false; +}; + +std::error_code MatchNames(const std::filesystem::path& directory, std::span names) { + std::error_code error; + std::filesystem::directory_iterator entry(directory, error), end; + for (; !error && entry != end; entry.increment(error)) { + const auto filename = entry->path().filename().string(); + for (auto& name : names) { + if (!SameName(filename, name.name)) continue; + name.ambiguous = name.ambiguous || name.path.has_value(); + name.path = entry->path(); + } + } + return error; +} + +std::filesystem::path MatchedModuleOrPath(const std::filesystem::path& path, const std::filesystem::path& relinked, const NameMatch& match) { + if (match.ambiguous) throw std::runtime_error("Ambiguous case-insensitive guest path: " + relinked.string()); + std::error_code error; + return match.path && std::filesystem::is_regular_file(*match.path, error) ? *match.path : path; +} + +std::filesystem::path PathOrRelinkedModule(const std::filesystem::path& path) { + std::error_code error; + if (std::filesystem::exists(std::filesystem::symlink_status(path, error))) return path; + return RelinkedModulePath_nid_no_patch(path); +} + +std::filesystem::path ResolveHostPath(std::filesystem::path root, const std::filesystem::path& relative, bool module = false) { const auto direct = root / relative; std::error_code error; if (std::filesystem::exists(direct, error)) return direct; - for (const auto& part : relative) { - const auto candidate = root / part; + for (auto part = relative.begin(); part != relative.end(); ++part) { + const auto candidate = root / *part; const auto status = std::filesystem::symlink_status(candidate, error); if (std::filesystem::exists(status) || (error && error != std::errc::no_such_file_or_directory)) { root = candidate; continue; } - std::optional matched; - const auto name = part.string(); - std::filesystem::directory_iterator entry(root, error), end; - for (; !error && entry != end; entry.increment(error)) { - if (!SameName(entry->path().filename().string(), name)) continue; - if (matched) throw std::runtime_error("Ambiguous case-insensitive guest path: " + candidate.string()); - matched = entry->path(); - } - root = !error && matched ? *matched : candidate; + const bool last = module && std::next(part) == relative.end(); + const auto relinked = RelinkedModuleName(candidate); + NameMatch names[] = {{part->string()}, {relinked.filename().string()}}; + error = MatchNames(root, std::span(names, last ? 2 : 1)); + if (names[0].ambiguous) throw std::runtime_error("Ambiguous case-insensitive guest path: " + candidate.string()); + const bool matched = !error && names[0].path; + root = matched ? *names[0].path : candidate; + if (!last || matched) continue; + if (std::filesystem::is_regular_file(relinked, error)) return relinked; + return MatchedModuleOrPath(candidate, relinked, names[1]); } - return root; + return module ? PathOrRelinkedModule(root) : root; } -std::optional ResolveAlias(const std::string& guestPath) { +std::optional ResolveAlias(const std::string& guestPath, bool module) { const auto relative = TrimSlashes(guestPath.c_str()); auto& aliases = Aliases(); std::lock_guard lock(aliases.mutex); @@ -105,12 +144,18 @@ std::optional ResolveAlias(const std::string& guestPath) } for (const auto& [prefix, host] : aliases.entries) { if (!HasPrefix(relative, prefix)) continue; - if (relative.size() == prefix.size()) return std::filesystem::path(host).make_preferred(); - std::filesystem::path result = ResolveHostPath(host, relative.substr(prefix.size() + 1)); + if (relative.size() == prefix.size()) { + const auto result = std::filesystem::path(host).make_preferred(); + return module ? PathOrRelinkedModule(result) : result; + } #ifndef _WIN32 - if (guestPath.back() == '/') result /= ""; + if (guestPath.back() == '/') { + auto result = ResolveHostPath(host, relative.substr(prefix.size() + 1)); + result /= ""; + return module ? PathOrRelinkedModule(result) : result; + } #endif - return result.make_preferred(); + return ResolveHostPath(host, relative.substr(prefix.size() + 1), module).make_preferred(); } return std::nullopt; } @@ -124,9 +169,9 @@ WorkingDirectory& Directories() { static auto* state = new WorkingDirectory; return *state; } -std::filesystem::path HostPath(WorkingDirectory& state, const std::filesystem::path& guest) { - if (auto aliased = ResolveAlias(guest.relative_path().generic_string())) return *aliased; - return ResolveHostPath(state.root, guest.relative_path()).make_preferred(); +std::filesystem::path HostPath(WorkingDirectory& state, const std::filesystem::path& guest, bool module = false) { + if (auto aliased = ResolveAlias(guest.relative_path().generic_string(), module)) return *aliased; + return ResolveHostPath(state.root, guest.relative_path(), module).make_preferred(); } void RequireHostDirectory(WorkingDirectory& state, const std::filesystem::path& guest) { const auto host = HostPath(state, guest); @@ -147,13 +192,13 @@ void RequireHostDirectory(WorkingDirectory& state, const std::filesystem::path& #endif throw std::filesystem::filesystem_error("Guest path component is not a directory", host, error); } -std::filesystem::path Resolve(WorkingDirectory& state, const char* path) { +std::filesystem::path Resolve(WorkingDirectory& state, const char* path, bool module = false) { std::string text(path); for (auto& character : text) if (character == '\\') character = '/'; std::filesystem::path input(text); #ifdef _WIN32 // Preserve the existing ability to pass explicit native drive paths. - if (input.has_root_name()) return input; + if (input.has_root_name()) return module ? PathOrRelinkedModule(input) : input; #endif const auto current = std::filesystem::path("/") / state.current.lexically_relative(state.root); const auto relative = (input.is_absolute() ? input : current / input).relative_path(); @@ -165,7 +210,7 @@ std::filesystem::path Resolve(WorkingDirectory& state, const char* path) { } guest /= *part; } - return HostPath(state, guest.lexically_normal()); + return HostPath(state, guest.lexically_normal(), module); } #ifdef _WIN32 using WriteMark = FILETIME; @@ -386,20 +431,20 @@ extern "C" std::filesystem::path ResolvePath_nid_no_patch(const char* path) { return Resolve(state, path); } +extern "C" std::filesystem::path ResolvePathOrRelinkedModule_nid_no_patch(const char* path) { + if (!path) { APS5_INVALID_ARG_EX; } + auto& state = Directories(); + std::lock_guard lock(state.mutex); + return Resolve(state, path, true); +} + extern "C" std::filesystem::path RelinkedModulePath_nid_no_patch(const std::filesystem::path& path) { - auto relinked = path; - relinked += ".guest.prx"; + const auto relinked = RelinkedModuleName(path); std::error_code error; if (std::filesystem::is_regular_file(relinked, error)) return relinked; - const auto name = relinked.filename().string(); - std::optional matched; - std::filesystem::directory_iterator entry(relinked.parent_path().empty() ? "." : relinked.parent_path(), error), end; - for (; !error && entry != end; entry.increment(error)) { - if (!SameName(entry->path().filename().string(), name)) continue; - if (matched) throw std::runtime_error("Ambiguous case-insensitive guest path: " + relinked.string()); - matched = entry->path(); - } - return matched && std::filesystem::is_regular_file(*matched, error) ? *matched : path; + NameMatch names[] = {{relinked.filename().string()}}; + MatchNames(relinked.parent_path().empty() ? "." : relinked.parent_path(), names); + return MatchedModuleOrPath(path, relinked, names[0]); } extern "C" int SetWorkingDirectory_nid_no_patch(const char* resolved) { diff --git a/core/libs/prx/libkernel/File/include/NativeStat.hpp b/core/libs/prx/libkernel/File/include/NativeStat.hpp index 0d0e00369765..5715f0211980 100644 --- a/core/libs/prx/libkernel/File/include/NativeStat.hpp +++ b/core/libs/prx/libkernel/File/include/NativeStat.hpp @@ -6,7 +6,6 @@ namespace File { -std::filesystem::path PathOrRelinkedModule(const std::filesystem::path& nativePath); void FillFileStat(const std::filesystem::path& nativePath, FileStat* sb); void FillFileStat(int nativeDescriptor, FileStat* sb); bool FillFileStatFromDescriptor(int fd, FileStat* sb); diff --git a/core/libs/prx/libkernel/File/src/NativeStat.cpp b/core/libs/prx/libkernel/File/src/NativeStat.cpp index 7950599933b9..9d097eec22d6 100644 --- a/core/libs/prx/libkernel/File/src/NativeStat.cpp +++ b/core/libs/prx/libkernel/File/src/NativeStat.cpp @@ -139,12 +139,6 @@ static void CopyNativeStat(const NativeStat& st, FileStat* sb) { namespace File { -std::filesystem::path PathOrRelinkedModule(const std::filesystem::path& nativePath) { - std::error_code error; - if (std::filesystem::exists(std::filesystem::symlink_status(nativePath, error))) return nativePath; - return RelinkedModulePath_nid_no_patch(nativePath); -} - void FillFileStat(const std::filesystem::path& nativePath, FileStat* sb) { NativeStat st{}; if (DoStat(nativePath, &st) != 0) { diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 0f90fb1694de..9760bbe879bf 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -221,8 +221,8 @@ int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode) { return fd; } std::filesystem::path native; - if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); - if ((flags & (SCE_KERNEL_O_ACCMODE | SCE_KERNEL_O_CREAT)) == SCE_KERNEL_O_RDONLY) native = File::PathOrRelinkedModule(native); + const bool module = (flags & (SCE_KERNEL_O_ACCMODE | SCE_KERNEL_O_CREAT)) == SCE_KERNEL_O_RDONLY; + if (const int error = ResolvePathError_nid_no_patch(path, native, module)) return SceKernelError(error); return OpenNative(native, flags, mode); } @@ -302,8 +302,7 @@ int APS5_VABI sceKernelStat(const char* path, FileStat* sb) { } if (*path == '\0') return SceErrorFromErrno(2); std::filesystem::path native; - if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); - native = File::PathOrRelinkedModule(native); + if (const int error = ResolvePathError_nid_no_patch(path, native, true)) return SceKernelError(error); std::error_code error; constexpr int GuestEnotdir = 20; constexpr int GuestEfault = 14; diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 55cfd9736ba9..5fd51f538684 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -875,8 +875,7 @@ int APS5_VABI sceKernelCheckReachability(const char* path) { if (*path == '\0') return SCE_KERNEL_ERROR_ENOENT; if (std::strlen(path) > 255) return SCE_KERNEL_ERROR_ENAMETOOLONG; std::filesystem::path native; - if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); - native = File::PathOrRelinkedModule(native); + if (const int error = ResolvePathError_nid_no_patch(path, native, true)) return SceKernelError(error); std::error_code error; return std::filesystem::exists(native, error) ? 0 : SCE_KERNEL_ERROR_ENOENT; } diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 2c17a9dc9c93..b87cf1344c61 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -20,6 +20,9 @@ #else #include #endif +#ifdef __linux__ +#include +#endif extern "C" { int APS5_VABI remove_nid_postfix(const char*); int APS5_VABI rename_nid_postfix(const char*, const char*); @@ -246,10 +249,50 @@ static void TestRelinkedModuleNameCase() { std::filesystem::remove_all(root); } +#ifdef __linux__ +static int DirectoryOpens(int watch) { + alignas(inotify_event) char buffer[4096]; + int opens = 0; + for (ssize_t size; (size = read(watch, buffer, sizeof buffer)) > 0;) { + for (char* event = buffer; event < buffer + size; event += sizeof(inotify_event) + reinterpret_cast(event)->len) + if (reinterpret_cast(event)->mask & IN_OPEN) ++opens; + } + return opens; +} + +static void TestMissingPathListsDirectoryOnce() { + const auto root = std::filesystem::path("anyps5-missing-listing-" + + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directories(root)); + { std::ofstream file(root / "present.txt"); file << "present"; } + const int watch = inotify_init1(IN_NONBLOCK); + Require(watch >= 0 && inotify_add_watch(watch, root.c_str(), IN_OPEN | IN_CLOSE_NOWRITE) >= 0); + const auto missing = (root / "Missing.txt").generic_string(); + FileStat status{}; + Require(sceKernelStat(missing.c_str(), &status) == static_cast(0x80020002u) && DirectoryOpens(watch) == 1); + Require(sceKernelCheckReachability(missing.c_str()) == static_cast(0x80020002u) && DirectoryOpens(watch) == 1); + Require(sceKernelOpen(missing.c_str(), 0, 0) == static_cast(0x80020002u) && DirectoryOpens(watch) == 1); + close(watch); + { std::ofstream file(root / "plugin.prx.guest.prx"); file << "lower"; } + { std::ofstream file(root / "Plugin.prx.guest.prx"); file << "upper"; } + bool ambiguous = false; + try { + sceKernelStat((root / "PLUGIN.prx").generic_string().c_str(), &status); + } catch (const std::runtime_error&) { + ambiguous = true; + } + Require(ambiguous); + std::filesystem::remove_all(root); +} +#endif + int main() { TestParentTraversal(); TestRelinkedModuleNames(); TestRelinkedModuleNameCase(); +#ifdef __linux__ + TestMissingPathListsDirectoryOnce(); +#endif Require(chmod_nid_postfix(nullptr, 0600) == -1 && *__error_nid_postfix() == 14); Require(sceKernelChmod_nid_postfix(nullptr, 0600) == static_cast(0x8002000eu)); Require(chmod_nid_postfix("", 0600) == -1 && *__error_nid_postfix() == 2);