diff --git a/core/libs/prx/libkernel/File/src/Open.cpp b/core/libs/prx/libkernel/File/src/Open.cpp index 7609a30cf4..9b76e96fa2 100644 --- a/core/libs/prx/libkernel/File/src/Open.cpp +++ b/core/libs/prx/libkernel/File/src/Open.cpp @@ -469,7 +469,20 @@ int APS5_VABI sceKernelUnlink(const char* path) { if (const int error = ResolvePathError_nid_no_patch(path, native)) return SceKernelError(error); constexpr int GuestEperm = 1; std::error_code error; - if (std::filesystem::is_directory(std::filesystem::symlink_status(native, error))) return SceErrorFromErrno(GuestEperm); + if (std::filesystem::is_directory(std::filesystem::symlink_status(native, error))) { +#ifdef _WIN32 + const DWORD attributes = GetFileAttributesW(native.c_str()); + if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { + std::string junctionTarget; + if (File::WindowsSymbolicLink::ReadJunction(native, junctionTarget, "unlink")) { + if (RemoveDirectoryW(native.c_str()) == 0) return SceErrorFromErrno(GuestEperm); + RecordWrittenPath_nid_no_patch(native); + return 0; + } + } +#endif + return SceErrorFromErrno(GuestEperm); + } if (NativeUnlink(native) != 0) { #ifdef _WIN32 if (NativeSymbolicLinkLoop(native.parent_path())) return SCE_KERNEL_ERROR_ELOOP; diff --git a/core/libs/prx/libkernel/File/src/Stdio.cpp b/core/libs/prx/libkernel/File/src/Stdio.cpp index 9068334dea..841955e5d4 100644 --- a/core/libs/prx/libkernel/File/src/Stdio.cpp +++ b/core/libs/prx/libkernel/File/src/Stdio.cpp @@ -1610,6 +1610,13 @@ int APS5_VABI sceKernelRmdir(const char* path) { std::error_code error; if (std::filesystem::is_symlink(native, error)) return SceErrorFromErrno(GUEST_ENOTDIR); if (const int failure = PathLookupFailure(error)) return failure; +#ifdef _WIN32 + const DWORD attributes = GetFileAttributesW(native.c_str()); + if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0 && (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { + std::string junctionTarget; + if (File::WindowsSymbolicLink::ReadJunction(native, junctionTarget, "rmdir")) return SceErrorFromErrno(GUEST_ENOTDIR); + } +#endif if (!std::filesystem::is_directory(native, error)) { #ifdef _WIN32 if (File::WindowsSymbolicLinkLoop(WithoutTrailingSeparator(native).parent_path())) return SceKernelError(GUEST_ELOOP); diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 3157f7a862..7d740fba2e 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -823,6 +823,13 @@ int APS5_VABI unlinkat_nid_postfix(int fd, const char* path, int flag) { if (IsDotOrDotDot(path)) return static_cast(Fail(GuestEinval)); if (flag & GuestAtRemovedir) { if (std::filesystem::is_symlink(symlinkStatus)) return static_cast(Fail(GuestEnotdir)); +#ifdef _WIN32 + const DWORD attributes = GetFileAttributesW(native.c_str()); + if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0 && (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { + std::string junctionTarget; + if (File::WindowsSymbolicLink::ReadJunction(native, junctionTarget, "unlinkat")) return static_cast(Fail(GuestEnotdir)); + } +#endif if (!std::filesystem::is_directory(symlinkStatus)) { if (HasNonDirectoryPrefix(native) || std::filesystem::exists(symlinkStatus)) return static_cast(Fail(GuestEnotdir)); @@ -852,6 +859,17 @@ int APS5_VABI unlinkat_nid_postfix(int fd, const char* path, int flag) { return static_cast(Fail(GuestEnotdir)); return static_cast(Fail(GuestEnoent)); } +#ifdef _WIN32 + const DWORD attributes = GetFileAttributesW(native.c_str()); + if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0 && (attributes & FILE_ATTRIBUTE_DIRECTORY) != 0) { + std::string junctionTarget; + if (File::WindowsSymbolicLink::ReadJunction(native, junctionTarget, "unlinkat")) { + if (RemoveDirectoryW(native.c_str()) == 0) return static_cast(Fail(GuestEperm)); + RecordWrittenPath_nid_no_patch(native); + return 0; + } + } +#endif if (std::filesystem::is_directory(symlinkStatus)) return static_cast(Fail(GuestEperm)); #ifdef _WIN32 struct _stat64 status{}; diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 043a147a54..a412640fac 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -1194,6 +1194,22 @@ int main() { nullptr, 0, &returned, nullptr) != 0); Require(CloseHandle(taggedCleanup) != 0 && std::filesystem::remove(tagged)); } + + const auto fullTarget = root / "junction-full-target"; + const auto fullJunction = root / "junction-full"; + Require(std::filesystem::create_directory(fullTarget)); + FILE* const inside = std::fopen((fullTarget / "inside.txt").string().c_str(), "wb"); + Require(inside != nullptr); + std::fclose(inside); + const auto fullCommand = L"cmd /c mklink /J \"" + fullJunction.wstring() + L"\" \"" + fullTarget.wstring() + L"\""; + Require(_wsystem(fullCommand.c_str()) == 0); + Require(rmdir_nid_postfix(fullJunction.string().c_str()) == -1 && *__error_nid_postfix() == 20); + Require(sceKernelRmdir(fullJunction.string().c_str()) == static_cast(0x80020014u)); + Require(std::filesystem::is_regular_file(fullTarget / "inside.txt")); + Require(sceKernelUnlink(fullJunction.string().c_str()) == 0); + Require(!std::filesystem::exists(std::filesystem::symlink_status(fullJunction))); + Require(std::filesystem::is_regular_file(fullTarget / "inside.txt")); + Require(std::filesystem::remove(fullTarget / "inside.txt") && std::filesystem::remove(fullTarget)); #endif FileStat identity{}; FileStat byDescriptor{}; diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 126dce19e7..d622db0f8b 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -972,6 +972,29 @@ int main() { std::filesystem::create_directory(root / "nonempty"); { std::ofstream out(root / "nonempty" / "f.bin"); out << 'x'; } Require(unlinkat_nid_postfix(base, "nonempty", 0x800) == -1 && Error() == 66); +#ifdef _WIN32 + const auto fullJunctionTarget = root / "junction-full-target"; + Require(std::filesystem::create_directory(fullJunctionTarget)); + { std::ofstream out(fullJunctionTarget / "inside.txt"); out << 'x'; } + const auto fullJunctionCommand = L"cmd /c mklink /J \"" + (root / "junction-full").wstring() + L"\" \"" + std::filesystem::absolute(fullJunctionTarget).wstring() + L"\""; + Require(_wsystem(fullJunctionCommand.c_str()) == 0); + Require(unlinkat_nid_postfix(base, "junction-full", 0x800) == -1 && Error() == 20); + Require(std::filesystem::is_regular_file(fullJunctionTarget / "inside.txt")); + Require(unlinkat_nid_postfix(base, "junction-full", 0) == 0); + Require(!std::filesystem::exists(std::filesystem::symlink_status(root / "junction-full"))); + Require(std::filesystem::is_regular_file(fullJunctionTarget / "inside.txt")); + Require(std::filesystem::remove(fullJunctionTarget / "inside.txt") && std::filesystem::remove(fullJunctionTarget)); + { std::ofstream out(root / "symlink-target.bin"); out << 'x'; } + const int madeLink = symlink_nid_postfix("symlink-target.bin", (root / "symlink-file").string().c_str()); + if (madeLink == 0) { + Require(unlinkat_nid_postfix(base, "symlink-file", 0) == 0); + Require(!std::filesystem::exists(std::filesystem::symlink_status(root / "symlink-file"))); + Require(std::filesystem::is_regular_file(root / "symlink-target.bin")); + Require(std::filesystem::remove(root / "symlink-target.bin")); + } else { + Require(std::filesystem::remove(root / "symlink-target.bin")); + } +#endif Require(unlinkat_nid_postfix(base, "nonempty/f.bin", 0) == 0 && !std::filesystem::exists(root / "nonempty" / "f.bin")); Require(unlinkat_nid_postfix(base, "nonempty", 0x800) == 0 && !std::filesystem::exists(root / "nonempty")); { std::ofstream out(root / "to_unlink.bin"); out << 'x'; } diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 34b34b71f8..3df210cbe9 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -155,7 +155,7 @@ - [Windows file timestamps](../../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes`, `futimes`, and `lutimes` reject timestamps outside nonnegative signed 64-bit Windows file-time ticks with `EINVAL`, including exactly 1601-01-01 00:00:00 UTC, whose zero value means "leave unchanged" to `SetFileTime`. Directory timestamp updates resolve the current path of the opened handle. Filesystems with coarser timestamp resolution cannot retain every guest microsecond. Birth-time clamping is not atomic with concurrent host timestamp updates and has not been measured on a console - [EVFILT_VNODE on macOS](../../../core/libs/prx/libkernel/Equeue/src/FileEvent.cpp) (libkernel) registers `EVFILT_VNODE` on a duplicate of the descriptor in a host kqueue, whose notes have FreeBSD's values, and drops the `NOTE_LINK` that XNU adds to `NOTE_DELETE` when the watched file or directory is removed, since FreeBSD's `vop_remove_post` and `vop_rmdir_post` report `NOTE_DELETE` alone. As on Windows, a directory registered with `NOTE_EXTEND` is accepted but never receives it: XNU reports a move between directories as `NOTE_WRITE` alone, with `NOTE_LINK` for a subdirectory - [Windows file identity](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) - `stat` and `fstat` report the volume serial number as `st_dev` and the 64-bit file index folded into the guest's 32-bit `st_ino` (never 0), so two different files can in principle share an `st_ino`; the fold is not taken from a reference. `st_nlink` is clamped to 65535, the width of the guest field. The call keeps the `_wstat64` values when `GetFileInformationByHandle` fails -- [lstat of a Windows symbolic link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a link whose target is relative, as `symlink` creates them, with the link's own times, `st_dev` and `st_ino`, mode `S_IFLNK | 0777` and the length of the target `readlink` returns as `st_size`. FreeBSD gives a new link `0777` less the creating process's umask, which the host does not keep, so the permission bits are not the console's. A link with an absolute target is reported the same way, using its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction is reported the same way when the name left after that prefix is a drive path (`X:\...`): the print name when that name is a drive path, otherwise the substitute name. `open` with `O_NOFOLLOW` returns `EMLINK` for that junction and does not open the target +- [lstat of a Windows symbolic link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a link whose target is relative, as `symlink` creates them, with the link's own times, `st_dev` and `st_ino`, mode `S_IFLNK | 0777` and the length of the target `readlink` returns as `st_size`. FreeBSD gives a new link `0777` less the creating process's umask, which the host does not keep, so the permission bits are not the console's. A link with an absolute target is reported the same way, using its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction is treated as a link only when `ReadJunction` in [WindowsSymbolicLink.hpp](../../../core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp) accepts a drive-path target (`X:\...`) after stripping `\??\` / `\\?\`; volume mounts and other non-drive substitutes are not. Consumers of that drive-target path: [NativeStat.cpp](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (`lstat`/`readlink`), [Open.cpp](../../../core/libs/prx/libkernel/File/src/Open.cpp) (`O_NOFOLLOW` → `EMLINK`, and `unlink` removing the junction), [Stdio.cpp](../../../core/libs/prx/libkernel/File/src/Stdio.cpp) (`rmdir` → `ENOTDIR`), and [Export.cpp](../../../core/libs/prx/libkernel_sys/Export.cpp) (`unlinkat` with and without `AT_REMOVEDIR`). `unlink` / plain `unlinkat` leave the target, including when it is not empty - [lstat of a Windows reparse point that is not a link](../../../core/libs/prx/libkernel/File/src/NativeStat.cpp) (libkernel) reports a mounted volume (`\??\Volume{GUID}\`) or another name surrogate as the reparse point itself, not followed, with the mode of its attributes, as CPython's `os.lstat` does, and a reparse point without the name surrogate bit (deduplication, cloud files) as `stat` reports it - [setitimer](../../../core/libs/prx/libkernel/System/src/Signals.cpp) (libkernel) follows FreeBSD's `sys_setitimer` and `kern_setitimer`: `EINVAL` for timers other than `ITIMER_REAL`, `ITIMER_VIRTUAL` and `ITIMER_PROF`, and for a value, or the interval of a non-zero value, with a negative field, `tv_usec` of 1000000 or more, or more than `INT32_MAX / 2` seconds; a null value reads the timer as `getitimer` does, with `EFAULT` when the output is null too. `ITIMER_REAL` shares the timer thread of `alarm`, which clears its interval: a zero value disarms it, the previous value is returned, and each expiry re-arms it by `it_interval`. With `SIG_IGN` an expiry is dropped. With `SIGALRM` blocked it is left on the pending set of `sigtimedwait`, as `kern_psignal` does, where `sigwait`, `sigtimedwait` and `sigwaitinfo` take it; unblocking it runs a guest handler on the unblocking thread, and with the default disposition exits the process with status 142. Unblocked, the default disposition exits the process with status 142 and a handler throws, since guest `SIGALRM` handlers are never run from the timer thread. The value is not raised to `tick / 16` as `itimerfix` does, and an interval below 1 ms throws, since the console's `hz`, and so that rounding, is unknown (1 ms covers any `hz` of 63 or more). `ITIMER_VIRTUAL` and `ITIMER_PROF` throw when armed, as process CPU time is not accounted, and read and disarm as zero. PPSA99420 imports it and fails to load without it - On Linux, [direct memory](../../../core/libs/prx/libkernel/DirectMemory/DirectMemory.cpp) lives in one sparse memfd at file offset = physical address, so the [GPU imports](../../../core/libs/prx/libSceAgcDriver/Graphics/src/GuestBufferMemory.cpp) can share one udmabuf buffer object per 64 MiB of it. Released direct memory is not punched out of the file, because a live import of its chunk pins the pages and the GPU would keep reading the old ones: its pages stay committed (the process keeps the high-water mark of the direct memory it used) and are zeroed when the range is allocated again.