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).
This commit is contained in:
UAQ Taxi authored and Adrià Franch committed 2026-10-19 14:49:49 +02:00
1 parent 379dca5cac
commit a968336825
6 files changed
+73 -16

No files matched your search

+2 -1
View File
@@ -2,6 +2,7 @@
#include <cstddef>
#include "SceTypes.hpp"
#include "prx/libc/include/General.hpp"
#include "prx/libc/include/RemoveTree.hpp"
#include "prx/libkernel/AppMetadata/include/AppMetadata.hpp"
#include <algorithm>
#include <cstring>
@@ -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);
}
@@ -18,6 +18,7 @@
#include <string>
#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);
@@ -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;
+62
View File
@@ -0,0 +1,62 @@
#ifndef CORE_LIBS_PRX_LIBC_INCLUDE_REMOVETREE_HPP
#define CORE_LIBS_PRX_LIBC_INCLUDE_REMOVETREE_HPP
#include <filesystem>
#include <string>
#include <system_error>
#ifdef _WIN32
#ifndef NOMINMAX
#define NOMINMAX
#endif
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <windows.h>
#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<int>(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<int>(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
+2 -11
View File
@@ -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 <array>
@@ -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;
}
+1
View File
@@ -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.