From 6ad392dad3b7cabf25d279c5c6d793d707d6fdcc Mon Sep 17 00:00:00 2001 From: Maz Date: Sat, 17 Oct 2026 16:03:12 +0400 Subject: [PATCH 1/3] 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/3] 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/3] 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);