[libcxx-commits] [libcxx] Properly implement std::fs::is_socket() on Windows (PR #213233)
Yuriy Chernyshov via libcxx-commits
libcxx-commits at lists.llvm.org
Fri Aug 7 01:20:15 PDT 2026
https://github.com/georgthegreat updated https://github.com/llvm/llvm-project/pull/213233
>From 789e0236c464c0506198cea6002ff3890b9985d3 Mon Sep 17 00:00:00 2001
From: Yuriy Chernyshov <thegeorg at yandex-team.com>
Date: Fri, 31 Jul 2026 12:25:41 +0300
Subject: [PATCH 1/3] Properly implement std::fs::is_socket() on Windows
---
libcxx/src/filesystem/posix_compat.h | 52 ++++++++++++++++---
.../fs.op.is_socket/is_socket.pass.cpp | 24 +++++++++
libcxx/test/support/filesystem_test_helper.h | 39 ++++++++++++++
3 files changed, 107 insertions(+), 8 deletions(-)
diff --git a/libcxx/src/filesystem/posix_compat.h b/libcxx/src/filesystem/posix_compat.h
index ddd99d8aaf206..d03580daddddc 100644
--- a/libcxx/src/filesystem/posix_compat.h
+++ b/libcxx/src/filesystem/posix_compat.h
@@ -75,6 +75,12 @@ struct LIBCPP_REPARSE_DATA_BUFFER {
} GenericReparseBuffer;
};
};
+
+// The reparse tag used for AF_UNIX (Unix domain) socket files isn't always
+// present in the Windows SDK headers, so define it ourselves if needed.
+# ifndef IO_REPARSE_TAG_AF_UNIX
+# define IO_REPARSE_TAG_AF_UNIX 0x80000023L
+# endif
#endif
_LIBCPP_BEGIN_NAMESPACE_FILESYSTEM
@@ -161,21 +167,35 @@ inline int stat_handle(HANDLE h, StatT* buf) {
} else {
buf->st_mode |= _S_IFREG;
}
+ bool is_af_unix_socket = false;
if (basic.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {
FILE_ATTRIBUTE_TAG_INFO tag;
if (!GetFileInformationByHandleEx(h, FileAttributeTagInfo, &tag, sizeof(tag)))
return -1;
- if (tag.ReparseTag == IO_REPARSE_TAG_SYMLINK)
+ if (tag.ReparseTag == IO_REPARSE_TAG_SYMLINK) {
buf->st_mode = (buf->st_mode & ~_S_IFMT) | _S_IFLNK;
+ } else if (tag.ReparseTag == IO_REPARSE_TAG_AF_UNIX) {
+ buf->st_mode = (buf->st_mode & ~_S_IFMT) | _S_IFSOCK;
+ is_af_unix_socket = true;
+ }
}
FILE_STANDARD_INFO standard;
- if (!GetFileInformationByHandleEx(h, FileStandardInfo, &standard, sizeof(standard)))
+ if (!GetFileInformationByHandleEx(h, FileStandardInfo, &standard, sizeof(standard))) {
+ // AF_UNIX (Unix domain) socket files don't support all of the information
+ // queries below; report what we already have (mode/timestamps) instead of
+ // failing outright, matching the behavior of the system stat()/os.stat().
+ if (is_af_unix_socket)
+ return 0;
return -1;
+ }
buf->st_nlink = standard.NumberOfLinks;
buf->st_size = standard.EndOfFile.QuadPart;
BY_HANDLE_FILE_INFORMATION info;
- if (!GetFileInformationByHandle(h, &info))
+ if (!GetFileInformationByHandle(h, &info)) {
+ if (is_af_unix_socket)
+ return 0;
return -1;
+ }
buf->st_dev = info.dwVolumeSerialNumber;
memcpy(&buf->st_ino.id[0], &info.nFileIndexHigh, 4);
memcpy(&buf->st_ino.id[4], &info.nFileIndexLow, 4);
@@ -183,11 +203,27 @@ inline int stat_handle(HANDLE h, StatT* buf) {
}
inline int stat_file(const wchar_t* path, StatT* buf, DWORD flags) {
- WinHandle h(path, FILE_READ_ATTRIBUTES, flags);
- if (!h)
- return -1;
- int ret = stat_handle(h, buf);
- return ret;
+ {
+ WinHandle h(path, FILE_READ_ATTRIBUTES, flags);
+ if (h && stat_handle(h, buf) == 0)
+ return 0;
+ }
+ // Some reparse points, such as AF_UNIX (Unix domain) socket files, can't be
+ // opened/queried by following the reparse point (there is no target to
+ // follow). CreateFileW may fail with e.g. ERROR_CANT_ACCESS_FILE, or the
+ // subsequent GetFileInformationByHandle* calls may fail. In that case, fall
+ // back to opening the reparse point itself so that we can still report its
+ // attributes and type. This matches the behavior of the system
+ // stat()/os.stat() on such files.
+ if (!(flags & FILE_FLAG_OPEN_REPARSE_POINT)) {
+ DWORD attributes = GetFileAttributesW(path);
+ if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT)) {
+ WinHandle hr(path, FILE_READ_ATTRIBUTES, flags | FILE_FLAG_OPEN_REPARSE_POINT);
+ if (hr)
+ return stat_handle(hr, buf);
+ }
+ }
+ return -1;
}
inline int stat(const wchar_t* path, StatT* buf) { return stat_file(path, buf, 0); }
diff --git a/libcxx/test/std/input.output/filesystems/fs.op.funcs/fs.op.is_socket/is_socket.pass.cpp b/libcxx/test/std/input.output/filesystems/fs.op.funcs/fs.op.is_socket/is_socket.pass.cpp
index 382685c21dd99..18e5a19675cda 100644
--- a/libcxx/test/std/input.output/filesystems/fs.op.funcs/fs.op.is_socket/is_socket.pass.cpp
+++ b/libcxx/test/std/input.output/filesystems/fs.op.funcs/fs.op.is_socket/is_socket.pass.cpp
@@ -67,6 +67,29 @@ static void test_exist_not_found()
assert(is_socket(p) == false);
}
+static void test_is_socket_for_real_socket()
+{
+ // Some platforms don't support creating socket files.
+#if !defined(__FreeBSD__) && !defined(__APPLE__)
+ scoped_test_env env;
+ const path sock = env.create_socket("socket");
+
+ // A bound AF_UNIX socket file must be reported as a socket, without error.
+ // On Windows this is a regression test: the socket file is a reparse point
+ // that cannot be opened by following it, which previously made status()
+ // throw filesystem_error instead of reporting file_type::socket.
+ std::error_code ec = GetTestEC();
+ assert(is_socket(sock, ec) == true);
+ assert(!ec);
+
+ assert(is_socket(sock) == true);
+
+ assert(is_regular_file(sock) == false);
+ assert(is_directory(sock) == false);
+ assert(exists(sock) == true);
+#endif
+}
+
static void test_is_socket_fails()
{
scoped_test_env env;
@@ -94,6 +117,7 @@ int main(int, char**) {
signature_test();
is_socket_status_test();
test_exist_not_found();
+ test_is_socket_for_real_socket();
test_is_socket_fails();
return 0;
diff --git a/libcxx/test/support/filesystem_test_helper.h b/libcxx/test/support/filesystem_test_helper.h
index 2ad9efb32c60f..45fdafeaf4b2f 100644
--- a/libcxx/test/support/filesystem_test_helper.h
+++ b/libcxx/test/support/filesystem_test_helper.h
@@ -10,7 +10,14 @@
#else
#include <io.h>
#include <direct.h>
+// winsock2.h must be included before windows.h to avoid clashing with the
+// older winsock.h that windows.h would otherwise pull in.
+#include <winsock2.h> // for AF_UNIX sockets
+#include <afunix.h>
#include <windows.h> // for CreateSymbolicLink, CreateHardLink
+#if defined(_MSC_VER)
+#pragma comment(lib, "ws2_32")
+#endif
#endif
#include <cassert>
@@ -34,6 +41,8 @@
# include <sys/socket.h>
# include <sys/un.h>
#endif
+// On Windows the AF_UNIX socket headers (winsock2.h/afunix.h) are included
+// above, before windows.h.
namespace fs = std::filesystem;
namespace utils {
@@ -341,6 +350,36 @@ struct scoped_test_env
assert(::bind(fd, reinterpret_cast<::sockaddr*>(&address), sizeof(address)) == 0);
return file;
}
+#elif defined(_WIN32)
+ // Windows supports AF_UNIX (Unix domain) sockets, which materialize as a
+ // reparse-point file on disk. Winsock must be initialized before use; this
+ // is done once per process by WinsockInit below.
+ std::string create_socket(std::string file) {
+ static const int wsaInit = WinsockInit();
+ (void)wsaInit;
+
+ file = sanitize_path(std::move(file));
+
+ ::sockaddr_un address;
+ address.sun_family = AF_UNIX;
+ assert(file.size() < sizeof(address.sun_path));
+ std::memcpy(address.sun_path, file.c_str(), file.size() + 1);
+
+ SOCKET fd = ::socket(AF_UNIX, SOCK_STREAM, 0);
+ assert(fd != INVALID_SOCKET);
+ assert(::bind(fd, reinterpret_cast<::sockaddr*>(&address), sizeof(address)) == 0);
+ // Intentionally leave the socket open: the file exists on disk as long as
+ // the socket lives, which is sufficient for the duration of the test.
+ return file;
+ }
+
+private:
+ static int WinsockInit() {
+ WSADATA wsaData;
+ return ::WSAStartup(MAKEWORD(2, 2), &wsaData);
+ }
+
+public:
#endif
fs::path test_root;
>From 6a80927139bb3de2ae2756ff66308f408b52a6dd Mon Sep 17 00:00:00 2001
From: Yuriy Chernyshov <thegeorg at yandex-team.com>
Date: Fri, 31 Jul 2026 13:21:31 +0300
Subject: [PATCH 2/3] Deslop
---
libcxx/src/filesystem/posix_compat.h | 17 +++++++++++------
1 file changed, 11 insertions(+), 6 deletions(-)
diff --git a/libcxx/src/filesystem/posix_compat.h b/libcxx/src/filesystem/posix_compat.h
index d03580daddddc..f9b4516d04af0 100644
--- a/libcxx/src/filesystem/posix_compat.h
+++ b/libcxx/src/filesystem/posix_compat.h
@@ -215,13 +215,18 @@ inline int stat_file(const wchar_t* path, StatT* buf, DWORD flags) {
// back to opening the reparse point itself so that we can still report its
// attributes and type. This matches the behavior of the system
// stat()/os.stat() on such files.
+ //
+ // We only need this fallback when following was requested (otherwise the open
+ // above already used FILE_FLAG_OPEN_REPARSE_POINT). We can't tell a symlink
+ // (which must be followed) apart from a non-followable reparse point without
+ // the reparse tag, and GetFileAttributesW does not report it, so simply retry
+ // with the reparse-point flag; for a regular file/symlink whose target exists
+ // the first attempt already succeeded, so this second open only happens for
+ // paths the follow-open couldn't handle.
if (!(flags & FILE_FLAG_OPEN_REPARSE_POINT)) {
- DWORD attributes = GetFileAttributesW(path);
- if (attributes != INVALID_FILE_ATTRIBUTES && (attributes & FILE_ATTRIBUTE_REPARSE_POINT)) {
- WinHandle hr(path, FILE_READ_ATTRIBUTES, flags | FILE_FLAG_OPEN_REPARSE_POINT);
- if (hr)
- return stat_handle(hr, buf);
- }
+ WinHandle hr(path, FILE_READ_ATTRIBUTES, flags | FILE_FLAG_OPEN_REPARSE_POINT);
+ if (hr)
+ return stat_handle(hr, buf);
}
return -1;
}
>From 5af10923249a41cf8de2622a5cdfa56266c09a1c Mon Sep 17 00:00:00 2001
From: Yuriy Chernyshov <thegeorg at yandex-team.com>
Date: Fri, 31 Jul 2026 18:58:11 +0300
Subject: [PATCH 3/3] Better layout
---
libcxx/src/filesystem/posix_compat.h | 76 +++++++++++++---------------
1 file changed, 35 insertions(+), 41 deletions(-)
diff --git a/libcxx/src/filesystem/posix_compat.h b/libcxx/src/filesystem/posix_compat.h
index f9b4516d04af0..e920668cba814 100644
--- a/libcxx/src/filesystem/posix_compat.h
+++ b/libcxx/src/filesystem/posix_compat.h
@@ -167,7 +167,6 @@ inline int stat_handle(HANDLE h, StatT* buf) {
} else {
buf->st_mode |= _S_IFREG;
}
- bool is_af_unix_socket = false;
if (basic.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) {
FILE_ATTRIBUTE_TAG_INFO tag;
if (!GetFileInformationByHandleEx(h, FileAttributeTagInfo, &tag, sizeof(tag)))
@@ -175,65 +174,60 @@ inline int stat_handle(HANDLE h, StatT* buf) {
if (tag.ReparseTag == IO_REPARSE_TAG_SYMLINK) {
buf->st_mode = (buf->st_mode & ~_S_IFMT) | _S_IFLNK;
} else if (tag.ReparseTag == IO_REPARSE_TAG_AF_UNIX) {
- buf->st_mode = (buf->st_mode & ~_S_IFMT) | _S_IFSOCK;
- is_af_unix_socket = true;
+ buf->st_mode = (buf->st_mode & ~_S_IFMT) | _S_IFSOCK;
+ // AF_UNIX (Unix domain) socket files don't support the information
+ // queries below; report what we already have (mode/timestamps) instead
+ // of failing outright, matching the behavior of the system
+ // stat()/os.stat().
+ return 0;
}
}
FILE_STANDARD_INFO standard;
- if (!GetFileInformationByHandleEx(h, FileStandardInfo, &standard, sizeof(standard))) {
- // AF_UNIX (Unix domain) socket files don't support all of the information
- // queries below; report what we already have (mode/timestamps) instead of
- // failing outright, matching the behavior of the system stat()/os.stat().
- if (is_af_unix_socket)
- return 0;
+ if (!GetFileInformationByHandleEx(h, FileStandardInfo, &standard, sizeof(standard)))
return -1;
- }
buf->st_nlink = standard.NumberOfLinks;
buf->st_size = standard.EndOfFile.QuadPart;
BY_HANDLE_FILE_INFORMATION info;
- if (!GetFileInformationByHandle(h, &info)) {
- if (is_af_unix_socket)
- return 0;
+ if (!GetFileInformationByHandle(h, &info))
return -1;
- }
buf->st_dev = info.dwVolumeSerialNumber;
memcpy(&buf->st_ino.id[0], &info.nFileIndexHigh, 4);
memcpy(&buf->st_ino.id[4], &info.nFileIndexLow, 4);
return 0;
}
-inline int stat_file(const wchar_t* path, StatT* buf, DWORD flags) {
- {
- WinHandle h(path, FILE_READ_ATTRIBUTES, flags);
- if (h && stat_handle(h, buf) == 0)
- return 0;
- }
- // Some reparse points, such as AF_UNIX (Unix domain) socket files, can't be
- // opened/queried by following the reparse point (there is no target to
- // follow). CreateFileW may fail with e.g. ERROR_CANT_ACCESS_FILE, or the
- // subsequent GetFileInformationByHandle* calls may fail. In that case, fall
- // back to opening the reparse point itself so that we can still report its
- // attributes and type. This matches the behavior of the system
- // stat()/os.stat() on such files.
+inline int stat_file(const wchar_t* path, StatT* buf, bool follow_symlinks) {
+ // Win32 has no open mode that means "follow symlinks, but open any other
+ // reparse point (e.g. an AF_UNIX socket) as itself": omitting
+ // FILE_FLAG_OPEN_REPARSE_POINT follows *every* reparse point, which fails for
+ // ones that have no target to follow, while passing it opens *every* reparse
+ // point as itself, including symlinks we do want to follow.
//
- // We only need this fallback when following was requested (otherwise the open
- // above already used FILE_FLAG_OPEN_REPARSE_POINT). We can't tell a symlink
- // (which must be followed) apart from a non-followable reparse point without
- // the reparse tag, and GetFileAttributesW does not report it, so simply retry
- // with the reparse-point flag; for a regular file/symlink whose target exists
- // the first attempt already succeeded, so this second open only happens for
- // paths the follow-open couldn't handle.
- if (!(flags & FILE_FLAG_OPEN_REPARSE_POINT)) {
- WinHandle hr(path, FILE_READ_ATTRIBUTES, flags | FILE_FLAG_OPEN_REPARSE_POINT);
- if (hr)
- return stat_handle(hr, buf);
+ // So we always open the reparse point itself first. This succeeds for any
+ // existing object regardless of its reparse tag, and lets stat_handle
+ // determine the type from that tag. We then emulate symlink following
+ // ourselves below, only for the objects that actually are symlinks.
+ WinHandle h(path, FILE_READ_ATTRIBUTES, FILE_FLAG_OPEN_REPARSE_POINT);
+ if (!h || stat_handle(h, buf) != 0)
+ return -1;
+ // For lstat, or for anything that isn't a symlink, we're already done. Only a
+ // genuine symlink needs to be resolved to its target for stat; do so by
+ // reopening the path without FILE_FLAG_OPEN_REPARSE_POINT so the OS follows
+ // it, and stat'ing the target handle. A failure here (dangling/cyclic/
+ // inaccessible target) is a real error, matching the behavior of the system
+ // stat()/os.stat().
+ if (follow_symlinks && S_ISLNK(buf->st_mode)) {
+ WinHandle ht(path, FILE_READ_ATTRIBUTES, 0);
+ if (!ht)
+ return -1;
+ return stat_handle(ht, buf);
}
- return -1;
+ return 0;
}
-inline int stat(const wchar_t* path, StatT* buf) { return stat_file(path, buf, 0); }
+inline int stat(const wchar_t* path, StatT* buf) { return stat_file(path, buf, /*follow_symlinks=*/true); }
-inline int lstat(const wchar_t* path, StatT* buf) { return stat_file(path, buf, FILE_FLAG_OPEN_REPARSE_POINT); }
+inline int lstat(const wchar_t* path, StatT* buf) { return stat_file(path, buf, /*follow_symlinks=*/false); }
inline int fstat(int fd, StatT* buf) {
HANDLE h = reinterpret_cast<HANDLE>(_get_osfhandle(fd));
More information about the libcxx-commits
mailing list