From 8f69214a12469d5026f75fdb4fce3680c2b2e784 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 22:46:35 +0200 Subject: [PATCH 1/2] fix(libkernel): fail unlink, remove, rename and open of a file named with a trailing slash with ENOTDIR on Windows FreeBSD 11.4 lookup answers ENOTDIR when a name with a trailing slash is not a directory (sys/kern/vfs_cache.c:2104-2105). On Windows the guest path of a mount alias lost the trailing slash, and these calls failed with ENOENT or EINVAL. The resolver now keeps the slash on Windows too, and sceKernelUnlink/unlink, remove, rename and sceKernelOpen/open answer ENOTDIR. The trailing-slash checks in guest_filesystem, which only built off Windows, run there. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/prx/libc/include/FilesystemError.hpp | 9 +++++++++ core/libs/prx/libc/src/Filesystem.cpp | 1 + core/libs/prx/libc/src/General.cpp | 2 -- core/libs/prx/libc/src/Rename.cpp | 1 + core/libs/prx/libkernel/File/src/Open.cpp | 2 ++ core/libs/tests/GuestFilesystem.cpp | 2 -- 6 files changed, 13 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libc/include/FilesystemError.hpp b/core/libs/prx/libc/include/FilesystemError.hpp index bb50c8d70e60..2d2abfa5291f 100644 --- a/core/libs/prx/libc/include/FilesystemError.hpp +++ b/core/libs/prx/libc/include/FilesystemError.hpp @@ -33,6 +33,15 @@ inline bool HasNonDirectoryPrefix_nid_no_patch(const std::filesystem::path& nati return false; } +inline bool NamesNonDirectoryWithSlash_nid_no_patch(const std::filesystem::path& native) { + if (native.has_filename()) return false; + auto named = native; + while (!named.has_filename() && named.has_relative_path()) named = named.parent_path(); + std::error_code error; + const auto status = std::filesystem::status(named, error); + return std::filesystem::exists(status) && !std::filesystem::is_directory(status); +} + inline int NotFoundError_nid_no_patch(const std::filesystem::path& native) { return HasNonDirectoryPrefix_nid_no_patch(native) ? 20 : 2; } diff --git a/core/libs/prx/libc/src/Filesystem.cpp b/core/libs/prx/libc/src/Filesystem.cpp index 510477eb7ae0..1f3723cc0748 100644 --- a/core/libs/prx/libc/src/Filesystem.cpp +++ b/core/libs/prx/libc/src/Filesystem.cpp @@ -85,6 +85,7 @@ extern "C" int APS5_VABI remove_nid_postfix(const char* path) { errno = 22; return -1; } + if (NamesNonDirectoryWithSlash_nid_no_patch(resolved)) { errno = 20; return -1; } std::error_code error; #ifdef _WIN32 const DWORD attributes = GetFileAttributesW(resolved.c_str()); diff --git a/core/libs/prx/libc/src/General.cpp b/core/libs/prx/libc/src/General.cpp index d3c8d0753cd6..058673bf4b63 100644 --- a/core/libs/prx/libc/src/General.cpp +++ b/core/libs/prx/libc/src/General.cpp @@ -178,13 +178,11 @@ std::optional ResolveAlias(const std::string& guestPath, const auto result = std::filesystem::path(host).make_preferred(); return module ? PathOrRelinkedModule(result) : result; } -#ifndef _WIN32 if (guestPath.back() == '/') { auto result = ResolveHostPath(host, SplitNames(std::string_view(relative).substr(prefix.size() + 1))); result /= ""; return module ? PathOrRelinkedModule(result) : result; } -#endif return ResolveHostPath(host, SplitNames(std::string_view(relative).substr(prefix.size() + 1)), module).make_preferred(); } return std::nullopt; diff --git a/core/libs/prx/libc/src/Rename.cpp b/core/libs/prx/libc/src/Rename.cpp index 973c142ff6ef..27d823d94a31 100644 --- a/core/libs/prx/libc/src/Rename.cpp +++ b/core/libs/prx/libc/src/Rename.cpp @@ -115,6 +115,7 @@ extern "C" int RenameGuestPath_nid_no_patch(const char* from, const char* to) { return -1; } const auto destination = ResolvePath_nid_no_patch(to); + if (NamesNonDirectoryWithSlash_nid_no_patch(source) || (std::filesystem::exists(source) && NamesNonDirectoryWithSlash_nid_no_patch(destination))) { errno = 20; return -1; } int error = RenamePath_nid_no_patch(source, destination); if (error == 2) { std::error_code status; diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 186ce63ff3b6..44759b28ac4c 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -270,6 +270,7 @@ int APS5_VABI sceKernelOpen(const char* path, int flags, std::uint16_t mode) { APS5_LOG_ERR("open failed path=%s resolveError=%d result=0x%08X", path != nullptr ? path : "", error, static_cast(result)); return result; } + if (NamesNonDirectoryWithSlash_nid_no_patch(native)) return SceErrorFromErrno(ENOTDIR); const int result = OpenNative(native, flags, mode); if (result < 0) { const auto resolved = native.u8string(); @@ -416,6 +417,7 @@ int APS5_VABI sceKernelUnlink(const char* path) { std::filesystem::path native; if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); constexpr int GuestEperm = 1; + if (NamesNonDirectoryWithSlash_nid_no_patch(native)) return SceErrorFromErrno(20); std::error_code error; if (std::filesystem::is_directory(std::filesystem::symlink_status(native, error))) return SceErrorFromErrno(GuestEperm); if (NativeUnlink(native) != 0) { diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 07b86670f3a8..26e3e48d2d18 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -238,7 +238,6 @@ static void TestParentTraversal() { checkInvalid("/parent-test-mount/present.txt/..", 20); FileStat status{}; Require(sceKernelStat("/parent-test-mount/existing/../present.txt", &status) == 0 && status.st_size == 9); -#ifndef _WIN32 for (const char* name : {"slash-unlink", "slash-remove", "slash-rename", "slash-open", "slash-kernel"}) std::ofstream(root / name) << "kept"; Require(unlink_nid_postfix("/parent-test-mount/slash-unlink/") == -1 && *__error_nid_postfix() == 20); Require(remove_nid_postfix("/parent-test-mount/slash-remove/") == -1 && *__error_nid_postfix() == 20); @@ -250,7 +249,6 @@ static void TestParentTraversal() { Require(sceKernelStat("/parent-test-mount/Existing/", &status) == 0 && (status.st_mode & 0170000) == 0040000); Require(mkdir_nid_postfix("/parent-test-mount/slash-directory/", 0755) == 0 && std::filesystem::is_directory(root / "slash-directory")); Require(rmdir_nid_postfix("/parent-test-mount/slash-directory/") == 0 && !std::filesystem::exists(root / "slash-directory")); -#endif RemovePathAlias_nid_no_patch("parent-test-mount"); std::error_code linkError; std::filesystem::create_directory_symlink("Existing", root / "directory-link", linkError); From 9eb680d17a0e3782084d4513e60bc21d67ae9055 Mon Sep 17 00:00:00 2001 From: xXPBSXx <20614131+xXPBSXx@users.noreply.github.com> Date: Tue, 20 Oct 2026 23:25:33 +0200 Subject: [PATCH 2/2] test(libkernel): cover a nested name with a trailing slash below a file file/child/ strips to file/child, which does not exist; unlink, remove, open and sceKernelUnlink still answer ENOTDIR through the non-directory prefix check. Co-Authored-By: Claude Sonnet 5.5 --- core/libs/tests/GuestFilesystem.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 26e3e48d2d18..9ddde71df8d5 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -244,6 +244,10 @@ static void TestParentTraversal() { Require(rename_nid_postfix("/parent-test-mount/slash-rename/", "/parent-test-mount/slash-renamed") == -1 && *__error_nid_postfix() == 20); Require(open_nid_postfix("/parent-test-mount/slash-open/", 0, 0) == -1 && *__error_nid_postfix() == 20); Require(sceKernelUnlink("/parent-test-mount/slash-kernel/") == static_cast(0x80020014u)); + Require(unlink_nid_postfix("/parent-test-mount/slash-unlink/child/") == -1 && *__error_nid_postfix() == 20); + Require(remove_nid_postfix("/parent-test-mount/slash-remove/child/") == -1 && *__error_nid_postfix() == 20); + Require(open_nid_postfix("/parent-test-mount/slash-open/child/", 0, 0) == -1 && *__error_nid_postfix() == 20); + Require(sceKernelUnlink("/parent-test-mount/slash-kernel/child/") == static_cast(0x80020014u)); for (const char* name : {"slash-unlink", "slash-remove", "slash-rename", "slash-open", "slash-kernel"}) Require(std::filesystem::is_regular_file(root / name)); Require(!std::filesystem::exists(root / "slash-renamed")); Require(sceKernelStat("/parent-test-mount/Existing/", &status) == 0 && (status.st_mode & 0170000) == 0040000);