From a9683368253b13beefae004771ce39e142ec4eeb Mon Sep 17 00:00:00 2001 From: UAQ Taxi Date: Sat, 17 Oct 2026 12:32:26 +0400 Subject: [PATCH] fix(libc): remove Windows trees without the remove_all symlink hang MinGW libstdc++ up to GCC 16 does not treat a reparse point as a link in std::filesystem::remove_all, so a host tree that holds a symlink is walked through the link and never returns. Add RemoveTree, which removes the tree itself on Windows, and use it where save data, app content and the shader disk cache removed host trees. The libstdc++ fix is in GCC 17 (88856ad855ae). --- core/libs/prx/libSceAppContent/Export.cpp | 3 +- .../libs/prx/libSceSaveData.native/Export.cpp | 3 +- core/libs/prx/libSceSaveData/SaveDataCore.hpp | 7 ++- core/libs/prx/libc/include/RemoveTree.hpp | 62 +++++++++++++++++++ core/libs/tests/GuestKernelSys.cpp | 13 +--- docs/dev/debt/libc.md | 1 + 6 files changed, 73 insertions(+), 16 deletions(-) create mode 100644 core/libs/prx/libc/include/RemoveTree.hpp diff --git a/core/libs/prx/libSceAppContent/Export.cpp b/core/libs/prx/libSceAppContent/Export.cpp index be7cad24d9..20ec69a1e9 100644 --- a/core/libs/prx/libSceAppContent/Export.cpp +++ b/core/libs/prx/libSceAppContent/Export.cpp @@ -2,6 +2,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/RemoveTree.hpp" #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include #include @@ -49,7 +50,7 @@ bool IsTemporaryMountPoint(const AppContentMountPoint* mountPoint) { } static void ClearDirectory(const std::filesystem::path& directory) { - for (const auto& entry : std::filesystem::directory_iterator(directory)) std::filesystem::remove_all(entry.path()); + for (const auto& entry : std::filesystem::directory_iterator(directory)) RemoveTree(entry.path()); RecordWrittenPath_nid_no_patch(directory); } diff --git a/core/libs/prx/libSceSaveData.native/Export.cpp b/core/libs/prx/libSceSaveData.native/Export.cpp index 925cabeb13..b5d52e5013 100644 --- a/core/libs/prx/libSceSaveData.native/Export.cpp +++ b/core/libs/prx/libSceSaveData.native/Export.cpp @@ -18,6 +18,7 @@ #include #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/RemoveTree.hpp" #include "prx/libkernel/AppMetadata/include/AppMetadata.hpp" #include "SaveData.hpp" #include "prx/libSceSaveData/SaveDataFile.hpp" @@ -340,7 +341,7 @@ static int deleteSave(const SaveDataDelete* del) { std::lock_guard lock(g_slots_mutex); if (isMountedDirectory(path)) return SAVE_DATA_ERROR_BUSY; if (std::filesystem::is_directory(path)) { - std::filesystem::remove_all(path); + RemoveTree(path); } std::error_code ec; std::filesystem::remove(param_path(path), ec); diff --git a/core/libs/prx/libSceSaveData/SaveDataCore.hpp b/core/libs/prx/libSceSaveData/SaveDataCore.hpp index 4705d8d62e..58afdfa721 100644 --- a/core/libs/prx/libSceSaveData/SaveDataCore.hpp +++ b/core/libs/prx/libSceSaveData/SaveDataCore.hpp @@ -22,6 +22,7 @@ #include "SceTypes.hpp" #include "prx/libc/include/General.hpp" +#include "prx/libc/include/RemoveTree.hpp" #include "prx/libSceSaveData/SaveDataFile.hpp" namespace savedata { @@ -498,14 +499,14 @@ int mount_internal(const SceSaveDataDirName* dir_name, std::uint32_t mount_mode, created = true; if (write_u64(save_blocks_path(directory), blocks) != SD_OK) { std::error_code cleanup; - std::filesystem::remove_all(directory, cleanup); + RemoveTree(directory, cleanup); return SD_ERROR_INTERNAL; } SaveDataParam initial {}; std::strncpy(initial.title, "Saved Data", sizeof(initial.title) - 1); if (!write_blob(save_param_path(directory), &initial, sizeof(initial))) { std::error_code cleanup; - std::filesystem::remove_all(directory, cleanup); + RemoveTree(directory, cleanup); return SD_ERROR_INTERNAL; } } @@ -574,7 +575,7 @@ int delete_internal(std::int32_t user_id, const SceSaveDataDirName* dir_name, if (!std::filesystem::exists(directory, error) || error) { return SD_ERROR_NOT_FOUND; } - std::filesystem::remove_all(directory, error); + RemoveTree(directory, error); RecordWrittenPath_nid_no_patch(directory); if (error) { return SD_ERROR_INTERNAL; diff --git a/core/libs/prx/libc/include/RemoveTree.hpp b/core/libs/prx/libc/include/RemoveTree.hpp new file mode 100644 index 0000000000..ec5429a0aa --- /dev/null +++ b/core/libs/prx/libc/include/RemoveTree.hpp @@ -0,0 +1,62 @@ +#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_REMOVETREE_HPP +#define CORE_LIBS_PRX_LIBC_INCLUDE_REMOVETREE_HPP + +#include +#include +#include +#ifdef _WIN32 +#ifndef NOMINMAX +#define NOMINMAX +#endif +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#include +#endif + +inline void RemoveTree(const std::filesystem::path& target, std::error_code& error) { + error.clear(); +#ifdef _WIN32 + const std::wstring wide = target.wstring(); + const DWORD attributes = GetFileAttributesW(wide.c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES) { + const DWORD missing = GetLastError(); + if (missing != ERROR_FILE_NOT_FOUND && missing != ERROR_PATH_NOT_FOUND) + error = std::error_code(static_cast(missing), std::system_category()); + return; + } + const bool directory = (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0; + if (directory && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) { + std::error_code listing; + { + std::filesystem::directory_iterator entries(target, listing); + while (!listing && entries != std::filesystem::directory_iterator()) { + RemoveTree(entries->path(), error); + if (error) return; + entries.increment(listing); + } + } + if (listing) { + error = listing; + return; + } + } + if (directory ? RemoveDirectoryW(wide.c_str()) != 0 : DeleteFileW(wide.c_str()) != 0) return; + const DWORD failed = GetLastError(); + SetFileAttributesW(wide.c_str(), FILE_ATTRIBUTE_NORMAL); + if (directory ? RemoveDirectoryW(wide.c_str()) != 0 : DeleteFileW(wide.c_str()) != 0) return; + error = std::error_code(static_cast(failed), std::system_category()); +#else + std::error_code removal; + std::filesystem::remove_all(target, removal); + if (removal) error = removal; +#endif +} + +inline void RemoveTree(const std::filesystem::path& target) { + std::error_code error; + RemoveTree(target, error); + if (error) throw std::filesystem::filesystem_error("RemoveTree", target, error); +} + +#endif diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index a00be4c9e3..803b105272 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -1,4 +1,5 @@ #include "prx/libc/include/general/VabiMacros.hpp" +#include "prx/libc/include/RemoveTree.hpp" #include "prx/libkernel/File/include/FileFlags.hpp" #include "SceTypes.hpp" #include @@ -658,16 +659,6 @@ int main() { Require(!std::filesystem::exists(std::filesystem::symlink_status(throughLink))); } #endif -#ifdef _WIN32 - // std::filesystem::remove_all loops forever when the tree contains a - // dangling symbolic link: fs::remove reports such a link as missing and - // clears the error, so recursive_directory_iterator::__erase keeps - // retrying it. The fixture always contains dangling links here, so - // remove it through the shell instead. - const auto cleanup = L"cmd /c rd /s /q \"" + root.wstring() + L"\""; - Require(_wsystem(cleanup.c_str()) == 0); -#else - std::filesystem::remove_all(root); -#endif + RemoveTree(root); return 0; } diff --git a/docs/dev/debt/libc.md b/docs/dev/debt/libc.md index ff94841f67..886ded4f9f 100644 --- a/docs/dev/debt/libc.md +++ b/docs/dev/debt/libc.md @@ -84,6 +84,7 @@ - [sceKernelSync](../../../core/libs/prx/libc/src/General.cpp) (libkernel) on Windows skips a recorded file it cannot open for writing (read-only, or held open by a handle that denies write sharing), since `FlushFileBuffers` needs a writable handle. On Linux, it skips a recorded file it cannot open for reading. Such a file's data may not reach the disk before a host crash; the guest reads the same data either way - [`fopen` streams](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) on Windows share delete access, like the descriptors [`sceKernelOpen`](../../../core/libs/prx/libkernel/File/src/Open.cpp) opens, so a file the guest holds open can be removed or renamed. A stream reopened with `freopen` still goes through `_wfreopen`, which does not share delete access. On Windows, opening a directory in a read mode fails with `EACCES`, where FreeBSD opens it; directory streams are not emulated - [`fopen` of /dev/random and /dev/urandom](../../../core/libs/prx/libc/src/FileAndHeap.cpp) (libc) in a read mode returns a stream over the host null device whose `fread` fills the buffer with host OS entropy, as the libkernel [random device](../../../core/libs/prx/libkernel/File/src/RandomDevice.cpp) descriptors do. Only the exact paths are recognised, the stream has no descriptor, `setvbuf`, `feof`, `ferror`, `clearerr`, `flockfile`, `ftrylockfile`, `funlockfile` and `fclose` work on it, `fflush` returns 0 as on a FreeBSD read-only stream, and every other stream function throws, as does opening the paths for writing. `freopen` onto these paths still fails with `ENOENT`. ProsperoTV (PPSA99003) reads its phone pairing code this way +- [RemoveTree](../../../core/libs/prx/libc/include/RemoveTree.hpp) (libc) removes a host tree itself on Windows instead of calling `std::filesystem::remove_all`: MinGW libstdc++ up to GCC 16 does not treat a reparse point as a link, so `remove_all` recurses through it and never terminates, and any tree that holds a symlink hangs. It builds the entry list from `GetFileAttributesW`, removes a reparse point as the link it is rather than its target, clears the read-only attribute and retries as `remove_all` does, and reports the first native error. The libstdc++ fix is in GCC 17 (commit 88856ad855ae); other hosts call `remove_all`. - The [`_Unwind_Context`](../../../core/libs/prx/libc/include/exceptions/Unwind.hpp) that `_Unwind_Backtrace` passes to its callback starts with a table laid out like LLVM libunwind's `AbstractUnwindCursor` virtual table, because homebrew built with the ps5-payload-dev SDK links libunwind statically and reads the frames with its own `_Unwind_GetIP` (`__unw_get_reg`), which calls through that table. Only register reads and writes (`validReg`, `getReg`, `setReg`, with `UNW_REG_IP` and `UNW_REG_SP`), `validFloatReg` (always false) and `isSignalFrame` are answered; the other slots throw. The layout follows libunwind's `UnwindCursor.hpp`, not the console's own `libSceLibcInternal`, which is not checked - The Windows libc [exception unwinder](../../../core/libs/prx/libc/src/exception/Unwind.cpp) steps through frames of modules without the project's `.ehmeta` and `.ehfram` tables (system DLLs, and WinLibs GCC 15.2's `libstdc++-6.dll` and `libwinpthread-1.dll`, whose `.eh_frame` sections are empty) with their PE unwind data through `RtlVirtualUnwind`, but does not run Windows exception or termination handlers: it stops at a frame whose unwind data has one, such as a C++ cleanup through `__gxx_personality_seh0` in `libstdc++-6.dll` or a system DLL function like `qsort` in `ucrtbase.dll` (Windows 10 22H2), and the exception is then reported as unhandled - The libc [SSE4a trap emulation](../../../core/libs/prx/libc/src/specifics/windows/Sse4aEmulation.hpp) on Windows is superseded by `--to-intel` and remains only until the relinked title has been verified without it.