From 6ad392dad3b7cabf25d279c5c6d793d707d6fdcc Mon Sep 17 00:00:00 2001 From: Maz Date: Sat, 17 Oct 2026 16:03:12 +0400 Subject: [PATCH 1/5] fix(libkernel): report absolute Windows symbolic links lstat and readlink aborted on an absolute symbolic link because only a relative reparse point was accepted. Read the link text, and still refuse reparse points that are not symbolic links. --- .../File/include/WindowsSymbolicLink.hpp | 29 ++++++++++++++----- .../prx/libkernel/File/src/NativeStat.cpp | 4 +-- core/libs/prx/libkernel_sys/Export.cpp | 4 +-- core/libs/tests/GuestFilesystem.cpp | 20 ++----------- core/libs/tests/GuestKernelSys.cpp | 11 +++---- 5 files changed, 33 insertions(+), 35 deletions(-) diff --git a/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp b/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp index 849b88004e..4b49188e2e 100644 --- a/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp +++ b/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp @@ -26,8 +26,22 @@ class WindowsSymbolicLink { }; static constexpr ULONG relativeFlag = 1; + static std::wstring TextAt(const std::byte* data, DWORD size, USHORT offset, USHORT length, const char* operation) { + const std::size_t begin = sizeof(ReparseData) + offset; + if (length % sizeof(wchar_t) != 0 || begin + length > size) + throw std::runtime_error(std::string(operation) + ": malformed symbolic link reparse data"); + std::wstring name(length / sizeof(wchar_t), L'\0'); + if (!name.empty()) std::memcpy(name.data(), data + begin, name.size() * sizeof(wchar_t)); + return name; + } + + static std::wstring WithoutNtPrefix(std::wstring name) { + if (name.rfind(L"\\??\\", 0) == 0 || name.rfind(L"\\\\?\\", 0) == 0) name.erase(0, 4); + return name; + } + public: - static bool ReadRelative(const std::filesystem::path& native, std::string& target, const char* operation) { + static bool Read(const std::filesystem::path& native, std::string& target, const char* operation) { 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) @@ -42,12 +56,13 @@ public: ReparseData header{}; if (size < sizeof(header)) return false; std::memcpy(&header, data.data(), sizeof(header)); - if (header.tag != IO_REPARSE_TAG_SYMLINK || (header.flags & relativeFlag) == 0) return false; - const std::size_t begin = sizeof(header) + header.substituteNameOffset; - if (begin + header.substituteNameLength > size) - throw std::runtime_error(std::string(operation) + ": 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)); + if (header.tag != IO_REPARSE_TAG_SYMLINK) return false; + const bool relative = (header.flags & relativeFlag) != 0; + std::wstring name = relative + ? TextAt(data.data(), size, header.substituteNameOffset, header.substituteNameLength, operation) + : TextAt(data.data(), size, header.printNameOffset, header.printNameLength, operation); + if (!relative && name.empty()) + name = WithoutNtPrefix(TextAt(data.data(), size, header.substituteNameOffset, header.substituteNameLength, operation)); target = std::filesystem::path(name).generic_string(); return true; } diff --git a/core/libs/prx/libkernel/File/src/NativeStat.cpp b/core/libs/prx/libkernel/File/src/NativeStat.cpp index b110e980df..b433847495 100644 --- a/core/libs/prx/libkernel/File/src/NativeStat.cpp +++ b/core/libs/prx/libkernel/File/src/NativeStat.cpp @@ -51,8 +51,8 @@ static int DoLstat(const std::filesystem::path& p, NativeStat* st) { const auto attributes = GetFileAttributesW(p.c_str()); if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) return DoStat(p, st); std::string target; - if (!File::WindowsSymbolicLink::ReadRelative(p, target, "lstat")) - NotImplemented_nid_no_patch("lstat of a Windows reparse point other than a relative symbolic link"); + if (!File::WindowsSymbolicLink::Read(p, target, "lstat")) + NotImplemented_nid_no_patch("lstat of a Windows reparse point other than a symbolic link"); const auto handle = CreateFileW(p.c_str(), FILE_READ_ATTRIBUTES, 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) return File::WindowsFileTime::Failure(GetLastError()); diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 481ff4b823..a1643984d3 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -130,8 +130,8 @@ std::int64_t ReadNativeLink(const std::filesystem::path& native, char* buffer, s const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { std::string target; - if (!File::WindowsSymbolicLink::ReadRelative(native, target, "readlink")) { - NotImplemented_nid_no_patch("readlink of a Windows reparse point other than a relative symbolic link"); + if (!File::WindowsSymbolicLink::Read(native, target, "readlink")) { + NotImplemented_nid_no_patch("readlink of a Windows reparse point other than a symbolic link"); return -1; } return CopyLinkTarget(target, buffer, size); diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index b87cf1344c..e17c262c18 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -713,24 +713,10 @@ int main() { Require(stat_nid_postfix(fileLink.string().c_str(), &followed) == 0 && (followed.st_mode & 0170000) == 0100000); Require(followed.st_ino == status.st_ino && linkStatus.st_ino != status.st_ino && linkStatus.st_dev == status.st_dev); Require(stat_nid_postfix(directoryLink.string().c_str(), &followed) == 0 && (followed.st_mode & 0170000) == 0040000); - const auto checkUnsupportedLink = [](const std::filesystem::path& link) { - FileStat untouched; - std::memset(&untouched, 0x5a, sizeof(untouched)); - constexpr int sentinelError = 123; - *__error_nid_postfix() = sentinelError; - bool rejected = false; - try { - lstat_nid_postfix(link.string().c_str(), &untouched); - } catch (const std::runtime_error& error) { - rejected = std::strcmp(error.what(), "lstat of a Windows reparse point other than a relative symbolic link not implemented") == 0; - } - const bool errorUnchanged = *__error_nid_postfix() == sentinelError; - const auto* bytes = reinterpret_cast(&untouched); - const bool statusUnchanged = std::all_of(bytes, bytes + sizeof(untouched), [](unsigned char value) { return value == 0x5a; }); - return rejected && errorUnchanged && statusUnchanged; - }; Require(stat_nid_postfix(fileLink.string().c_str(), &status) == 0 && status.st_size == 5); - Require(checkUnsupportedLink(absoluteLink)); + const auto absoluteTarget = std::filesystem::absolute(present).generic_string(); + Require(checkRelativeLink(absoluteLink, static_cast(absoluteTarget.size()))); + Require(stat_nid_postfix(absoluteLink.string().c_str(), &followed) == 0 && followed.st_ino == status.st_ino); Require(std::filesystem::remove(fileLink) && std::filesystem::remove(directoryLink) && DeleteFileW(danglingLink.c_str()) != 0 && std::filesystem::remove(absoluteLink) && std::filesystem::remove(targetDirectory)); } diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index d16ff6789a..4d004d5378 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -287,15 +287,12 @@ int main() { 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"); + const auto absoluteTarget = std::filesystem::absolute(original).generic_string(); 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); + std::memset(buffer, '#', sizeof(buffer)); + Require(readlink_nid_postfix((root / "absolute").string().c_str(), buffer, sizeof(buffer)) == static_cast(absoluteTarget.size())); + Require(std::memcmp(buffer, absoluteTarget.data(), absoluteTarget.size()) == 0 && buffer[absoluteTarget.size()] == '#'); #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); From 6e331b38234aa8bb675a4347d0a9f6a50c39216e Mon Sep 17 00:00:00 2001 From: Maz Date: Sat, 17 Oct 2026 16:46:55 +0400 Subject: [PATCH 2/5] test(libkernel): compare absolute links by the file they name --- core/libs/tests/GuestFilesystem.cpp | 5 +++-- core/libs/tests/GuestKernelSys.cpp | 9 ++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index e17c262c18..a42206a49d 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -714,8 +714,9 @@ int main() { Require(followed.st_ino == status.st_ino && linkStatus.st_ino != status.st_ino && linkStatus.st_dev == status.st_dev); Require(stat_nid_postfix(directoryLink.string().c_str(), &followed) == 0 && (followed.st_mode & 0170000) == 0040000); Require(stat_nid_postfix(fileLink.string().c_str(), &status) == 0 && status.st_size == 5); - const auto absoluteTarget = std::filesystem::absolute(present).generic_string(); - Require(checkRelativeLink(absoluteLink, static_cast(absoluteTarget.size()))); + FileStat absoluteStatus{}; + Require(lstat_nid_postfix(absoluteLink.string().c_str(), &absoluteStatus) == 0); + Require((absoluteStatus.st_mode & 0170000) == 0120000 && absoluteStatus.st_size > 0 && absoluteStatus.st_nlink == 1); Require(stat_nid_postfix(absoluteLink.string().c_str(), &followed) == 0 && followed.st_ino == status.st_ino); Require(std::filesystem::remove(fileLink) && std::filesystem::remove(directoryLink) && DeleteFileW(danglingLink.c_str()) != 0 && std::filesystem::remove(absoluteLink) && std::filesystem::remove(targetDirectory)); diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 4d004d5378..89a102b08d 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -287,12 +287,15 @@ int main() { 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"); - const auto absoluteTarget = std::filesystem::absolute(original).generic_string(); Require(CreateSymbolicLinkW(absolute.wstring().c_str(), std::filesystem::absolute(original).wstring().c_str(), SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE) != 0); std::memset(buffer, '#', sizeof(buffer)); - Require(readlink_nid_postfix((root / "absolute").string().c_str(), buffer, sizeof(buffer)) == static_cast(absoluteTarget.size())); - Require(std::memcmp(buffer, absoluteTarget.data(), absoluteTarget.size()) == 0 && buffer[absoluteTarget.size()] == '#'); + const auto absoluteLength = readlink_nid_postfix((root / "absolute").string().c_str(), buffer, sizeof(buffer)); + Require(absoluteLength > 0 && static_cast(absoluteLength) < sizeof(buffer) && buffer[absoluteLength] == '#'); + const std::string absoluteTarget(buffer, static_cast(absoluteLength)); + Require(absoluteTarget.find('?') == std::string::npos); + std::error_code absoluteError; + Require(std::filesystem::equivalent(absoluteTarget, original, absoluteError) && !absoluteError); #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); From dd70ca83fed064298a39e2fb246846dd0bf3eeba Mon Sep 17 00:00:00 2001 From: Maz Date: Sat, 17 Oct 2026 17:06:37 +0400 Subject: [PATCH 3/5] test(libkernel): read absolute links into a buffer that fits --- core/libs/tests/GuestKernelSys.cpp | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index 89a102b08d..e9f439fafe 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -289,13 +289,18 @@ int main() { 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); - std::memset(buffer, '#', sizeof(buffer)); - const auto absoluteLength = readlink_nid_postfix((root / "absolute").string().c_str(), buffer, sizeof(buffer)); - Require(absoluteLength > 0 && static_cast(absoluteLength) < sizeof(buffer) && buffer[absoluteLength] == '#'); - const std::string absoluteTarget(buffer, static_cast(absoluteLength)); - Require(absoluteTarget.find('?') == std::string::npos); + char absoluteBuffer[512]; + std::memset(absoluteBuffer, '#', sizeof(absoluteBuffer)); + const auto absoluteLength = readlink_nid_postfix((root / "absolute").string().c_str(), absoluteBuffer, sizeof(absoluteBuffer)); + const bool absoluteFits = absoluteLength > 0 && static_cast(absoluteLength) < sizeof(absoluteBuffer) && + absoluteBuffer[absoluteLength] == '#'; + const std::string absoluteTarget = absoluteFits ? std::string(absoluteBuffer, static_cast(absoluteLength)) : std::string(); std::error_code absoluteError; - Require(std::filesystem::equivalent(absoluteTarget, original, absoluteError) && !absoluteError); + const bool absoluteMatches = absoluteFits && absoluteTarget.find('?') == std::string::npos && + std::filesystem::equivalent(absoluteTarget, original, absoluteError) && !absoluteError; + if (!absoluteMatches) + std::fprintf(stderr, "absolute link returned %lld bytes\n", static_cast(absoluteLength)); + Require(absoluteMatches); #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); From 01f3938fe1bd8041821de85fe92a1fc0830d3727 Mon Sep 17 00:00:00 2001 From: Maz Date: Sun, 18 Oct 2026 01:05:40 +0400 Subject: [PATCH 4/5] fix(libkernel): report Windows directory junctions from lstat and readlink --- .../File/include/WindowsSymbolicLink.hpp | 39 +++++++++++++++++++ .../prx/libkernel/File/src/NativeStat.cpp | 2 +- core/libs/prx/libkernel_sys/Export.cpp | 2 +- core/libs/tests/GuestFilesystem.cpp | 11 ++++++ core/libs/tests/GuestKernelSys.cpp | 14 +++++++ docs/dev/debt/libkernel.md | 2 +- docs/dev/debt/libkernel_sys.md | 2 +- 7 files changed, 68 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp b/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp index 849b88004e..d4a17f6abb 100644 --- a/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp +++ b/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp @@ -51,6 +51,45 @@ public: target = std::filesystem::path(name).generic_string(); return true; } + + static bool ReadJunction(const std::filesystem::path& native, std::string& target, const char* operation) { + 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(), std::string(operation) + ": 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(), std::string(operation) + ": FSCTL_GET_REPARSE_POINT"); + struct MountPoint { + ULONG tag; + USHORT dataLength; + USHORT reserved; + USHORT substituteNameOffset; + USHORT substituteNameLength; + USHORT printNameOffset; + USHORT printNameLength; + } header{}; + if (size < sizeof(header)) return false; + std::memcpy(&header, data.data(), sizeof(header)); + if (header.tag != IO_REPARSE_TAG_MOUNT_POINT) return false; + const auto textAt = [&](USHORT offset, USHORT length) { + const std::size_t begin = sizeof(header) + offset; + if (length % sizeof(wchar_t) != 0 || begin + length > size) + throw std::runtime_error(std::string(operation) + ": malformed junction reparse data"); + std::wstring name(length / sizeof(wchar_t), L'\0'); + if (!name.empty()) std::memcpy(name.data(), data.data() + begin, name.size() * sizeof(wchar_t)); + return name; + }; + std::wstring name = textAt(header.printNameOffset, header.printNameLength); + if (name.empty()) name = textAt(header.substituteNameOffset, header.substituteNameLength); + if (name.rfind(L"\\??\\", 0) == 0 || name.rfind(L"\\\\?\\", 0) == 0) name.erase(0, 4); + target = std::filesystem::path(name).generic_string(); + return true; + } }; } diff --git a/core/libs/prx/libkernel/File/src/NativeStat.cpp b/core/libs/prx/libkernel/File/src/NativeStat.cpp index b110e980df..a0314cb45c 100644 --- a/core/libs/prx/libkernel/File/src/NativeStat.cpp +++ b/core/libs/prx/libkernel/File/src/NativeStat.cpp @@ -51,7 +51,7 @@ static int DoLstat(const std::filesystem::path& p, NativeStat* st) { const auto attributes = GetFileAttributesW(p.c_str()); if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) return DoStat(p, st); std::string target; - if (!File::WindowsSymbolicLink::ReadRelative(p, target, "lstat")) + if (!File::WindowsSymbolicLink::ReadRelative(p, target, "lstat") && !File::WindowsSymbolicLink::ReadJunction(p, target, "lstat")) NotImplemented_nid_no_patch("lstat of a Windows reparse point other than a relative symbolic link"); const auto handle = CreateFileW(p.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, nullptr); diff --git a/core/libs/prx/libkernel_sys/Export.cpp b/core/libs/prx/libkernel_sys/Export.cpp index 481ff4b823..80688e21df 100644 --- a/core/libs/prx/libkernel_sys/Export.cpp +++ b/core/libs/prx/libkernel_sys/Export.cpp @@ -130,7 +130,7 @@ std::int64_t ReadNativeLink(const std::filesystem::path& native, char* buffer, s const DWORD attributes = GetFileAttributesW(native.wstring().c_str()); if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0) { std::string target; - if (!File::WindowsSymbolicLink::ReadRelative(native, target, "readlink")) { + if (!File::WindowsSymbolicLink::ReadRelative(native, target, "readlink") && !File::WindowsSymbolicLink::ReadJunction(native, target, "readlink")) { NotImplemented_nid_no_patch("readlink of a Windows reparse point other than a relative symbolic link"); return -1; } diff --git a/core/libs/tests/GuestFilesystem.cpp b/core/libs/tests/GuestFilesystem.cpp index 8147f4d743..b358b88503 100644 --- a/core/libs/tests/GuestFilesystem.cpp +++ b/core/libs/tests/GuestFilesystem.cpp @@ -735,6 +735,17 @@ int main() { Require(std::filesystem::remove(fileLink) && std::filesystem::remove(directoryLink) && DeleteFileW(danglingLink.c_str()) != 0 && std::filesystem::remove(absoluteLink) && std::filesystem::remove(targetDirectory)); } + const auto junctionTarget = root / "junction-target"; + const auto junction = root / "junction"; + Require(std::filesystem::create_directory(junctionTarget)); + const auto junctionCommand = L"cmd /c mklink /J \"" + junction.wstring() + L"\" \"" + junctionTarget.wstring() + L"\""; + Require(_wsystem(junctionCommand.c_str()) == 0); + FileStat junctionStatus{}; + Require(lstat_nid_postfix(junction.string().c_str(), &junctionStatus) == 0); + Require((junctionStatus.st_mode & 0170000) == 0120000 && junctionStatus.st_size > 0); + FileStat junctionFollowed{}; + Require(stat_nid_postfix(junction.string().c_str(), &junctionFollowed) == 0 && (junctionFollowed.st_mode & 0170000) == 0040000); + Require(std::filesystem::remove(junction) && std::filesystem::remove(junctionTarget)); #endif FileStat identity{}; FileStat byDescriptor{}; diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index d16ff6789a..d2084f6920 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -465,6 +465,20 @@ int main() { Require(fchmodat_nid_postfix(base, longName.c_str(), 0600, 0) == -1 && Error() == 63); #endif Require(sceKernelClose(base) == 0); +#ifdef _WIN32 + const auto junctionTarget = root / "junction-target"; + const auto junction = root / "junction"; + Require(std::filesystem::create_directory(junctionTarget)); + const auto junctionCommand = L"cmd /c mklink /J \"" + junction.wstring() + L"\" \"" + std::filesystem::absolute(junctionTarget).wstring() + L"\""; + Require(_wsystem(junctionCommand.c_str()) == 0); + char junctionBuffer[512]; + std::memset(junctionBuffer, '#', sizeof(junctionBuffer)); + const auto junctionLength = readlink_nid_postfix(junction.string().c_str(), junctionBuffer, sizeof(junctionBuffer)); + Require(junctionLength > 0 && static_cast(junctionLength) < sizeof(junctionBuffer) && junctionBuffer[junctionLength] == '#'); + const std::string junctionText(junctionBuffer, static_cast(junctionLength)); + std::error_code junctionError; + Require(junctionText.find('?') == std::string::npos && std::filesystem::equivalent(junctionText, junctionTarget, junctionError) && !junctionError); +#endif #ifndef _WIN32 if (created == 0) { const auto throughLink = (root / "through-link.bin").string(); diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index dd37013553..7089662ef2 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -116,7 +116,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`. Guest directory descriptors still resolve through their recorded path. Filesystems with coarser timestamp resolution cannot retain every guest microsecond - [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, a junction and any other reparse point throw, as for `readlink` +- [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 directory junction is reported the same way, using its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A link with an absolute target and any other reparse point throw, as for `readlink` - [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`. An expiry delivers nothing: with `SIG_IGN` it is dropped, with the default disposition the process exits with status 142, and with a handler, or with `SIGALRM` blocked, it throws, since guest `SIGALRM` handlers are never run and signals are never left pending. 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. - [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 diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index 4caf7940b7..ec4578c5e3 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -10,7 +10,7 @@ - [fchmodat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `chmod`; any other relative path is joined to the descriptor's host directory the same way as for `openat`, so guest path aliases and case-insensitive name matching do not apply to it and a `..` component is not kept inside the guest root. A flag other than `0` or `AT_SYMLINK_NOFOLLOW` (0x200) returns `EINVAL`; the mode is masked to `07777`. With `AT_SYMLINK_NOFOLLOW` a symbolic link returns success silently on Windows and Linux, since the host cannot change a link's own mode, while a regular file is changed either way. The mode maps to the host file mode, which on Windows is only the CRT's read-only bit. PPSA99621 calls it through `std::filesystem::permissions`. Console behaviour is unverified - [pathconf](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) looks the path up as FreeBSD 11's `kern_pathconf` does (links followed, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255) and returns the constants of FreeBSD 11's UFS `ufs_pathconf` for files and directories alike: `_PC_LINK_MAX` 32767, `_PC_NAME_MAX` 255, `_PC_PATH_MAX` and `_PC_SYMLINK_MAX` 1024, `_PC_PIPE_BUF` 512, `_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` 1, `_PC_FILESIZEBITS` 64, `_PC_REC_MAX_XFER_SIZE` -1, `_PC_PRIO_IO` and `_PC_SYNC_IO` 0. Whether the console's filesystems report the same is not verified. It throws for the names whose UFS value depends on the mount (`_PC_ALLOC_SIZE_MIN`, `_PC_REC_INCR_XFER_SIZE`, `_PC_REC_MIN_XFER_SIZE`, `_PC_MIN_HOLE_SIZE`), the kernel options (`_PC_ACL_EXTENDED`, `_PC_ACL_PATH_MAX`, `_PC_ACL_NFS4`, `_PC_MAC_PRESENT`) or the page size (`_PC_REC_XFER_ALIGN`), and on which UFS and `vop_stdpathconf` disagree (`_PC_MAX_CANON`, `_PC_MAX_INPUT`, `_PC_VDISABLE`, `_PC_ASYNC_IO`); other names fail with `EINVAL` - [mkdirat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `mkdir`. 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. On Windows, `mode` is ignored. 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 +- [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 throws, as a host path has no guest form. A directory junction returns its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. Any other reparse point throws too, where FreeBSD reports `EINVAL` for a file that is not a link - [readlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `readlink`. 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. Console behaviour is unverified - [unlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rmdir` (with `AT_REMOVEDIR`, 0x800) or `unlink`. 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. Removing a directory without `AT_REMOVEDIR` fails with `EPERM`, removing `.` or `..` with `AT_REMOVEDIR` fails with `EINVAL`, and flags other than `AT_REMOVEDIR` fail with `EINVAL`. Console behaviour is unverified - [renameat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rename`. Any other relative path is joined to its descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to relative descriptors, 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. Console behaviour is unverified From 7212dae71a26eb12c617137f5376fd5f2ac9d778 Mon Sep 17 00:00:00 2001 From: Maz Date: Sun, 18 Oct 2026 12:53:48 +0400 Subject: [PATCH 5/5] fix(libkernel): reject Windows volume mount points in junction reads A mounted folder uses the junction reparse tag and would otherwise be returned as a relative Volume GUID path. Only a drive path is reported, through the absolute-link text helpers. --- .../File/include/WindowsSymbolicLink.hpp | 33 ++++++++++--------- core/libs/tests/GuestKernelSys.cpp | 1 + docs/dev/debt/libkernel.md | 2 +- docs/dev/debt/libkernel_sys.md | 2 +- 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp b/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp index cfe60f586d..cdc089e731 100644 --- a/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp +++ b/core/libs/prx/libkernel/File/include/WindowsSymbolicLink.hpp @@ -26,10 +26,10 @@ class WindowsSymbolicLink { }; static constexpr ULONG relativeFlag = 1; - static std::wstring TextAt(const std::byte* data, DWORD size, USHORT offset, USHORT length, const char* operation) { - const std::size_t begin = sizeof(ReparseData) + offset; + static std::wstring TextAt(const std::byte* data, DWORD size, std::size_t origin, USHORT offset, USHORT length, const char* operation, const char* malformed) { + const std::size_t begin = origin + offset; if (length % sizeof(wchar_t) != 0 || begin + length > size) - throw std::runtime_error(std::string(operation) + ": malformed symbolic link reparse data"); + throw std::runtime_error(std::string(operation) + ": " + malformed); std::wstring name(length / sizeof(wchar_t), L'\0'); if (!name.empty()) std::memcpy(name.data(), data + begin, name.size() * sizeof(wchar_t)); return name; @@ -40,6 +40,12 @@ class WindowsSymbolicLink { return name; } + static bool IsDriveAbsolute(const std::wstring& name) { + if (name.size() < 3 || name[1] != L':' || (name[2] != L'\\' && name[2] != L'/')) return false; + const wchar_t letter = name[0]; + return (letter >= L'A' && letter <= L'Z') || (letter >= L'a' && letter <= L'z'); + } + public: static bool Read(const std::filesystem::path& native, std::string& target, const char* operation) { const HANDLE handle = CreateFileW(native.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, @@ -59,10 +65,10 @@ public: if (header.tag != IO_REPARSE_TAG_SYMLINK) return false; const bool relative = (header.flags & relativeFlag) != 0; std::wstring name = relative - ? TextAt(data.data(), size, header.substituteNameOffset, header.substituteNameLength, operation) - : TextAt(data.data(), size, header.printNameOffset, header.printNameLength, operation); + ? TextAt(data.data(), size, sizeof(ReparseData), header.substituteNameOffset, header.substituteNameLength, operation, "malformed symbolic link reparse data") + : TextAt(data.data(), size, sizeof(ReparseData), header.printNameOffset, header.printNameLength, operation, "malformed symbolic link reparse data"); if (!relative && name.empty()) - name = WithoutNtPrefix(TextAt(data.data(), size, header.substituteNameOffset, header.substituteNameLength, operation)); + name = WithoutNtPrefix(TextAt(data.data(), size, sizeof(ReparseData), header.substituteNameOffset, header.substituteNameLength, operation, "malformed symbolic link reparse data")); target = std::filesystem::path(name).generic_string(); return true; } @@ -91,17 +97,12 @@ public: if (size < sizeof(header)) return false; std::memcpy(&header, data.data(), sizeof(header)); if (header.tag != IO_REPARSE_TAG_MOUNT_POINT) return false; - const auto textAt = [&](USHORT offset, USHORT length) { - const std::size_t begin = sizeof(header) + offset; - if (length % sizeof(wchar_t) != 0 || begin + length > size) - throw std::runtime_error(std::string(operation) + ": malformed junction reparse data"); - std::wstring name(length / sizeof(wchar_t), L'\0'); - if (!name.empty()) std::memcpy(name.data(), data.data() + begin, name.size() * sizeof(wchar_t)); - return name; + const auto nameAt = [&](USHORT offset, USHORT length) { + return WithoutNtPrefix(TextAt(data.data(), size, sizeof(header), offset, length, operation, "malformed junction reparse data")); }; - std::wstring name = textAt(header.printNameOffset, header.printNameLength); - if (name.empty()) name = textAt(header.substituteNameOffset, header.substituteNameLength); - if (name.rfind(L"\\??\\", 0) == 0 || name.rfind(L"\\\\?\\", 0) == 0) name.erase(0, 4); + std::wstring name = nameAt(header.printNameOffset, header.printNameLength); + if (!IsDriveAbsolute(name)) name = nameAt(header.substituteNameOffset, header.substituteNameLength); + if (!IsDriveAbsolute(name)) return false; target = std::filesystem::path(name).generic_string(); return true; } diff --git a/core/libs/tests/GuestKernelSys.cpp b/core/libs/tests/GuestKernelSys.cpp index d005fbc525..9041bbc22a 100644 --- a/core/libs/tests/GuestKernelSys.cpp +++ b/core/libs/tests/GuestKernelSys.cpp @@ -483,6 +483,7 @@ int main() { const std::string junctionText(junctionBuffer, static_cast(junctionLength)); std::error_code junctionError; Require(junctionText.find('?') == std::string::npos && std::filesystem::equivalent(junctionText, junctionTarget, junctionError) && !junctionError); + Require(std::filesystem::remove(junction) && std::filesystem::remove(junctionTarget)); #endif #ifndef _WIN32 if (created == 0) { diff --git a/docs/dev/debt/libkernel.md b/docs/dev/debt/libkernel.md index 7089662ef2..c3989d4641 100644 --- a/docs/dev/debt/libkernel.md +++ b/docs/dev/debt/libkernel.md @@ -116,7 +116,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`. Guest directory descriptors still resolve through their recorded path. Filesystems with coarser timestamp resolution cannot retain every guest microsecond - [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 directory junction is reported the same way, using its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A link with an absolute target and any other reparse point throw, as for `readlink` +- [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. A mounted volume, whose substitute name is `\??\Volume{GUID}\`, and any other reparse point throw, as for `readlink` - [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`. An expiry delivers nothing: with `SIG_IGN` it is dropped, with the default disposition the process exits with status 142, and with a handler, or with `SIGALRM` blocked, it throws, since guest `SIGALRM` handlers are never run and signals are never left pending. 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. - [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 diff --git a/docs/dev/debt/libkernel_sys.md b/docs/dev/debt/libkernel_sys.md index ec4578c5e3..0995c0cf8c 100644 --- a/docs/dev/debt/libkernel_sys.md +++ b/docs/dev/debt/libkernel_sys.md @@ -10,7 +10,7 @@ - [fchmodat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `chmod`; any other relative path is joined to the descriptor's host directory the same way as for `openat`, so guest path aliases and case-insensitive name matching do not apply to it and a `..` component is not kept inside the guest root. A flag other than `0` or `AT_SYMLINK_NOFOLLOW` (0x200) returns `EINVAL`; the mode is masked to `07777`. With `AT_SYMLINK_NOFOLLOW` a symbolic link returns success silently on Windows and Linux, since the host cannot change a link's own mode, while a regular file is changed either way. The mode maps to the host file mode, which on Windows is only the CRT's read-only bit. PPSA99621 calls it through `std::filesystem::permissions`. Console behaviour is unverified - [pathconf](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) looks the path up as FreeBSD 11's `kern_pathconf` does (links followed, `ENAMETOOLONG` for a path of 1024 bytes or more or a component over 255) and returns the constants of FreeBSD 11's UFS `ufs_pathconf` for files and directories alike: `_PC_LINK_MAX` 32767, `_PC_NAME_MAX` 255, `_PC_PATH_MAX` and `_PC_SYMLINK_MAX` 1024, `_PC_PIPE_BUF` 512, `_PC_CHOWN_RESTRICTED` and `_PC_NO_TRUNC` 1, `_PC_FILESIZEBITS` 64, `_PC_REC_MAX_XFER_SIZE` -1, `_PC_PRIO_IO` and `_PC_SYNC_IO` 0. Whether the console's filesystems report the same is not verified. It throws for the names whose UFS value depends on the mount (`_PC_ALLOC_SIZE_MIN`, `_PC_REC_INCR_XFER_SIZE`, `_PC_REC_MIN_XFER_SIZE`, `_PC_MIN_HOLE_SIZE`), the kernel options (`_PC_ACL_EXTENDED`, `_PC_ACL_PATH_MAX`, `_PC_ACL_NFS4`, `_PC_MAC_PRESENT`) or the page size (`_PC_REC_XFER_ALIGN`), and on which UFS and `vop_stdpathconf` disagree (`_PC_MAX_CANON`, `_PC_MAX_INPUT`, `_PC_VDISABLE`, `_PC_ASYNC_IO`); other names fail with `EINVAL` - [mkdirat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `mkdir`. 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. On Windows, `mode` is ignored. 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 throws, as a host path has no guest form. A directory junction returns its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. Any other reparse point throws too, where FreeBSD reports `EINVAL` for a file that is not a link +- [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 returns its print name, or its substitute name when the print name is empty, without a `\??\` or `\\?\` prefix. A directory junction returns the same kind of name when it is a drive path (`X:\...`), using the print name when that is a drive path and the substitute name otherwise. A mounted volume, whose substitute name is `\??\Volume{GUID}\`, stays a throw, because the name left after the prefix is not a drive path. Any other reparse point throws too, where FreeBSD reports `EINVAL` for a file that is not a link - [readlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `readlink`. 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. Console behaviour is unverified - [unlinkat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rmdir` (with `AT_REMOVEDIR`, 0x800) or `unlink`. 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. Removing a directory without `AT_REMOVEDIR` fails with `EPERM`, removing `.` or `..` with `AT_REMOVEDIR` fails with `EINVAL`, and flags other than `AT_REMOVEDIR` fail with `EINVAL`. Console behaviour is unverified - [renameat](../../../core/libs/prx/libkernel_sys/Export.cpp) (libkernel_sys) passes `AT_FDCWD` (-100) and absolute paths to `rename`. Any other relative path is joined to its descriptor's host directory, found the same way as for `fchdir`, so guest path aliases and case-insensitive name matching do not apply to relative descriptors, 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. Console behaviour is unverified