[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