From 3e96c132171f8d3ebc3a70d0f9311cbba27b8faf Mon Sep 17 00:00:00 2001 From: Gruzzberry Date: Wed, 21 Oct 2026 23:58:45 +0100 Subject: [PATCH] fix(libc): follow a Windows symbolic link in access access read the attributes and the security descriptor of the link itself on Windows, so a dangling link passed F_OK and R_OK and a link to a read-only file passed W_OK. A reparse point is now opened through its target and the target's path is checked, as FreeBSD 11.4's kern_accessat looks the path up with FOLLOW: a missing target fails with ENOENT and a link loop with ELOOP. Co-Authored-By: Claude Opus 5.5 --- core/libs/prx/libc/src/Filesystem.cpp | 34 +++++++++++++++++++--- core/libs/tests/GuestAccess.cpp | 41 +++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/core/libs/prx/libc/src/Filesystem.cpp b/core/libs/prx/libc/src/Filesystem.cpp index 510477eb7ae0..c2fbebd7d82d 100644 --- a/core/libs/prx/libc/src/Filesystem.cpp +++ b/core/libs/prx/libc/src/Filesystem.cpp @@ -11,6 +11,30 @@ #include #endif +#ifdef _WIN32 +static std::filesystem::path FollowedPath(const std::filesystem::path& path) { + const DWORD attributes = GetFileAttributesW(path.c_str()); + if (attributes == INVALID_FILE_ATTRIBUTES || (attributes & FILE_ATTRIBUTE_REPARSE_POINT) == 0) return path; + const HANDLE handle = CreateFileW(path.c_str(), 0, FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, nullptr, + OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, nullptr); + if (handle == INVALID_HANDLE_VALUE) return {}; + std::wstring target(MAX_PATH, L'\0'); + DWORD length = GetFinalPathNameByHandleW(handle, target.data(), static_cast(target.size()), FILE_NAME_NORMALIZED); + if (length >= target.size()) { + target.resize(length); + length = GetFinalPathNameByHandleW(handle, target.data(), length, FILE_NAME_NORMALIZED); + } + const DWORD error = GetLastError(); + CloseHandle(handle); + if (length == 0 || length >= target.size()) { + SetLastError(length == 0 ? error : ERROR_FILENAME_EXCED_RANGE); + return {}; + } + target.resize(length); + return target; +} +#endif + extern "C" int APS5_VABI access_nid_postfix(const char* path, int mode) { if (mode < 0 || (mode & ~7)) { errno = 22; return -1; } if (!path) { errno = 14; return -1; } @@ -18,9 +42,11 @@ extern "C" int APS5_VABI access_nid_postfix(const char* path, int mode) { try { const auto resolved = ResolvePath_nid_no_patch(path); #ifdef _WIN32 - const DWORD attributes = GetFileAttributesW(resolved.c_str()); + const auto target = FollowedPath(resolved); + const DWORD attributes = target.empty() ? INVALID_FILE_ATTRIBUTES : GetFileAttributesW(target.c_str()); if (attributes == INVALID_FILE_ATTRIBUTES) { - errno = FilesystemError(std::error_code(static_cast(GetLastError()), std::system_category())); + const DWORD failure = GetLastError(); + errno = failure == ERROR_CANT_RESOLVE_FILENAME ? 62 : FilesystemError(std::error_code(static_cast(failure), std::system_category())); if (errno == 2) errno = NotFoundError_nid_no_patch(resolved); return -1; } @@ -30,10 +56,10 @@ extern "C" int APS5_VABI access_nid_postfix(const char* path, int mode) { } DWORD size = 0; constexpr auto information = OWNER_SECURITY_INFORMATION | GROUP_SECURITY_INFORMATION | DACL_SECURITY_INFORMATION; - GetFileSecurityW(resolved.c_str(), information, nullptr, 0, &size); + GetFileSecurityW(target.c_str(), information, nullptr, 0, &size); if (!size) { errno = 13; return -1; } std::vector descriptor(size); - if (!GetFileSecurityW(resolved.c_str(), information, descriptor.data(), size, &size)) { + if (!GetFileSecurityW(target.c_str(), information, descriptor.data(), size, &size)) { errno = 13; return -1; } struct Token { diff --git a/core/libs/tests/GuestAccess.cpp b/core/libs/tests/GuestAccess.cpp index f4fa4fb8e8f1..b55facbffaf1 100644 --- a/core/libs/tests/GuestAccess.cpp +++ b/core/libs/tests/GuestAccess.cpp @@ -1,5 +1,6 @@ #include "prx/libc/include/General.hpp" #include +#include #include #include #ifdef _WIN32 @@ -76,6 +77,46 @@ int main() { Require(access_nid_postfix("sample.txt", 1) == -1 && *__error_nid_postfix() == 13); std::filesystem::permissions(file, std::filesystem::perms::owner_exec, std::filesystem::perm_options::add); Require(access_nid_postfix("sample.txt", 1) == 0); +#endif + const auto dangling = directory / "dangling.link"; + const auto pointing = directory / "sample.link"; + const auto loop = directory / "loop.link"; +#ifdef _WIN32 + const DWORD linkFlags = SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE; + const bool linked = CreateSymbolicLinkW(dangling.c_str(), L"missing.bin", linkFlags) && + CreateSymbolicLinkW(pointing.c_str(), L"sample.txt", linkFlags) && CreateSymbolicLinkW(loop.c_str(), L"loop.link", linkFlags); +#else + std::error_code linkError; + std::filesystem::create_symlink("missing.bin", dangling, linkError); + if (!linkError) std::filesystem::create_symlink("sample.txt", pointing, linkError); + if (!linkError) std::filesystem::create_symlink("loop.link", loop, linkError); + const bool linked = !linkError; +#endif + if (linked) { + Require(access_nid_postfix("dangling.link", 0) == -1 && *__error_nid_postfix() == 2); + Require(access_nid_postfix("dangling.link", 4) == -1 && *__error_nid_postfix() == 2); + Require(access_nid_postfix("loop.link", 0) == -1 && *__error_nid_postfix() == 62); + Require(access_nid_postfix("sample.link", 6) == 0); +#ifdef _WIN32 + Require(SetFileAttributesW(file.c_str(), FILE_ATTRIBUTE_READONLY)); + Require(access_nid_postfix("sample.link", 4) == 0); + Require(access_nid_postfix("sample.link", 2) == -1 && *__error_nid_postfix() == 13); + Require(SetFileAttributesW(file.c_str(), FILE_ATTRIBUTE_NORMAL)); + Require(SetFileAttributesW(pointing.c_str(), FILE_ATTRIBUTE_READONLY)); + Require(access_nid_postfix("sample.link", 2) == 0); + Require(SetFileAttributesW(pointing.c_str(), FILE_ATTRIBUTE_NORMAL)); +#endif + } else { + std::fprintf(stderr, "symbolic links unavailable: the access link cases not tested\n"); + } +#ifdef _WIN32 + DeleteFileW(dangling.c_str()); + DeleteFileW(pointing.c_str()); + DeleteFileW(loop.c_str()); +#else + std::filesystem::remove(dangling, linkError); + std::filesystem::remove(pointing, linkError); + std::filesystem::remove(loop, linkError); #endif Require(chdir_nid_postfix("/") == 0); std::filesystem::remove(file);