[libcxx-commits] [libcxx] Properly implement std::fs::is_socket() on Windows (PR #213233)
via libcxx-commits
libcxx-commits at lists.llvm.org
Fri Jul 31 02:28:51 PDT 2026
llvmorg-github-actions[bot] wrote:
<!--LLVM PR SUMMARY COMMENT-->
@llvm/pr-subscribers-libcxx
Author: Yuriy Chernyshov (georgthegreat)
<details>
<summary>Changes</summary>
On Windows, `std::filesystem::is_socket()` (and any following-`status()` query such as `exists()`) throws `filesystem_error` when called on an AF_UNIX (Unix domain) socket file:
```
filesystem error: in posix_stat: failed to determine attributes for the
specified path: The file cannot be accessed by the system. ["daemon.socket"]
```
### Root cause
An AF_UNIX socket file is a reparse point (`IO_REPARSE_TAG_AF_UNIX`). The non-symlink-following `stat()` path opens it via `CreateFileW` *without* `FILE_FLAG_OPEN_REPARSE_POINT`, which tries to follow the reparse point. Since a socket has no target to follow, `CreateFileW`/`GetFileInformationByHandle*` fail with `ERROR_CANT_ACCESS_FILE`, and `create_file_status` reports a hard error instead of `file_type::socket`.
For comparison, CPython's `os.stat()` handles this correctly by opening with `FILE_FLAG_OPEN_REPARSE_POINT` and mapping `IO_REPARSE_TAG_AF_UNIX` to `S_IFSOCK`.
### Fix (`libcxx/src/filesystem/posix_compat.h`)
- Recognize the `IO_REPARSE_TAG_AF_UNIX` reparse tag as `_S_IFSOCK` in `stat_handle`, and tolerate the follow-up `FileStandardInfo`/`GetFileInformationByHandle` queries failing for such sockets.
- In `stat_file`, when the follow open (or stat) fails on a reparse point, retry with `FILE_FLAG_OPEN_REPARSE_POINT` so the socket file itself can be stat'd — matching `os.stat()`.
---
Full diff: https://github.com/llvm/llvm-project/pull/213233.diff
3 Files Affected:
- (modified) libcxx/src/filesystem/posix_compat.h (+44-8)
- (modified) libcxx/test/std/input.output/filesystems/fs.op.funcs/fs.op.is_socket/is_socket.pass.cpp (+24)
- (modified) libcxx/test/support/filesystem_test_helper.h (+39)
``````````diff
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;
``````````
</details>
https://github.com/llvm/llvm-project/pull/213233
More information about the libcxx-commits
mailing list