From 7a68e11ab4992bc4b9e1cbe99ea59717fc39308b Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Sat, 10 Oct 2026 13:33:16 +0300 Subject: [PATCH 1/4] fix(libkernel): rewind Windows directory descriptors on lseek lseek on a directory descriptor returned 0 without moving the listing, so getdents after lseek(fd, 0, SEEK_SET) reported nothing. --- core/libs/prx/libkernel/CMakeLists.txt | 9 +++ .../File/include/DirectoryDescriptor.hpp | 2 + .../File/src/DirectoryDescriptor.cpp | 56 +++++++++++--- core/libs/prx/libkernel/File/src/Open.cpp | 8 ++ core/libs/tests/GuestDirectoryRewind.cpp | 75 +++++++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 6 files changed, 141 insertions(+), 10 deletions(-) create mode 100644 core/libs/tests/GuestDirectoryRewind.cpp diff --git a/core/libs/prx/libkernel/CMakeLists.txt b/core/libs/prx/libkernel/CMakeLists.txt index 41cb24584..db2752fa4 100644 --- a/core/libs/prx/libkernel/CMakeLists.txt +++ b/core/libs/prx/libkernel/CMakeLists.txt @@ -908,6 +908,15 @@ if(BUILD_TESTING) set_tests_properties(guest_directory_buffer PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + if(WIN32) + add_test_executable(guest_directory_rewind_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectoryRewind.cpp) + target_include_directories(guest_directory_rewind_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_directory_rewind_tests PRIVATE libkernel libc) + configure_windows_unwind(guest_directory_rewind_tests) + add_test(NAME guest_directory_rewind COMMAND guest_directory_rewind_tests) + set_tests_properties(guest_directory_rewind PROPERTIES TIMEOUT 15 ENVIRONMENT_MODIFICATION + "PATH=path_list_prepend:$") + endif() if(NOT WIN32) add_test_executable(guest_directory_entry_tests ${LIBS_INCLUDE_DIR}/tests/GuestDirectoryEntries.cpp) target_include_directories(guest_directory_entry_tests PRIVATE ${LIBS_INCLUDE_DIR}) diff --git a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp index 9efda2d7c..2031b6c4c 100644 --- a/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp +++ b/core/libs/prx/libkernel/File/include/DirectoryDescriptor.hpp @@ -1,6 +1,7 @@ #ifndef CORE_LIBS_PRX_LIBKERNEL_FILE_DIRECTORYDESCRIPTOR_HPP #define CORE_LIBS_PRX_LIBKERNEL_FILE_DIRECTORYDESCRIPTOR_HPP +#include #include #include @@ -11,6 +12,7 @@ namespace File { int OpenDirectoryDescriptor(const std::filesystem::path& path); std::optional DirectoryDescriptorPath(int fd); void ForgetDirectoryDescriptor(int fd); +std::optional SeekDirectoryDescriptor(int fd, std::int64_t offset, int whence); int ReadDirectoryDescriptor(int fd, char* buf, int nbytes); } diff --git a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp index 06b665940..9210e95a3 100644 --- a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp +++ b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp @@ -7,8 +7,12 @@ #ifdef _WIN32 +#include "prx/libc/include/General.hpp" + +#include #include #include +#include #include #include #include @@ -32,6 +36,18 @@ struct DirectoryState { std::mutex g_mutex; std::map g_directories; +void Load(DirectoryState& state) { + if (state.loaded) return; + state.loaded = true; + state.entries.emplace_back(".", GuestDirectoryType); + state.entries.emplace_back("..", GuestDirectoryType); + std::error_code error; + for (const auto& entry : std::filesystem::directory_iterator(state.path, error)) { + std::error_code typeError; + state.entries.emplace_back(entry.path().filename().string(), entry.is_directory(typeError) ? GuestDirectoryType : GuestRegularType); + } +} + } namespace File { @@ -56,21 +72,41 @@ void ForgetDirectoryDescriptor(int fd) { g_directories.erase(fd); } +std::optional SeekDirectoryDescriptor(int fd, std::int64_t offset, int whence) { + constexpr int SeekSet = 0; + constexpr int SeekCurrent = 1; + std::lock_guard lock(g_mutex); + const auto found = g_directories.find(fd); + if (found == g_directories.end()) return std::nullopt; + auto& state = found->second; + if (whence != SeekSet && whence != SeekCurrent) NotImplemented_nid_no_patch("lseek from the end of a directory descriptor"); + const auto cursor = static_cast(state.cursor); + if (whence == SeekCurrent && offset > std::numeric_limits::max() - cursor) { + errno = EINVAL; + return std::int64_t{-1}; + } + const std::int64_t target = whence == SeekSet ? offset : cursor + offset; + if (target < 0) { + errno = EINVAL; + return std::int64_t{-1}; + } + if (target == 0) { + state.entries.clear(); + state.loaded = false; + state.cursor = 0; + return std::int64_t{0}; + } + Load(state); + state.cursor = static_cast(target); + return target; +} + int ReadDirectoryDescriptor(int fd, char* buf, int nbytes) { std::lock_guard lock(g_mutex); const auto found = g_directories.find(fd); if (found == g_directories.end()) return SCE_KERNEL_ERROR_ENOTDIR; auto& state = found->second; - if (!state.loaded) { - state.loaded = true; - state.entries.emplace_back(".", GuestDirectoryType); - state.entries.emplace_back("..", GuestDirectoryType); - std::error_code error; - for (const auto& entry : std::filesystem::directory_iterator(state.path, error)) { - std::error_code typeError; - state.entries.emplace_back(entry.path().filename().string(), entry.is_directory(typeError) ? GuestDirectoryType : GuestRegularType); - } - } + Load(state); std::size_t used = 0; while (state.cursor < state.entries.size()) { const auto& [name, type] = state.entries[state.cursor]; diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index ca7cbe5f1..50ad1b34a 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -215,6 +215,14 @@ std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence) { if (whence < 0 || whence > 2) { throw std::invalid_argument(std::string(__func__) + ": invalid whence=" + std::to_string(whence)); } +#ifdef _WIN32 + if (const auto directory = File::SeekDirectoryDescriptor(d, offset, whence)) { + if (*directory < 0) { + throw std::runtime_error(std::string(__func__) + ": lseek failed, fd=" + std::to_string(d) + ", errno=" + std::to_string(errno)); + } + return *directory; + } +#endif std::int64_t result = NativeLseek(d, offset, whence); if (result < 0) return SceIoErrorFromErrno(errno); return result; diff --git a/core/libs/tests/GuestDirectoryRewind.cpp b/core/libs/tests/GuestDirectoryRewind.cpp new file mode 100644 index 000000000..da36e6ad0 --- /dev/null +++ b/core/libs/tests/GuestDirectoryRewind.cpp @@ -0,0 +1,75 @@ +#include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libkernel/File/include/FileFlags.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern "C" { +int APS5_VABI sceKernelOpen(const char*, int, std::uint16_t); +int APS5_VABI sceKernelClose(int); +int APS5_VABI sceKernelGetdents(int, char*, int); +std::int64_t APS5_VABI sceKernelLseek(int, std::int64_t, int); +} + +static void Check(bool value, int line) { + if (!value) { + std::fprintf(stderr, "Directory rewind check failed at line %d\n", line); + std::abort(); + } +} +#define Require(value) Check((value), __LINE__) + +static std::set ReadAll(int directory) { + std::set names; + std::array buffer{}; + for (;;) { + const int bytes = sceKernelGetdents(directory, buffer.data(), static_cast(buffer.size())); + Require(bytes >= 0); + if (bytes == 0) return names; + for (int offset = 0; offset < bytes;) { + names.emplace(buffer.data() + offset + 8); + offset += static_cast(buffer[offset + 4]) | (static_cast(buffer[offset + 5]) << 8); + } + } +} + +int main() { + const auto root = std::filesystem::temp_directory_path() / + ("anyps5-directory-rewind-" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); + Require(std::filesystem::create_directory(root)); + { std::ofstream file(root / "a"); Require(static_cast(file)); } + { std::ofstream file(root / "b"); Require(static_cast(file)); } + + const int directory = sceKernelOpen(root.string().c_str(), SCE_KERNEL_O_RDONLY | SCE_KERNEL_O_DIRECTORY, 0); + Require(directory >= 0); + const std::set expected{".", "..", "a", "b"}; + Require(ReadAll(directory) == expected); + Require(ReadAll(directory).empty()); + + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(ReadAll(directory) == expected); + + { std::ofstream file(root / "c"); Require(static_cast(file)); } + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(ReadAll(directory) == (std::set{".", "..", "a", "b", "c"})); + + std::array two{}; + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(sceKernelGetdents(directory, two.data(), static_cast(two.size())) == 24); + const auto position = sceKernelLseek(directory, 0, 1); + Require(position > 0); + const auto rest = ReadAll(directory); + Require(sceKernelLseek(directory, position, 0) == position); + Require(ReadAll(directory) == rest); + Require(!rest.contains(".") && !rest.contains("..")); + + Require(sceKernelClose(directory) == 0); + std::filesystem::remove_all(root); +} diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 0c72b62a7..81478df5a 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -366,6 +366,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceHttp2 asynchronous requests](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) never reach the network: `sceHttp2SendRequestAsync` accepts the request, queues a completion whose result is the library's network error and triggers the user event named by the kqueue option (equeue at offset 0, event id at 8, user data at 16, layout inferred from PPSA17221); `sceHttp2WaitAsync` returns that completion. The completion's `event_type` value is unknown and left 0. PPSA17221 completes its request object only after `sceHttp2SendRequestAsync` returns, and its handler for a synchronous failure calls `shared_from_this` before that, so a send must not fail synchronously. A kqueue option or user event that is missing, an extra option, and a wait with no completed operation throw - [system](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified - [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 +- [Windows directory descriptors](../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) snapshot the directory on the first `getdents` or `getdirentries`; `lseek` on them takes `SEEK_SET` and `SEEK_CUR` offsets as entry indexes and `SEEK_END` throws. Offset 0 rewinds and takes a new snapshot, as FreeBSD `getdirentries(2)` allows. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies - [/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. From cb6dcbf8449f89ac15e5dd085ed9ed2556bff092 Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Sat, 10 Oct 2026 13:46:10 +0300 Subject: [PATCH 2/4] fix(libkernel): keep directory lseek off the shared lock and report offset overflow lseek on a non-directory descriptor no longer takes the directory table lock when no directory is open, and a listing is read under the descriptor's own lock. SEEK_CUR with offset 0 only reports the position, and an overflowing SEEK_CUR offset reports EOVERFLOW. --- .../File/src/DirectoryDescriptor.cpp | 47 ++++++++++------ core/libs/tests/GuestDirectoryRewind.cpp | 53 ++++++++++++++++++- docs/dev/TechnicalDebt.md | 2 +- 3 files changed, 85 insertions(+), 17 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp index 9210e95a3..3b9be91af 100644 --- a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp +++ b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp @@ -11,11 +11,13 @@ #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -25,8 +27,10 @@ namespace { constexpr std::uint8_t GuestDirectoryType = 4; constexpr std::uint8_t GuestRegularType = 8; +constexpr int GuestEoverflow = 84; struct DirectoryState { + std::mutex mutex; std::filesystem::path path; std::vector> entries; std::size_t cursor = 0; @@ -34,7 +38,14 @@ struct DirectoryState { }; std::mutex g_mutex; -std::map g_directories; +std::map> g_directories; +std::atomic g_directoryCount{0}; + +std::shared_ptr Find(int fd) { + std::lock_guard lock(g_mutex); + const auto found = g_directories.find(fd); + return found == g_directories.end() ? nullptr : found->second; +} void Load(DirectoryState& state) { if (state.loaded) return; @@ -56,33 +67,39 @@ int OpenDirectoryDescriptor(const std::filesystem::path& path) { const int fd = ::_open("NUL", _O_RDONLY | _O_BINARY); if (fd < 0) return -1; std::lock_guard lock(g_mutex); - g_directories[fd] = DirectoryState{path}; + const auto state = std::make_shared(); + state->path = path; + g_directories[fd] = state; + g_directoryCount = g_directories.size(); return fd; } std::optional DirectoryDescriptorPath(int fd) { - std::lock_guard lock(g_mutex); - const auto found = g_directories.find(fd); - if (found == g_directories.end()) return std::nullopt; - return found->second.path; + const auto state = Find(fd); + if (!state) return std::nullopt; + return state->path; } void ForgetDirectoryDescriptor(int fd) { + if (g_directoryCount == 0) return; std::lock_guard lock(g_mutex); g_directories.erase(fd); + g_directoryCount = g_directories.size(); } std::optional SeekDirectoryDescriptor(int fd, std::int64_t offset, int whence) { constexpr int SeekSet = 0; constexpr int SeekCurrent = 1; - std::lock_guard lock(g_mutex); - const auto found = g_directories.find(fd); - if (found == g_directories.end()) return std::nullopt; - auto& state = found->second; + if (g_directoryCount == 0) return std::nullopt; + const auto found = Find(fd); + if (!found) return std::nullopt; + auto& state = *found; + std::lock_guard lock(state.mutex); if (whence != SeekSet && whence != SeekCurrent) NotImplemented_nid_no_patch("lseek from the end of a directory descriptor"); const auto cursor = static_cast(state.cursor); + if (whence == SeekCurrent && offset == 0) return cursor; if (whence == SeekCurrent && offset > std::numeric_limits::max() - cursor) { - errno = EINVAL; + errno = GuestEoverflow; return std::int64_t{-1}; } const std::int64_t target = whence == SeekSet ? offset : cursor + offset; @@ -102,10 +119,10 @@ std::optional SeekDirectoryDescriptor(int fd, std::int64_t offset, } int ReadDirectoryDescriptor(int fd, char* buf, int nbytes) { - std::lock_guard lock(g_mutex); - const auto found = g_directories.find(fd); - if (found == g_directories.end()) return SCE_KERNEL_ERROR_ENOTDIR; - auto& state = found->second; + const auto found = Find(fd); + if (!found) return SCE_KERNEL_ERROR_ENOTDIR; + auto& state = *found; + std::lock_guard lock(state.mutex); Load(state); std::size_t used = 0; while (state.cursor < state.entries.size()) { diff --git a/core/libs/tests/GuestDirectoryRewind.cpp b/core/libs/tests/GuestDirectoryRewind.cpp index da36e6ad0..cfb258e7e 100644 --- a/core/libs/tests/GuestDirectoryRewind.cpp +++ b/core/libs/tests/GuestDirectoryRewind.cpp @@ -8,7 +8,9 @@ #include #include #include +#include #include +#include #include extern "C" { @@ -26,6 +28,16 @@ static void Check(bool value, int line) { } #define Require(value) Check((value), __LINE__) +template +static bool ThrowsRuntimeError(TFunction function) { + try { + function(); + } catch (const std::runtime_error&) { + return true; + } + return false; +} + static std::set ReadAll(int directory) { std::set names; std::array buffer{}; @@ -64,12 +76,51 @@ int main() { Require(sceKernelLseek(directory, 0, 0) == 0); Require(sceKernelGetdents(directory, two.data(), static_cast(two.size())) == 24); const auto position = sceKernelLseek(directory, 0, 1); - Require(position > 0); + Require(position == 2); + Require(sceKernelLseek(directory, 0, 1) == 2); const auto rest = ReadAll(directory); Require(sceKernelLseek(directory, position, 0) == position); Require(ReadAll(directory) == rest); Require(!rest.contains(".") && !rest.contains("..")); + Require(sceKernelLseek(directory, 100, 0) == 100); + Require(sceKernelGetdents(directory, two.data(), static_cast(two.size())) == 0); + Require(sceKernelLseek(directory, 0, 1) == 100); + Require(sceKernelLseek(directory, -98, 1) == 2); + Require(ReadAll(directory) == rest); + + Require(sceKernelLseek(directory, 3, 0) == 3); + Require(ThrowsRuntimeError([&] { sceKernelLseek(directory, -1, 0); })); + Require(ThrowsRuntimeError([&] { sceKernelLseek(directory, -4, 1); })); + Require(sceKernelLseek(directory, 0, 1) == 3); + Require(sceKernelLseek(directory, std::numeric_limits::max(), 0) == std::numeric_limits::max()); + Require(ThrowsRuntimeError([&] { sceKernelLseek(directory, 1, 1); })); + Require(sceKernelLseek(directory, 0, 1) == std::numeric_limits::max()); + + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(sceKernelGetdents(directory, two.data(), static_cast(two.size())) == 24); + Require(sceKernelLseek(directory, 0, 1) == 2); + const auto afterQuery = ReadAll(directory); + Require(afterQuery == rest); + + std::array tiny{}; + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(sceKernelGetdents(directory, tiny.data(), static_cast(tiny.size())) < 0); + { std::ofstream file(root / "d"); Require(static_cast(file)); } + Require(sceKernelLseek(directory, 0, 1) == 0); + Require(ReadAll(directory) == (std::set{".", "..", "a", "b", "c"})); + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(sceKernelGetdents(directory, tiny.data(), static_cast(tiny.size())) < 0); + Require(sceKernelLseek(directory, 0, 0) == 0); + Require(ReadAll(directory) == (std::set{".", "..", "a", "b", "c", "d"})); + Require(sceKernelClose(directory) == 0); + + const int reused = sceKernelOpen((root / "a").string().c_str(), SCE_KERNEL_O_RDWR, 0); + Require(reused == directory); + Require(sceKernelLseek(reused, 5, 0) == 5); + Require(sceKernelLseek(reused, 0, 1) == 5); + Require(sceKernelLseek(reused, 0, 2) == 0); + Require(sceKernelClose(reused) == 0); std::filesystem::remove_all(root); } diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 81478df5a..dd42eaca2 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -366,7 +366,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceHttp2 asynchronous requests](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) never reach the network: `sceHttp2SendRequestAsync` accepts the request, queues a completion whose result is the library's network error and triggers the user event named by the kqueue option (equeue at offset 0, event id at 8, user data at 16, layout inferred from PPSA17221); `sceHttp2WaitAsync` returns that completion. The completion's `event_type` value is unknown and left 0. PPSA17221 completes its request object only after `sceHttp2SendRequestAsync` returns, and its handler for a synchronous failure calls `shared_from_this` before that, so a send must not fail synchronously. A kqueue option or user event that is missing, an extra option, and a wait with no completed operation throw - [system](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified - [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 -- [Windows directory descriptors](../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) snapshot the directory on the first `getdents` or `getdirentries`; `lseek` on them takes `SEEK_SET` and `SEEK_CUR` offsets as entry indexes and `SEEK_END` throws. Offset 0 rewinds and takes a new snapshot, as FreeBSD `getdirentries(2)` allows. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies +- [Windows directory descriptors](../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) snapshot the directory on the first `getdents` or `getdirentries` and on `lseek` with `SEEK_SET` or `SEEK_CUR` to a non-zero index; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws, where the NUL descriptor used to return 0, because FreeBSD defines it relative to the directory size, which is not modelled. A negative result throws like any other `lseek` failure (`EINVAL`) and a `SEEK_CUR` offset that overflows throws with `EOVERFLOW`, as FreeBSD `vn_seek` reports them. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies - [/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. From 6db8c81aae164b3fac732593fd1c0f5e8dda1c22 Mon Sep 17 00:00:00 2001 From: Hussein Kadhim Date: Sat, 10 Oct 2026 13:48:59 +0300 Subject: [PATCH 3/4] docs(technical-debt): describe the Windows directory snapshot lifetime exactly --- docs/dev/TechnicalDebt.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index dd42eaca2..dde068966 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -366,7 +366,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceHttp2 asynchronous requests](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) never reach the network: `sceHttp2SendRequestAsync` accepts the request, queues a completion whose result is the library's network error and triggers the user event named by the kqueue option (equeue at offset 0, event id at 8, user data at 16, layout inferred from PPSA17221); `sceHttp2WaitAsync` returns that completion. The completion's `event_type` value is unknown and left 0. PPSA17221 completes its request object only after `sceHttp2SendRequestAsync` returns, and its handler for a synchronous failure calls `shared_from_this` before that, so a send must not fail synchronously. A kqueue option or user event that is missing, an extra option, and a wait with no completed operation throw - [system](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified - [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 -- [Windows directory descriptors](../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) snapshot the directory on the first `getdents` or `getdirentries` and on `lseek` with `SEEK_SET` or `SEEK_CUR` to a non-zero index; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws, where the NUL descriptor used to return 0, because FreeBSD defines it relative to the directory size, which is not modelled. A negative result throws like any other `lseek` failure (`EINVAL`) and a `SEEK_CUR` offset that overflows throws with `EOVERFLOW`, as FreeBSD `vn_seek` reports them. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies +- [Windows directory descriptors](../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws, where the NUL descriptor used to return 0, because FreeBSD defines it relative to the directory size, which is not modelled. A negative result throws like any other `lseek` failure (`EINVAL`) and a `SEEK_CUR` offset that overflows throws with `EOVERFLOW`, as FreeBSD `vn_seek` reports them. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies - [/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. From ce0239c940a3811408f9dfff6890c29e7660d209 Mon Sep 17 00:00:00 2001 From: LeandroLP23 Date: Sun, 11 Oct 2026 13:59:49 +0200 Subject: [PATCH 4/4] fix(libkernel): return lseek errors for directory descriptors instead of throwing Co-Authored-By: Claude Opus 5.5 --- .../libkernel/File/src/DirectoryDescriptor.cpp | 3 +-- core/libs/prx/libkernel/File/src/Open.cpp | 4 +--- core/libs/tests/GuestDirectoryRewind.cpp | 16 +++------------- docs/dev/TechnicalDebt.md | 2 +- 4 files changed, 6 insertions(+), 19 deletions(-) diff --git a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp index 3b9be91af..9bbe6569b 100644 --- a/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp +++ b/core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp @@ -27,7 +27,6 @@ namespace { constexpr std::uint8_t GuestDirectoryType = 4; constexpr std::uint8_t GuestRegularType = 8; -constexpr int GuestEoverflow = 84; struct DirectoryState { std::mutex mutex; @@ -99,7 +98,7 @@ std::optional SeekDirectoryDescriptor(int fd, std::int64_t offset, const auto cursor = static_cast(state.cursor); if (whence == SeekCurrent && offset == 0) return cursor; if (whence == SeekCurrent && offset > std::numeric_limits::max() - cursor) { - errno = GuestEoverflow; + errno = EOVERFLOW; return std::int64_t{-1}; } const std::int64_t target = whence == SeekSet ? offset : cursor + offset; diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 50ad1b34a..93151b4d6 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -217,9 +217,7 @@ std::int64_t APS5_VABI sceKernelLseek(int d, std::int64_t offset, int whence) { } #ifdef _WIN32 if (const auto directory = File::SeekDirectoryDescriptor(d, offset, whence)) { - if (*directory < 0) { - throw std::runtime_error(std::string(__func__) + ": lseek failed, fd=" + std::to_string(d) + ", errno=" + std::to_string(errno)); - } + if (*directory < 0) return SceIoErrorFromErrno(errno); return *directory; } #endif diff --git a/core/libs/tests/GuestDirectoryRewind.cpp b/core/libs/tests/GuestDirectoryRewind.cpp index cfb258e7e..ee576b3c0 100644 --- a/core/libs/tests/GuestDirectoryRewind.cpp +++ b/core/libs/tests/GuestDirectoryRewind.cpp @@ -10,7 +10,6 @@ #include #include #include -#include #include extern "C" { @@ -28,15 +27,6 @@ static void Check(bool value, int line) { } #define Require(value) Check((value), __LINE__) -template -static bool ThrowsRuntimeError(TFunction function) { - try { - function(); - } catch (const std::runtime_error&) { - return true; - } - return false; -} static std::set ReadAll(int directory) { std::set names; @@ -90,11 +80,11 @@ int main() { Require(ReadAll(directory) == rest); Require(sceKernelLseek(directory, 3, 0) == 3); - Require(ThrowsRuntimeError([&] { sceKernelLseek(directory, -1, 0); })); - Require(ThrowsRuntimeError([&] { sceKernelLseek(directory, -4, 1); })); + Require(sceKernelLseek(directory, -1, 0) == static_cast(0x80020016u)); + Require(sceKernelLseek(directory, -4, 1) == static_cast(0x80020016u)); Require(sceKernelLseek(directory, 0, 1) == 3); Require(sceKernelLseek(directory, std::numeric_limits::max(), 0) == std::numeric_limits::max()); - Require(ThrowsRuntimeError([&] { sceKernelLseek(directory, 1, 1); })); + Require(sceKernelLseek(directory, 1, 1) == static_cast(0x80020054u)); Require(sceKernelLseek(directory, 0, 1) == std::numeric_limits::max()); Require(sceKernelLseek(directory, 0, 0) == 0); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index dde068966..3f1dfd618 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -366,7 +366,7 @@ Throughout the project, every function at every stage either **does exactly what - [libSceHttp2 asynchronous requests](../../core/libs/prx/libSceHttp2/Export.cpp) (libSceHttp2) never reach the network: `sceHttp2SendRequestAsync` accepts the request, queues a completion whose result is the library's network error and triggers the user event named by the kqueue option (equeue at offset 0, event id at 8, user data at 16, layout inferred from PPSA17221); `sceHttp2WaitAsync` returns that completion. The completion's `event_type` value is unknown and left 0. PPSA17221 completes its request object only after `sceHttp2SendRequestAsync` returns, and its handler for a synchronous failure calls `shared_from_this` before that, so a send must not fail synchronously. A kqueue option or user event that is missing, an extra option, and a wait with no completed operation throw - [system](../../core/libs/prx/libkernel/System/src/Process.cpp) (libkernel) runs no command. It follows FreeBSD 11's `lib/libc/stdlib/system.c` for a sandbox without `/bin/sh`: a null command returns 1 unconditionally, as that version does, and any command returns the wait status of a child that could not exec the shell and called `_exit(127)` (`0x7f00`). Whether the console's application sandbox has no shell, and whether `vfork` succeeds there (FreeBSD returns -1 when it fails), is unverified - [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 -- [Windows directory descriptors](../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws, where the NUL descriptor used to return 0, because FreeBSD defines it relative to the directory size, which is not modelled. A negative result throws like any other `lseek` failure (`EINVAL`) and a `SEEK_CUR` offset that overflows throws with `EOVERFLOW`, as FreeBSD `vn_seek` reports them. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies +- [Windows directory descriptors](../../core/libs/prx/libkernel/File/src/DirectoryDescriptor.cpp) (libkernel) take one snapshot of the directory, on the first `getdents`, `getdirentries` or non-zero `lseek` (`SEEK_SET` or `SEEK_CUR`), and keep it until it is discarded; `lseek` to offset 0 only discards the snapshot, and the next read lists the directory again, so every rewind costs O(entries) on the next read. `lseek` takes the offset as an entry index and `SEEK_CUR` with offset 0 only reports the index. `SEEK_END` throws, where the NUL descriptor used to return 0, because FreeBSD defines it relative to the directory size, which is not modelled. A negative result returns `EINVAL` and a `SEEK_CUR` offset that overflows returns `EOVERFLOW`, as FreeBSD `vn_seek` reports them. `getdirentries` still reports 0 through `basep` instead of the position of the block it read, so only 0 and values returned by `lseek` are valid offsets; FreeBSD offsets are filesystem cookies - [/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.