From c6be0e95ccec3794233210439fad18e5f285d0b3 Mon Sep 17 00:00:00 2001 From: codingreapermaps <233954859+codingreapermaps@users.noreply.github.com> Date: Mon, 12 Oct 2026 02:17:00 +0100 Subject: [PATCH] fix(libkernel_sys): read relative symbolic links on Windows readlink threw for every Windows reparse point, including the links symlink creates. Read the target of a relative symbolic link from its reparse data and return it with forward slashes. Absolute targets, junctions and other reparse points still throw. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libkernel_sys/Export.cpp | 58 +++++++++++++++++++++++--- core/libs/tests/GuestKernelSys.cpp | 31 ++++++++++++++ docs/dev/TechnicalDebt.md | 1 + 3 files changed, 84 insertions(+), 6 deletions(-) diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index ffe9d557d..a50b7c0bb 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -1,4 +1,5 @@ #include +#include #include #include #include @@ -63,6 +64,50 @@ int GuestError(const std::error_code& error) { const int value = condition.value(); return value > 0 && value <= 34 ? value : GuestEio; } + +std::int64_t CopyLinkTarget(const std::string& target, char* buffer, std::size_t size) { + const auto count = std::min(size, target.size()); + if (count != 0 && buffer == nullptr) return Fail(GuestEfault); + if (count != 0) std::memcpy(buffer, target.data(), count); + return static_cast(count); +} + +#ifdef _WIN32 +struct SymbolicLinkReparseData { + ULONG tag; + USHORT dataLength; + USHORT reserved; + USHORT substituteNameOffset; + USHORT substituteNameLength; + USHORT printNameOffset; + USHORT printNameLength; + ULONG flags; +}; +constexpr ULONG SymbolicLinkRelative = 1; + +bool ReadRelativeSymbolicLink(const std::filesystem::path& native, std::string& target) { + const HANDLE handle = CreateFileW(native.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, + OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) + throw std::system_error(static_cast(GetLastError()), std::system_category(), "readlink: CreateFileW"); + std::array data{}; + DWORD size = 0; + const BOOL read = DeviceIoControl(handle, FSCTL_GET_REPARSE_POINT, nullptr, 0, data.data(), static_cast(data.size()), &size, nullptr); + const DWORD code = GetLastError(); + CloseHandle(handle); + if (!read) throw std::system_error(static_cast(code), std::system_category(), "readlink: FSCTL_GET_REPARSE_POINT"); + SymbolicLinkReparseData header{}; + if (size < sizeof(header)) return false; + std::memcpy(&header, data.data(), sizeof(header)); + if (header.tag != IO_REPARSE_TAG_SYMLINK || (header.flags & SymbolicLinkRelative) == 0) return false; + const std::size_t begin = sizeof(header) + header.substituteNameOffset; + if (begin + header.substituteNameLength > size) throw std::runtime_error("readlink: malformed symbolic link reparse data"); + std::wstring name(header.substituteNameLength / sizeof(wchar_t), L'\0'); + std::memcpy(name.data(), data.data() + begin, name.size() * sizeof(wchar_t)); + target = std::filesystem::path(name).generic_string(); + return true; +} +#endif } extern "C" { @@ -95,8 +140,12 @@ std::int64_t APS5_VABI readlink_nid_postfix(const char* path, char* buffer, std: #ifdef _WIN32 const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { - NotImplemented_nid_no_patch("readlink of a Windows symbolic link"); - return -1; + std::string target; + if (!ReadRelativeSymbolicLink(native, target)) { + NotImplemented_nid_no_patch("readlink of a Windows reparse point other than a relative symbolic link"); + return -1; + } + return CopyLinkTarget(target, buffer, size); } #endif std::error_code error; @@ -106,10 +155,7 @@ std::int64_t APS5_VABI readlink_nid_postfix(const char* path, char* buffer, std: if (!std::filesystem::is_symlink(status)) return Fail(GuestEinval); const auto target = std::filesystem::read_symlink(native, error).generic_string(); if (error) return Fail(GuestError(error)); - const auto count = std::min(size, target.size()); - if (count != 0 && buffer == nullptr) return Fail(GuestEfault); - if (count != 0) std::memcpy(buffer, target.data(), count); - return static_cast(count); + return CopyLinkTarget(target, buffer, size); } int APS5_VABI link_nid_postfix(const char* existing, const char* created) { diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 8803d085c..8483b1be8 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -8,9 +8,13 @@ #include #include #include +#include #include #ifdef _WIN32 #include +#ifndef SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE +#define SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE 0x2 +#endif #endif #ifdef __linux__ #include @@ -161,6 +165,33 @@ int main() { Require(symlink_nid_postfix("missing.bin", dangling.c_str()) == 0); Require((GetFileAttributesW(std::filesystem::path(dangling).wstring().c_str()) & FILE_ATTRIBUTE_REPARSE_POINT) != 0); Require(symlink_nid_postfix("missing.bin", dangling.c_str()) == -1 && Error() == 17); + Require(readlink_nid_postfix(symbolic.c_str(), buffer, sizeof(buffer)) == 12 && std::memcmp(buffer, "original.bin", 12) == 0); + Require(readlink_nid_postfix(dangling.c_str(), buffer, sizeof(buffer)) == 11 && std::memcmp(buffer, "missing.bin", 11) == 0); + const auto nested = (root / "nested").string(); + Require(symlink_nid_postfix("sub/target.txt", nested.c_str()) == 0); + std::memset(buffer, '#', sizeof(buffer)); + Require(readlink_nid_postfix(nested.c_str(), buffer, sizeof(buffer)) == 14); + Require(std::memcmp(buffer, "sub/target.txt", 14) == 0 && buffer[14] == '#'); + Require(readlink_nid_postfix(nested.c_str(), buffer, 3) == 3 && std::memcmp(buffer, "sub", 3) == 0); + Require(readlink_nid_postfix(nested.c_str(), nullptr, 0) == 0); + Require(readlink_nid_postfix(nested.c_str(), nullptr, sizeof(buffer)) == -1 && Error() == 14); + const auto accented = (root / "accented").string(); + Require(symlink_nid_postfix("a\xc3\xa7\xc3\xa3o.bin", accented.c_str()) == 0); + Require(readlink_nid_postfix(accented.c_str(), buffer, sizeof(buffer)) == 10 && std::memcmp(buffer, "a\xc3\xa7\xc3\xa3o.bin", 10) == 0); + Require(std::filesystem::create_directory(root / "sub")); + const auto directoryLink = (root / "directory-link").string(); + Require(symlink_nid_postfix("sub", directoryLink.c_str()) == 0); + Require(readlink_nid_postfix(directoryLink.c_str(), buffer, sizeof(buffer)) == 3 && std::memcmp(buffer, "sub", 3) == 0); + const auto absolute = std::filesystem::absolute(root / "absolute"); + Require(CreateSymbolicLinkW(absolute.wstring().c_str(), std::filesystem::absolute(original).wstring().c_str(), + SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); + bool refused = false; + try { + readlink_nid_postfix((root / "absolute").string().c_str(), buffer, sizeof(buffer)); + } catch (const std::runtime_error& failure) { + refused = std::string(failure.what()) == "readlink of a Windows reparse point other than a relative symbolic link not implemented"; + } + Require(refused); #else Require(std::filesystem::is_symlink(std::filesystem::symlink_status(symbolic))); Require(readlink_nid_postfix(symbolic.c_str(), buffer, sizeof(buffer)) == 12 && std::memcmp(buffer, "original.bin", 12) == 0); diff --git a/docs/dev/TechnicalDebt.md b/docs/dev/TechnicalDebt.md index 51f08cc9e..83d9930e9 100644 --- a/docs/dev/TechnicalDebt.md +++ b/docs/dev/TechnicalDebt.md @@ -390,6 +390,7 @@ Throughout the project, every function at every stage either **does exactly what - [Windows file timestamps](../../core/libs/prx/libkernel/File/include/WindowsFileTime.hpp) use the host filesystem's timestamp range and resolution. `utimes` and `futimes` 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`. Guest directory descriptors still resolve through their recorded path. Filesystems with coarser timestamp resolution cannot retain every guest microsecond - [fchdir](../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) changes the guest working directory to the descriptor's directory: on Windows through the path recorded when the descriptor was opened, on Linux through the `/proc/self/fd` link target. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. FreeBSD `change_dir`'s search-permission check (`EACCES`) is not modelled, and a directory renamed after it was opened is still reached through the recorded path. Console behaviour is unverified - [openat](../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `open`. Any other relative path is joined to the descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to it, and a `..` component is not kept inside the guest root. It returns `EBADF` for a descriptor that is not open and `ENOTDIR` for one that is not a directory. As with `open`, `O_NOFOLLOW` and `O_NONBLOCK` are ignored. PPSA99003 opens directory entries this way. Console behaviour is unverified +- [readlink](../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) on Windows reads a symbolic link's target from its reparse data and returns it with `/` separators, only when the target is relative, as `symlink` creates them. A symbolic link with an absolute target and a junction throw, as a host path has no guest form. Any other reparse point throws too, where FreeBSD reports `EINVAL` for a file that is not a link - [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 - 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. - [Per-face depth bias](../../core/libs/prx/libSceAgcDriver/Graphics/src/Draw.cpp) (libSceAgcDriver): Vulkan has one depth bias per draw, so a draw that rasterizes both faces with different front and back bias (PA_SU_SC_MODE_CNTL, PA_SU_POLY_OFFSET_FRONT/BACK_*) is recorded twice, front faces with the back faces culled and then back faces with the front faces culled. The vertex stages run twice, so a vertex shader that writes memory writes twice, and all front faces land before all back faces instead of in primitive order, which differs only for blending or stencil updates that depend on the order.