From 9ceefd61fbcb24137380a7f2e81af7b7bec3760f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 18 Feb 2026 17:06:21 +0000
Subject: [PATCH 01/58] Initial plan
From 6f4ff5afa068292a1d254ebc6f8b453e3dc48b61 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 18 Feb 2026 17:13:34 +0000
Subject: [PATCH 02/58] Add FileType enum and GetFileType method to
SafeFileHandle
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../Kernel32/Interop.GetNamedPipeInfo.cs | 10 +++
.../Win32/SafeHandles/SafeFileHandle.Unix.cs | 45 +++++++++++++
.../SafeHandles/SafeFileHandle.Windows.cs | 66 +++++++++++++++++++
.../Win32/SafeHandles/SafeFileHandle.cs | 8 +++
.../src/System/IO/FileType.cs | 52 +++++++++++++++
.../System.Runtime/ref/System.Runtime.cs | 13 ++++
6 files changed, 194 insertions(+)
create mode 100644 src/libraries/System.Private.CoreLib/src/System/IO/FileType.cs
diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetNamedPipeInfo.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetNamedPipeInfo.cs
index 5945fa95c28263..f883e4120f70a4 100644
--- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetNamedPipeInfo.cs
+++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetNamedPipeInfo.cs
@@ -17,5 +17,15 @@ internal static unsafe partial bool GetNamedPipeInfo(
uint* lpInBufferSize,
uint* lpMaxInstances
);
+
+ [LibraryImport(Libraries.Kernel32, SetLastError = true)]
+ [return: MarshalAs(UnmanagedType.Bool)]
+ internal static unsafe partial bool GetNamedPipeInfo(
+ SafeFileHandle hNamedPipe,
+ uint* lpFlags,
+ uint* lpOutBufferSize,
+ uint* lpInBufferSize,
+ uint* lpMaxInstances
+ );
}
}
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
index 196ec738116e6a..7478e13b0ad986 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
@@ -339,6 +339,9 @@ private bool Init(string path, FileMode mode, FileAccess access, FileShare share
Debug.Assert(status.Size == 0 || Interop.Sys.LSeek(this, 0, Interop.Sys.SeekWhence.SEEK_CUR) >= 0);
}
+ // Cache the file type from the status
+ _cachedFileType = (int)MapUnixFileTypeToFileType(status.Mode & Interop.Sys.FileTypes.S_IFMT);
+
fileLength = status.Size;
filePermissions = ((UnixFileMode)status.Mode) & PermissionMask;
}
@@ -494,6 +497,48 @@ private bool GetCanSeek()
return canSeek == NullableBool.True;
}
+ ///
+ /// Gets the type of the file that this handle represents.
+ ///
+ /// The type of the file.
+ /// The handle is closed.
+ public new System.IO.FileType GetFileType()
+ {
+ ObjectDisposedException.ThrowIf(IsClosed, this);
+
+ int cachedType = _cachedFileType;
+ if (cachedType != -1)
+ {
+ return (System.IO.FileType)cachedType;
+ }
+
+ // If we don't have a cached value, call FStat to get it
+ if (Interop.Sys.FStat(this, out Interop.Sys.FileStatus status) == 0)
+ {
+ System.IO.FileType fileType = MapUnixFileTypeToFileType(status.Mode & Interop.Sys.FileTypes.S_IFMT);
+ _cachedFileType = (int)fileType;
+ return fileType;
+ }
+
+ // If FStat fails, return Unknown
+ return System.IO.FileType.Unknown;
+ }
+
+ private static System.IO.FileType MapUnixFileTypeToFileType(int unixFileType)
+ {
+ return unixFileType switch
+ {
+ Interop.Sys.FileTypes.S_IFREG => System.IO.FileType.RegularFile,
+ Interop.Sys.FileTypes.S_IFDIR => System.IO.FileType.Directory,
+ Interop.Sys.FileTypes.S_IFLNK => System.IO.FileType.SymbolicLink,
+ Interop.Sys.FileTypes.S_IFIFO => System.IO.FileType.Pipe,
+ Interop.Sys.FileTypes.S_IFSOCK => System.IO.FileType.Socket,
+ Interop.Sys.FileTypes.S_IFCHR => System.IO.FileType.CharacterDevice,
+ Interop.Sys.FileTypes.S_IFBLK => System.IO.FileType.BlockDevice,
+ _ => System.IO.FileType.Unknown
+ };
+ }
+
internal long GetFileLength()
{
int result = Interop.Sys.FStat(this, out Interop.Sys.FileStatus status);
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
index 8ee1660b705640..e704f0279c7a2c 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
@@ -270,6 +270,72 @@ internal int GetFileType()
return fileType;
}
+ ///
+ /// Gets the type of the file that this handle represents.
+ ///
+ /// The type of the file.
+ /// The handle is closed.
+ public new unsafe System.IO.FileType GetFileType()
+ {
+ ObjectDisposedException.ThrowIf(IsClosed, this);
+
+ int cachedType = _cachedFileType;
+ if (cachedType != -1)
+ {
+ return (System.IO.FileType)cachedType;
+ }
+
+ int kernelFileType = GetFileType();
+
+ System.IO.FileType result = kernelFileType switch
+ {
+ Interop.Kernel32.FileTypes.FILE_TYPE_CHAR => System.IO.FileType.CharacterDevice,
+ Interop.Kernel32.FileTypes.FILE_TYPE_PIPE => GetPipeOrSocketType(),
+ Interop.Kernel32.FileTypes.FILE_TYPE_DISK => GetDiskBasedType(),
+ _ => System.IO.FileType.Unknown
+ };
+
+ _cachedFileType = (int)result;
+ return result;
+ }
+
+ private unsafe System.IO.FileType GetPipeOrSocketType()
+ {
+ // Try to call GetNamedPipeInfo to determine if it's a pipe or socket
+ uint flags;
+ if (Interop.Kernel32.GetNamedPipeInfo(this, &flags, null, null, null))
+ {
+ return System.IO.FileType.Pipe;
+ }
+
+ // If GetNamedPipeInfo fails, it's likely a socket
+ return System.IO.FileType.Socket;
+ }
+
+ private unsafe System.IO.FileType GetDiskBasedType()
+ {
+ // First check if it's a directory using GetFileInformationByHandle
+ if (Interop.Kernel32.GetFileInformationByHandle(this, out Interop.Kernel32.BY_HANDLE_FILE_INFORMATION fileInfo))
+ {
+ if ((fileInfo.dwFileAttributes & Interop.Kernel32.FileAttributes.FILE_ATTRIBUTE_DIRECTORY) != 0)
+ {
+ return System.IO.FileType.Directory;
+ }
+ }
+
+ // Check if it's a reparse point (symbolic link) using GetFileInformationByHandleEx
+ Interop.Kernel32.FILE_BASIC_INFO basicInfo;
+ if (Interop.Kernel32.GetFileInformationByHandleEx(this, Interop.Kernel32.FileBasicInfo, &basicInfo, (uint)sizeof(Interop.Kernel32.FILE_BASIC_INFO)))
+ {
+ if ((basicInfo.FileAttributes & Interop.Kernel32.FileAttributes.FILE_ATTRIBUTE_REPARSE_POINT) != 0)
+ {
+ return System.IO.FileType.SymbolicLink;
+ }
+ }
+
+ return System.IO.FileType.RegularFile;
+ }
+
internal long GetFileLength()
{
if (!_lengthCanBeCached)
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
index 76dc55b74dba71..c2446bcb8d1b79 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
@@ -8,6 +8,7 @@ namespace Microsoft.Win32.SafeHandles
public sealed partial class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid
{
private string? _path;
+ private volatile int _cachedFileType = -1;
///
/// Creates a around a file handle.
@@ -20,5 +21,12 @@ public SafeFileHandle(IntPtr preexistingHandle, bool ownsHandle) : base(ownsHand
}
internal string? Path => _path;
+
+ ///
+ /// Gets the type of the file that this handle represents.
+ ///
+ /// The type of the file.
+ /// The handle is closed.
+ public System.IO.FileType GetFileType() => throw new PlatformNotSupportedException();
}
}
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileType.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileType.cs
new file mode 100644
index 00000000000000..d83bff7ab762df
--- /dev/null
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileType.cs
@@ -0,0 +1,52 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace System.IO
+{
+ ///
+ /// Specifies the type of a file.
+ ///
+ public enum FileType
+ {
+ ///
+ /// The file type is unknown.
+ ///
+ Unknown,
+
+ ///
+ /// The file is a regular file.
+ ///
+ RegularFile,
+
+ ///
+ /// The file is a pipe (FIFO).
+ ///
+ Pipe,
+
+ ///
+ /// The file is a socket.
+ ///
+ Socket,
+
+ ///
+ /// The file is a character device.
+ ///
+ CharacterDevice,
+
+ ///
+ /// The file is a directory.
+ ///
+ Directory,
+
+ ///
+ /// The file is a symbolic link.
+ ///
+ SymbolicLink,
+
+ ///
+ /// The file is a block device.
+ ///
+ [UnsupportedOSPlatform("windows")]
+ BlockDevice
+ }
+}
diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs
index 65b98d13078b29..14d13061412189 100644
--- a/src/libraries/System.Runtime/ref/System.Runtime.cs
+++ b/src/libraries/System.Runtime/ref/System.Runtime.cs
@@ -22,6 +22,7 @@ public SafeFileHandle() : base (default(bool)) { }
public SafeFileHandle(System.IntPtr preexistingHandle, bool ownsHandle) : base (default(bool)) { }
public override bool IsInvalid { get { throw null; } }
public bool IsAsync { get { throw null; } }
+ public System.IO.FileType GetFileType() { throw null; }
protected override bool ReleaseHandle() { throw null; }
}
public abstract partial class SafeHandleMinusOneIsInvalid : System.Runtime.InteropServices.SafeHandle
@@ -10445,6 +10446,18 @@ public enum FileAttributes
IntegrityStream = 32768,
NoScrubData = 131072,
}
+ public enum FileType
+ {
+ Unknown = 0,
+ RegularFile = 1,
+ Pipe = 2,
+ Socket = 3,
+ CharacterDevice = 4,
+ Directory = 5,
+ SymbolicLink = 6,
+ [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("windows")]
+ BlockDevice = 7,
+ }
public sealed partial class FileInfo : System.IO.FileSystemInfo
{
public FileInfo(string fileName) { }
From 71e5a815b8f5f3241661422dfae0cc134f547011 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 18 Feb 2026 17:16:00 +0000
Subject: [PATCH 03/58] Add comprehensive tests for SafeFileHandle.GetFileType
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../SafeFileHandle/GetFileType.cs | 254 ++++++++++++++++++
1 file changed, 254 insertions(+)
create mode 100644 src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs
new file mode 100644
index 00000000000000..d3d4d7507372ce
--- /dev/null
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs
@@ -0,0 +1,254 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.DotNet.XUnitExtensions;
+using Microsoft.Win32.SafeHandles;
+using System.IO.Pipes;
+using System.Net;
+using System.Net.Sockets;
+using Xunit;
+
+namespace System.IO.Tests
+{
+ public class SafeFileHandle_GetFileType : FileSystemTest
+ {
+ [Fact]
+ public void GetFileType_RegularFile()
+ {
+ string path = GetTestFilePath();
+ File.WriteAllText(path, "test");
+
+ using SafeFileHandle handle = File.OpenHandle(path, FileMode.Open, FileAccess.Read);
+ Assert.Equal(FileType.RegularFile, handle.GetFileType());
+ }
+
+ [Fact]
+ public void GetFileType_Directory()
+ {
+ string path = GetTestFilePath();
+ Directory.CreateDirectory(path);
+
+ if (OperatingSystem.IsWindows())
+ {
+ IntPtr hFile = Interop.Kernel32.CreateFile(
+ path,
+ Interop.Kernel32.GenericOperations.GENERIC_READ,
+ FileShare.ReadWrite,
+ null,
+ FileMode.Open,
+ Interop.Kernel32.FileOperations.FILE_FLAG_BACKUP_SEMANTICS,
+ IntPtr.Zero);
+
+ using SafeFileHandle handle = new SafeFileHandle(hFile, ownsHandle: true);
+ Assert.False(handle.IsInvalid);
+ Assert.Equal(FileType.Directory, handle.GetFileType());
+ }
+ else
+ {
+ IntPtr fd = Interop.Sys.Open(path, Interop.Sys.OpenFlags.O_RDONLY, 0);
+ using SafeFileHandle handle = new SafeFileHandle(fd, ownsHandle: true);
+ Assert.False(handle.IsInvalid);
+ Assert.Equal(FileType.Directory, handle.GetFileType());
+ }
+ }
+
+ [Fact]
+ public void GetFileType_NullDevice()
+ {
+ using SafeFileHandle handle = File.OpenHandle(
+ OperatingSystem.IsWindows() ? "NUL" : "/dev/null",
+ FileMode.Open,
+ FileAccess.Write);
+
+ Assert.Equal(FileType.CharacterDevice, handle.GetFileType());
+ }
+
+ [Fact]
+ public void GetFileType_AnonymousPipe()
+ {
+ using AnonymousPipeServerStream server = new AnonymousPipeServerStream(PipeDirection.Out);
+ using SafeFileHandle serverHandle = new SafeFileHandle(server.SafePipeHandle.DangerousGetHandle(), ownsHandle: false);
+
+ Assert.Equal(FileType.Pipe, serverHandle.GetFileType());
+
+ using SafeFileHandle clientHandle = new SafeFileHandle(server.ClientSafePipeHandle.DangerousGetHandle(), ownsHandle: false);
+ Assert.Equal(FileType.Pipe, clientHandle.GetFileType());
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsSubsystemForLinux))]
+ [PlatformSpecific(TestPlatforms.Windows)]
+ public void GetFileType_NamedPipe_Windows()
+ {
+ string pipeName = Path.GetRandomFileName();
+ using NamedPipeServerStream server = new NamedPipeServerStream(pipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
+
+ Task serverTask = Task.Run(async () => await server.WaitForConnectionAsync());
+
+ using NamedPipeClientStream client = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.None);
+ client.Connect();
+ serverTask.Wait();
+
+ using SafeFileHandle serverHandle = new SafeFileHandle(server.SafePipeHandle.DangerousGetHandle(), ownsHandle: false);
+ Assert.Equal(FileType.Pipe, serverHandle.GetFileType());
+
+ using SafeFileHandle clientHandle = new SafeFileHandle(client.SafePipeHandle.DangerousGetHandle(), ownsHandle: false);
+ Assert.Equal(FileType.Pipe, clientHandle.GetFileType());
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsSubsystemForLinux))]
+ [PlatformSpecific(TestPlatforms.AnyUnix)]
+ public void GetFileType_NamedPipe_Unix()
+ {
+ string pipePath = GetTestFilePath();
+ Assert.Equal(0, Interop.Sys.MkFifo(pipePath, (int)UnixFileMode.UserRead | (int)UnixFileMode.UserWrite));
+
+ Task readerTask = Task.Run(() =>
+ {
+ using SafeFileHandle reader = File.OpenHandle(pipePath, FileMode.Open, FileAccess.Read);
+ Assert.Equal(FileType.Pipe, reader.GetFileType());
+ });
+
+ using SafeFileHandle writer = File.OpenHandle(pipePath, FileMode.Open, FileAccess.Write);
+ Assert.Equal(FileType.Pipe, writer.GetFileType());
+
+ readerTask.Wait();
+ }
+
+ [Fact]
+ public void GetFileType_Socket()
+ {
+ using Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
+ listener.Bind(new IPEndPoint(IPAddress.Loopback, 0));
+ listener.Listen(1);
+
+ using Socket client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
+ client.Connect(listener.LocalEndPoint);
+
+ using Socket server = listener.Accept();
+
+ using SafeFileHandle serverHandle = new SafeFileHandle(server.Handle, ownsHandle: false);
+ using SafeFileHandle clientHandle = new SafeFileHandle(client.Handle, ownsHandle: false);
+
+ Assert.Equal(FileType.Socket, serverHandle.GetFileType());
+ Assert.Equal(FileType.Socket, clientHandle.GetFileType());
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))]
+ public void GetFileType_ConsoleInput()
+ {
+ if (!Console.IsInputRedirected)
+ {
+ using SafeFileHandle handle = new SafeFileHandle(Console.OpenStandardInput().SafeFileHandle.DangerousGetHandle(), ownsHandle: false);
+ FileType type = handle.GetFileType();
+
+ Assert.True(type == FileType.CharacterDevice || type == FileType.Pipe || type == FileType.RegularFile,
+ $"Expected CharacterDevice, Pipe, or RegularFile but got {type}");
+ }
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))]
+ [PlatformSpecific(TestPlatforms.AnyUnix)]
+ public void GetFileType_SymbolicLink_Unix()
+ {
+ string targetPath = GetTestFilePath();
+ string linkPath = GetTestFilePath();
+ File.WriteAllText(targetPath, "test");
+ File.CreateSymbolicLink(linkPath, targetPath);
+
+ IntPtr fd = Interop.Sys.Open(linkPath, Interop.Sys.OpenFlags.O_RDONLY | Interop.Sys.OpenFlags.O_NOFOLLOW, 0);
+ using SafeFileHandle handle = new SafeFileHandle(fd, ownsHandle: true);
+
+ if (!handle.IsInvalid)
+ {
+ Assert.Equal(FileType.SymbolicLink, handle.GetFileType());
+ }
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))]
+ [PlatformSpecific(TestPlatforms.Windows)]
+ public void GetFileType_SymbolicLink_Windows()
+ {
+ string targetPath = GetTestFilePath();
+ string linkPath = GetTestFilePath();
+ File.WriteAllText(targetPath, "test");
+ File.CreateSymbolicLink(linkPath, targetPath);
+
+ IntPtr hFile = Interop.Kernel32.CreateFile(
+ linkPath,
+ Interop.Kernel32.GenericOperations.GENERIC_READ,
+ FileShare.ReadWrite,
+ null,
+ FileMode.Open,
+ Interop.Kernel32.FileOperations.FILE_FLAG_OPEN_REPARSE_POINT | Interop.Kernel32.FileOperations.FILE_FLAG_BACKUP_SEMANTICS,
+ IntPtr.Zero);
+
+ using SafeFileHandle handle = new SafeFileHandle(hFile, ownsHandle: true);
+ if (!handle.IsInvalid)
+ {
+ Assert.Equal(FileType.SymbolicLink, handle.GetFileType());
+ }
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))]
+ [PlatformSpecific(TestPlatforms.AnyUnix & ~TestPlatforms.Browser & ~TestPlatforms.Wasi)]
+ public void GetFileType_BlockDevice_Unix()
+ {
+ string[] possibleBlockDevices = { "/dev/sda", "/dev/loop0", "/dev/vda", "/dev/nvme0n1" };
+
+ string blockDevice = null;
+ foreach (string device in possibleBlockDevices)
+ {
+ if (File.Exists(device))
+ {
+ blockDevice = device;
+ break;
+ }
+ }
+
+ if (blockDevice == null)
+ {
+ throw new SkipTestException("No accessible block device found for testing");
+ }
+
+ try
+ {
+ IntPtr fd = Interop.Sys.Open(blockDevice, Interop.Sys.OpenFlags.O_RDONLY, 0);
+ if (fd == (IntPtr)(-1))
+ {
+ throw new SkipTestException($"Could not open {blockDevice}");
+ }
+
+ using SafeFileHandle handle = new SafeFileHandle(fd, ownsHandle: true);
+ Assert.Equal(FileType.BlockDevice, handle.GetFileType());
+ }
+ catch (UnauthorizedAccessException)
+ {
+ throw new SkipTestException("Insufficient privileges to open block device");
+ }
+ }
+
+ [Fact]
+ public void GetFileType_ClosedHandle_ThrowsObjectDisposedException()
+ {
+ SafeFileHandle handle = File.OpenHandle(GetTestFilePath(), FileMode.Create);
+ handle.Dispose();
+
+ Assert.Throws(() => handle.GetFileType());
+ }
+
+ [Fact]
+ public void GetFileType_CachesResult()
+ {
+ string path = GetTestFilePath();
+ File.WriteAllText(path, "test");
+
+ using SafeFileHandle handle = File.OpenHandle(path, FileMode.Open, FileAccess.Read);
+
+ FileType firstCall = handle.GetFileType();
+ FileType secondCall = handle.GetFileType();
+
+ Assert.Equal(firstCall, secondCall);
+ Assert.Equal(FileType.RegularFile, firstCall);
+ }
+ }
+}
From 603de644380a622e1ae58ff61b9f6be667481752 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 18 Feb 2026 17:20:58 +0000
Subject: [PATCH 04/58] Fix build errors for FileType enum and GetFileType
method
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs | 4 +++-
.../Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs | 2 +-
.../src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs | 7 -------
.../src/System.Private.CoreLib.Shared.projitems | 1 +
.../System.Private.CoreLib/src/System/IO/FileType.cs | 2 +-
5 files changed, 6 insertions(+), 10 deletions(-)
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
index 7478e13b0ad986..f4aa338e77f988 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
@@ -502,7 +502,7 @@ private bool GetCanSeek()
///
/// The type of the file.
/// The handle is closed.
- public new System.IO.FileType GetFileType()
+ public System.IO.FileType GetFileType()
{
ObjectDisposedException.ThrowIf(IsClosed, this);
@@ -526,6 +526,7 @@ private bool GetCanSeek()
private static System.IO.FileType MapUnixFileTypeToFileType(int unixFileType)
{
+#pragma warning disable CA1416 // BlockDevice is only returned on Unix platforms
return unixFileType switch
{
Interop.Sys.FileTypes.S_IFREG => System.IO.FileType.RegularFile,
@@ -537,6 +538,7 @@ private static System.IO.FileType MapUnixFileTypeToFileType(int unixFileType)
Interop.Sys.FileTypes.S_IFBLK => System.IO.FileType.BlockDevice,
_ => System.IO.FileType.Unknown
};
+#pragma warning restore CA1416
}
internal long GetFileLength()
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
index e704f0279c7a2c..074fa175196277 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
@@ -275,7 +275,7 @@ internal int GetFileType()
///
/// The type of the file.
/// The handle is closed.
- public new unsafe System.IO.FileType GetFileType()
+ public unsafe System.IO.FileType GetFileType()
{
ObjectDisposedException.ThrowIf(IsClosed, this);
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
index c2446bcb8d1b79..45d05a97454e7f 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
@@ -21,12 +21,5 @@ public SafeFileHandle(IntPtr preexistingHandle, bool ownsHandle) : base(ownsHand
}
internal string? Path => _path;
-
- ///
- /// Gets the type of the file that this handle represents.
- ///
- /// The type of the file.
- /// The handle is closed.
- public System.IO.FileType GetFileType() => throw new PlatformNotSupportedException();
}
}
diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
index c744f85d3e05ce..41890be47fba0c 100644
--- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
+++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
@@ -514,6 +514,7 @@
+
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileType.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileType.cs
index d83bff7ab762df..3f4290c2286388 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/FileType.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileType.cs
@@ -46,7 +46,7 @@ public enum FileType
///
/// The file is a block device.
///
- [UnsupportedOSPlatform("windows")]
+ [System.Runtime.Versioning.UnsupportedOSPlatform("windows")]
BlockDevice
}
}
From 9edcc961c043c099e9e22c865ce4cc74003ce683 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 18 Feb 2026 17:49:18 +0000
Subject: [PATCH 05/58] Address PR feedback: refactor GetFileType, split tests,
use SafeHandle
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../Kernel32/Interop.GetNamedPipeInfo.cs | 13 +-
.../Win32/SafeHandles/SafeFileHandle.Unix.cs | 21 +--
.../SafeHandles/SafeFileHandle.Windows.cs | 37 ++--
.../Win32/SafeHandles/SafeFileHandle.cs | 11 ++
.../SafeFileHandle/GetFileType.Unix.cs | 98 ++++++++++
.../SafeFileHandle/GetFileType.Windows.cs | 90 +++++++++
.../SafeFileHandle/GetFileType.cs | 172 +-----------------
.../System.IO.FileSystem.Tests.csproj | 3 +
8 files changed, 228 insertions(+), 217 deletions(-)
create mode 100644 src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs
create mode 100644 src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs
diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetNamedPipeInfo.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetNamedPipeInfo.cs
index f883e4120f70a4..686432415150a4 100644
--- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetNamedPipeInfo.cs
+++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetNamedPipeInfo.cs
@@ -2,7 +2,6 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.Runtime.InteropServices;
-using Microsoft.Win32.SafeHandles;
internal static partial class Interop
{
@@ -11,17 +10,7 @@ internal static partial class Kernel32
[LibraryImport(Libraries.Kernel32, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static unsafe partial bool GetNamedPipeInfo(
- SafePipeHandle hNamedPipe,
- uint* lpFlags,
- uint* lpOutBufferSize,
- uint* lpInBufferSize,
- uint* lpMaxInstances
- );
-
- [LibraryImport(Libraries.Kernel32, SetLastError = true)]
- [return: MarshalAs(UnmanagedType.Bool)]
- internal static unsafe partial bool GetNamedPipeInfo(
- SafeFileHandle hNamedPipe,
+ SafeHandle hNamedPipe,
uint* lpFlags,
uint* lpOutBufferSize,
uint* lpInBufferSize,
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
index f4aa338e77f988..261307a4c57365 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
@@ -497,15 +497,8 @@ private bool GetCanSeek()
return canSeek == NullableBool.True;
}
- ///
- /// Gets the type of the file that this handle represents.
- ///
- /// The type of the file.
- /// The handle is closed.
- public System.IO.FileType GetFileType()
+ internal System.IO.FileType GetFileTypeCore()
{
- ObjectDisposedException.ThrowIf(IsClosed, this);
-
int cachedType = _cachedFileType;
if (cachedType != -1)
{
@@ -513,15 +506,15 @@ public System.IO.FileType GetFileType()
}
// If we don't have a cached value, call FStat to get it
- if (Interop.Sys.FStat(this, out Interop.Sys.FileStatus status) == 0)
+ int result = Interop.Sys.FStat(this, out Interop.Sys.FileStatus status);
+ if (result != 0)
{
- System.IO.FileType fileType = MapUnixFileTypeToFileType(status.Mode & Interop.Sys.FileTypes.S_IFMT);
- _cachedFileType = (int)fileType;
- return fileType;
+ throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo());
}
- // If FStat fails, return Unknown
- return System.IO.FileType.Unknown;
+ System.IO.FileType fileType = MapUnixFileTypeToFileType(status.Mode & Interop.Sys.FileTypes.S_IFMT);
+ _cachedFileType = (int)fileType;
+ return fileType;
}
private static System.IO.FileType MapUnixFileTypeToFileType(int unixFileType)
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
index 074fa175196277..694df37937fd8b 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
@@ -16,7 +16,6 @@ public sealed partial class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid
private long _length = -1; // negative means that hasn't been fetched.
private bool _lengthCanBeCached; // file has been opened for reading and not shared for writing.
private volatile FileOptions _fileOptions = (FileOptions)(-1);
- private volatile int _fileType = -1;
public SafeFileHandle() : base(true)
{
@@ -26,7 +25,7 @@ public SafeFileHandle() : base(true)
internal bool IsNoBuffering => (GetFileOptions() & NoBuffering) != 0;
- internal bool CanSeek => !IsClosed && GetFileType() == Interop.Kernel32.FileTypes.FILE_TYPE_DISK;
+ internal bool CanSeek => !IsClosed && GetKernelFileType() == Interop.Kernel32.FileTypes.FILE_TYPE_DISK;
internal ThreadPoolBoundHandle? ThreadPoolBinding { get; set; }
@@ -254,38 +253,34 @@ internal unsafe FileOptions GetFileOptions()
return _fileOptions = result;
}
- internal int GetFileType()
+ private int GetKernelFileType()
{
- int fileType = _fileType;
- if (fileType == -1)
+ int cachedType = _cachedFileType;
+ if (cachedType != -1)
{
- _fileType = fileType = Interop.Kernel32.GetFileType(this);
-
- Debug.Assert(fileType == Interop.Kernel32.FileTypes.FILE_TYPE_DISK
- || fileType == Interop.Kernel32.FileTypes.FILE_TYPE_PIPE
- || fileType == Interop.Kernel32.FileTypes.FILE_TYPE_CHAR,
- $"Unknown file type: {fileType}");
+ // Return the Kernel file type from cached FileType enum
+ System.IO.FileType fileType = (System.IO.FileType)cachedType;
+ return fileType switch
+ {
+ System.IO.FileType.CharacterDevice => Interop.Kernel32.FileTypes.FILE_TYPE_CHAR,
+ System.IO.FileType.Pipe or System.IO.FileType.Socket => Interop.Kernel32.FileTypes.FILE_TYPE_PIPE,
+ System.IO.FileType.RegularFile or System.IO.FileType.Directory or System.IO.FileType.SymbolicLink => Interop.Kernel32.FileTypes.FILE_TYPE_DISK,
+ _ => Interop.Kernel32.FileTypes.FILE_TYPE_UNKNOWN
+ };
}
- return fileType;
+ return Interop.Kernel32.GetFileType(this);
}
- ///
- /// Gets the type of the file that this handle represents.
- ///
- /// The type of the file.
- /// The handle is closed.
- public unsafe System.IO.FileType GetFileType()
+ internal unsafe System.IO.FileType GetFileTypeCore()
{
- ObjectDisposedException.ThrowIf(IsClosed, this);
-
int cachedType = _cachedFileType;
if (cachedType != -1)
{
return (System.IO.FileType)cachedType;
}
- int kernelFileType = GetFileType();
+ int kernelFileType = Interop.Kernel32.GetFileType(this);
System.IO.FileType result = kernelFileType switch
{
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
index 45d05a97454e7f..493720deb4b896 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
@@ -21,5 +21,16 @@ public SafeFileHandle(IntPtr preexistingHandle, bool ownsHandle) : base(ownsHand
}
internal string? Path => _path;
+
+ ///
+ /// Gets the type of the file that this handle represents.
+ ///
+ /// The type of the file.
+ /// The handle is closed.
+ public System.IO.FileType GetFileType()
+ {
+ ObjectDisposedException.ThrowIf(IsClosed, this);
+ return GetFileTypeCore();
+ }
}
}
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs
new file mode 100644
index 00000000000000..385df3f381000d
--- /dev/null
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs
@@ -0,0 +1,98 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.DotNet.XUnitExtensions;
+using Microsoft.Win32.SafeHandles;
+using Xunit;
+
+namespace System.IO.Tests
+{
+ [PlatformSpecific(TestPlatforms.AnyUnix)]
+ public class SafeFileHandle_GetFileType_Unix : FileSystemTest
+ {
+ [Fact]
+ public void GetFileType_Directory()
+ {
+ string path = GetTestFilePath();
+ Directory.CreateDirectory(path);
+
+ IntPtr fd = Interop.Sys.Open(path, Interop.Sys.OpenFlags.O_RDONLY, 0);
+ using SafeFileHandle handle = new SafeFileHandle(fd, ownsHandle: true);
+ Assert.False(handle.IsInvalid);
+ Assert.Equal(FileType.Directory, handle.GetFileType());
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsSubsystemForLinux))]
+ public void GetFileType_NamedPipe()
+ {
+ string pipePath = GetTestFilePath();
+ Assert.Equal(0, Interop.Sys.MkFifo(pipePath, (int)UnixFileMode.UserRead | (int)UnixFileMode.UserWrite));
+
+ Task readerTask = Task.Run(() =>
+ {
+ using SafeFileHandle reader = File.OpenHandle(pipePath, FileMode.Open, FileAccess.Read);
+ Assert.Equal(FileType.Pipe, reader.GetFileType());
+ });
+
+ using SafeFileHandle writer = File.OpenHandle(pipePath, FileMode.Open, FileAccess.Write);
+ Assert.Equal(FileType.Pipe, writer.GetFileType());
+
+ readerTask.Wait();
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))]
+ public void GetFileType_SymbolicLink()
+ {
+ string targetPath = GetTestFilePath();
+ string linkPath = GetTestFilePath();
+ File.WriteAllText(targetPath, "test");
+ File.CreateSymbolicLink(linkPath, targetPath);
+
+ IntPtr fd = Interop.Sys.Open(linkPath, Interop.Sys.OpenFlags.O_RDONLY | Interop.Sys.OpenFlags.O_NOFOLLOW, 0);
+ using SafeFileHandle handle = new SafeFileHandle(fd, ownsHandle: true);
+
+ if (!handle.IsInvalid)
+ {
+ Assert.Equal(FileType.SymbolicLink, handle.GetFileType());
+ }
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))]
+ [PlatformSpecific(TestPlatforms.AnyUnix & ~TestPlatforms.Browser & ~TestPlatforms.Wasi)]
+ public void GetFileType_BlockDevice()
+ {
+ string[] possibleBlockDevices = { "/dev/sda", "/dev/loop0", "/dev/vda", "/dev/nvme0n1" };
+
+ string blockDevice = null;
+ foreach (string device in possibleBlockDevices)
+ {
+ if (File.Exists(device))
+ {
+ blockDevice = device;
+ break;
+ }
+ }
+
+ if (blockDevice == null)
+ {
+ throw new SkipTestException("No accessible block device found for testing");
+ }
+
+ try
+ {
+ IntPtr fd = Interop.Sys.Open(blockDevice, Interop.Sys.OpenFlags.O_RDONLY, 0);
+ if (fd == (IntPtr)(-1))
+ {
+ throw new SkipTestException($"Could not open {blockDevice}");
+ }
+
+ using SafeFileHandle handle = new SafeFileHandle(fd, ownsHandle: true);
+ Assert.Equal(FileType.BlockDevice, handle.GetFileType());
+ }
+ catch (UnauthorizedAccessException)
+ {
+ throw new SkipTestException("Insufficient privileges to open block device");
+ }
+ }
+ }
+}
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs
new file mode 100644
index 00000000000000..6a161a4fd69fbd
--- /dev/null
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs
@@ -0,0 +1,90 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.DotNet.XUnitExtensions;
+using Microsoft.Win32.SafeHandles;
+using System.IO.Pipes;
+using Xunit;
+
+namespace System.IO.Tests
+{
+ [PlatformSpecific(TestPlatforms.Windows)]
+ public class SafeFileHandle_GetFileType_Windows : FileSystemTest
+ {
+ [Fact]
+ public void GetFileType_Directory()
+ {
+ string path = GetTestFilePath();
+ Directory.CreateDirectory(path);
+
+ IntPtr hFile = Interop.Kernel32.CreateFile(
+ path,
+ Interop.Kernel32.GenericOperations.GENERIC_READ,
+ FileShare.ReadWrite,
+ null,
+ FileMode.Open,
+ Interop.Kernel32.FileOperations.FILE_FLAG_BACKUP_SEMANTICS,
+ IntPtr.Zero);
+
+ using SafeFileHandle handle = new SafeFileHandle(hFile, ownsHandle: true);
+ Assert.False(handle.IsInvalid);
+ Assert.Equal(FileType.Directory, handle.GetFileType());
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsSubsystemForLinux))]
+ public void GetFileType_NamedPipe()
+ {
+ string pipeName = Path.GetRandomFileName();
+ using NamedPipeServerStream server = new NamedPipeServerStream(pipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
+
+ Task serverTask = Task.Run(async () => await server.WaitForConnectionAsync());
+
+ using NamedPipeClientStream client = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.None);
+ client.Connect();
+ serverTask.Wait();
+
+ using SafeFileHandle serverHandle = new SafeFileHandle(server.SafePipeHandle.DangerousGetHandle(), ownsHandle: false);
+ Assert.Equal(FileType.Pipe, serverHandle.GetFileType());
+
+ using SafeFileHandle clientHandle = new SafeFileHandle(client.SafePipeHandle.DangerousGetHandle(), ownsHandle: false);
+ Assert.Equal(FileType.Pipe, clientHandle.GetFileType());
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))]
+ public void GetFileType_ConsoleInput()
+ {
+ if (!Console.IsInputRedirected)
+ {
+ using SafeFileHandle handle = new SafeFileHandle(Console.OpenStandardInput().SafeFileHandle.DangerousGetHandle(), ownsHandle: false);
+ FileType type = handle.GetFileType();
+
+ Assert.True(type == FileType.CharacterDevice || type == FileType.Pipe || type == FileType.RegularFile,
+ $"Expected CharacterDevice, Pipe, or RegularFile but got {type}");
+ }
+ }
+
+ [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))]
+ public void GetFileType_SymbolicLink()
+ {
+ string targetPath = GetTestFilePath();
+ string linkPath = GetTestFilePath();
+ File.WriteAllText(targetPath, "test");
+ File.CreateSymbolicLink(linkPath, targetPath);
+
+ IntPtr hFile = Interop.Kernel32.CreateFile(
+ linkPath,
+ Interop.Kernel32.GenericOperations.GENERIC_READ,
+ FileShare.ReadWrite,
+ null,
+ FileMode.Open,
+ Interop.Kernel32.FileOperations.FILE_FLAG_OPEN_REPARSE_POINT | Interop.Kernel32.FileOperations.FILE_FLAG_BACKUP_SEMANTICS,
+ IntPtr.Zero);
+
+ using SafeFileHandle handle = new SafeFileHandle(hFile, ownsHandle: true);
+ if (!handle.IsInvalid)
+ {
+ Assert.Equal(FileType.SymbolicLink, handle.GetFileType());
+ }
+ }
+ }
+}
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs
index d3d4d7507372ce..9cf46dfdeb9cc0 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs
@@ -1,7 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using Microsoft.DotNet.XUnitExtensions;
using Microsoft.Win32.SafeHandles;
using System.IO.Pipes;
using System.Net;
@@ -22,51 +21,17 @@ public void GetFileType_RegularFile()
Assert.Equal(FileType.RegularFile, handle.GetFileType());
}
- [Fact]
- public void GetFileType_Directory()
- {
- string path = GetTestFilePath();
- Directory.CreateDirectory(path);
-
- if (OperatingSystem.IsWindows())
- {
- IntPtr hFile = Interop.Kernel32.CreateFile(
- path,
- Interop.Kernel32.GenericOperations.GENERIC_READ,
- FileShare.ReadWrite,
- null,
- FileMode.Open,
- Interop.Kernel32.FileOperations.FILE_FLAG_BACKUP_SEMANTICS,
- IntPtr.Zero);
-
- using SafeFileHandle handle = new SafeFileHandle(hFile, ownsHandle: true);
- Assert.False(handle.IsInvalid);
- Assert.Equal(FileType.Directory, handle.GetFileType());
- }
- else
- {
- IntPtr fd = Interop.Sys.Open(path, Interop.Sys.OpenFlags.O_RDONLY, 0);
- using SafeFileHandle handle = new SafeFileHandle(fd, ownsHandle: true);
- Assert.False(handle.IsInvalid);
- Assert.Equal(FileType.Directory, handle.GetFileType());
- }
- }
-
[Fact]
public void GetFileType_NullDevice()
{
- using SafeFileHandle handle = File.OpenHandle(
- OperatingSystem.IsWindows() ? "NUL" : "/dev/null",
- FileMode.Open,
- FileAccess.Write);
-
+ using SafeFileHandle handle = File.OpenNullHandle();
Assert.Equal(FileType.CharacterDevice, handle.GetFileType());
}
[Fact]
public void GetFileType_AnonymousPipe()
{
- using AnonymousPipeServerStream server = new AnonymousPipeServerStream(PipeDirection.Out);
+ using AnonymousPipeServerStream server = new(PipeDirection.Out);
using SafeFileHandle serverHandle = new SafeFileHandle(server.SafePipeHandle.DangerousGetHandle(), ownsHandle: false);
Assert.Equal(FileType.Pipe, serverHandle.GetFileType());
@@ -75,45 +40,6 @@ public void GetFileType_AnonymousPipe()
Assert.Equal(FileType.Pipe, clientHandle.GetFileType());
}
- [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsSubsystemForLinux))]
- [PlatformSpecific(TestPlatforms.Windows)]
- public void GetFileType_NamedPipe_Windows()
- {
- string pipeName = Path.GetRandomFileName();
- using NamedPipeServerStream server = new NamedPipeServerStream(pipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
-
- Task serverTask = Task.Run(async () => await server.WaitForConnectionAsync());
-
- using NamedPipeClientStream client = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.None);
- client.Connect();
- serverTask.Wait();
-
- using SafeFileHandle serverHandle = new SafeFileHandle(server.SafePipeHandle.DangerousGetHandle(), ownsHandle: false);
- Assert.Equal(FileType.Pipe, serverHandle.GetFileType());
-
- using SafeFileHandle clientHandle = new SafeFileHandle(client.SafePipeHandle.DangerousGetHandle(), ownsHandle: false);
- Assert.Equal(FileType.Pipe, clientHandle.GetFileType());
- }
-
- [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsSubsystemForLinux))]
- [PlatformSpecific(TestPlatforms.AnyUnix)]
- public void GetFileType_NamedPipe_Unix()
- {
- string pipePath = GetTestFilePath();
- Assert.Equal(0, Interop.Sys.MkFifo(pipePath, (int)UnixFileMode.UserRead | (int)UnixFileMode.UserWrite));
-
- Task readerTask = Task.Run(() =>
- {
- using SafeFileHandle reader = File.OpenHandle(pipePath, FileMode.Open, FileAccess.Read);
- Assert.Equal(FileType.Pipe, reader.GetFileType());
- });
-
- using SafeFileHandle writer = File.OpenHandle(pipePath, FileMode.Open, FileAccess.Write);
- Assert.Equal(FileType.Pipe, writer.GetFileType());
-
- readerTask.Wait();
- }
-
[Fact]
public void GetFileType_Socket()
{
@@ -133,100 +59,6 @@ public void GetFileType_Socket()
Assert.Equal(FileType.Socket, clientHandle.GetFileType());
}
- [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))]
- public void GetFileType_ConsoleInput()
- {
- if (!Console.IsInputRedirected)
- {
- using SafeFileHandle handle = new SafeFileHandle(Console.OpenStandardInput().SafeFileHandle.DangerousGetHandle(), ownsHandle: false);
- FileType type = handle.GetFileType();
-
- Assert.True(type == FileType.CharacterDevice || type == FileType.Pipe || type == FileType.RegularFile,
- $"Expected CharacterDevice, Pipe, or RegularFile but got {type}");
- }
- }
-
- [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))]
- [PlatformSpecific(TestPlatforms.AnyUnix)]
- public void GetFileType_SymbolicLink_Unix()
- {
- string targetPath = GetTestFilePath();
- string linkPath = GetTestFilePath();
- File.WriteAllText(targetPath, "test");
- File.CreateSymbolicLink(linkPath, targetPath);
-
- IntPtr fd = Interop.Sys.Open(linkPath, Interop.Sys.OpenFlags.O_RDONLY | Interop.Sys.OpenFlags.O_NOFOLLOW, 0);
- using SafeFileHandle handle = new SafeFileHandle(fd, ownsHandle: true);
-
- if (!handle.IsInvalid)
- {
- Assert.Equal(FileType.SymbolicLink, handle.GetFileType());
- }
- }
-
- [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))]
- [PlatformSpecific(TestPlatforms.Windows)]
- public void GetFileType_SymbolicLink_Windows()
- {
- string targetPath = GetTestFilePath();
- string linkPath = GetTestFilePath();
- File.WriteAllText(targetPath, "test");
- File.CreateSymbolicLink(linkPath, targetPath);
-
- IntPtr hFile = Interop.Kernel32.CreateFile(
- linkPath,
- Interop.Kernel32.GenericOperations.GENERIC_READ,
- FileShare.ReadWrite,
- null,
- FileMode.Open,
- Interop.Kernel32.FileOperations.FILE_FLAG_OPEN_REPARSE_POINT | Interop.Kernel32.FileOperations.FILE_FLAG_BACKUP_SEMANTICS,
- IntPtr.Zero);
-
- using SafeFileHandle handle = new SafeFileHandle(hFile, ownsHandle: true);
- if (!handle.IsInvalid)
- {
- Assert.Equal(FileType.SymbolicLink, handle.GetFileType());
- }
- }
-
- [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))]
- [PlatformSpecific(TestPlatforms.AnyUnix & ~TestPlatforms.Browser & ~TestPlatforms.Wasi)]
- public void GetFileType_BlockDevice_Unix()
- {
- string[] possibleBlockDevices = { "/dev/sda", "/dev/loop0", "/dev/vda", "/dev/nvme0n1" };
-
- string blockDevice = null;
- foreach (string device in possibleBlockDevices)
- {
- if (File.Exists(device))
- {
- blockDevice = device;
- break;
- }
- }
-
- if (blockDevice == null)
- {
- throw new SkipTestException("No accessible block device found for testing");
- }
-
- try
- {
- IntPtr fd = Interop.Sys.Open(blockDevice, Interop.Sys.OpenFlags.O_RDONLY, 0);
- if (fd == (IntPtr)(-1))
- {
- throw new SkipTestException($"Could not open {blockDevice}");
- }
-
- using SafeFileHandle handle = new SafeFileHandle(fd, ownsHandle: true);
- Assert.Equal(FileType.BlockDevice, handle.GetFileType());
- }
- catch (UnauthorizedAccessException)
- {
- throw new SkipTestException("Insufficient privileges to open block device");
- }
- }
-
[Fact]
public void GetFileType_ClosedHandle_ThrowsObjectDisposedException()
{
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj
index d6ca52e5e34c12..bccdc010b62184 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj
@@ -227,6 +227,9 @@
+
+
+
From 1f0655fe83d2848b3f44cb96a0570e83637b2259 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 18 Feb 2026 18:07:06 +0000
Subject: [PATCH 06/58] Address feedback: move test files, delete
GetKernelFileType, use new API in CanSeek
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../SafeHandles/SafeFileHandle.Windows.cs | 21 +------------------
.../System.IO.FileSystem.Tests.csproj | 6 +++---
2 files changed, 4 insertions(+), 23 deletions(-)
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
index 694df37937fd8b..9680ac6fe25d86 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
@@ -25,7 +25,7 @@ public SafeFileHandle() : base(true)
internal bool IsNoBuffering => (GetFileOptions() & NoBuffering) != 0;
- internal bool CanSeek => !IsClosed && GetKernelFileType() == Interop.Kernel32.FileTypes.FILE_TYPE_DISK;
+ internal bool CanSeek => !IsClosed && GetFileType() == System.IO.FileType.RegularFile;
internal ThreadPoolBoundHandle? ThreadPoolBinding { get; set; }
@@ -253,25 +253,6 @@ internal unsafe FileOptions GetFileOptions()
return _fileOptions = result;
}
- private int GetKernelFileType()
- {
- int cachedType = _cachedFileType;
- if (cachedType != -1)
- {
- // Return the Kernel file type from cached FileType enum
- System.IO.FileType fileType = (System.IO.FileType)cachedType;
- return fileType switch
- {
- System.IO.FileType.CharacterDevice => Interop.Kernel32.FileTypes.FILE_TYPE_CHAR,
- System.IO.FileType.Pipe or System.IO.FileType.Socket => Interop.Kernel32.FileTypes.FILE_TYPE_PIPE,
- System.IO.FileType.RegularFile or System.IO.FileType.Directory or System.IO.FileType.SymbolicLink => Interop.Kernel32.FileTypes.FILE_TYPE_DISK,
- _ => Interop.Kernel32.FileTypes.FILE_TYPE_UNKNOWN
- };
- }
-
- return Interop.Kernel32.GetFileType(this);
- }
-
internal unsafe System.IO.FileType GetFileTypeCore()
{
int cachedType = _cachedFileType;
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj
index bccdc010b62184..ee394f212c3f9c 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj
@@ -93,12 +93,14 @@
+
+
@@ -112,6 +114,7 @@
+
@@ -227,9 +230,6 @@
-
-
-
From 89944640f76cbcaa129133df97523f3a128fc6f4 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 18 Feb 2026 19:22:51 +0000
Subject: [PATCH 07/58] Add GetNamedPipeInfo reference and fix Unix test
attribute
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../src/System.Private.CoreLib.Shared.projitems | 3 +++
.../SafeFileHandle/GetFileType.Unix.cs | 2 +-
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
index 41890be47fba0c..b4f043fd3e3063 100644
--- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
+++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
@@ -1954,6 +1954,9 @@
Common\Interop\Windows\Kernel32\Interop.GetFileType_SafeHandle.cs
+
+ Common\Interop\Windows\Kernel32\Interop.GetNamedPipeInfo.cs
+
Common\Interop\Windows\Kernel32\Interop.GetFinalPathNameByHandle.cs
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs
index 385df3f381000d..197e19c38acf5d 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs
@@ -22,7 +22,7 @@ public void GetFileType_Directory()
Assert.Equal(FileType.Directory, handle.GetFileType());
}
- [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsSubsystemForLinux))]
+ [Fact]
public void GetFileType_NamedPipe()
{
string pipePath = GetTestFilePath();
From 98347983793beb300a3141e01bea76f397d92153 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 19 Feb 2026 10:00:43 +0000
Subject: [PATCH 08/58] Add Unix and Windows Interop references to fix test
build errors
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../System.IO.FileSystem.Tests.csproj | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj
index ee394f212c3f9c..f9cbc5f387f21a 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj
@@ -101,6 +101,10 @@
+
+
@@ -244,6 +248,7 @@
+
From ef2da05b9263d18f132722bab4092d66af161022 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 19 Feb 2026 16:24:38 +0000
Subject: [PATCH 09/58] Fix Unix test build errors: use Open correctly, add
Task using, add MkFifo
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../SafeFileHandle/GetFileType.Unix.cs | 12 +++++-------
.../System.IO.FileSystem.Tests.csproj | 2 ++
2 files changed, 7 insertions(+), 7 deletions(-)
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs
index 197e19c38acf5d..35069ebfaf28b8 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs
@@ -3,6 +3,7 @@
using Microsoft.DotNet.XUnitExtensions;
using Microsoft.Win32.SafeHandles;
+using System.Threading.Tasks;
using Xunit;
namespace System.IO.Tests
@@ -16,8 +17,7 @@ public void GetFileType_Directory()
string path = GetTestFilePath();
Directory.CreateDirectory(path);
- IntPtr fd = Interop.Sys.Open(path, Interop.Sys.OpenFlags.O_RDONLY, 0);
- using SafeFileHandle handle = new SafeFileHandle(fd, ownsHandle: true);
+ using SafeFileHandle handle = Interop.Sys.Open(path, Interop.Sys.OpenFlags.O_RDONLY, 0);
Assert.False(handle.IsInvalid);
Assert.Equal(FileType.Directory, handle.GetFileType());
}
@@ -48,8 +48,7 @@ public void GetFileType_SymbolicLink()
File.WriteAllText(targetPath, "test");
File.CreateSymbolicLink(linkPath, targetPath);
- IntPtr fd = Interop.Sys.Open(linkPath, Interop.Sys.OpenFlags.O_RDONLY | Interop.Sys.OpenFlags.O_NOFOLLOW, 0);
- using SafeFileHandle handle = new SafeFileHandle(fd, ownsHandle: true);
+ using SafeFileHandle handle = Interop.Sys.Open(linkPath, Interop.Sys.OpenFlags.O_RDONLY | Interop.Sys.OpenFlags.O_NOFOLLOW, 0);
if (!handle.IsInvalid)
{
@@ -80,13 +79,12 @@ public void GetFileType_BlockDevice()
try
{
- IntPtr fd = Interop.Sys.Open(blockDevice, Interop.Sys.OpenFlags.O_RDONLY, 0);
- if (fd == (IntPtr)(-1))
+ using SafeFileHandle handle = Interop.Sys.Open(blockDevice, Interop.Sys.OpenFlags.O_RDONLY, 0);
+ if (handle.IsInvalid)
{
throw new SkipTestException($"Could not open {blockDevice}");
}
- using SafeFileHandle handle = new SafeFileHandle(fd, ownsHandle: true);
Assert.Equal(FileType.BlockDevice, handle.GetFileType());
}
catch (UnauthorizedAccessException)
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj
index f9cbc5f387f21a..19a3719f682d97 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/System.IO.FileSystem.Tests.csproj
@@ -105,6 +105,8 @@
Link="Common\Interop\Unix\Interop.Open.cs" />
+
From 28ec926560ecf27da13d322d7b699ba7e58d61f5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 19 Feb 2026 17:29:42 +0000
Subject: [PATCH 10/58] Remove invalid ConditionalFact from Windows NamedPipe
test
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../SafeFileHandle/GetFileType.Windows.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs
index 6a161a4fd69fbd..0b45b1d547dcc1 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs
@@ -31,7 +31,7 @@ public void GetFileType_Directory()
Assert.Equal(FileType.Directory, handle.GetFileType());
}
- [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsSubsystemForLinux))]
+ [Fact]
public void GetFileType_NamedPipe()
{
string pipeName = Path.GetRandomFileName();
From 58bdc9b55a779daca7e112efd7af189ad376aa78 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 26 Feb 2026 10:51:39 +0000
Subject: [PATCH 11/58] Fix Windows and OS-agnostic test build errors and test
failures
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../SafeFileHandle/GetFileType.Windows.cs | 8 +++++---
.../SafeFileHandle/GetFileType.cs | 4 +++-
2 files changed, 8 insertions(+), 4 deletions(-)
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs
index 0b45b1d547dcc1..96a4c971b69e4e 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs
@@ -4,6 +4,7 @@
using Microsoft.DotNet.XUnitExtensions;
using Microsoft.Win32.SafeHandles;
using System.IO.Pipes;
+using System.Threading.Tasks;
using Xunit;
namespace System.IO.Tests
@@ -12,7 +13,7 @@ namespace System.IO.Tests
public class SafeFileHandle_GetFileType_Windows : FileSystemTest
{
[Fact]
- public void GetFileType_Directory()
+ public unsafe void GetFileType_Directory()
{
string path = GetTestFilePath();
Directory.CreateDirectory(path);
@@ -55,7 +56,8 @@ public void GetFileType_ConsoleInput()
{
if (!Console.IsInputRedirected)
{
- using SafeFileHandle handle = new SafeFileHandle(Console.OpenStandardInput().SafeFileHandle.DangerousGetHandle(), ownsHandle: false);
+ using FileStream consoleStream = Console.OpenStandardInput();
+ using SafeFileHandle handle = new SafeFileHandle(consoleStream.SafeFileHandle.DangerousGetHandle(), ownsHandle: false);
FileType type = handle.GetFileType();
Assert.True(type == FileType.CharacterDevice || type == FileType.Pipe || type == FileType.RegularFile,
@@ -64,7 +66,7 @@ public void GetFileType_ConsoleInput()
}
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))]
- public void GetFileType_SymbolicLink()
+ public unsafe void GetFileType_SymbolicLink()
{
string targetPath = GetTestFilePath();
string linkPath = GetTestFilePath();
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs
index 9cf46dfdeb9cc0..67ee2e5b0fca1d 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs
@@ -29,6 +29,7 @@ public void GetFileType_NullDevice()
}
[Fact]
+ [SkipOnPlatform(TestPlatforms.Browser, "System.IO.Pipes aren't supported on browser")]
public void GetFileType_AnonymousPipe()
{
using AnonymousPipeServerStream server = new(PipeDirection.Out);
@@ -41,6 +42,7 @@ public void GetFileType_AnonymousPipe()
}
[Fact]
+ [SkipOnPlatform(TestPlatforms.Browser, "System.Net.Sockets aren't supported on browser")]
public void GetFileType_Socket()
{
using Socket listener = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
@@ -62,7 +64,7 @@ public void GetFileType_Socket()
[Fact]
public void GetFileType_ClosedHandle_ThrowsObjectDisposedException()
{
- SafeFileHandle handle = File.OpenHandle(GetTestFilePath(), FileMode.Create);
+ SafeFileHandle handle = File.OpenHandle(GetTestFilePath(), FileMode.Create, FileAccess.Write);
handle.Dispose();
Assert.Throws(() => handle.GetFileType());
From ec48f9104f599bd83db151e2fece3166a7e1820a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 26 Feb 2026 11:04:12 +0000
Subject: [PATCH 12/58] Use Console.OpenStandardInputHandle for direct handle
access
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../SafeFileHandle/GetFileType.Windows.cs | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs
index 96a4c971b69e4e..74bec293047af4 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs
@@ -56,8 +56,7 @@ public void GetFileType_ConsoleInput()
{
if (!Console.IsInputRedirected)
{
- using FileStream consoleStream = Console.OpenStandardInput();
- using SafeFileHandle handle = new SafeFileHandle(consoleStream.SafeFileHandle.DangerousGetHandle(), ownsHandle: false);
+ using SafeFileHandle handle = Console.OpenStandardInputHandle();
FileType type = handle.GetFileType();
Assert.True(type == FileType.CharacterDevice || type == FileType.Pipe || type == FileType.RegularFile,
From 3dc635a274614b85a66e40af2bd36c401c716c4a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 26 Feb 2026 12:21:51 +0000
Subject: [PATCH 13/58] Use CreateFile correctly - returns SafeFileHandle,
remove unsafe modifiers
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../SafeFileHandle/GetFileType.Windows.cs | 10 ++++------
1 file changed, 4 insertions(+), 6 deletions(-)
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs
index 74bec293047af4..ee3a62e92bdb21 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs
@@ -13,12 +13,12 @@ namespace System.IO.Tests
public class SafeFileHandle_GetFileType_Windows : FileSystemTest
{
[Fact]
- public unsafe void GetFileType_Directory()
+ public void GetFileType_Directory()
{
string path = GetTestFilePath();
Directory.CreateDirectory(path);
- IntPtr hFile = Interop.Kernel32.CreateFile(
+ using SafeFileHandle handle = Interop.Kernel32.CreateFile(
path,
Interop.Kernel32.GenericOperations.GENERIC_READ,
FileShare.ReadWrite,
@@ -27,7 +27,6 @@ public unsafe void GetFileType_Directory()
Interop.Kernel32.FileOperations.FILE_FLAG_BACKUP_SEMANTICS,
IntPtr.Zero);
- using SafeFileHandle handle = new SafeFileHandle(hFile, ownsHandle: true);
Assert.False(handle.IsInvalid);
Assert.Equal(FileType.Directory, handle.GetFileType());
}
@@ -65,14 +64,14 @@ public void GetFileType_ConsoleInput()
}
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))]
- public unsafe void GetFileType_SymbolicLink()
+ public void GetFileType_SymbolicLink()
{
string targetPath = GetTestFilePath();
string linkPath = GetTestFilePath();
File.WriteAllText(targetPath, "test");
File.CreateSymbolicLink(linkPath, targetPath);
- IntPtr hFile = Interop.Kernel32.CreateFile(
+ using SafeFileHandle handle = Interop.Kernel32.CreateFile(
linkPath,
Interop.Kernel32.GenericOperations.GENERIC_READ,
FileShare.ReadWrite,
@@ -81,7 +80,6 @@ public unsafe void GetFileType_SymbolicLink()
Interop.Kernel32.FileOperations.FILE_FLAG_OPEN_REPARSE_POINT | Interop.Kernel32.FileOperations.FILE_FLAG_BACKUP_SEMANTICS,
IntPtr.Zero);
- using SafeFileHandle handle = new SafeFileHandle(hFile, ownsHandle: true);
if (!handle.IsInvalid)
{
Assert.Equal(FileType.SymbolicLink, handle.GetFileType());
From 2b662b9806944a73a7d0b7bc84a7e3a753843f3d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 26 Feb 2026 14:31:06 +0000
Subject: [PATCH 14/58] Add unsafe modifiers back - required for CreateFile
with null pointer
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../SafeFileHandle/GetFileType.Windows.cs | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs
index ee3a62e92bdb21..c280dbe6461654 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs
@@ -13,7 +13,7 @@ namespace System.IO.Tests
public class SafeFileHandle_GetFileType_Windows : FileSystemTest
{
[Fact]
- public void GetFileType_Directory()
+ public unsafe void GetFileType_Directory()
{
string path = GetTestFilePath();
Directory.CreateDirectory(path);
@@ -64,7 +64,7 @@ public void GetFileType_ConsoleInput()
}
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsPrivilegedProcess))]
- public void GetFileType_SymbolicLink()
+ public unsafe void GetFileType_SymbolicLink()
{
string targetPath = GetTestFilePath();
string linkPath = GetTestFilePath();
From db1394972c48a6c77d4fd9bfa711f9db15394b6e Mon Sep 17 00:00:00 2001
From: Adam Sitnik
Date: Tue, 3 Mar 2026 20:09:25 +0100
Subject: [PATCH 15/58] Apply suggestions from code review
---
.../src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs | 6 +-----
.../SafeFileHandle/GetFileType.Unix.cs | 4 ++--
2 files changed, 3 insertions(+), 7 deletions(-)
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
index 261307a4c57365..a9547bd72dfc88 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
@@ -518,9 +518,7 @@ internal System.IO.FileType GetFileTypeCore()
}
private static System.IO.FileType MapUnixFileTypeToFileType(int unixFileType)
- {
-#pragma warning disable CA1416 // BlockDevice is only returned on Unix platforms
- return unixFileType switch
+ => unixFileType switch
{
Interop.Sys.FileTypes.S_IFREG => System.IO.FileType.RegularFile,
Interop.Sys.FileTypes.S_IFDIR => System.IO.FileType.Directory,
@@ -531,8 +529,6 @@ private static System.IO.FileType MapUnixFileTypeToFileType(int unixFileType)
Interop.Sys.FileTypes.S_IFBLK => System.IO.FileType.BlockDevice,
_ => System.IO.FileType.Unknown
};
-#pragma warning restore CA1416
- }
internal long GetFileLength()
{
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs
index 35069ebfaf28b8..f7de737481f2a8 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs
@@ -23,7 +23,7 @@ public void GetFileType_Directory()
}
[Fact]
- public void GetFileType_NamedPipe()
+ public async Task GetFileType_NamedPipe()
{
string pipePath = GetTestFilePath();
Assert.Equal(0, Interop.Sys.MkFifo(pipePath, (int)UnixFileMode.UserRead | (int)UnixFileMode.UserWrite));
@@ -37,7 +37,7 @@ public void GetFileType_NamedPipe()
using SafeFileHandle writer = File.OpenHandle(pipePath, FileMode.Open, FileAccess.Write);
Assert.Equal(FileType.Pipe, writer.GetFileType());
- readerTask.Wait();
+ await readerTask;
}
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))]
From 785e5c436e096b4c55e739e6f2886d0dc0bf7691 Mon Sep 17 00:00:00 2001
From: Adam Sitnik
Date: Tue, 3 Mar 2026 20:11:57 +0100
Subject: [PATCH 16/58] Apply suggestion from @adamsitnik
---
src/libraries/System.Private.CoreLib/src/System/IO/FileType.cs | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileType.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileType.cs
index 3f4290c2286388..5f5c6209a21636 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/FileType.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileType.cs
@@ -46,7 +46,6 @@ public enum FileType
///
/// The file is a block device.
///
- [System.Runtime.Versioning.UnsupportedOSPlatform("windows")]
BlockDevice
}
}
From 06c24d39cae02d8712a2b41d865b368194f2981f Mon Sep 17 00:00:00 2001
From: Adam Sitnik
Date: Tue, 3 Mar 2026 20:21:51 +0100
Subject: [PATCH 17/58] Remove UnsupportedOSPlatformAttribute from BlockDevice
---
src/libraries/System.Runtime/ref/System.Runtime.cs | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs
index 14d13061412189..2be3d670d87c8d 100644
--- a/src/libraries/System.Runtime/ref/System.Runtime.cs
+++ b/src/libraries/System.Runtime/ref/System.Runtime.cs
@@ -10455,7 +10455,6 @@ public enum FileType
CharacterDevice = 4,
Directory = 5,
SymbolicLink = 6,
- [System.Runtime.Versioning.UnsupportedOSPlatformAttribute("windows")]
BlockDevice = 7,
}
public sealed partial class FileInfo : System.IO.FileSystemInfo
From 18f12c0db04cb4235f9d3a8e6d8b74a4394c3b6d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Mar 2026 19:54:38 +0000
Subject: [PATCH 18/58] Rename FileType to FileHandleType and change
GetFileType() to Type property
- Renamed enum System.IO.FileType to System.IO.FileHandleType
- Changed SafeFileHandle.GetFileType() method to Type property
- Updated all references in implementation and tests
- Updated project file references
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../Win32/SafeHandles/SafeFileHandle.Unix.cs | 24 ++++++++---------
.../SafeHandles/SafeFileHandle.Windows.cs | 26 +++++++++----------
.../Win32/SafeHandles/SafeFileHandle.cs | 11 +++++---
.../System.Private.CoreLib.Shared.projitems | 2 +-
.../IO/{FileType.cs => FileHandleType.cs} | 2 +-
.../System.Runtime/ref/System.Runtime.cs | 4 +--
.../SafeFileHandle/GetFileType.Unix.cs | 12 ++++-----
.../SafeFileHandle/GetFileType.Windows.cs | 16 ++++++------
.../SafeFileHandle/GetFileType.cs | 24 ++++++++---------
9 files changed, 62 insertions(+), 59 deletions(-)
rename src/libraries/System.Private.CoreLib/src/System/IO/{FileType.cs => FileHandleType.cs} (97%)
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
index a9547bd72dfc88..b370f76ff32cd5 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
@@ -497,12 +497,12 @@ private bool GetCanSeek()
return canSeek == NullableBool.True;
}
- internal System.IO.FileType GetFileTypeCore()
+ internal System.IO.FileHandleType GetFileTypeCore()
{
int cachedType = _cachedFileType;
if (cachedType != -1)
{
- return (System.IO.FileType)cachedType;
+ return (System.IO.FileHandleType)cachedType;
}
// If we don't have a cached value, call FStat to get it
@@ -512,22 +512,22 @@ internal System.IO.FileType GetFileTypeCore()
throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo());
}
- System.IO.FileType fileType = MapUnixFileTypeToFileType(status.Mode & Interop.Sys.FileTypes.S_IFMT);
+ System.IO.FileHandleType fileType = MapUnixFileTypeToFileType(status.Mode & Interop.Sys.FileTypes.S_IFMT);
_cachedFileType = (int)fileType;
return fileType;
}
- private static System.IO.FileType MapUnixFileTypeToFileType(int unixFileType)
+ private static System.IO.FileHandleType MapUnixFileTypeToFileType(int unixFileType)
=> unixFileType switch
{
- Interop.Sys.FileTypes.S_IFREG => System.IO.FileType.RegularFile,
- Interop.Sys.FileTypes.S_IFDIR => System.IO.FileType.Directory,
- Interop.Sys.FileTypes.S_IFLNK => System.IO.FileType.SymbolicLink,
- Interop.Sys.FileTypes.S_IFIFO => System.IO.FileType.Pipe,
- Interop.Sys.FileTypes.S_IFSOCK => System.IO.FileType.Socket,
- Interop.Sys.FileTypes.S_IFCHR => System.IO.FileType.CharacterDevice,
- Interop.Sys.FileTypes.S_IFBLK => System.IO.FileType.BlockDevice,
- _ => System.IO.FileType.Unknown
+ Interop.Sys.FileTypes.S_IFREG => System.IO.FileHandleType.RegularFile,
+ Interop.Sys.FileTypes.S_IFDIR => System.IO.FileHandleType.Directory,
+ Interop.Sys.FileTypes.S_IFLNK => System.IO.FileHandleType.SymbolicLink,
+ Interop.Sys.FileTypes.S_IFIFO => System.IO.FileHandleType.Pipe,
+ Interop.Sys.FileTypes.S_IFSOCK => System.IO.FileHandleType.Socket,
+ Interop.Sys.FileTypes.S_IFCHR => System.IO.FileHandleType.CharacterDevice,
+ Interop.Sys.FileTypes.S_IFBLK => System.IO.FileHandleType.BlockDevice,
+ _ => System.IO.FileHandleType.Unknown
};
internal long GetFileLength()
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
index 9680ac6fe25d86..4da30f0a2c95d7 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
@@ -25,7 +25,7 @@ public SafeFileHandle() : base(true)
internal bool IsNoBuffering => (GetFileOptions() & NoBuffering) != 0;
- internal bool CanSeek => !IsClosed && GetFileType() == System.IO.FileType.RegularFile;
+ internal bool CanSeek => !IsClosed && Type == System.IO.FileHandleType.RegularFile;
internal ThreadPoolBoundHandle? ThreadPoolBinding { get; set; }
@@ -253,49 +253,49 @@ internal unsafe FileOptions GetFileOptions()
return _fileOptions = result;
}
- internal unsafe System.IO.FileType GetFileTypeCore()
+ internal unsafe System.IO.FileHandleType GetFileTypeCore()
{
int cachedType = _cachedFileType;
if (cachedType != -1)
{
- return (System.IO.FileType)cachedType;
+ return (System.IO.FileHandleType)cachedType;
}
int kernelFileType = Interop.Kernel32.GetFileType(this);
- System.IO.FileType result = kernelFileType switch
+ System.IO.FileHandleType result = kernelFileType switch
{
- Interop.Kernel32.FileTypes.FILE_TYPE_CHAR => System.IO.FileType.CharacterDevice,
+ Interop.Kernel32.FileTypes.FILE_TYPE_CHAR => System.IO.FileHandleType.CharacterDevice,
Interop.Kernel32.FileTypes.FILE_TYPE_PIPE => GetPipeOrSocketType(),
Interop.Kernel32.FileTypes.FILE_TYPE_DISK => GetDiskBasedType(),
- _ => System.IO.FileType.Unknown
+ _ => System.IO.FileHandleType.Unknown
};
_cachedFileType = (int)result;
return result;
}
- private unsafe System.IO.FileType GetPipeOrSocketType()
+ private unsafe System.IO.FileHandleType GetPipeOrSocketType()
{
// Try to call GetNamedPipeInfo to determine if it's a pipe or socket
uint flags;
if (Interop.Kernel32.GetNamedPipeInfo(this, &flags, null, null, null))
{
- return System.IO.FileType.Pipe;
+ return System.IO.FileHandleType.Pipe;
}
// If GetNamedPipeInfo fails, it's likely a socket
- return System.IO.FileType.Socket;
+ return System.IO.FileHandleType.Socket;
}
- private unsafe System.IO.FileType GetDiskBasedType()
+ private unsafe System.IO.FileHandleType GetDiskBasedType()
{
// First check if it's a directory using GetFileInformationByHandle
if (Interop.Kernel32.GetFileInformationByHandle(this, out Interop.Kernel32.BY_HANDLE_FILE_INFORMATION fileInfo))
{
if ((fileInfo.dwFileAttributes & Interop.Kernel32.FileAttributes.FILE_ATTRIBUTE_DIRECTORY) != 0)
{
- return System.IO.FileType.Directory;
+ return System.IO.FileHandleType.Directory;
}
}
@@ -305,11 +305,11 @@ private unsafe System.IO.FileType GetDiskBasedType()
{
if ((basicInfo.FileAttributes & Interop.Kernel32.FileAttributes.FILE_ATTRIBUTE_REPARSE_POINT) != 0)
{
- return System.IO.FileType.SymbolicLink;
+ return System.IO.FileHandleType.SymbolicLink;
}
}
- return System.IO.FileType.RegularFile;
+ return System.IO.FileHandleType.RegularFile;
}
internal long GetFileLength()
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
index 493720deb4b896..0f036d8ac2d7bb 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
@@ -25,12 +25,15 @@ public SafeFileHandle(IntPtr preexistingHandle, bool ownsHandle) : base(ownsHand
///
/// Gets the type of the file that this handle represents.
///
- /// The type of the file.
+ /// The type of the file.
/// The handle is closed.
- public System.IO.FileType GetFileType()
+ public System.IO.FileHandleType Type
{
- ObjectDisposedException.ThrowIf(IsClosed, this);
- return GetFileTypeCore();
+ get
+ {
+ ObjectDisposedException.ThrowIf(IsClosed, this);
+ return GetFileTypeCore();
+ }
}
}
}
diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
index b4f043fd3e3063..d8d2252e8b7b6f 100644
--- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
+++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
@@ -514,7 +514,7 @@
-
+
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileType.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileHandleType.cs
similarity index 97%
rename from src/libraries/System.Private.CoreLib/src/System/IO/FileType.cs
rename to src/libraries/System.Private.CoreLib/src/System/IO/FileHandleType.cs
index 5f5c6209a21636..16be22f4d7d7dd 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/FileType.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileHandleType.cs
@@ -6,7 +6,7 @@ namespace System.IO
///
/// Specifies the type of a file.
///
- public enum FileType
+ public enum FileHandleType
{
///
/// The file type is unknown.
diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs
index 2be3d670d87c8d..d14d54b71e0e4f 100644
--- a/src/libraries/System.Runtime/ref/System.Runtime.cs
+++ b/src/libraries/System.Runtime/ref/System.Runtime.cs
@@ -22,7 +22,7 @@ public SafeFileHandle() : base (default(bool)) { }
public SafeFileHandle(System.IntPtr preexistingHandle, bool ownsHandle) : base (default(bool)) { }
public override bool IsInvalid { get { throw null; } }
public bool IsAsync { get { throw null; } }
- public System.IO.FileType GetFileType() { throw null; }
+ public System.IO.FileHandleType Type { get { throw null; } }
protected override bool ReleaseHandle() { throw null; }
}
public abstract partial class SafeHandleMinusOneIsInvalid : System.Runtime.InteropServices.SafeHandle
@@ -10446,7 +10446,7 @@ public enum FileAttributes
IntegrityStream = 32768,
NoScrubData = 131072,
}
- public enum FileType
+ public enum FileHandleType
{
Unknown = 0,
RegularFile = 1,
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs
index f7de737481f2a8..cf877c45d21b5d 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs
@@ -19,7 +19,7 @@ public void GetFileType_Directory()
using SafeFileHandle handle = Interop.Sys.Open(path, Interop.Sys.OpenFlags.O_RDONLY, 0);
Assert.False(handle.IsInvalid);
- Assert.Equal(FileType.Directory, handle.GetFileType());
+ Assert.Equal(FileHandleType.Directory, handle.Type);
}
[Fact]
@@ -31,11 +31,11 @@ public async Task GetFileType_NamedPipe()
Task readerTask = Task.Run(() =>
{
using SafeFileHandle reader = File.OpenHandle(pipePath, FileMode.Open, FileAccess.Read);
- Assert.Equal(FileType.Pipe, reader.GetFileType());
+ Assert.Equal(FileHandleType.Pipe, reader.Type);
});
using SafeFileHandle writer = File.OpenHandle(pipePath, FileMode.Open, FileAccess.Write);
- Assert.Equal(FileType.Pipe, writer.GetFileType());
+ Assert.Equal(FileHandleType.Pipe, writer.Type);
await readerTask;
}
@@ -49,10 +49,10 @@ public void GetFileType_SymbolicLink()
File.CreateSymbolicLink(linkPath, targetPath);
using SafeFileHandle handle = Interop.Sys.Open(linkPath, Interop.Sys.OpenFlags.O_RDONLY | Interop.Sys.OpenFlags.O_NOFOLLOW, 0);
-
+
if (!handle.IsInvalid)
{
- Assert.Equal(FileType.SymbolicLink, handle.GetFileType());
+ Assert.Equal(FileHandleType.SymbolicLink, handle.Type);
}
}
@@ -85,7 +85,7 @@ public void GetFileType_BlockDevice()
throw new SkipTestException($"Could not open {blockDevice}");
}
- Assert.Equal(FileType.BlockDevice, handle.GetFileType());
+ Assert.Equal(FileHandleType.BlockDevice, handle.Type);
}
catch (UnauthorizedAccessException)
{
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs
index c280dbe6461654..21ee6148830e2e 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs
@@ -28,7 +28,7 @@ public unsafe void GetFileType_Directory()
IntPtr.Zero);
Assert.False(handle.IsInvalid);
- Assert.Equal(FileType.Directory, handle.GetFileType());
+ Assert.Equal(FileHandleType.Directory, handle.Type);
}
[Fact]
@@ -36,7 +36,7 @@ public void GetFileType_NamedPipe()
{
string pipeName = Path.GetRandomFileName();
using NamedPipeServerStream server = new NamedPipeServerStream(pipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
-
+
Task serverTask = Task.Run(async () => await server.WaitForConnectionAsync());
using NamedPipeClientStream client = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.None);
@@ -44,10 +44,10 @@ public void GetFileType_NamedPipe()
serverTask.Wait();
using SafeFileHandle serverHandle = new SafeFileHandle(server.SafePipeHandle.DangerousGetHandle(), ownsHandle: false);
- Assert.Equal(FileType.Pipe, serverHandle.GetFileType());
+ Assert.Equal(FileHandleType.Pipe, serverHandle.Type);
using SafeFileHandle clientHandle = new SafeFileHandle(client.SafePipeHandle.DangerousGetHandle(), ownsHandle: false);
- Assert.Equal(FileType.Pipe, clientHandle.GetFileType());
+ Assert.Equal(FileHandleType.Pipe, clientHandle.Type);
}
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))]
@@ -56,9 +56,9 @@ public void GetFileType_ConsoleInput()
if (!Console.IsInputRedirected)
{
using SafeFileHandle handle = Console.OpenStandardInputHandle();
- FileType type = handle.GetFileType();
-
- Assert.True(type == FileType.CharacterDevice || type == FileType.Pipe || type == FileType.RegularFile,
+ FileHandleType type = handle.Type;
+
+ Assert.True(type == FileHandleType.CharacterDevice || type == FileHandleType.Pipe || type == FileHandleType.RegularFile,
$"Expected CharacterDevice, Pipe, or RegularFile but got {type}");
}
}
@@ -82,7 +82,7 @@ public unsafe void GetFileType_SymbolicLink()
if (!handle.IsInvalid)
{
- Assert.Equal(FileType.SymbolicLink, handle.GetFileType());
+ Assert.Equal(FileHandleType.SymbolicLink, handle.Type);
}
}
}
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs
index 67ee2e5b0fca1d..84098ba021efc1 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs
@@ -18,14 +18,14 @@ public void GetFileType_RegularFile()
File.WriteAllText(path, "test");
using SafeFileHandle handle = File.OpenHandle(path, FileMode.Open, FileAccess.Read);
- Assert.Equal(FileType.RegularFile, handle.GetFileType());
+ Assert.Equal(FileHandleType.RegularFile, handle.Type);
}
[Fact]
public void GetFileType_NullDevice()
{
using SafeFileHandle handle = File.OpenNullHandle();
- Assert.Equal(FileType.CharacterDevice, handle.GetFileType());
+ Assert.Equal(FileHandleType.CharacterDevice, handle.Type);
}
[Fact]
@@ -34,11 +34,11 @@ public void GetFileType_AnonymousPipe()
{
using AnonymousPipeServerStream server = new(PipeDirection.Out);
using SafeFileHandle serverHandle = new SafeFileHandle(server.SafePipeHandle.DangerousGetHandle(), ownsHandle: false);
-
- Assert.Equal(FileType.Pipe, serverHandle.GetFileType());
+
+ Assert.Equal(FileHandleType.Pipe, serverHandle.Type);
using SafeFileHandle clientHandle = new SafeFileHandle(server.ClientSafePipeHandle.DangerousGetHandle(), ownsHandle: false);
- Assert.Equal(FileType.Pipe, clientHandle.GetFileType());
+ Assert.Equal(FileHandleType.Pipe, clientHandle.Type);
}
[Fact]
@@ -57,8 +57,8 @@ public void GetFileType_Socket()
using SafeFileHandle serverHandle = new SafeFileHandle(server.Handle, ownsHandle: false);
using SafeFileHandle clientHandle = new SafeFileHandle(client.Handle, ownsHandle: false);
- Assert.Equal(FileType.Socket, serverHandle.GetFileType());
- Assert.Equal(FileType.Socket, clientHandle.GetFileType());
+ Assert.Equal(FileHandleType.Socket, serverHandle.Type);
+ Assert.Equal(FileHandleType.Socket, clientHandle.Type);
}
[Fact]
@@ -67,7 +67,7 @@ public void GetFileType_ClosedHandle_ThrowsObjectDisposedException()
SafeFileHandle handle = File.OpenHandle(GetTestFilePath(), FileMode.Create, FileAccess.Write);
handle.Dispose();
- Assert.Throws(() => handle.GetFileType());
+ Assert.Throws(() => handle.Type);
}
[Fact]
@@ -77,12 +77,12 @@ public void GetFileType_CachesResult()
File.WriteAllText(path, "test");
using SafeFileHandle handle = File.OpenHandle(path, FileMode.Open, FileAccess.Read);
-
- FileType firstCall = handle.GetFileType();
- FileType secondCall = handle.GetFileType();
+
+ FileHandleType firstCall = handle.Type;
+ FileHandleType secondCall = handle.Type;
Assert.Equal(firstCall, secondCall);
- Assert.Equal(FileType.RegularFile, firstCall);
+ Assert.Equal(FileHandleType.RegularFile, firstCall);
}
}
}
From 760a45203dc079251b09dd73942c1845bd36bfcc Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Mar 2026 21:00:04 +0000
Subject: [PATCH 19/58] Move caching logic to Type property to eliminate
duplication
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../Win32/SafeHandles/SafeFileHandle.Unix.cs | 11 +----------
.../Win32/SafeHandles/SafeFileHandle.Windows.cs | 12 +-----------
.../Microsoft/Win32/SafeHandles/SafeFileHandle.cs | 9 ++++++++-
3 files changed, 10 insertions(+), 22 deletions(-)
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
index b370f76ff32cd5..93c472ecb915c0 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
@@ -499,22 +499,13 @@ private bool GetCanSeek()
internal System.IO.FileHandleType GetFileTypeCore()
{
- int cachedType = _cachedFileType;
- if (cachedType != -1)
- {
- return (System.IO.FileHandleType)cachedType;
- }
-
- // If we don't have a cached value, call FStat to get it
int result = Interop.Sys.FStat(this, out Interop.Sys.FileStatus status);
if (result != 0)
{
throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo());
}
- System.IO.FileHandleType fileType = MapUnixFileTypeToFileType(status.Mode & Interop.Sys.FileTypes.S_IFMT);
- _cachedFileType = (int)fileType;
- return fileType;
+ return MapUnixFileTypeToFileType(status.Mode & Interop.Sys.FileTypes.S_IFMT);
}
private static System.IO.FileHandleType MapUnixFileTypeToFileType(int unixFileType)
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
index 4da30f0a2c95d7..f8798c9ec513ac 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
@@ -255,24 +255,14 @@ internal unsafe FileOptions GetFileOptions()
internal unsafe System.IO.FileHandleType GetFileTypeCore()
{
- int cachedType = _cachedFileType;
- if (cachedType != -1)
- {
- return (System.IO.FileHandleType)cachedType;
- }
-
int kernelFileType = Interop.Kernel32.GetFileType(this);
-
- System.IO.FileHandleType result = kernelFileType switch
+ return kernelFileType switch
{
Interop.Kernel32.FileTypes.FILE_TYPE_CHAR => System.IO.FileHandleType.CharacterDevice,
Interop.Kernel32.FileTypes.FILE_TYPE_PIPE => GetPipeOrSocketType(),
Interop.Kernel32.FileTypes.FILE_TYPE_DISK => GetDiskBasedType(),
_ => System.IO.FileHandleType.Unknown
};
-
- _cachedFileType = (int)result;
- return result;
}
private unsafe System.IO.FileHandleType GetPipeOrSocketType()
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
index 0f036d8ac2d7bb..0a13a420cff8e0 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
@@ -32,7 +32,14 @@ public System.IO.FileHandleType Type
get
{
ObjectDisposedException.ThrowIf(IsClosed, this);
- return GetFileTypeCore();
+
+ int cachedType = _cachedFileType;
+ if (cachedType == -1)
+ {
+ cachedType = _cachedFileType = (int)GetFileTypeCore();
+ }
+
+ return (System.IO.FileHandleType)cachedType;
}
}
}
From a30ca8ec7cb3f82ff61db3345b4bd9d125c4996c Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Mar 2026 21:18:28 +0000
Subject: [PATCH 20/58] Remove trailing comma from BlockDevice enum value in
ref assembly
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
src/libraries/System.Runtime/ref/System.Runtime.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs
index d14d54b71e0e4f..8ca5183387e7e7 100644
--- a/src/libraries/System.Runtime/ref/System.Runtime.cs
+++ b/src/libraries/System.Runtime/ref/System.Runtime.cs
@@ -10455,7 +10455,7 @@ public enum FileHandleType
CharacterDevice = 4,
Directory = 5,
SymbolicLink = 6,
- BlockDevice = 7,
+ BlockDevice = 7
}
public sealed partial class FileInfo : System.IO.FileSystemInfo
{
From b025b70ff97ebae69d8eda7eab10b085250d76c9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 3 Mar 2026 22:26:23 +0000
Subject: [PATCH 21/58] Address code review feedback - improve error handling
and correctness
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../Win32/SafeHandles/SafeFileHandle.Unix.cs | 3 +-
.../SafeHandles/SafeFileHandle.Windows.cs | 39 ++++++++++++++-----
.../SafeFileHandle/GetFileType.Unix.cs | 6 +--
3 files changed, 35 insertions(+), 13 deletions(-)
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
index 93c472ecb915c0..29590491f043aa 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
@@ -502,7 +502,8 @@ internal System.IO.FileHandleType GetFileTypeCore()
int result = Interop.Sys.FStat(this, out Interop.Sys.FileStatus status);
if (result != 0)
{
- throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo());
+ Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo();
+ throw Interop.GetExceptionForIoErrno(error, Path);
}
return MapUnixFileTypeToFileType(status.Mode & Interop.Sys.FileTypes.S_IFMT);
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
index f8798c9ec513ac..8595d221b4490d 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
@@ -274,8 +274,16 @@ private unsafe System.IO.FileHandleType GetPipeOrSocketType()
return System.IO.FileHandleType.Pipe;
}
- // If GetNamedPipeInfo fails, it's likely a socket
- return System.IO.FileHandleType.Socket;
+ // GetNamedPipeInfo failed - check the error code
+ int error = Marshal.GetLastPInvokeError();
+ if (error == Interop.Errors.ERROR_INVALID_HANDLE)
+ {
+ // ERROR_INVALID_HANDLE means this is a socket (pipes return true, sockets return this error)
+ return System.IO.FileHandleType.Socket;
+ }
+
+ // Unexpected error - throw
+ throw new Win32Exception(error);
}
private unsafe System.IO.FileHandleType GetDiskBasedType()
@@ -287,21 +295,34 @@ private unsafe System.IO.FileHandleType GetDiskBasedType()
{
return System.IO.FileHandleType.Directory;
}
- }
- // Check if it's a reparse point (symbolic link) using GetFileInformationByHandleEx
- Interop.Kernel32.FILE_BASIC_INFO basicInfo;
- if (Interop.Kernel32.GetFileInformationByHandleEx(this, Interop.Kernel32.FileBasicInfo, &basicInfo, (uint)sizeof(Interop.Kernel32.FILE_BASIC_INFO)))
- {
- if ((basicInfo.FileAttributes & Interop.Kernel32.FileAttributes.FILE_ATTRIBUTE_REPARSE_POINT) != 0)
+ // Check if it's a reparse point - only symlinks should return SymbolicLink
+ if ((fileInfo.dwFileAttributes & Interop.Kernel32.FileAttributes.FILE_ATTRIBUTE_REPARSE_POINT) != 0)
{
- return System.IO.FileHandleType.SymbolicLink;
+ // Check the reparse tag to distinguish symlinks from other reparse points (junctions, mount points, etc.)
+ const int FileAttributeTagInfo = 35; // FileAttributeTagInformation
+ FILE_ATTRIBUTE_TAG_INFO tagInfo;
+ if (Interop.Kernel32.GetFileInformationByHandleEx(this, FileAttributeTagInfo, &tagInfo, (uint)sizeof(FILE_ATTRIBUTE_TAG_INFO)))
+ {
+ if (tagInfo.ReparseTag == Interop.Kernel32.IOReparseOptions.IO_REPARSE_TAG_SYMLINK)
+ {
+ return System.IO.FileHandleType.SymbolicLink;
+ }
+ }
+ // Other reparse points (junctions, mount points, etc.) are still regular files/directories
}
}
return System.IO.FileHandleType.RegularFile;
}
+ [StructLayout(LayoutKind.Sequential)]
+ private struct FILE_ATTRIBUTE_TAG_INFO
+ {
+ internal uint FileAttributes;
+ internal uint ReparseTag;
+ }
+
internal long GetFileLength()
{
if (!_lengthCanBeCached)
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs
index cf877c45d21b5d..7ae78a2cdf035b 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs
@@ -40,7 +40,7 @@ public async Task GetFileType_NamedPipe()
await readerTask;
}
- [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))]
+ [Fact]
public void GetFileType_SymbolicLink()
{
string targetPath = GetTestFilePath();
@@ -62,7 +62,7 @@ public void GetFileType_BlockDevice()
{
string[] possibleBlockDevices = { "/dev/sda", "/dev/loop0", "/dev/vda", "/dev/nvme0n1" };
- string blockDevice = null;
+ string? blockDevice = null;
foreach (string device in possibleBlockDevices)
{
if (File.Exists(device))
@@ -72,7 +72,7 @@ public void GetFileType_BlockDevice()
}
}
- if (blockDevice == null)
+ if (blockDevice is null)
{
throw new SkipTestException("No accessible block device found for testing");
}
From fed1c84f8cad769cc3e26e21dd8b177da1f1fac5 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 4 Mar 2026 08:43:15 +0000
Subject: [PATCH 22/58] Fix Win32Exception build error, remove obvious comment,
move FILE_ATTRIBUTE_TAG_INFO to separate file
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../Interop.FILE_ATTRIBUTE_TAG_INFO.cs | 21 +++++++++++++++++++
.../SafeHandles/SafeFileHandle.Windows.cs | 16 +++-----------
.../System.Private.CoreLib.Shared.projitems | 3 +++
3 files changed, 27 insertions(+), 13 deletions(-)
create mode 100644 src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FILE_ATTRIBUTE_TAG_INFO.cs
diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FILE_ATTRIBUTE_TAG_INFO.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FILE_ATTRIBUTE_TAG_INFO.cs
new file mode 100644
index 00000000000000..aa4b8862eff7e2
--- /dev/null
+++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FILE_ATTRIBUTE_TAG_INFO.cs
@@ -0,0 +1,21 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.InteropServices;
+
+internal static partial class Interop
+{
+ internal static partial class Kernel32
+ {
+ // From FILE_INFO_BY_HANDLE_CLASS
+ // Use for GetFileInformationByHandleEx
+ internal const int FileAttributeTagInfo = 35;
+
+ [StructLayout(LayoutKind.Sequential)]
+ internal struct FILE_ATTRIBUTE_TAG_INFO
+ {
+ internal uint FileAttributes;
+ internal uint ReparseTag;
+ }
+ }
+}
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
index 8595d221b4490d..0cdb6341598ec1 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
@@ -274,7 +274,6 @@ private unsafe System.IO.FileHandleType GetPipeOrSocketType()
return System.IO.FileHandleType.Pipe;
}
- // GetNamedPipeInfo failed - check the error code
int error = Marshal.GetLastPInvokeError();
if (error == Interop.Errors.ERROR_INVALID_HANDLE)
{
@@ -282,8 +281,7 @@ private unsafe System.IO.FileHandleType GetPipeOrSocketType()
return System.IO.FileHandleType.Socket;
}
- // Unexpected error - throw
- throw new Win32Exception(error);
+ throw Win32Marshal.GetExceptionForWin32Error(error);
}
private unsafe System.IO.FileHandleType GetDiskBasedType()
@@ -300,9 +298,8 @@ private unsafe System.IO.FileHandleType GetDiskBasedType()
if ((fileInfo.dwFileAttributes & Interop.Kernel32.FileAttributes.FILE_ATTRIBUTE_REPARSE_POINT) != 0)
{
// Check the reparse tag to distinguish symlinks from other reparse points (junctions, mount points, etc.)
- const int FileAttributeTagInfo = 35; // FileAttributeTagInformation
- FILE_ATTRIBUTE_TAG_INFO tagInfo;
- if (Interop.Kernel32.GetFileInformationByHandleEx(this, FileAttributeTagInfo, &tagInfo, (uint)sizeof(FILE_ATTRIBUTE_TAG_INFO)))
+ Interop.Kernel32.FILE_ATTRIBUTE_TAG_INFO tagInfo;
+ if (Interop.Kernel32.GetFileInformationByHandleEx(this, Interop.Kernel32.FileAttributeTagInfo, &tagInfo, (uint)sizeof(Interop.Kernel32.FILE_ATTRIBUTE_TAG_INFO)))
{
if (tagInfo.ReparseTag == Interop.Kernel32.IOReparseOptions.IO_REPARSE_TAG_SYMLINK)
{
@@ -316,13 +313,6 @@ private unsafe System.IO.FileHandleType GetDiskBasedType()
return System.IO.FileHandleType.RegularFile;
}
- [StructLayout(LayoutKind.Sequential)]
- private struct FILE_ATTRIBUTE_TAG_INFO
- {
- internal uint FileAttributes;
- internal uint ReparseTag;
- }
-
internal long GetFileLength()
{
if (!_lengthCanBeCached)
diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
index d8d2252e8b7b6f..104cc0dfc90b4d 100644
--- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
+++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
@@ -1867,6 +1867,9 @@
Common\Interop\Windows\Kernel32\Interop.FILE_ALLOCATION_INFO.cs
+
+ Common\Interop\Windows\Kernel32\Interop.FILE_ATTRIBUTE_TAG_INFO.cs
+
Common\Interop\Windows\Kernel32\Interop.FILE_END_OF_FILE_INFO.cs
From 4f182504fbf71d08a6030908fc1b9ddb73a6f59d Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 4 Mar 2026 10:47:39 +0000
Subject: [PATCH 23/58] Treat ERROR_PIPE_NOT_CONNECTED as pipe; return Unknown
for non-symlink reparse points
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../Win32/SafeHandles/SafeFileHandle.Windows.cs | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
index 0cdb6341598ec1..ab84b9baeebf1e 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
@@ -275,13 +275,12 @@ private unsafe System.IO.FileHandleType GetPipeOrSocketType()
}
int error = Marshal.GetLastPInvokeError();
- if (error == Interop.Errors.ERROR_INVALID_HANDLE)
+ return error switch
{
- // ERROR_INVALID_HANDLE means this is a socket (pipes return true, sockets return this error)
- return System.IO.FileHandleType.Socket;
- }
-
- throw Win32Marshal.GetExceptionForWin32Error(error);
+ Interop.Errors.ERROR_PIPE_NOT_CONNECTED => System.IO.FileHandleType.Pipe,
+ Interop.Errors.ERROR_INVALID_HANDLE => System.IO.FileHandleType.Socket,
+ _ => throw Win32Marshal.GetExceptionForWin32Error(error)
+ };
}
private unsafe System.IO.FileHandleType GetDiskBasedType()
@@ -306,7 +305,8 @@ private unsafe System.IO.FileHandleType GetDiskBasedType()
return System.IO.FileHandleType.SymbolicLink;
}
}
- // Other reparse points (junctions, mount points, etc.) are still regular files/directories
+ // Other reparse points (junctions, mount points, etc.) are not recognized as of now
+ return System.IO.FileHandleType.Unknown;
}
}
From 75a4eeae446daaeba7902deb3d03bec1b9962f15 Mon Sep 17 00:00:00 2001
From: Adam Sitnik
Date: Wed, 4 Mar 2026 18:50:35 +0100
Subject: [PATCH 24/58] address review feedback:
- don't use fully qualified names when using System.IO.FileHandleType
- fix GetPipeOrSocketType and describe the reasoning in the comments
- fix hallucinations (big thanks to @jkotas who spotted the bug)
- use the new API across the repo
---
.../Interop.FILE_ATTRIBUTE_TAG_INFO.cs | 4 +--
.../src/System.IO.MemoryMappedFiles.csproj | 2 --
.../MemoryMappedFile.Unix.cs | 8 +----
.../System/IO/Ports/SerialStream.Windows.cs | 7 ++--
.../Win32/SafeHandles/SafeFileHandle.Unix.cs | 26 +++++++-------
.../SafeHandles/SafeFileHandle.Windows.cs | 35 ++++++++++---------
.../SafeFileHandle/GetFileType.Unix.cs | 1 +
.../SafeFileHandle/GetFileType.Windows.cs | 21 ++++++-----
.../SafeFileHandle/GetFileType.cs | 2 ++
9 files changed, 53 insertions(+), 53 deletions(-)
diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FILE_ATTRIBUTE_TAG_INFO.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FILE_ATTRIBUTE_TAG_INFO.cs
index aa4b8862eff7e2..537e637f0b1f65 100644
--- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FILE_ATTRIBUTE_TAG_INFO.cs
+++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FILE_ATTRIBUTE_TAG_INFO.cs
@@ -7,9 +7,7 @@ internal static partial class Interop
{
internal static partial class Kernel32
{
- // From FILE_INFO_BY_HANDLE_CLASS
- // Use for GetFileInformationByHandleEx
- internal const int FileAttributeTagInfo = 35;
+ internal const int FileAttributeTagInfo = 9;
[StructLayout(LayoutKind.Sequential)]
internal struct FILE_ATTRIBUTE_TAG_INFO
diff --git a/src/libraries/System.IO.MemoryMappedFiles/src/System.IO.MemoryMappedFiles.csproj b/src/libraries/System.IO.MemoryMappedFiles/src/System.IO.MemoryMappedFiles.csproj
index 8e01c1fab825bb..9e831f386d4ea9 100644
--- a/src/libraries/System.IO.MemoryMappedFiles/src/System.IO.MemoryMappedFiles.csproj
+++ b/src/libraries/System.IO.MemoryMappedFiles/src/System.IO.MemoryMappedFiles.csproj
@@ -107,8 +107,6 @@
Link="Common\Interop\Unix\Interop.Open.cs" />
-
diff --git a/src/libraries/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedFile.Unix.cs b/src/libraries/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedFile.Unix.cs
index ae9e24c5722973..43f05c94272a8e 100644
--- a/src/libraries/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedFile.Unix.cs
+++ b/src/libraries/System.IO.MemoryMappedFiles/src/System/IO/MemoryMappedFiles/MemoryMappedFile.Unix.cs
@@ -13,7 +13,7 @@ public partial class MemoryMappedFile
private static void VerifyMemoryMappedFileAccess(MemoryMappedFileAccess access, long capacity, SafeFileHandle? fileHandle, long fileSize, out bool isRegularFile)
{
// if the length has already been fetched and it's more than 0 it's a regular file and there is no need for the FStat sys-call
- isRegularFile = fileHandle is not null && (fileSize > 0 || IsRegularFile(fileHandle));
+ isRegularFile = fileHandle is not null && (fileSize > 0 || fileHandle.Type is FileHandleType.RegularFile);
if (isRegularFile)
{
@@ -33,12 +33,6 @@ private static void VerifyMemoryMappedFileAccess(MemoryMappedFileAccess access,
throw new ArgumentException(SR.Argument_NewMMFWriteAccessNotAllowed, nameof(access));
}
}
-
- static bool IsRegularFile(SafeFileHandle fileHandle)
- {
- Interop.CheckIo(Interop.Sys.FStat(fileHandle, out Interop.Sys.FileStatus status));
- return (status.Mode & Interop.Sys.FileTypes.S_IFREG) != 0;
- }
}
///
diff --git a/src/libraries/System.IO.Ports/src/System/IO/Ports/SerialStream.Windows.cs b/src/libraries/System.IO.Ports/src/System/IO/Ports/SerialStream.Windows.cs
index 2ef1ffdaa79b41..6aeff6651dc9cf 100644
--- a/src/libraries/System.IO.Ports/src/System/IO/Ports/SerialStream.Windows.cs
+++ b/src/libraries/System.IO.Ports/src/System/IO/Ports/SerialStream.Windows.cs
@@ -4,6 +4,7 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
+using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
@@ -585,11 +586,13 @@ internal SerialStream(string portName, int baudRate, Parity parity, int dataBits
try
{
- int fileType = Interop.Kernel32.GetFileType(tempHandle);
+ FileHandleType fileType = tempHandle.Type;
// Allowing FILE_TYPE_UNKNOWN for legitimate serial device such as USB to serial adapter device
- if ((fileType != Interop.Kernel32.FileTypes.FILE_TYPE_CHAR) && (fileType != Interop.Kernel32.FileTypes.FILE_TYPE_UNKNOWN))
+ if (fileType is not FileHandleType.CharacterDevice and not FileHandleType.Unknown)
+ {
throw new ArgumentException(SR.Format(SR.Arg_InvalidSerialPort, portName), nameof(portName));
+ }
_handle = tempHandle;
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
index 29590491f043aa..c96b85607a1705 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
@@ -340,7 +340,7 @@ private bool Init(string path, FileMode mode, FileAccess access, FileShare share
}
// Cache the file type from the status
- _cachedFileType = (int)MapUnixFileTypeToFileType(status.Mode & Interop.Sys.FileTypes.S_IFMT);
+ _cachedFileType = (int)MapUnixFileTypeToFileType(status);
fileLength = status.Size;
filePermissions = ((UnixFileMode)status.Mode) & PermissionMask;
@@ -497,7 +497,7 @@ private bool GetCanSeek()
return canSeek == NullableBool.True;
}
- internal System.IO.FileHandleType GetFileTypeCore()
+ internal FileHandleType GetFileTypeCore()
{
int result = Interop.Sys.FStat(this, out Interop.Sys.FileStatus status);
if (result != 0)
@@ -506,20 +506,20 @@ internal System.IO.FileHandleType GetFileTypeCore()
throw Interop.GetExceptionForIoErrno(error, Path);
}
- return MapUnixFileTypeToFileType(status.Mode & Interop.Sys.FileTypes.S_IFMT);
+ return MapUnixFileTypeToFileType(status);
}
- private static System.IO.FileHandleType MapUnixFileTypeToFileType(int unixFileType)
- => unixFileType switch
+ private static FileHandleType MapUnixFileTypeToFileType(Interop.Sys.FileStatus status)
+ => status.Mode & Interop.Sys.FileTypes.S_IFMT switch
{
- Interop.Sys.FileTypes.S_IFREG => System.IO.FileHandleType.RegularFile,
- Interop.Sys.FileTypes.S_IFDIR => System.IO.FileHandleType.Directory,
- Interop.Sys.FileTypes.S_IFLNK => System.IO.FileHandleType.SymbolicLink,
- Interop.Sys.FileTypes.S_IFIFO => System.IO.FileHandleType.Pipe,
- Interop.Sys.FileTypes.S_IFSOCK => System.IO.FileHandleType.Socket,
- Interop.Sys.FileTypes.S_IFCHR => System.IO.FileHandleType.CharacterDevice,
- Interop.Sys.FileTypes.S_IFBLK => System.IO.FileHandleType.BlockDevice,
- _ => System.IO.FileHandleType.Unknown
+ Interop.Sys.FileTypes.S_IFREG => FileHandleType.RegularFile,
+ Interop.Sys.FileTypes.S_IFDIR => FileHandleType.Directory,
+ Interop.Sys.FileTypes.S_IFLNK => FileHandleType.SymbolicLink,
+ Interop.Sys.FileTypes.S_IFIFO => FileHandleType.Pipe,
+ Interop.Sys.FileTypes.S_IFSOCK => FileHandleType.Socket,
+ Interop.Sys.FileTypes.S_IFCHR => FileHandleType.CharacterDevice,
+ Interop.Sys.FileTypes.S_IFBLK => FileHandleType.BlockDevice,
+ _ => FileHandleType.Unknown
};
internal long GetFileLength()
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
index ab84b9baeebf1e..a1d3b215eba57f 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
@@ -25,7 +25,7 @@ public SafeFileHandle() : base(true)
internal bool IsNoBuffering => (GetFileOptions() & NoBuffering) != 0;
- internal bool CanSeek => !IsClosed && Type == System.IO.FileHandleType.RegularFile;
+ internal bool CanSeek => !IsClosed && Type == FileHandleType.RegularFile;
internal ThreadPoolBoundHandle? ThreadPoolBinding { get; set; }
@@ -253,44 +253,45 @@ internal unsafe FileOptions GetFileOptions()
return _fileOptions = result;
}
- internal unsafe System.IO.FileHandleType GetFileTypeCore()
+ internal FileHandleType GetFileTypeCore()
{
int kernelFileType = Interop.Kernel32.GetFileType(this);
return kernelFileType switch
{
- Interop.Kernel32.FileTypes.FILE_TYPE_CHAR => System.IO.FileHandleType.CharacterDevice,
+ Interop.Kernel32.FileTypes.FILE_TYPE_CHAR => FileHandleType.CharacterDevice,
Interop.Kernel32.FileTypes.FILE_TYPE_PIPE => GetPipeOrSocketType(),
Interop.Kernel32.FileTypes.FILE_TYPE_DISK => GetDiskBasedType(),
- _ => System.IO.FileHandleType.Unknown
+ _ => FileHandleType.Unknown
};
}
- private unsafe System.IO.FileHandleType GetPipeOrSocketType()
+ private unsafe FileHandleType GetPipeOrSocketType()
{
- // Try to call GetNamedPipeInfo to determine if it's a pipe or socket
+ // When GetFileType returns FILE_TYPE_PIPE, the handle can be either a pipe or a socket.
+ // Use GetNamedPipeInfo to determine if it's a pipe.
uint flags;
if (Interop.Kernel32.GetNamedPipeInfo(this, &flags, null, null, null))
{
- return System.IO.FileHandleType.Pipe;
+ return FileHandleType.Pipe;
}
- int error = Marshal.GetLastPInvokeError();
- return error switch
+ return Marshal.GetLastPInvokeError() switch
{
- Interop.Errors.ERROR_PIPE_NOT_CONNECTED => System.IO.FileHandleType.Pipe,
- Interop.Errors.ERROR_INVALID_HANDLE => System.IO.FileHandleType.Socket,
- _ => throw Win32Marshal.GetExceptionForWin32Error(error)
+ Interop.Errors.ERROR_PIPE_NOT_CONNECTED => FileHandleType.Pipe,
+ // Since we got here, it means the handle itself is valid.
+ // So treat all other errors as an indication that it's not a pipe, and thus a socket.
+ _ => FileHandleType.Socket,
};
}
- private unsafe System.IO.FileHandleType GetDiskBasedType()
+ private unsafe FileHandleType GetDiskBasedType()
{
// First check if it's a directory using GetFileInformationByHandle
if (Interop.Kernel32.GetFileInformationByHandle(this, out Interop.Kernel32.BY_HANDLE_FILE_INFORMATION fileInfo))
{
if ((fileInfo.dwFileAttributes & Interop.Kernel32.FileAttributes.FILE_ATTRIBUTE_DIRECTORY) != 0)
{
- return System.IO.FileHandleType.Directory;
+ return FileHandleType.Directory;
}
// Check if it's a reparse point - only symlinks should return SymbolicLink
@@ -302,15 +303,15 @@ private unsafe System.IO.FileHandleType GetDiskBasedType()
{
if (tagInfo.ReparseTag == Interop.Kernel32.IOReparseOptions.IO_REPARSE_TAG_SYMLINK)
{
- return System.IO.FileHandleType.SymbolicLink;
+ return FileHandleType.SymbolicLink;
}
}
// Other reparse points (junctions, mount points, etc.) are not recognized as of now
- return System.IO.FileHandleType.Unknown;
+ return FileHandleType.Unknown;
}
}
- return System.IO.FileHandleType.RegularFile;
+ return FileHandleType.RegularFile;
}
internal long GetFileLength()
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs
index 7ae78a2cdf035b..f679d6617e34d8 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Unix.cs
@@ -28,6 +28,7 @@ public async Task GetFileType_NamedPipe()
string pipePath = GetTestFilePath();
Assert.Equal(0, Interop.Sys.MkFifo(pipePath, (int)UnixFileMode.UserRead | (int)UnixFileMode.UserWrite));
+ // The reader blocks until a writer opens the pipe, so run it in a separate task.
Task readerTask = Task.Run(() =>
{
using SafeFileHandle reader = File.OpenHandle(pipePath, FileMode.Open, FileAccess.Read);
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs
index 21ee6148830e2e..00fba057c13d09 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.Windows.cs
@@ -32,16 +32,16 @@ public unsafe void GetFileType_Directory()
}
[Fact]
- public void GetFileType_NamedPipe()
+ public async Task GetFileType_NamedPipe()
{
string pipeName = Path.GetRandomFileName();
using NamedPipeServerStream server = new NamedPipeServerStream(pipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous);
- Task serverTask = Task.Run(async () => await server.WaitForConnectionAsync());
+ Task serverTask = server.WaitForConnectionAsync();
using NamedPipeClientStream client = new NamedPipeClientStream(".", pipeName, PipeDirection.InOut, PipeOptions.None);
client.Connect();
- serverTask.Wait();
+ await serverTask;
using SafeFileHandle serverHandle = new SafeFileHandle(server.SafePipeHandle.DangerousGetHandle(), ownsHandle: false);
Assert.Equal(FileHandleType.Pipe, serverHandle.Type);
@@ -53,13 +53,16 @@ public void GetFileType_NamedPipe()
[ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotWindowsNanoServer))]
public void GetFileType_ConsoleInput()
{
- if (!Console.IsInputRedirected)
- {
- using SafeFileHandle handle = Console.OpenStandardInputHandle();
- FileHandleType type = handle.Type;
+ using SafeFileHandle handle = Console.OpenStandardInputHandle();
+ FileHandleType type = handle.Type;
- Assert.True(type == FileHandleType.CharacterDevice || type == FileHandleType.Pipe || type == FileHandleType.RegularFile,
- $"Expected CharacterDevice, Pipe, or RegularFile but got {type}");
+ if (Console.IsInputRedirected)
+ {
+ Assert.True(type == FileHandleType.Pipe || type == FileHandleType.RegularFile, $"Expected Pipe or RegularFile but got {type}");
+ }
+ else
+ {
+ Assert.Equal(FileHandleType.CharacterDevice, type);
}
}
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs
index 84098ba021efc1..086d9e850ec6c4 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/SafeFileHandle/GetFileType.cs
@@ -65,6 +65,8 @@ public void GetFileType_Socket()
public void GetFileType_ClosedHandle_ThrowsObjectDisposedException()
{
SafeFileHandle handle = File.OpenHandle(GetTestFilePath(), FileMode.Create, FileAccess.Write);
+ Assert.Equal(FileHandleType.RegularFile, handle.Type);
+
handle.Dispose();
Assert.Throws(() => handle.Type);
From 5ad778f2cff6f28f5ff1befd25947c317da0191f Mon Sep 17 00:00:00 2001
From: Adam Sitnik
Date: Wed, 4 Mar 2026 20:06:31 +0100
Subject: [PATCH 25/58] Apply suggestions from code review
Co-authored-by: Stephen Toub
---
.../src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
index a1d3b215eba57f..7af2c8beb71c9e 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
@@ -306,6 +306,7 @@ private unsafe FileHandleType GetDiskBasedType()
return FileHandleType.SymbolicLink;
}
}
+
// Other reparse points (junctions, mount points, etc.) are not recognized as of now
return FileHandleType.Unknown;
}
From 83024cc23997c6e7012f05690eeacd10cf8dc007 Mon Sep 17 00:00:00 2001
From: Adam Sitnik
Date: Wed, 4 Mar 2026 20:07:13 +0100
Subject: [PATCH 26/58] Apply suggestion from @adamsitnik
---
.../System.IO.Ports/src/System/IO/Ports/SerialStream.Windows.cs | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/libraries/System.IO.Ports/src/System/IO/Ports/SerialStream.Windows.cs b/src/libraries/System.IO.Ports/src/System/IO/Ports/SerialStream.Windows.cs
index 6aeff6651dc9cf..fa63303c2c9291 100644
--- a/src/libraries/System.IO.Ports/src/System/IO/Ports/SerialStream.Windows.cs
+++ b/src/libraries/System.IO.Ports/src/System/IO/Ports/SerialStream.Windows.cs
@@ -4,7 +4,6 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
-using System.IO;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
From 3a29adef999d0b864d644808555fe410a02bb157 Mon Sep 17 00:00:00 2001
From: Adam Sitnik
Date: Wed, 4 Mar 2026 20:09:26 +0100
Subject: [PATCH 27/58] stop including GetFileType_SafeHandle in IO Ports
---
src/libraries/System.IO.Ports/src/System.IO.Ports.csproj | 2 --
1 file changed, 2 deletions(-)
diff --git a/src/libraries/System.IO.Ports/src/System.IO.Ports.csproj b/src/libraries/System.IO.Ports/src/System.IO.Ports.csproj
index 3f1b6dfa5fc352..a1404447a8255e 100644
--- a/src/libraries/System.IO.Ports/src/System.IO.Ports.csproj
+++ b/src/libraries/System.IO.Ports/src/System.IO.Ports.csproj
@@ -103,8 +103,6 @@ System.IO.Ports.SerialPort
Link="Common\System\Text\ValueStringBuilder.cs" />
-
Date: Wed, 4 Mar 2026 20:15:30 +0100
Subject: [PATCH 28/58] Apply suggestion from @jkotas
Co-authored-by: Jan Kotas
---
.../Interop/Windows/Kernel32/Interop.FILE_ATTRIBUTE_TAG_INFO.cs | 2 ++
1 file changed, 2 insertions(+)
diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FILE_ATTRIBUTE_TAG_INFO.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FILE_ATTRIBUTE_TAG_INFO.cs
index 537e637f0b1f65..45c19ddda0ffa9 100644
--- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FILE_ATTRIBUTE_TAG_INFO.cs
+++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.FILE_ATTRIBUTE_TAG_INFO.cs
@@ -7,6 +7,8 @@ internal static partial class Interop
{
internal static partial class Kernel32
{
+ // From FILE_INFO_BY_HANDLE_CLASS
+ // Use for GetFileInformationByHandleEx
internal const int FileAttributeTagInfo = 9;
[StructLayout(LayoutKind.Sequential)]
From 694c37bcbd3173b51fc784edbbb658661a9178a4 Mon Sep 17 00:00:00 2001
From: Adam Sitnik
Date: Wed, 4 Mar 2026 20:23:15 +0100
Subject: [PATCH 29/58] address code review feedback
---
src/libraries/System.IO.Ports/src/System.IO.Ports.csproj | 2 ++
.../src/System/IO/Ports/SerialStream.Windows.cs | 8 ++++++--
2 files changed, 8 insertions(+), 2 deletions(-)
diff --git a/src/libraries/System.IO.Ports/src/System.IO.Ports.csproj b/src/libraries/System.IO.Ports/src/System.IO.Ports.csproj
index a1404447a8255e..3f1b6dfa5fc352 100644
--- a/src/libraries/System.IO.Ports/src/System.IO.Ports.csproj
+++ b/src/libraries/System.IO.Ports/src/System.IO.Ports.csproj
@@ -103,6 +103,8 @@ System.IO.Ports.SerialPort
Link="Common\System\Text\ValueStringBuilder.cs" />
+
Date: Thu, 5 Mar 2026 14:04:23 +0100
Subject: [PATCH 30/58] Revert SerialStream changes
---
.../src/System/IO/Ports/SerialStream.Windows.cs | 10 ++--------
1 file changed, 2 insertions(+), 8 deletions(-)
diff --git a/src/libraries/System.IO.Ports/src/System/IO/Ports/SerialStream.Windows.cs b/src/libraries/System.IO.Ports/src/System/IO/Ports/SerialStream.Windows.cs
index 8532404eaefb12..2ef1ffdaa79b41 100644
--- a/src/libraries/System.IO.Ports/src/System/IO/Ports/SerialStream.Windows.cs
+++ b/src/libraries/System.IO.Ports/src/System/IO/Ports/SerialStream.Windows.cs
@@ -585,17 +585,11 @@ internal SerialStream(string portName, int baudRate, Parity parity, int dataBits
try
{
- // Allowing FILE_TYPE_UNKNOWN for legitimate serial device such as USB to serial adapter device
-#if NET11_0_OR_GREATER
- FileHandleType fileType = tempHandle.Type;
- if (fileType is not FileHandleType.CharacterDevice and not FileHandleType.Unknown)
-#else
int fileType = Interop.Kernel32.GetFileType(tempHandle);
+
+ // Allowing FILE_TYPE_UNKNOWN for legitimate serial device such as USB to serial adapter device
if ((fileType != Interop.Kernel32.FileTypes.FILE_TYPE_CHAR) && (fileType != Interop.Kernel32.FileTypes.FILE_TYPE_UNKNOWN))
-#endif
- {
throw new ArgumentException(SR.Format(SR.Arg_InvalidSerialPort, portName), nameof(portName));
- }
_handle = tempHandle;
From 54772bfb71a6b03df1516b1c4dc408712074f4fe Mon Sep 17 00:00:00 2001
From: Adam Sitnik
Date: Thu, 5 Mar 2026 14:04:42 +0100
Subject: [PATCH 31/58] Apply suggestion from @adamsitnik
---
.../src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
index c96b85607a1705..247e005b08767e 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
@@ -510,7 +510,7 @@ internal FileHandleType GetFileTypeCore()
}
private static FileHandleType MapUnixFileTypeToFileType(Interop.Sys.FileStatus status)
- => status.Mode & Interop.Sys.FileTypes.S_IFMT switch
+ => (status.Mode & Interop.Sys.FileTypes.S_IFMT) switch
{
Interop.Sys.FileTypes.S_IFREG => FileHandleType.RegularFile,
Interop.Sys.FileTypes.S_IFDIR => FileHandleType.Directory,
From 9ada1e98dfa20412fc5f207de356148582737c0e Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 5 Mar 2026 13:40:28 +0000
Subject: [PATCH 32/58] Initial plan
From 9197913beeb006fb134efa0dbc09562c837ac79b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 5 Mar 2026 14:32:03 +0000
Subject: [PATCH 33/58] Add SafeFileHandle.CreateAnonymousPipe API and platform
implementations
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../Unix/System.Native/Interop.Pipe.cs | 2 +
.../Kernel32/Interop.CreateNamedPipe.cs | 11 ++++
.../Win32/SafeHandles/SafeFileHandle.Unix.cs | 53 ++++++++++++++-
.../SafeHandles/SafeFileHandle.Windows.cs | 62 ++++++++++++++++++
.../Win32/SafeHandles/SafeFileHandle.cs | 12 ++++
.../System.Private.CoreLib.Shared.projitems | 8 ++-
.../System.Runtime/ref/System.Runtime.cs | 1 +
.../File/OpenHandle.cs | 54 ++++++++++++++++
src/native/libs/System.Native/pal_io.c | 64 +++++++++++++++----
src/native/libs/System.Native/pal_io.h | 5 +-
10 files changed, 256 insertions(+), 16 deletions(-)
diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Pipe.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Pipe.cs
index 3574339966880d..30ff2b514acc8f 100644
--- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Pipe.cs
+++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Pipe.cs
@@ -12,6 +12,8 @@ internal static partial class Sys
internal enum PipeFlags
{
O_CLOEXEC = 0x0010,
+ AsyncReads = 0x0400,
+ AsyncWrites = 0x0800,
}
///
diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateNamedPipe.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateNamedPipe.cs
index 6cf9c648506ba7..f0b31a8eae8d51 100644
--- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateNamedPipe.cs
+++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateNamedPipe.cs
@@ -19,5 +19,16 @@ internal static partial SafePipeHandle CreateNamedPipe(
int inBufferSize,
int defaultTimeout,
ref SECURITY_ATTRIBUTES securityAttributes);
+
+ [LibraryImport(Libraries.Kernel32, EntryPoint = "CreateNamedPipeW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
+ internal static partial SafeFileHandle CreateNamedPipeFileHandle(
+ string pipeName,
+ int openMode,
+ int pipeMode,
+ int maxInstances,
+ int outBufferSize,
+ int inBufferSize,
+ int defaultTimeout,
+ ref SECURITY_ATTRIBUTES securityAttributes);
}
}
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
index 247e005b08767e..e1246c8176b77b 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
@@ -40,6 +40,7 @@ public sealed partial class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid
// not using bool? as it's not thread safe
private volatile NullableBool _canSeek = NullableBool.Undefined;
private volatile NullableBool _supportsRandomAccess = NullableBool.Undefined;
+ private volatile int _isAsync = -1;
private bool _deleteOnClose;
private bool _isLocked;
@@ -53,7 +54,25 @@ private SafeFileHandle(bool ownsHandle)
SetHandle(new IntPtr(-1));
}
- public bool IsAsync { get; private set; }
+ public bool IsAsync
+ {
+ get
+ {
+ int isAsync = _isAsync;
+ if (isAsync == -1)
+ {
+ if (Interop.Sys.Fcntl.GetIsNonBlocking(this, out bool isNonBlocking) != 0)
+ {
+ throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo(), Path);
+ }
+
+ _isAsync = isAsync = isNonBlocking ? 1 : 0;
+ }
+
+ return isAsync != 0;
+ }
+ private set => _isAsync = value ? 1 : 0;
+ }
internal bool CanSeek => !IsClosed && GetCanSeek();
@@ -161,6 +180,38 @@ public override bool IsInvalid
}
}
+ private static unsafe partial void CreateAnonymousPipeCore(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite)
+ {
+ readHandle = new SafeFileHandle();
+ writeHandle = new SafeFileHandle();
+
+ int* fds = stackalloc int[2];
+ Interop.Sys.PipeFlags flags = Interop.Sys.PipeFlags.O_CLOEXEC;
+ if (asyncRead)
+ {
+ flags |= Interop.Sys.PipeFlags.AsyncReads;
+ }
+
+ if (asyncWrite)
+ {
+ flags |= Interop.Sys.PipeFlags.AsyncWrites;
+ }
+
+ if (Interop.Sys.Pipe(fds, flags) != 0)
+ {
+ Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo();
+ readHandle.Dispose();
+ writeHandle.Dispose();
+ throw Interop.GetExceptionForIoErrno(error);
+ }
+
+ readHandle.SetHandle(new IntPtr(fds[Interop.Sys.ReadEndOfPipe]));
+ writeHandle.SetHandle(new IntPtr(fds[Interop.Sys.WriteEndOfPipe]));
+
+ readHandle.IsAsync = asyncRead;
+ writeHandle.IsAsync = asyncWrite;
+ }
+
// Specialized Open that returns the file length and permissions of the opened file.
// This information is retrieved from the 'stat' syscall that must be performed to ensure the path is not a directory.
internal static SafeFileHandle OpenReadOnly(string fullPath, FileOptions options, out long fileLength, out UnixFileMode filePermissions)
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
index 7af2c8beb71c9e..edbf721a220782 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
@@ -3,6 +3,7 @@
using System;
using System.Buffers;
+using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Runtime.InteropServices;
@@ -21,6 +22,67 @@ public SafeFileHandle() : base(true)
{
}
+ private static unsafe partial void CreateAnonymousPipeCore(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite)
+ {
+ Interop.Kernel32.SECURITY_ATTRIBUTES securityAttributes = default;
+
+ // When neither end is async, use the simple CreatePipe API
+ if (!asyncRead && !asyncWrite)
+ {
+ bool ret = Interop.Kernel32.CreatePipe(out readHandle, out writeHandle, ref securityAttributes, 0);
+ if (!ret || readHandle.IsInvalid || writeHandle.IsInvalid)
+ {
+ throw new Win32Exception();
+ }
+
+ readHandle._fileOptions = FileOptions.None;
+ writeHandle._fileOptions = FileOptions.None;
+ return;
+ }
+
+ // When one or both ends are async, use named pipes to support async I/O.
+ string pipeName = $@"\\.\pipe\{Guid.NewGuid()}";
+
+ // Security: we don't need to specify a security descriptor, because
+ // we allow only for 1 instance of the pipe and immediately open the write end,
+ // so there is no time window for another process to open the pipe with different permissions.
+ // Even if that happens, we are going to fail to open the write end and throw an exception, so there is no security risk.
+
+ // Determine the open mode for the read end
+ int openMode = (int)Interop.Kernel32.PipeOptions.PIPE_ACCESS_INBOUND |
+ Interop.Kernel32.FileOperations.FILE_FLAG_FIRST_PIPE_INSTANCE; // Only one can be created with this name
+
+ if (asyncRead)
+ {
+ openMode |= Interop.Kernel32.FileOperations.FILE_FLAG_OVERLAPPED; // Asynchronous I/O
+ }
+
+ int pipeMode = (int)(Interop.Kernel32.PipeOptions.PIPE_TYPE_BYTE | // the alternative would be to use "Message"
+ Interop.Kernel32.PipeOptions.PIPE_READMODE_BYTE); // Data is read from the pipe as a stream of bytes
+
+ // We could consider specifying a larger buffer size.
+ readHandle = Interop.Kernel32.CreateNamedPipeFileHandle(pipeName, openMode, pipeMode, 1, 0, 0, 0, ref securityAttributes);
+
+ if (readHandle.IsInvalid)
+ {
+ throw new Win32Exception();
+ }
+
+ try
+ {
+ FileOptions writeOptions = asyncWrite ? FileOptions.Asynchronous : FileOptions.None;
+ writeHandle = Open(pipeName, FileMode.Open, FileAccess.Write, FileShare.Read, writeOptions, preallocationSize: 0);
+ }
+ catch
+ {
+ readHandle.Dispose();
+ throw;
+ }
+
+ readHandle._fileOptions = asyncRead ? FileOptions.Asynchronous : FileOptions.None;
+ writeHandle._fileOptions = asyncWrite ? FileOptions.Asynchronous : FileOptions.None;
+ }
+
public bool IsAsync => (GetFileOptions() & FileOptions.Asynchronous) != 0;
internal bool IsNoBuffering => (GetFileOptions() & NoBuffering) != 0;
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
index 0a13a420cff8e0..e1daea829a48f5 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
@@ -10,6 +10,18 @@ public sealed partial class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid
private string? _path;
private volatile int _cachedFileType = -1;
+ ///
+ /// Creates an anonymous pipe.
+ ///
+ /// When this method returns, contains the read end of the pipe.
+ /// When this method returns, contains the write end of the pipe.
+ /// to enable asynchronous operations for the read end of the pipe; otherwise, .
+ /// to enable asynchronous operations for the write end of the pipe; otherwise, .
+ public static void CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead = false, bool asyncWrite = false) =>
+ CreateAnonymousPipeCore(out readHandle, out writeHandle, asyncRead, asyncWrite);
+
+ private static unsafe partial void CreateAnonymousPipeCore(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite);
+
///
/// Creates a around a file handle.
///
diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
index 104cc0dfc90b4d..eb780a0deccbdf 100644
--- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
+++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
@@ -2402,6 +2402,9 @@
Common\Interop\Unix\System.Native\Interop.Close.cs
+
+ Common\Interop\Unix\System.Native\Interop.Fcntl.cs
+
Common\Interop\Unix\System.Native\Interop.CopyFile.cs
@@ -2426,6 +2429,9 @@
Common\Interop\Unix\System.Native\Interop.GetCpuUtilization.cs
+
+ Common\Interop\Unix\System.Native\Interop.Pipe.cs
+
Common\Interop\Unix\System.Native\Interop.GetCwd.cs
@@ -2949,4 +2955,4 @@
-
\ No newline at end of file
+
diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs
index 8ca5183387e7e7..51297fb9b83a0d 100644
--- a/src/libraries/System.Runtime/ref/System.Runtime.cs
+++ b/src/libraries/System.Runtime/ref/System.Runtime.cs
@@ -18,6 +18,7 @@ protected CriticalHandleZeroOrMinusOneIsInvalid() : base (default(System.IntPtr)
}
public sealed partial class SafeFileHandle : Microsoft.Win32.SafeHandles.SafeHandleZeroOrMinusOneIsInvalid
{
+ public static void CreateAnonymousPipe(out Microsoft.Win32.SafeHandles.SafeFileHandle readHandle, out Microsoft.Win32.SafeHandles.SafeFileHandle writeHandle, bool asyncRead = false, bool asyncWrite = false) { throw null; }
public SafeFileHandle() : base (default(bool)) { }
public SafeFileHandle(System.IntPtr preexistingHandle, bool ownsHandle) : base (default(bool)) { }
public override bool IsInvalid { get { throw null; } }
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
index 3d7bcb303b4ce7..e23458eddc6ab3 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.IO.Pipes;
using Microsoft.Win32.SafeHandles;
using Xunit;
@@ -82,6 +83,59 @@ public void SafeFileHandle_IsAsync_ReturnsCorrectInformation(FileOptions options
}
}
+ [Theory]
+ [InlineData(false, false)]
+ [InlineData(false, true)]
+ [InlineData(true, false)]
+ [InlineData(true, true)]
+ public void SafeFileHandle_CreateAnonymousPipe_SetsIsAsyncAndTransfersData(bool asyncRead, bool asyncWrite)
+ {
+ SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, asyncRead, asyncWrite);
+ Assert.Equal(asyncRead, readHandle.IsAsync);
+ Assert.Equal(asyncWrite, writeHandle.IsAsync);
+
+ using Stream readStream = CreatePipeReadStream(readHandle, asyncRead);
+ using Stream writeStream = CreatePipeWriteStream(writeHandle, asyncWrite);
+
+ byte[] expected = [1, 2, 3, 4];
+ writeStream.Write(expected);
+ writeStream.Flush();
+
+ byte[] actual = new byte[expected.Length];
+ int bytesRead = 0;
+ while (bytesRead < actual.Length)
+ {
+ int read = readStream.Read(actual, bytesRead, actual.Length - bytesRead);
+ if (read == 0)
+ {
+ break;
+ }
+
+ bytesRead += read;
+ }
+
+ Assert.Equal(expected.Length, bytesRead);
+ Assert.Equal(expected, actual);
+ }
+
+ private static Stream CreatePipeReadStream(SafeFileHandle readHandle, bool asyncRead) =>
+ !OperatingSystem.IsWindows() && asyncRead
+ ? new AnonymousPipeClientStream(PipeDirection.In, TransferOwnershipToPipeHandle(readHandle))
+ : new FileStream(readHandle, FileAccess.Read, 1, asyncRead);
+
+ private static Stream CreatePipeWriteStream(SafeFileHandle writeHandle, bool asyncWrite) =>
+ !OperatingSystem.IsWindows() && asyncWrite
+ ? new AnonymousPipeClientStream(PipeDirection.Out, TransferOwnershipToPipeHandle(writeHandle))
+ : new FileStream(writeHandle, FileAccess.Write, 1, asyncWrite);
+
+ private static SafePipeHandle TransferOwnershipToPipeHandle(SafeFileHandle handle)
+ {
+ SafePipeHandle pipeHandle = new SafePipeHandle(handle.DangerousGetHandle(), ownsHandle: true);
+ handle.SetHandleAsInvalid();
+ handle.Dispose();
+ return pipeHandle;
+ }
+
[Theory]
[InlineData(FileOptions.DeleteOnClose)]
[InlineData(FileOptions.DeleteOnClose | FileOptions.Asynchronous)]
diff --git a/src/native/libs/System.Native/pal_io.c b/src/native/libs/System.Native/pal_io.c
index 7a7ea2b83c5dd2..7255cac74c89e3 100644
--- a/src/native/libs/System.Native/pal_io.c
+++ b/src/native/libs/System.Native/pal_io.c
@@ -567,32 +567,32 @@ int32_t SystemNative_Pipe(int32_t pipeFds[2], int32_t flags)
errno = ENOTSUP;
return -1;
#else // TARGET_WASM
- switch (flags)
+ if ((flags & ~(PAL_O_CLOEXEC | PAL_O_ASYNC_READ | PAL_O_ASYNC_WRITE)) != 0)
+ {
+ assert_msg(false, "Unknown pipe flag", (int)flags);
+ errno = EINVAL;
+ return -1;
+ }
+
+ int32_t pipeFlags = 0;
+ if ((flags & PAL_O_CLOEXEC) != 0)
{
- case 0:
- break;
- case PAL_O_CLOEXEC:
#if HAVE_O_CLOEXEC
- flags = O_CLOEXEC;
+ pipeFlags = O_CLOEXEC;
#endif
- break;
- default:
- assert_msg(false, "Unknown pipe flag", (int)flags);
- errno = EINVAL;
- return -1;
}
int32_t result;
#if HAVE_PIPE2
// If pipe2 is available, use it. This will handle O_CLOEXEC if it was set.
- while ((result = pipe2(pipeFds, flags)) < 0 && errno == EINTR);
+ while ((result = pipe2(pipeFds, pipeFlags)) < 0 && errno == EINTR);
#elif HAVE_PIPE
// Otherwise, use pipe.
while ((result = pipe(pipeFds)) < 0 && errno == EINTR);
// Then, if O_CLOEXEC was specified, use fcntl to configure the file descriptors appropriately.
#if HAVE_O_CLOEXEC
- if ((flags & O_CLOEXEC) != 0 && result == 0)
+ if ((pipeFlags & O_CLOEXEC) != 0 && result == 0)
#else
if ((flags & PAL_O_CLOEXEC) != 0 && result == 0)
#endif
@@ -614,6 +614,46 @@ int32_t SystemNative_Pipe(int32_t pipeFds[2], int32_t flags)
#else /* HAVE_PIPE */
result = -1;
#endif /* HAVE_PIPE */
+
+ if (result == 0 && ((flags & (PAL_O_ASYNC_READ | PAL_O_ASYNC_WRITE)) != 0))
+ {
+ if ((flags & PAL_O_ASYNC_READ) != 0)
+ {
+ int fdFlags;
+ while ((fdFlags = fcntl(pipeFds[0], F_GETFL)) < 0 && errno == EINTR);
+ if (fdFlags < 0)
+ {
+ result = -1;
+ }
+ else
+ {
+ while ((result = fcntl(pipeFds[0], F_SETFL, fdFlags | O_NONBLOCK)) < 0 && errno == EINTR);
+ }
+ }
+
+ if (result == 0 && (flags & PAL_O_ASYNC_WRITE) != 0)
+ {
+ int fdFlags;
+ while ((fdFlags = fcntl(pipeFds[1], F_GETFL)) < 0 && errno == EINTR);
+ if (fdFlags < 0)
+ {
+ result = -1;
+ }
+ else
+ {
+ while ((result = fcntl(pipeFds[1], F_SETFL, fdFlags | O_NONBLOCK)) < 0 && errno == EINTR);
+ }
+ }
+
+ if (result != 0)
+ {
+ int tmpErrno = errno;
+ close(pipeFds[0]);
+ close(pipeFds[1]);
+ errno = tmpErrno;
+ }
+ }
+
return result;
#endif // TARGET_WASM
}
diff --git a/src/native/libs/System.Native/pal_io.h b/src/native/libs/System.Native/pal_io.h
index 3d94e28bd1113d..e3ad34ff9f783e 100644
--- a/src/native/libs/System.Native/pal_io.h
+++ b/src/native/libs/System.Native/pal_io.h
@@ -180,6 +180,8 @@ enum
PAL_O_TRUNC = 0x0080, // Truncate file to length 0 if it already exists
PAL_O_SYNC = 0x0100, // Block writes call will block until physically written
PAL_O_NOFOLLOW = 0x0200, // Fails to open the target if it's a symlink, parent symlinks are allowed
+ PAL_O_ASYNC_READ = 0x0400, // Set O_NONBLOCK on the read end of a pipe
+ PAL_O_ASYNC_WRITE = 0x0800, // Set O_NONBLOCK on the write end of a pipe
};
/**
@@ -439,12 +441,11 @@ PALEXPORT int32_t SystemNative_CloseDir(DIR* dir);
/**
* Creates a pipe. Implemented as shim to pipe(2) or pipe2(2) if available.
- * Flags are ignored if pipe2 is not available.
*
* Returns 0 for success, -1 for failure. Sets errno on failure.
*/
PALEXPORT int32_t SystemNative_Pipe(int32_t pipefd[2], // [out] pipefds[0] gets read end, pipefd[1] gets write end.
- int32_t flags); // 0 for defaults or PAL_O_CLOEXEC for close-on-exec
+ int32_t flags); // 0 for defaults. Use PAL_O_CLOEXEC, PAL_O_ASYNC_READ, and PAL_O_ASYNC_WRITE for additional behavior.
// NOTE: Rather than a general fcntl shim, we opt to export separate functions
// for each command. This allows use to have strongly typed arguments and saves
From 47cbf8d78aa25e89a650cc6d82f4733497711d52 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 6 Mar 2026 18:47:22 +0000
Subject: [PATCH 34/58] Address PR feedback for anonymous pipe API changes
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../Kernel32/Interop.CreateNamedPipe.cs | 13 ---------
.../Interop.CreateNamedPipe_SafeFileHandle.cs | 22 +++++++++++++++
.../Win32/SafeHandles/SafeFileHandle.Unix.cs | 22 +++++++--------
.../SafeHandles/SafeFileHandle.Windows.cs | 2 +-
.../Win32/SafeHandles/SafeFileHandle.cs | 5 +---
.../System.Private.CoreLib.Shared.projitems | 3 +++
.../File/OpenHandle.cs | 27 ++++++++++---------
7 files changed, 52 insertions(+), 42 deletions(-)
create mode 100644 src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateNamedPipe_SafeFileHandle.cs
diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateNamedPipe.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateNamedPipe.cs
index f0b31a8eae8d51..6a2522eba28304 100644
--- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateNamedPipe.cs
+++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateNamedPipe.cs
@@ -3,8 +3,6 @@
using System;
using System.Runtime.InteropServices;
-using Microsoft.Win32.SafeHandles;
-
internal static partial class Interop
{
internal static partial class Kernel32
@@ -19,16 +17,5 @@ internal static partial SafePipeHandle CreateNamedPipe(
int inBufferSize,
int defaultTimeout,
ref SECURITY_ATTRIBUTES securityAttributes);
-
- [LibraryImport(Libraries.Kernel32, EntryPoint = "CreateNamedPipeW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
- internal static partial SafeFileHandle CreateNamedPipeFileHandle(
- string pipeName,
- int openMode,
- int pipeMode,
- int maxInstances,
- int outBufferSize,
- int inBufferSize,
- int defaultTimeout,
- ref SECURITY_ATTRIBUTES securityAttributes);
}
}
diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateNamedPipe_SafeFileHandle.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateNamedPipe_SafeFileHandle.cs
new file mode 100644
index 00000000000000..91af8b6f52da39
--- /dev/null
+++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateNamedPipe_SafeFileHandle.cs
@@ -0,0 +1,22 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Runtime.InteropServices;
+using Microsoft.Win32.SafeHandles;
+
+internal static partial class Interop
+{
+ internal static partial class Kernel32
+ {
+ [LibraryImport(Libraries.Kernel32, EntryPoint = "CreateNamedPipeW", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
+ internal static partial SafeFileHandle CreateNamedPipeFileHandle(
+ string pipeName,
+ int openMode,
+ int pipeMode,
+ int maxInstances,
+ int outBufferSize,
+ int inBufferSize,
+ int defaultTimeout,
+ ref SECURITY_ATTRIBUTES securityAttributes);
+ }
+}
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
index e1246c8176b77b..f20bc0c31ff5ed 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
@@ -40,7 +40,7 @@ public sealed partial class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid
// not using bool? as it's not thread safe
private volatile NullableBool _canSeek = NullableBool.Undefined;
private volatile NullableBool _supportsRandomAccess = NullableBool.Undefined;
- private volatile int _isAsync = -1;
+ private volatile NullableBool _isAsync = NullableBool.Undefined;
private bool _deleteOnClose;
private bool _isLocked;
@@ -58,20 +58,20 @@ public bool IsAsync
{
get
{
- int isAsync = _isAsync;
- if (isAsync == -1)
+ NullableBool isAsync = _isAsync;
+ if (isAsync == NullableBool.Undefined)
{
if (Interop.Sys.Fcntl.GetIsNonBlocking(this, out bool isNonBlocking) != 0)
{
throw Interop.GetExceptionForIoErrno(Interop.Sys.GetLastErrorInfo(), Path);
}
- _isAsync = isAsync = isNonBlocking ? 1 : 0;
+ _isAsync = isAsync = isNonBlocking ? NullableBool.True : NullableBool.False;
}
- return isAsync != 0;
+ return isAsync == NullableBool.True;
}
- private set => _isAsync = value ? 1 : 0;
+ private set => _isAsync = value ? NullableBool.True : NullableBool.False;
}
internal bool CanSeek => !IsClosed && GetCanSeek();
@@ -180,11 +180,8 @@ public override bool IsInvalid
}
}
- private static unsafe partial void CreateAnonymousPipeCore(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite)
+ public static unsafe partial void CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite)
{
- readHandle = new SafeFileHandle();
- writeHandle = new SafeFileHandle();
-
int* fds = stackalloc int[2];
Interop.Sys.PipeFlags flags = Interop.Sys.PipeFlags.O_CLOEXEC;
if (asyncRead)
@@ -200,11 +197,12 @@ private static unsafe partial void CreateAnonymousPipeCore(out SafeFileHandle re
if (Interop.Sys.Pipe(fds, flags) != 0)
{
Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo();
- readHandle.Dispose();
- writeHandle.Dispose();
throw Interop.GetExceptionForIoErrno(error);
}
+ readHandle = new SafeFileHandle();
+ writeHandle = new SafeFileHandle();
+
readHandle.SetHandle(new IntPtr(fds[Interop.Sys.ReadEndOfPipe]));
writeHandle.SetHandle(new IntPtr(fds[Interop.Sys.WriteEndOfPipe]));
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
index edbf721a220782..fc5037fc360d58 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
@@ -22,7 +22,7 @@ public SafeFileHandle() : base(true)
{
}
- private static unsafe partial void CreateAnonymousPipeCore(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite)
+ public static unsafe partial void CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite)
{
Interop.Kernel32.SECURITY_ATTRIBUTES securityAttributes = default;
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
index e1daea829a48f5..31c23450cd6ce3 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
@@ -17,10 +17,7 @@ public sealed partial class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid
/// When this method returns, contains the write end of the pipe.
/// to enable asynchronous operations for the read end of the pipe; otherwise, .
/// to enable asynchronous operations for the write end of the pipe; otherwise, .
- public static void CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead = false, bool asyncWrite = false) =>
- CreateAnonymousPipeCore(out readHandle, out writeHandle, asyncRead, asyncWrite);
-
- private static unsafe partial void CreateAnonymousPipeCore(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite);
+ public static unsafe partial void CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead = false, bool asyncWrite = false);
///
/// Creates a around a file handle.
diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
index eb780a0deccbdf..42705d59980544 100644
--- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
+++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
@@ -1798,6 +1798,9 @@
Common\Interop\Windows\Kernel32\Interop.CreateFile.cs
+
+ Common\Interop\Windows\Kernel32\Interop.CreateNamedPipe_SafeFileHandle.cs
+
Common\Interop\Windows\Kernel32\Interop.EventWaitHandle.cs
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
index e23458eddc6ab3..7ab26d74026ec1 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
@@ -98,23 +98,26 @@ public void SafeFileHandle_CreateAnonymousPipe_SetsIsAsyncAndTransfersData(bool
using Stream writeStream = CreatePipeWriteStream(writeHandle, asyncWrite);
byte[] expected = [1, 2, 3, 4];
- writeStream.Write(expected);
- writeStream.Flush();
-
byte[] actual = new byte[expected.Length];
- int bytesRead = 0;
- while (bytesRead < actual.Length)
+
+ if (!OperatingSystem.IsWindows() && asyncWrite)
{
- int read = readStream.Read(actual, bytesRead, actual.Length - bytesRead);
- if (read == 0)
- {
- break;
- }
+ writeStream.WriteAsync(expected).GetAwaiter().GetResult();
+ }
+ else
+ {
+ writeStream.Write(expected);
+ }
- bytesRead += read;
+ if (!OperatingSystem.IsWindows() && asyncRead)
+ {
+ readStream.ReadExactlyAsync(actual).GetAwaiter().GetResult();
+ }
+ else
+ {
+ readStream.ReadExactly(actual);
}
- Assert.Equal(expected.Length, bytesRead);
Assert.Equal(expected, actual);
}
From f9187b838437fcdd208b1ef88ce6808df86a801f Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 6 Mar 2026 19:20:45 +0000
Subject: [PATCH 35/58] Apply follow-up review suggestions for anonymous pipe
API
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs | 2 +-
.../Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs | 2 +-
.../src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs | 5 ++++-
.../tests/System.IO.FileSystem.Tests/File/OpenHandle.cs | 7 ++++---
4 files changed, 10 insertions(+), 6 deletions(-)
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
index f20bc0c31ff5ed..899c70367e3325 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
@@ -180,7 +180,7 @@ public override bool IsInvalid
}
}
- public static unsafe partial void CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite)
+ private static unsafe partial void CreateAnonymousPipeCore(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite)
{
int* fds = stackalloc int[2];
Interop.Sys.PipeFlags flags = Interop.Sys.PipeFlags.O_CLOEXEC;
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
index fc5037fc360d58..edbf721a220782 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
@@ -22,7 +22,7 @@ public SafeFileHandle() : base(true)
{
}
- public static unsafe partial void CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite)
+ private static unsafe partial void CreateAnonymousPipeCore(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite)
{
Interop.Kernel32.SECURITY_ATTRIBUTES securityAttributes = default;
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
index 31c23450cd6ce3..e1daea829a48f5 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
@@ -17,7 +17,10 @@ public sealed partial class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid
/// When this method returns, contains the write end of the pipe.
/// to enable asynchronous operations for the read end of the pipe; otherwise, .
/// to enable asynchronous operations for the write end of the pipe; otherwise, .
- public static unsafe partial void CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead = false, bool asyncWrite = false);
+ public static void CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead = false, bool asyncWrite = false) =>
+ CreateAnonymousPipeCore(out readHandle, out writeHandle, asyncRead, asyncWrite);
+
+ private static unsafe partial void CreateAnonymousPipeCore(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite);
///
/// Creates a around a file handle.
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
index 7ab26d74026ec1..afafde661bcde6 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
@@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.
using System.IO.Pipes;
+using System.Threading.Tasks;
using Microsoft.Win32.SafeHandles;
using Xunit;
@@ -88,7 +89,7 @@ public void SafeFileHandle_IsAsync_ReturnsCorrectInformation(FileOptions options
[InlineData(false, true)]
[InlineData(true, false)]
[InlineData(true, true)]
- public void SafeFileHandle_CreateAnonymousPipe_SetsIsAsyncAndTransfersData(bool asyncRead, bool asyncWrite)
+ public async Task SafeFileHandle_CreateAnonymousPipe_SetsIsAsyncAndTransfersData(bool asyncRead, bool asyncWrite)
{
SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, asyncRead, asyncWrite);
Assert.Equal(asyncRead, readHandle.IsAsync);
@@ -102,7 +103,7 @@ public void SafeFileHandle_CreateAnonymousPipe_SetsIsAsyncAndTransfersData(bool
if (!OperatingSystem.IsWindows() && asyncWrite)
{
- writeStream.WriteAsync(expected).GetAwaiter().GetResult();
+ await writeStream.WriteAsync(expected);
}
else
{
@@ -111,7 +112,7 @@ public void SafeFileHandle_CreateAnonymousPipe_SetsIsAsyncAndTransfersData(bool
if (!OperatingSystem.IsWindows() && asyncRead)
{
- readStream.ReadExactlyAsync(actual).GetAwaiter().GetResult();
+ await readStream.ReadExactlyAsync(actual);
}
else
{
From 1bd3651a93af27e2d661dbc2f103ee1df9b55c96 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 7 Mar 2026 10:22:44 +0000
Subject: [PATCH 36/58] Address jkotas feedback for anonymous pipe
implementation
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../Unix/System.Native/Interop.Pipe.cs | 4 +-
.../Win32/SafeHandles/SafeFileHandle.Unix.cs | 42 +++++---
.../SafeHandles/SafeFileHandle.Windows.cs | 102 +++++++++++-------
.../Win32/SafeHandles/SafeFileHandle.cs | 5 +-
4 files changed, 93 insertions(+), 60 deletions(-)
diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Pipe.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Pipe.cs
index 30ff2b514acc8f..d9e0db4281e5b3 100644
--- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Pipe.cs
+++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Pipe.cs
@@ -12,8 +12,8 @@ internal static partial class Sys
internal enum PipeFlags
{
O_CLOEXEC = 0x0010,
- AsyncReads = 0x0400,
- AsyncWrites = 0x0800,
+ O_ASYNC_READ = 0x0400,
+ O_ASYNC_WRITE = 0x0800,
}
///
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
index 899c70367e3325..ae5bac033c3041 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
@@ -38,9 +38,9 @@ public sealed partial class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid
|| AppContextConfigHelper.GetBooleanConfig("System.IO.DisableFileLocking", "DOTNET_SYSTEM_IO_DISABLEFILELOCKING", defaultValue: false);
// not using bool? as it's not thread safe
- private volatile NullableBool _canSeek = NullableBool.Undefined;
- private volatile NullableBool _supportsRandomAccess = NullableBool.Undefined;
- private volatile NullableBool _isAsync = NullableBool.Undefined;
+ private volatile NullableBool _canSeek /* = NullableBool.Undefined */;
+ private volatile NullableBool _supportsRandomAccess /* = NullableBool.Undefined */;
+ private volatile NullableBool _isAsync /* = NullableBool.Undefined */;
private bool _deleteOnClose;
private bool _isLocked;
@@ -180,18 +180,18 @@ public override bool IsInvalid
}
}
- private static unsafe partial void CreateAnonymousPipeCore(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite)
+ public static unsafe partial void CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite)
{
int* fds = stackalloc int[2];
Interop.Sys.PipeFlags flags = Interop.Sys.PipeFlags.O_CLOEXEC;
if (asyncRead)
{
- flags |= Interop.Sys.PipeFlags.AsyncReads;
+ flags |= Interop.Sys.PipeFlags.O_ASYNC_READ;
}
if (asyncWrite)
{
- flags |= Interop.Sys.PipeFlags.AsyncWrites;
+ flags |= Interop.Sys.PipeFlags.O_ASYNC_WRITE;
}
if (Interop.Sys.Pipe(fds, flags) != 0)
@@ -200,14 +200,30 @@ private static unsafe partial void CreateAnonymousPipeCore(out SafeFileHandle re
throw Interop.GetExceptionForIoErrno(error);
}
- readHandle = new SafeFileHandle();
- writeHandle = new SafeFileHandle();
+ int readFd = fds[Interop.Sys.ReadEndOfPipe];
+ int writeFd = fds[Interop.Sys.WriteEndOfPipe];
- readHandle.SetHandle(new IntPtr(fds[Interop.Sys.ReadEndOfPipe]));
- writeHandle.SetHandle(new IntPtr(fds[Interop.Sys.WriteEndOfPipe]));
+ SafeFileHandle? tempReadHandle = null;
+ SafeFileHandle? tempWriteHandle = null;
+ try
+ {
+ tempReadHandle = new SafeFileHandle((IntPtr)readFd, ownsHandle: true);
+ tempWriteHandle = new SafeFileHandle((IntPtr)writeFd, ownsHandle: true);
+
+ tempReadHandle.IsAsync = asyncRead;
+ tempWriteHandle.IsAsync = asyncWrite;
+
+ readHandle = tempReadHandle;
+ writeHandle = tempWriteHandle;
- readHandle.IsAsync = asyncRead;
- writeHandle.IsAsync = asyncWrite;
+ tempReadHandle = null;
+ tempWriteHandle = null;
+ }
+ finally
+ {
+ tempReadHandle?.Dispose();
+ tempWriteHandle?.Dispose();
+ }
}
// Specialized Open that returns the file length and permissions of the opened file.
@@ -578,7 +594,7 @@ internal long GetFileLength()
return status.Size;
}
- private enum NullableBool
+ private enum NullableBool : sbyte
{
Undefined = 0,
False = -1,
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
index edbf721a220782..9f3d331ce25e5e 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
@@ -22,65 +22,85 @@ public SafeFileHandle() : base(true)
{
}
- private static unsafe partial void CreateAnonymousPipeCore(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite)
+ public static unsafe partial void CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite)
{
Interop.Kernel32.SECURITY_ATTRIBUTES securityAttributes = default;
+ SafeFileHandle? tempReadHandle = null;
+ SafeFileHandle? tempWriteHandle = null;
- // When neither end is async, use the simple CreatePipe API
- if (!asyncRead && !asyncWrite)
+ try
{
- bool ret = Interop.Kernel32.CreatePipe(out readHandle, out writeHandle, ref securityAttributes, 0);
- if (!ret || readHandle.IsInvalid || writeHandle.IsInvalid)
+ // When neither end is async, use the simple CreatePipe API
+ if (!asyncRead && !asyncWrite)
{
- throw new Win32Exception();
+ bool ret = Interop.Kernel32.CreatePipe(out tempReadHandle, out tempWriteHandle, ref securityAttributes, 0);
+ if (!ret)
+ {
+ throw new Win32Exception();
+ }
+
+ Debug.Assert(!tempReadHandle.IsInvalid);
+ Debug.Assert(!tempWriteHandle.IsInvalid);
+
+ if (tempReadHandle.IsInvalid || tempWriteHandle.IsInvalid)
+ {
+ throw new Win32Exception(Interop.Errors.ERROR_INVALID_HANDLE);
+ }
+
+ tempReadHandle._fileOptions = FileOptions.None;
+ tempWriteHandle._fileOptions = FileOptions.None;
}
+ else
+ {
+ // When one or both ends are async, use named pipes to support async I/O.
+ string pipeName = $@"\\.\pipe\{Guid.NewGuid()}";
- readHandle._fileOptions = FileOptions.None;
- writeHandle._fileOptions = FileOptions.None;
- return;
- }
+ // Security: we don't need to specify a security descriptor, because
+ // we allow only for 1 instance of the pipe and immediately open the write end,
+ // so there is no time window for another process to open the pipe with different permissions.
+ // Even if that happens, we are going to fail to open the write end and throw an exception, so there is no security risk.
+
+ // Determine the open mode for the read end
+ int openMode = (int)Interop.Kernel32.PipeOptions.PIPE_ACCESS_INBOUND |
+ Interop.Kernel32.FileOperations.FILE_FLAG_FIRST_PIPE_INSTANCE; // Only one can be created with this name
+
+ if (asyncRead)
+ {
+ openMode |= Interop.Kernel32.FileOperations.FILE_FLAG_OVERLAPPED; // Asynchronous I/O
+ }
- // When one or both ends are async, use named pipes to support async I/O.
- string pipeName = $@"\\.\pipe\{Guid.NewGuid()}";
+ int pipeMode = (int)(Interop.Kernel32.PipeOptions.PIPE_TYPE_BYTE | // the alternative would be to use "Message"
+ Interop.Kernel32.PipeOptions.PIPE_READMODE_BYTE); // Data is read from the pipe as a stream of bytes
- // Security: we don't need to specify a security descriptor, because
- // we allow only for 1 instance of the pipe and immediately open the write end,
- // so there is no time window for another process to open the pipe with different permissions.
- // Even if that happens, we are going to fail to open the write end and throw an exception, so there is no security risk.
+ // We could consider specifying a larger buffer size.
+ tempReadHandle = Interop.Kernel32.CreateNamedPipeFileHandle(pipeName, openMode, pipeMode, 1, 0, 0, 0, ref securityAttributes);
- // Determine the open mode for the read end
- int openMode = (int)Interop.Kernel32.PipeOptions.PIPE_ACCESS_INBOUND |
- Interop.Kernel32.FileOperations.FILE_FLAG_FIRST_PIPE_INSTANCE; // Only one can be created with this name
+ if (tempReadHandle.IsInvalid)
+ {
+ throw new Win32Exception();
+ }
- if (asyncRead)
- {
- openMode |= Interop.Kernel32.FileOperations.FILE_FLAG_OVERLAPPED; // Asynchronous I/O
- }
+ FileOptions writeOptions = asyncWrite ? FileOptions.Asynchronous : FileOptions.None;
+ tempWriteHandle = Open(pipeName, FileMode.Open, FileAccess.Write, FileShare.Read, writeOptions, preallocationSize: 0);
- int pipeMode = (int)(Interop.Kernel32.PipeOptions.PIPE_TYPE_BYTE | // the alternative would be to use "Message"
- Interop.Kernel32.PipeOptions.PIPE_READMODE_BYTE); // Data is read from the pipe as a stream of bytes
+ tempReadHandle._fileOptions = asyncRead ? FileOptions.Asynchronous : FileOptions.None;
+ tempWriteHandle._fileOptions = asyncWrite ? FileOptions.Asynchronous : FileOptions.None;
+ }
- // We could consider specifying a larger buffer size.
- readHandle = Interop.Kernel32.CreateNamedPipeFileHandle(pipeName, openMode, pipeMode, 1, 0, 0, 0, ref securityAttributes);
+ Debug.Assert(tempReadHandle is not null);
+ Debug.Assert(tempWriteHandle is not null);
- if (readHandle.IsInvalid)
- {
- throw new Win32Exception();
- }
+ readHandle = tempReadHandle;
+ writeHandle = tempWriteHandle;
- try
- {
- FileOptions writeOptions = asyncWrite ? FileOptions.Asynchronous : FileOptions.None;
- writeHandle = Open(pipeName, FileMode.Open, FileAccess.Write, FileShare.Read, writeOptions, preallocationSize: 0);
+ tempReadHandle = null;
+ tempWriteHandle = null;
}
- catch
+ finally
{
- readHandle.Dispose();
- throw;
+ tempReadHandle?.Dispose();
+ tempWriteHandle?.Dispose();
}
-
- readHandle._fileOptions = asyncRead ? FileOptions.Asynchronous : FileOptions.None;
- writeHandle._fileOptions = asyncWrite ? FileOptions.Asynchronous : FileOptions.None;
}
public bool IsAsync => (GetFileOptions() & FileOptions.Asynchronous) != 0;
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
index e1daea829a48f5..31c23450cd6ce3 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
@@ -17,10 +17,7 @@ public sealed partial class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid
/// When this method returns, contains the write end of the pipe.
/// to enable asynchronous operations for the read end of the pipe; otherwise, .
/// to enable asynchronous operations for the write end of the pipe; otherwise, .
- public static void CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead = false, bool asyncWrite = false) =>
- CreateAnonymousPipeCore(out readHandle, out writeHandle, asyncRead, asyncWrite);
-
- private static unsafe partial void CreateAnonymousPipeCore(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite);
+ public static unsafe partial void CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead = false, bool asyncWrite = false);
///
/// Creates a around a file handle.
From 7499127e8e0b4afe91c336fffc82f7f6bb301b65 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Sat, 7 Mar 2026 10:28:23 +0000
Subject: [PATCH 37/58] Fix remaining review issues in pipe handle error paths
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../Win32/SafeHandles/SafeFileHandle.Unix.cs | 14 ++++++++++++++
.../Win32/SafeHandles/SafeFileHandle.Windows.cs | 5 -----
2 files changed, 14 insertions(+), 5 deletions(-)
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
index ae5bac033c3041..eafc2bfbeffdd6 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
@@ -202,13 +202,17 @@ public static unsafe partial void CreateAnonymousPipe(out SafeFileHandle readHan
int readFd = fds[Interop.Sys.ReadEndOfPipe];
int writeFd = fds[Interop.Sys.WriteEndOfPipe];
+ bool closeReadFd = true;
+ bool closeWriteFd = true;
SafeFileHandle? tempReadHandle = null;
SafeFileHandle? tempWriteHandle = null;
try
{
tempReadHandle = new SafeFileHandle((IntPtr)readFd, ownsHandle: true);
+ closeReadFd = false;
tempWriteHandle = new SafeFileHandle((IntPtr)writeFd, ownsHandle: true);
+ closeWriteFd = false;
tempReadHandle.IsAsync = asyncRead;
tempWriteHandle.IsAsync = asyncWrite;
@@ -223,6 +227,16 @@ public static unsafe partial void CreateAnonymousPipe(out SafeFileHandle readHan
{
tempReadHandle?.Dispose();
tempWriteHandle?.Dispose();
+
+ if (closeReadFd)
+ {
+ Interop.Sys.Close(readFd);
+ }
+
+ if (closeWriteFd)
+ {
+ Interop.Sys.Close(writeFd);
+ }
}
}
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
index 9f3d331ce25e5e..80834b19f9f981 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
@@ -42,11 +42,6 @@ public static unsafe partial void CreateAnonymousPipe(out SafeFileHandle readHan
Debug.Assert(!tempReadHandle.IsInvalid);
Debug.Assert(!tempWriteHandle.IsInvalid);
- if (tempReadHandle.IsInvalid || tempWriteHandle.IsInvalid)
- {
- throw new Win32Exception(Interop.Errors.ERROR_INVALID_HANDLE);
- }
-
tempReadHandle._fileOptions = FileOptions.None;
tempWriteHandle._fileOptions = FileOptions.None;
}
From f292b53985c6c7008a32d8e22a844fb5919b4829 Mon Sep 17 00:00:00 2001
From: Adam Sitnik
Date: Sat, 7 Mar 2026 20:14:10 +0100
Subject: [PATCH 38/58] address the code review feedback and fix the build
---
.../Win32/SafeHandles/SafeFileHandle.Unix.cs | 50 +++------
.../SafeHandles/SafeFileHandle.Windows.cs | 106 +++++++++---------
.../Win32/SafeHandles/SafeFileHandle.cs | 3 +-
.../System.Private.CoreLib.Shared.projitems | 6 +
.../File/OpenHandle.cs | 55 +++------
5 files changed, 84 insertions(+), 136 deletions(-)
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
index eafc2bfbeffdd6..34a086a93b5c6f 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
@@ -180,8 +180,12 @@ public override bool IsInvalid
}
}
- public static unsafe partial void CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite)
+ private static unsafe void CreateAnonymousPipeCore(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite)
{
+ // Allocate the handles first, so in case of OOM we don't leak any handles.
+ SafeFileHandle tempReadHandle = new();
+ SafeFileHandle tempWriteHandle = new();
+
int* fds = stackalloc int[2];
Interop.Sys.PipeFlags flags = Interop.Sys.PipeFlags.O_CLOEXEC;
if (asyncRead)
@@ -197,47 +201,19 @@ public static unsafe partial void CreateAnonymousPipe(out SafeFileHandle readHan
if (Interop.Sys.Pipe(fds, flags) != 0)
{
Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo();
+ tempReadHandle.Dispose();
+ tempWriteHandle.Dispose();
throw Interop.GetExceptionForIoErrno(error);
}
- int readFd = fds[Interop.Sys.ReadEndOfPipe];
- int writeFd = fds[Interop.Sys.WriteEndOfPipe];
- bool closeReadFd = true;
- bool closeWriteFd = true;
-
- SafeFileHandle? tempReadHandle = null;
- SafeFileHandle? tempWriteHandle = null;
- try
- {
- tempReadHandle = new SafeFileHandle((IntPtr)readFd, ownsHandle: true);
- closeReadFd = false;
- tempWriteHandle = new SafeFileHandle((IntPtr)writeFd, ownsHandle: true);
- closeWriteFd = false;
-
- tempReadHandle.IsAsync = asyncRead;
- tempWriteHandle.IsAsync = asyncWrite;
-
- readHandle = tempReadHandle;
- writeHandle = tempWriteHandle;
-
- tempReadHandle = null;
- tempWriteHandle = null;
- }
- finally
- {
- tempReadHandle?.Dispose();
- tempWriteHandle?.Dispose();
+ tempReadHandle.SetHandle(fds[Interop.Sys.ReadEndOfPipe]);
+ tempReadHandle.IsAsync = asyncRead;
- if (closeReadFd)
- {
- Interop.Sys.Close(readFd);
- }
+ tempWriteHandle.SetHandle(fds[Interop.Sys.WriteEndOfPipe]);
+ tempWriteHandle.IsAsync = asyncWrite;
- if (closeWriteFd)
- {
- Interop.Sys.Close(writeFd);
- }
- }
+ readHandle = tempReadHandle;
+ writeHandle = tempWriteHandle;
}
// Specialized Open that returns the file length and permissions of the opened file.
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
index 80834b19f9f981..ed1bd9e75272b2 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
@@ -22,80 +22,74 @@ public SafeFileHandle() : base(true)
{
}
- public static unsafe partial void CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite)
+ private static void CreateAnonymousPipeCore(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite)
{
Interop.Kernel32.SECURITY_ATTRIBUTES securityAttributes = default;
- SafeFileHandle? tempReadHandle = null;
- SafeFileHandle? tempWriteHandle = null;
+ SafeFileHandle? tempReadHandle;
+ SafeFileHandle? tempWriteHandle;
- try
+ // When neither end is async, use the simple CreatePipe API
+ if (!asyncRead && !asyncWrite)
{
- // When neither end is async, use the simple CreatePipe API
- if (!asyncRead && !asyncWrite)
+ bool ret = Interop.Kernel32.CreatePipe(out tempReadHandle, out tempWriteHandle, ref securityAttributes, 0);
+ if (!ret)
{
- bool ret = Interop.Kernel32.CreatePipe(out tempReadHandle, out tempWriteHandle, ref securityAttributes, 0);
- if (!ret)
- {
- throw new Win32Exception();
- }
-
- Debug.Assert(!tempReadHandle.IsInvalid);
- Debug.Assert(!tempWriteHandle.IsInvalid);
-
- tempReadHandle._fileOptions = FileOptions.None;
- tempWriteHandle._fileOptions = FileOptions.None;
+ throw new Win32Exception();
}
- else
- {
- // When one or both ends are async, use named pipes to support async I/O.
- string pipeName = $@"\\.\pipe\{Guid.NewGuid()}";
-
- // Security: we don't need to specify a security descriptor, because
- // we allow only for 1 instance of the pipe and immediately open the write end,
- // so there is no time window for another process to open the pipe with different permissions.
- // Even if that happens, we are going to fail to open the write end and throw an exception, so there is no security risk.
- // Determine the open mode for the read end
- int openMode = (int)Interop.Kernel32.PipeOptions.PIPE_ACCESS_INBOUND |
- Interop.Kernel32.FileOperations.FILE_FLAG_FIRST_PIPE_INSTANCE; // Only one can be created with this name
+ Debug.Assert(!tempReadHandle.IsInvalid);
+ Debug.Assert(!tempWriteHandle.IsInvalid);
- if (asyncRead)
- {
- openMode |= Interop.Kernel32.FileOperations.FILE_FLAG_OVERLAPPED; // Asynchronous I/O
- }
+ tempReadHandle._fileOptions = FileOptions.None;
+ tempWriteHandle._fileOptions = FileOptions.None;
+ }
+ else
+ {
+ // When one or both ends are async, use named pipes to support async I/O.
+ string pipeName = $@"\\.\pipe\dotnet_{Guid.NewGuid()}";
- int pipeMode = (int)(Interop.Kernel32.PipeOptions.PIPE_TYPE_BYTE | // the alternative would be to use "Message"
- Interop.Kernel32.PipeOptions.PIPE_READMODE_BYTE); // Data is read from the pipe as a stream of bytes
+ // Security: we don't need to specify a security descriptor, because
+ // we allow only for 1 instance of the pipe and immediately open the write end,
+ // so there is no time window for another process to open the pipe with different permissions.
+ // Even if that happens, we are going to fail to open the write end and throw an exception, so there is no security risk.
- // We could consider specifying a larger buffer size.
- tempReadHandle = Interop.Kernel32.CreateNamedPipeFileHandle(pipeName, openMode, pipeMode, 1, 0, 0, 0, ref securityAttributes);
+ // Determine the open mode for the read end
+ int openMode = (int)Interop.Kernel32.PipeOptions.PIPE_ACCESS_INBOUND |
+ Interop.Kernel32.FileOperations.FILE_FLAG_FIRST_PIPE_INSTANCE; // Only one can be created with this name
- if (tempReadHandle.IsInvalid)
- {
- throw new Win32Exception();
- }
+ if (asyncRead)
+ {
+ openMode |= Interop.Kernel32.FileOperations.FILE_FLAG_OVERLAPPED; // Asynchronous I/O
+ }
- FileOptions writeOptions = asyncWrite ? FileOptions.Asynchronous : FileOptions.None;
- tempWriteHandle = Open(pipeName, FileMode.Open, FileAccess.Write, FileShare.Read, writeOptions, preallocationSize: 0);
+ int pipeMode = (int)(Interop.Kernel32.PipeOptions.PIPE_TYPE_BYTE | Interop.Kernel32.PipeOptions.PIPE_READMODE_BYTE); // Data is read from the pipe as a stream of bytes
- tempReadHandle._fileOptions = asyncRead ? FileOptions.Asynchronous : FileOptions.None;
- tempWriteHandle._fileOptions = asyncWrite ? FileOptions.Asynchronous : FileOptions.None;
+ // We could consider specifying a larger buffer size.
+ tempReadHandle = Interop.Kernel32.CreateNamedPipeFileHandle(pipeName, openMode, pipeMode, 1, 0, 0, 0, ref securityAttributes);
+ if (tempReadHandle.IsInvalid)
+ {
+ int error = Marshal.GetLastPInvokeError();
+ tempReadHandle.Dispose();
+ throw new Win32Exception(error);
}
- Debug.Assert(tempReadHandle is not null);
- Debug.Assert(tempWriteHandle is not null);
+ tempReadHandle._fileOptions = asyncRead ? FileOptions.Asynchronous : FileOptions.None;
+ FileOptions writeOptions = asyncWrite ? FileOptions.Asynchronous : FileOptions.None;
- readHandle = tempReadHandle;
- writeHandle = tempWriteHandle;
+ try
+ {
+ tempWriteHandle = Open(pipeName, FileMode.Open, FileAccess.Write, FileShare.Read, writeOptions, preallocationSize: 0);
+ }
+ catch
+ {
+ tempReadHandle.Dispose();
- tempReadHandle = null;
- tempWriteHandle = null;
- }
- finally
- {
- tempReadHandle?.Dispose();
- tempWriteHandle?.Dispose();
+ throw;
+ }
}
+
+ readHandle = tempReadHandle;
+ writeHandle = tempWriteHandle;
}
public bool IsAsync => (GetFileOptions() & FileOptions.Asynchronous) != 0;
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
index 31c23450cd6ce3..52b43379bd7d12 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
@@ -17,7 +17,8 @@ public sealed partial class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid
/// When this method returns, contains the write end of the pipe.
/// to enable asynchronous operations for the read end of the pipe; otherwise, .
/// to enable asynchronous operations for the write end of the pipe; otherwise, .
- public static unsafe partial void CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead = false, bool asyncWrite = false);
+ public static void CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead = false, bool asyncWrite = false)
+ => CreateAnonymousPipeCore(out readHandle, out writeHandle, asyncRead, asyncWrite);
///
/// Creates a around a file handle.
diff --git a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
index 42705d59980544..c8773fcbe30be4 100644
--- a/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
+++ b/src/libraries/System.Private.CoreLib/src/System.Private.CoreLib.Shared.projitems
@@ -1798,6 +1798,12 @@
Common\Interop\Windows\Kernel32\Interop.CreateFile.cs
+
+ Common\Interop\Windows\Kernel32\Interop.PipeOptions.cs
+
+
+ Common\Interop\Windows\Kernel32\Interop.CreatePipe_SafeFileHandle.cs
+
Common\Interop\Windows\Kernel32\Interop.CreateNamedPipe_SafeFileHandle.cs
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
index afafde661bcde6..8adb1e7d9b3f82 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
@@ -1,7 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.IO.Pipes;
using System.Threading.Tasks;
using Microsoft.Win32.SafeHandles;
using Xunit;
@@ -89,55 +88,27 @@ public void SafeFileHandle_IsAsync_ReturnsCorrectInformation(FileOptions options
[InlineData(false, true)]
[InlineData(true, false)]
[InlineData(true, true)]
- public async Task SafeFileHandle_CreateAnonymousPipe_SetsIsAsyncAndTransfersData(bool asyncRead, bool asyncWrite)
+ public static async Task SafeFileHandle_CreateAnonymousPipe_SetsIsAsyncAndTransfersData(bool asyncRead, bool asyncWrite)
{
+ byte[] message = "Hello, Pipe!"u8.ToArray();
+
SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, asyncRead, asyncWrite);
Assert.Equal(asyncRead, readHandle.IsAsync);
Assert.Equal(asyncWrite, writeHandle.IsAsync);
+ Assert.Equal(FileHandleType.Pipe, readHandle.Type);
+ Assert.Equal(FileHandleType.Pipe, writeHandle.Type);
- using Stream readStream = CreatePipeReadStream(readHandle, asyncRead);
- using Stream writeStream = CreatePipeWriteStream(writeHandle, asyncWrite);
-
- byte[] expected = [1, 2, 3, 4];
- byte[] actual = new byte[expected.Length];
-
- if (!OperatingSystem.IsWindows() && asyncWrite)
- {
- await writeStream.WriteAsync(expected);
- }
- else
- {
- writeStream.Write(expected);
- }
-
- if (!OperatingSystem.IsWindows() && asyncRead)
+ using (readHandle)
+ using (writeHandle)
+ using (FileStream readStream = new(readHandle, FileAccess.Read, bufferSize: 1, isAsync: asyncRead))
+ using (FileStream writeStream = new(writeHandle, FileAccess.Write, bufferSize: 1, isAsync: asyncWrite))
{
- await readStream.ReadExactlyAsync(actual);
- }
- else
- {
- readStream.ReadExactly(actual);
- }
-
- Assert.Equal(expected, actual);
- }
-
- private static Stream CreatePipeReadStream(SafeFileHandle readHandle, bool asyncRead) =>
- !OperatingSystem.IsWindows() && asyncRead
- ? new AnonymousPipeClientStream(PipeDirection.In, TransferOwnershipToPipeHandle(readHandle))
- : new FileStream(readHandle, FileAccess.Read, 1, asyncRead);
+ byte[] buffer = new byte[message.Length];
- private static Stream CreatePipeWriteStream(SafeFileHandle writeHandle, bool asyncWrite) =>
- !OperatingSystem.IsWindows() && asyncWrite
- ? new AnonymousPipeClientStream(PipeDirection.Out, TransferOwnershipToPipeHandle(writeHandle))
- : new FileStream(writeHandle, FileAccess.Write, 1, asyncWrite);
+ await Task.WhenAll(writeStream.WriteAsync(message, 0, message.Length), readStream.ReadAsync(buffer, 0, buffer.Length));
- private static SafePipeHandle TransferOwnershipToPipeHandle(SafeFileHandle handle)
- {
- SafePipeHandle pipeHandle = new SafePipeHandle(handle.DangerousGetHandle(), ownsHandle: true);
- handle.SetHandleAsInvalid();
- handle.Dispose();
- return pipeHandle;
+ Assert.Equal(message, buffer);
+ }
}
[Theory]
From 1c3e005b927d7175c5286aa7a56e61a1a7417e6f Mon Sep 17 00:00:00 2001
From: Adam Sitnik
Date: Sun, 8 Mar 2026 16:49:09 +0100
Subject: [PATCH 39/58] address code review feedback
---
.../Win32/SafeHandles/SafeFileHandle.Windows.cs | 16 +++++++---------
1 file changed, 7 insertions(+), 9 deletions(-)
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
index ed1bd9e75272b2..fdc0c643b50561 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
@@ -66,18 +66,16 @@ private static void CreateAnonymousPipeCore(out SafeFileHandle readHandle, out S
// We could consider specifying a larger buffer size.
tempReadHandle = Interop.Kernel32.CreateNamedPipeFileHandle(pipeName, openMode, pipeMode, 1, 0, 0, 0, ref securityAttributes);
- if (tempReadHandle.IsInvalid)
- {
- int error = Marshal.GetLastPInvokeError();
- tempReadHandle.Dispose();
- throw new Win32Exception(error);
- }
-
- tempReadHandle._fileOptions = asyncRead ? FileOptions.Asynchronous : FileOptions.None;
- FileOptions writeOptions = asyncWrite ? FileOptions.Asynchronous : FileOptions.None;
try
{
+ if (tempReadHandle.IsInvalid)
+ {
+ throw new Win32Exception();
+ }
+
+ tempReadHandle._fileOptions = asyncRead ? FileOptions.Asynchronous : FileOptions.None;
+ FileOptions writeOptions = asyncWrite ? FileOptions.Asynchronous : FileOptions.None;
tempWriteHandle = Open(pipeName, FileMode.Open, FileAccess.Write, FileShare.Read, writeOptions, preallocationSize: 0);
}
catch
From 8f447ea1cfdcd8b2b483ae58a611338ffe664b9e Mon Sep 17 00:00:00 2001
From: Adam Sitnik
Date: Sun, 8 Mar 2026 17:56:53 +0100
Subject: [PATCH 40/58] Apply suggestions from code review
---
.../src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs | 2 +-
.../src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs | 2 +-
.../src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs | 3 +--
3 files changed, 3 insertions(+), 4 deletions(-)
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
index 34a086a93b5c6f..df5ccc855552e7 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
@@ -180,7 +180,7 @@ public override bool IsInvalid
}
}
- private static unsafe void CreateAnonymousPipeCore(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite)
+ public static unsafe partial void CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite)
{
// Allocate the handles first, so in case of OOM we don't leak any handles.
SafeFileHandle tempReadHandle = new();
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
index fdc0c643b50561..54d3b3cf0fff80 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
@@ -22,7 +22,7 @@ public SafeFileHandle() : base(true)
{
}
- private static void CreateAnonymousPipeCore(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite)
+ public static unsafe partial void CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite)
{
Interop.Kernel32.SECURITY_ATTRIBUTES securityAttributes = default;
SafeFileHandle? tempReadHandle;
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
index 52b43379bd7d12..31c23450cd6ce3 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
@@ -17,8 +17,7 @@ public sealed partial class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid
/// When this method returns, contains the write end of the pipe.
/// to enable asynchronous operations for the read end of the pipe; otherwise, .
/// to enable asynchronous operations for the write end of the pipe; otherwise, .
- public static void CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead = false, bool asyncWrite = false)
- => CreateAnonymousPipeCore(out readHandle, out writeHandle, asyncRead, asyncWrite);
+ public static unsafe partial void CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead = false, bool asyncWrite = false);
///
/// Creates a around a file handle.
From 6d1a6c7c3620b292f24a327fd49aa7a56dbeeba5 Mon Sep 17 00:00:00 2001
From: Adam Sitnik
Date: Mon, 9 Mar 2026 15:51:14 +0100
Subject: [PATCH 41/58] use AnonymousPipeClientStream on Unix for async IO
---
.../File/OpenHandle.cs | 28 +++++++++++++++++--
1 file changed, 25 insertions(+), 3 deletions(-)
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
index 8adb1e7d9b3f82..f0c34de16f030e 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
@@ -1,6 +1,7 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
+using System.IO.Pipes;
using System.Threading.Tasks;
using Microsoft.Win32.SafeHandles;
using Xunit;
@@ -100,15 +101,36 @@ public static async Task SafeFileHandle_CreateAnonymousPipe_SetsIsAsyncAndTransf
using (readHandle)
using (writeHandle)
- using (FileStream readStream = new(readHandle, FileAccess.Read, bufferSize: 1, isAsync: asyncRead))
- using (FileStream writeStream = new(writeHandle, FileAccess.Write, bufferSize: 1, isAsync: asyncWrite))
+ using (Stream readStream = CreatePipeReadStream(readHandle, asyncRead))
+ using (Stream writeStream = CreatePipeWriteStream(writeHandle, asyncWrite))
{
byte[] buffer = new byte[message.Length];
- await Task.WhenAll(writeStream.WriteAsync(message, 0, message.Length), readStream.ReadAsync(buffer, 0, buffer.Length));
+ Task writeTask = writeStream.WriteAsync(message, 0, message.Length);
+ Task readTask = readStream.ReadAsync(buffer, 0, buffer.Length);
+
+ await Task.WhenAll(writeTask, readTask);
Assert.Equal(message, buffer);
}
+
+ static Stream CreatePipeReadStream(SafeFileHandle readHandle, bool asyncRead)
+ => !OperatingSystem.IsWindows() && asyncRead
+ ? new AnonymousPipeClientStream(PipeDirection.In, TransferOwnershipToPipeHandle(readHandle))
+ : new FileStream(readHandle, FileAccess.Read, 1, asyncRead);
+
+ static Stream CreatePipeWriteStream(SafeFileHandle writeHandle, bool asyncWrite)
+ => !OperatingSystem.IsWindows() && asyncWrite
+ ? new AnonymousPipeClientStream(PipeDirection.Out, TransferOwnershipToPipeHandle(writeHandle))
+ : new FileStream(writeHandle, FileAccess.Write, 1, asyncWrite);
+
+ static SafePipeHandle TransferOwnershipToPipeHandle(SafeFileHandle handle)
+ {
+ SafePipeHandle pipeHandle = new SafePipeHandle(handle.DangerousGetHandle(), ownsHandle: true);
+ handle.SetHandleAsInvalid();
+ handle.Dispose();
+ return pipeHandle;
+ }
}
[Theory]
From 187a16f96af20eb811d471e12058a82690c4dda3 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 9 Mar 2026 15:42:45 +0000
Subject: [PATCH 42/58] Add Unix async-handle guard in FileStream(handle) and
tests
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../src/System/IO/FileStream.cs | 4 ++++
.../FileStream/IsAsync.cs | 11 +++++++++--
.../FileStream/ctor_sfh_fa.cs | 8 ++++++++
.../FileStream/ctor_sfh_fa_buffer_async.cs | 17 +++++++++++++++--
4 files changed, 36 insertions(+), 4 deletions(-)
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs
index 50f15b4165c117..9404c2c2d45b27 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs
@@ -82,6 +82,10 @@ private static void ValidateHandle(SafeFileHandle handle, FileAccess access, int
{
ThrowHelper.ThrowObjectDisposedException_FileClosed();
}
+ else if (!OperatingSystem.IsWindows() && handle.IsAsync)
+ {
+ throw new ArgumentException(SR.Arg_InvalidHandle, nameof(handle));
+ }
}
public FileStream(SafeFileHandle handle, FileAccess access)
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/IsAsync.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/IsAsync.cs
index e22428c4b39780..f7f1a4111a2a3b 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/IsAsync.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/IsAsync.cs
@@ -42,9 +42,16 @@ public void FileOptionsAsynchronousConstructorArg()
public void AsyncDiscoveredFromHandle()
{
using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, 4096, true))
- using (FileStream fsh = new FileStream(fs.SafeFileHandle, FileAccess.ReadWrite))
{
- Assert.True(fsh.IsAsync);
+ if (OperatingSystem.IsWindows())
+ {
+ using FileStream fsh = new FileStream(fs.SafeFileHandle, FileAccess.ReadWrite);
+ Assert.True(fsh.IsAsync);
+ }
+ else
+ {
+ AssertExtensions.Throws("handle", () => new FileStream(fs.SafeFileHandle, FileAccess.ReadWrite));
+ }
}
using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, 4096, false))
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_sfh_fa.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_sfh_fa.cs
index 5ad232a6d3a94d..66c395fe774b14 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_sfh_fa.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_sfh_fa.cs
@@ -43,6 +43,14 @@ public void InvalidAccess_DoesNotCloseHandle()
}
}
+ [Fact]
+ [PlatformSpecific(TestPlatforms.AnyUnix)]
+ public void AsyncHandleOnUnix_Throws()
+ {
+ using SafeFileHandle handle = File.OpenHandle(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.None, FileOptions.Asynchronous);
+ AssertExtensions.Throws("handle", () => CreateFileStream(handle, FileAccess.ReadWrite));
+ }
+
[Fact]
public void FileAccessRead()
{
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_sfh_fa_buffer_async.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_sfh_fa_buffer_async.cs
index 665bf08dbb3892..ccb662b17b5457 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_sfh_fa_buffer_async.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_sfh_fa_buffer_async.cs
@@ -24,8 +24,15 @@ public void MatchedAsync()
{
using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite | FileShare.Delete, 4096, true))
{
- using (CreateFileStream(fs.SafeFileHandle, FileAccess.ReadWrite, 4096, true))
- { }
+ if (OperatingSystem.IsWindows())
+ {
+ using (CreateFileStream(fs.SafeFileHandle, FileAccess.ReadWrite, 4096, true))
+ { }
+ }
+ else
+ {
+ AssertExtensions.Throws("handle", () => CreateFileStream(fs.SafeFileHandle, FileAccess.ReadWrite, 4096, true));
+ }
}
}
@@ -36,6 +43,12 @@ public async Task UnmatchedAsyncIsAllowed(bool isAsync)
{
using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite | FileShare.Delete, 4096, isAsync))
{
+ if (!OperatingSystem.IsWindows() && isAsync)
+ {
+ AssertExtensions.Throws("handle", () => CreateFileStream(fs.SafeFileHandle, FileAccess.ReadWrite, 4096, !isAsync));
+ return;
+ }
+
// isAsync parameter is now ignored, handle.IsAsync is used instead
using (FileStream newFs = CreateFileStream(fs.SafeFileHandle, FileAccess.ReadWrite, 4096, !isAsync))
{
From 491e5cc580925280ab7646621f0e7b1acb0af3c7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 9 Mar 2026 15:52:21 +0000
Subject: [PATCH 43/58] Handle closed SafeFileHandle in Unix IsAsync getter
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
index df5ccc855552e7..c57dfffb234e16 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
@@ -59,7 +59,7 @@ public bool IsAsync
get
{
NullableBool isAsync = _isAsync;
- if (isAsync == NullableBool.Undefined)
+ if (isAsync == NullableBool.Undefined && !IsClosed)
{
if (Interop.Sys.Fcntl.GetIsNonBlocking(this, out bool isNonBlocking) != 0)
{
From f47891bcf4ed35f69f251f1a4f972c50d20e9f7f Mon Sep 17 00:00:00 2001
From: Adam Sitnik
Date: Mon, 9 Mar 2026 16:54:06 +0100
Subject: [PATCH 44/58] address my own feedback:
- Currently SafeFileHandle.CreateAnonymousPipe is the only public API that allows creating async handles on Unix
- fix Windows build
---
.../Kernel32/Interop.CreateNamedPipe.cs | 2 ++
.../File/OpenHandle.cs | 21 +++++++++++++++++++
.../FileStream/IsAsync.cs | 11 ++--------
.../FileStream/ctor_sfh_fa.cs | 8 -------
.../FileStream/ctor_sfh_fa_buffer_async.cs | 17 ++-------------
5 files changed, 27 insertions(+), 32 deletions(-)
diff --git a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateNamedPipe.cs b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateNamedPipe.cs
index 6a2522eba28304..6cf9c648506ba7 100644
--- a/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateNamedPipe.cs
+++ b/src/libraries/Common/src/Interop/Windows/Kernel32/Interop.CreateNamedPipe.cs
@@ -3,6 +3,8 @@
using System;
using System.Runtime.InteropServices;
+using Microsoft.Win32.SafeHandles;
+
internal static partial class Interop
{
internal static partial class Kernel32
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
index f0c34de16f030e..436ac6593fefae 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
@@ -133,6 +133,27 @@ static SafePipeHandle TransferOwnershipToPipeHandle(SafeFileHandle handle)
}
}
+ [Fact]
+ [PlatformSpecific(TestPlatforms.AnyUnix)]
+ public void AsyncHandleOnUnix_Throws()
+ {
+ // Currently SafeFileHandle.CreateAnonymousPipe is the only public API that allows creating async handles on Unix
+
+ SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, asyncRead: true, asyncWrite: true);
+
+ using (readHandle)
+ using (writeHandle)
+ {
+ Assert.True(readHandle.IsAsync);
+ Assert.True(writeHandle.IsAsync);
+ Assert.Equal(FileHandleType.Pipe, readHandle.Type);
+ Assert.Equal(FileHandleType.Pipe, writeHandle.Type);
+
+ AssertExtensions.Throws("handle", () => new FileStream(readHandle, FileAccess.ReadWrite));
+ AssertExtensions.Throws("handle", () => new FileStream(writeHandle, FileAccess.ReadWrite));
+ }
+ }
+
[Theory]
[InlineData(FileOptions.DeleteOnClose)]
[InlineData(FileOptions.DeleteOnClose | FileOptions.Asynchronous)]
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/IsAsync.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/IsAsync.cs
index f7f1a4111a2a3b..e22428c4b39780 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/IsAsync.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/IsAsync.cs
@@ -42,16 +42,9 @@ public void FileOptionsAsynchronousConstructorArg()
public void AsyncDiscoveredFromHandle()
{
using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, 4096, true))
+ using (FileStream fsh = new FileStream(fs.SafeFileHandle, FileAccess.ReadWrite))
{
- if (OperatingSystem.IsWindows())
- {
- using FileStream fsh = new FileStream(fs.SafeFileHandle, FileAccess.ReadWrite);
- Assert.True(fsh.IsAsync);
- }
- else
- {
- AssertExtensions.Throws("handle", () => new FileStream(fs.SafeFileHandle, FileAccess.ReadWrite));
- }
+ Assert.True(fsh.IsAsync);
}
using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, 4096, false))
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_sfh_fa.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_sfh_fa.cs
index 66c395fe774b14..5ad232a6d3a94d 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_sfh_fa.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_sfh_fa.cs
@@ -43,14 +43,6 @@ public void InvalidAccess_DoesNotCloseHandle()
}
}
- [Fact]
- [PlatformSpecific(TestPlatforms.AnyUnix)]
- public void AsyncHandleOnUnix_Throws()
- {
- using SafeFileHandle handle = File.OpenHandle(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.None, FileOptions.Asynchronous);
- AssertExtensions.Throws("handle", () => CreateFileStream(handle, FileAccess.ReadWrite));
- }
-
[Fact]
public void FileAccessRead()
{
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_sfh_fa_buffer_async.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_sfh_fa_buffer_async.cs
index ccb662b17b5457..665bf08dbb3892 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_sfh_fa_buffer_async.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_sfh_fa_buffer_async.cs
@@ -24,15 +24,8 @@ public void MatchedAsync()
{
using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite | FileShare.Delete, 4096, true))
{
- if (OperatingSystem.IsWindows())
- {
- using (CreateFileStream(fs.SafeFileHandle, FileAccess.ReadWrite, 4096, true))
- { }
- }
- else
- {
- AssertExtensions.Throws("handle", () => CreateFileStream(fs.SafeFileHandle, FileAccess.ReadWrite, 4096, true));
- }
+ using (CreateFileStream(fs.SafeFileHandle, FileAccess.ReadWrite, 4096, true))
+ { }
}
}
@@ -43,12 +36,6 @@ public async Task UnmatchedAsyncIsAllowed(bool isAsync)
{
using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.ReadWrite | FileShare.Delete, 4096, isAsync))
{
- if (!OperatingSystem.IsWindows() && isAsync)
- {
- AssertExtensions.Throws("handle", () => CreateFileStream(fs.SafeFileHandle, FileAccess.ReadWrite, 4096, !isAsync));
- return;
- }
-
// isAsync parameter is now ignored, handle.IsAsync is used instead
using (FileStream newFs = CreateFileStream(fs.SafeFileHandle, FileAccess.ReadWrite, 4096, !isAsync))
{
From c386459172499d052f1b861fe7f074b5fb712d33 Mon Sep 17 00:00:00 2001
From: Adam Sitnik
Date: Mon, 9 Mar 2026 22:04:14 +0100
Subject: [PATCH 45/58] Unix does not support O_NONBLOCK for regular files:
- SafeFileHandle.IsAsync should reflect the actual state and never report true for regular files on Unix
- update tests
- increase test coverage to cover E_WOULDBLOCK on Unix
---
.../Win32/SafeHandles/SafeFileHandle.Unix.cs | 4 +-
.../File/OpenHandle.cs | 65 ++++++++++++++-----
.../FileStream/FileStreamOptions.cs | 2 +-
.../FileStream/IsAsync.cs | 6 +-
.../FileStream/ctor_sfh_fa_buffer_async.cs | 2 +-
.../ctor_str_fm_fa_fs_buffer_async.cs | 2 +-
.../FileStream/ctor_str_fm_fa_fs_buffer_fo.cs | 2 +-
.../FileSystemTest.cs | 2 +
8 files changed, 59 insertions(+), 26 deletions(-)
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
index c57dfffb234e16..c7aba3d846bd19 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
@@ -348,7 +348,7 @@ private static Interop.Sys.OpenFlags PreOpenConfigurationFromOptions(FileMode mo
}
// Translate some FileOptions; some just aren't supported, and others will be handled after calling open.
- // - Asynchronous: Handled in ctor, setting _useAsync and SafeFileHandle.IsAsync to true
+ // - Asynchronous: Unix does not support O_NONBLOCK for regular files, only for pipes and sockets.
// - DeleteOnClose: Doesn't have a Unix equivalent, but we approximate it in Dispose
// - Encrypted: No equivalent on Unix and is ignored
// - RandomAccess: Implemented after open if posix_fadvise is available
@@ -401,7 +401,7 @@ private bool Init(string path, FileMode mode, FileAccess access, FileShare share
filePermissions = ((UnixFileMode)status.Mode) & PermissionMask;
}
- IsAsync = (options & FileOptions.Asynchronous) != 0;
+ IsAsync = false; // Unix does not support O_NONBLOCK for regular files.
// Lock the file if requested via FileShare. This is only advisory locking. FileShare.None implies an exclusive
// lock on the file and all other modes use a shared lock. While this is not as granular as Windows, not mandatory,
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
index 436ac6593fefae..581f43df2a0cec 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
@@ -68,15 +68,15 @@ public override void FileModeAppendExisting(string streamSpecifier)
[Theory]
[InlineData(FileOptions.None)]
[InlineData(FileOptions.Asynchronous)]
- public void SafeFileHandle_IsAsync_ReturnsCorrectInformation(FileOptions options)
+ public void SafeFileHandle_IsAsync_ReturnsCorrectInformation_ForRegularFiles(FileOptions options)
{
using (var handle = File.OpenHandle(GetTestFilePath(), FileMode.Create, FileAccess.Write, options: options))
{
- Assert.Equal((options & FileOptions.Asynchronous) != 0, handle.IsAsync);
+ Assert.Equal((options & FileOptions.Asynchronous) != 0 && IsAsyncIoSupportedForRegularFiles, handle.IsAsync);
// the following code exercises the code path where we don't know FileOptions used for opening the handle
// and instead we ask the OS about it
- if (OperatingSystem.IsWindows()) // async file handles are a Windows concept
+ if (IsAsyncIoSupportedForRegularFiles)
{
SafeFileHandle createdFromIntPtr = new SafeFileHandle(handle.DangerousGetHandle(), ownsHandle: false);
Assert.Equal((options & FileOptions.Asynchronous) != 0, createdFromIntPtr.IsAsync);
@@ -84,6 +84,30 @@ public void SafeFileHandle_IsAsync_ReturnsCorrectInformation(FileOptions options
}
}
+ [Theory]
+ [InlineData(FileOptions.None)]
+ [InlineData(FileOptions.Asynchronous)]
+ public void SafeFileHandle_IsAsync_ReturnsCorrectInformation_ForPipes(FileOptions options)
+ {
+ SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, asyncRead: (options & FileOptions.Asynchronous) != 0, asyncWrite: (options & FileOptions.Asynchronous) != 0);
+
+ using (readHandle)
+ using (writeHandle)
+ {
+ Verify(readHandle, options);
+ Verify(writeHandle, options);
+ }
+
+ static void Verify(SafeFileHandle fileHandle, FileOptions fileOptions)
+ {
+ Assert.Equal((fileOptions & FileOptions.Asynchronous) != 0, fileHandle.IsAsync);
+
+ // The following code exercises the code path where the information is fetched from OS.
+ using SafeFileHandle createdFromIntPtr = new(fileHandle.DangerousGetHandle(), ownsHandle: false);
+ Assert.Equal((fileOptions & FileOptions.Asynchronous) != 0, createdFromIntPtr.IsAsync);
+ }
+ }
+
[Theory]
[InlineData(false, false)]
[InlineData(false, true)]
@@ -92,6 +116,7 @@ public void SafeFileHandle_IsAsync_ReturnsCorrectInformation(FileOptions options
public static async Task SafeFileHandle_CreateAnonymousPipe_SetsIsAsyncAndTransfersData(bool asyncRead, bool asyncWrite)
{
byte[] message = "Hello, Pipe!"u8.ToArray();
+ byte[] buffer = new byte[message.Length];
SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, asyncRead, asyncWrite);
Assert.Equal(asyncRead, readHandle.IsAsync);
@@ -101,28 +126,34 @@ public static async Task SafeFileHandle_CreateAnonymousPipe_SetsIsAsyncAndTransf
using (readHandle)
using (writeHandle)
- using (Stream readStream = CreatePipeReadStream(readHandle, asyncRead))
- using (Stream writeStream = CreatePipeWriteStream(writeHandle, asyncWrite))
+ using (Stream readStream = CreatePipeStream(readHandle, FileAccess.Read, asyncRead))
+ using (Stream writeStream = CreatePipeStream(writeHandle, FileAccess.Write, asyncWrite))
{
- byte[] buffer = new byte[message.Length];
-
Task writeTask = writeStream.WriteAsync(message, 0, message.Length);
Task readTask = readStream.ReadAsync(buffer, 0, buffer.Length);
-
await Task.WhenAll(writeTask, readTask);
+ Assert.Equal(message, buffer);
+ // Now let's test the other direction,
+ // which is going to test the E_WOULDBLOCK code path on Unix.
+ buffer.AsSpan().Reverse();
+
+ readTask = readStream.ReadAsync(buffer, 0, buffer.Length);
+ writeTask = writeStream.WriteAsync(message, 0, message.Length);
+ await Task.WhenAll(readTask, writeTask);
Assert.Equal(message, buffer);
}
+ }
- static Stream CreatePipeReadStream(SafeFileHandle readHandle, bool asyncRead)
- => !OperatingSystem.IsWindows() && asyncRead
- ? new AnonymousPipeClientStream(PipeDirection.In, TransferOwnershipToPipeHandle(readHandle))
- : new FileStream(readHandle, FileAccess.Read, 1, asyncRead);
+ private static Stream CreatePipeStream(SafeFileHandle readHandle, FileAccess access, bool asyncIO)
+ {
+ if (!OperatingSystem.IsWindows() && asyncIO)
+ {
+ PipeDirection direction = access == FileAccess.Read ? PipeDirection.In : PipeDirection.Out;
+ return new AnonymousPipeClientStream(direction, TransferOwnershipToPipeHandle(readHandle));
+ }
- static Stream CreatePipeWriteStream(SafeFileHandle writeHandle, bool asyncWrite)
- => !OperatingSystem.IsWindows() && asyncWrite
- ? new AnonymousPipeClientStream(PipeDirection.Out, TransferOwnershipToPipeHandle(writeHandle))
- : new FileStream(writeHandle, FileAccess.Write, 1, asyncWrite);
+ return new FileStream(readHandle, access, 1, asyncIO);
static SafePipeHandle TransferOwnershipToPipeHandle(SafeFileHandle handle)
{
@@ -135,7 +166,7 @@ static SafePipeHandle TransferOwnershipToPipeHandle(SafeFileHandle handle)
[Fact]
[PlatformSpecific(TestPlatforms.AnyUnix)]
- public void AsyncHandleOnUnix_Throws()
+ public void AsyncHandleOnUnix_FileStream_ctor_Throws()
{
// Currently SafeFileHandle.CreateAnonymousPipe is the only public API that allows creating async handles on Unix
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/FileStreamOptions.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/FileStreamOptions.cs
index 5155fa41c490bd..3c13c634b5b0df 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/FileStreamOptions.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/FileStreamOptions.cs
@@ -192,7 +192,7 @@ static void Validate(FileStream fs, string expectedPath, bool expectedAsync, boo
using (fs)
{
Assert.Equal(expectedPath, fs.Name);
- Assert.Equal(expectedAsync, fs.IsAsync);
+ Assert.Equal(expectedAsync && IsAsyncIoSupportedForRegularFiles, fs.IsAsync);
Assert.Equal(expectedCanRead, fs.CanRead);
Assert.Equal(expectedCanWrite, fs.CanWrite);
}
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/IsAsync.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/IsAsync.cs
index e22428c4b39780..30e21b98de7510 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/IsAsync.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/IsAsync.cs
@@ -15,7 +15,7 @@ public void IsAsyncConstructorArg()
{
using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, 4096, true))
{
- Assert.True(fs.IsAsync);
+ Assert.Equal(IsAsyncIoSupportedForRegularFiles, fs.IsAsync);
}
using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, 4096, false))
@@ -29,7 +29,7 @@ public void FileOptionsAsynchronousConstructorArg()
{
using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, 4096, FileOptions.Asynchronous))
{
- Assert.True(fs.IsAsync);
+ Assert.Equal(IsAsyncIoSupportedForRegularFiles, fs.IsAsync);
}
using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, 4096, FileOptions.None))
@@ -44,7 +44,7 @@ public void AsyncDiscoveredFromHandle()
using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, 4096, true))
using (FileStream fsh = new FileStream(fs.SafeFileHandle, FileAccess.ReadWrite))
{
- Assert.True(fsh.IsAsync);
+ Assert.Equal(IsAsyncIoSupportedForRegularFiles, fsh.IsAsync);
}
using (FileStream fs = new FileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, 4096, false))
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_sfh_fa_buffer_async.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_sfh_fa_buffer_async.cs
index 665bf08dbb3892..256129d8c3a7e0 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_sfh_fa_buffer_async.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_sfh_fa_buffer_async.cs
@@ -40,7 +40,7 @@ public async Task UnmatchedAsyncIsAllowed(bool isAsync)
using (FileStream newFs = CreateFileStream(fs.SafeFileHandle, FileAccess.ReadWrite, 4096, !isAsync))
{
// Verify that the new FileStream uses handle's IsAsync, not the parameter
- Assert.Equal(isAsync, newFs.IsAsync);
+ Assert.Equal(IsAsyncIoSupportedForRegularFiles && isAsync, newFs.IsAsync);
// Perform async write, seek to beginning, and async read to verify functionality
byte[] writeBuffer = new byte[] { 1, 2, 3, 4, 5 };
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_str_fm_fa_fs_buffer_async.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_str_fm_fa_fs_buffer_async.cs
index ee4036a312fc5a..0ab7a860d9d9cd 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_str_fm_fa_fs_buffer_async.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_str_fm_fa_fs_buffer_async.cs
@@ -24,7 +24,7 @@ public void ValidUseAsync(bool isAsync)
{
using (FileStream fs = CreateFileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, c_DefaultBufferSize, isAsync))
{
- Assert.Equal(isAsync, fs.IsAsync);
+ Assert.Equal(IsAsyncIoSupportedForRegularFiles && isAsync, fs.IsAsync);
}
}
}
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_str_fm_fa_fs_buffer_fo.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_str_fm_fa_fs_buffer_fo.cs
index 54a7d3043472ba..9fc1a850666f24 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_str_fm_fa_fs_buffer_fo.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/ctor_str_fm_fa_fs_buffer_fo.cs
@@ -49,7 +49,7 @@ public void ValidFileOptions(FileOptions option)
using (FileStream fs = CreateFileStream(GetTestFilePath(), FileMode.Create, FileAccess.ReadWrite, FileShare.Read, c_DefaultBufferSize, option))
{
- Assert.Equal((option & FileOptions.Asynchronous) != 0, fs.IsAsync);
+ Assert.Equal((option & FileOptions.Asynchronous) != 0 && IsAsyncIoSupportedForRegularFiles, fs.IsAsync);
// make sure we can write, seek, and read data with this option set
fs.Write(data, 0, data.Length);
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileSystemTest.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileSystemTest.cs
index b2db26f0b50c4f..88e75df34c7333 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileSystemTest.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileSystemTest.cs
@@ -13,6 +13,8 @@ public abstract partial class FileSystemTest : FileCleanupTestBase
public static bool ReservedDeviceNamesAreBlocked => PlatformDetection.IsWindows && !PlatformDetection.IsWindows10OrLater;
+ public static bool IsAsyncIoSupportedForRegularFiles => PlatformDetection.IsWindows;
+
public static TheoryData PathsWithInvalidColons = TestData.PathsWithInvalidColons;
public static TheoryData PathsWithInvalidCharacters = TestData.PathsWithInvalidCharacters;
public static TheoryData TrailingCharacters = TestData.TrailingCharacters;
From 6830e7cc7157ae84cc3e330c68118b208767c9c7 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Mon, 9 Mar 2026 23:11:41 +0000
Subject: [PATCH 46/58] Rename O_ASYNC_READ/WRITE to O_NONBLOCK_READ/WRITE,
simplify test, refactor native code
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../Unix/System.Native/Interop.Pipe.cs | 4 +-
.../Win32/SafeHandles/SafeFileHandle.Unix.cs | 4 +-
.../File/OpenHandle.cs | 20 ++++-----
src/native/libs/System.Native/pal_io.c | 44 +++++++++----------
src/native/libs/System.Native/pal_io.h | 6 +--
5 files changed, 37 insertions(+), 41 deletions(-)
diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Pipe.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Pipe.cs
index d9e0db4281e5b3..f6fb59906e32c2 100644
--- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Pipe.cs
+++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Pipe.cs
@@ -12,8 +12,8 @@ internal static partial class Sys
internal enum PipeFlags
{
O_CLOEXEC = 0x0010,
- O_ASYNC_READ = 0x0400,
- O_ASYNC_WRITE = 0x0800,
+ O_NONBLOCK_READ = 0x0400,
+ O_NONBLOCK_WRITE = 0x0800,
}
///
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
index 115c11e6a6efba..72ccda1e671201 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
@@ -190,12 +190,12 @@ public static unsafe partial void CreateAnonymousPipe(out SafeFileHandle readHan
Interop.Sys.PipeFlags flags = Interop.Sys.PipeFlags.O_CLOEXEC;
if (asyncRead)
{
- flags |= Interop.Sys.PipeFlags.O_ASYNC_READ;
+ flags |= Interop.Sys.PipeFlags.O_NONBLOCK_READ;
}
if (asyncWrite)
{
- flags |= Interop.Sys.PipeFlags.O_ASYNC_WRITE;
+ flags |= Interop.Sys.PipeFlags.O_NONBLOCK_WRITE;
}
if (Interop.Sys.Pipe(fds, flags) != 0)
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
index 581f43df2a0cec..90aea5376d985b 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
@@ -85,26 +85,26 @@ public void SafeFileHandle_IsAsync_ReturnsCorrectInformation_ForRegularFiles(Fil
}
[Theory]
- [InlineData(FileOptions.None)]
- [InlineData(FileOptions.Asynchronous)]
- public void SafeFileHandle_IsAsync_ReturnsCorrectInformation_ForPipes(FileOptions options)
+ [InlineData(true)]
+ [InlineData(false)]
+ public void SafeFileHandle_IsAsync_ReturnsCorrectInformation_ForPipes(bool useAsync)
{
- SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, asyncRead: (options & FileOptions.Asynchronous) != 0, asyncWrite: (options & FileOptions.Asynchronous) != 0);
+ SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, asyncRead: useAsync, asyncWrite: useAsync);
using (readHandle)
using (writeHandle)
{
- Verify(readHandle, options);
- Verify(writeHandle, options);
+ Verify(readHandle, useAsync);
+ Verify(writeHandle, useAsync);
}
- static void Verify(SafeFileHandle fileHandle, FileOptions fileOptions)
+ static void Verify(SafeFileHandle fileHandle, bool useAsyncIO)
{
- Assert.Equal((fileOptions & FileOptions.Asynchronous) != 0, fileHandle.IsAsync);
+ Assert.Equal(useAsyncIO, fileHandle.IsAsync);
// The following code exercises the code path where the information is fetched from OS.
using SafeFileHandle createdFromIntPtr = new(fileHandle.DangerousGetHandle(), ownsHandle: false);
- Assert.Equal((fileOptions & FileOptions.Asynchronous) != 0, createdFromIntPtr.IsAsync);
+ Assert.Equal(useAsyncIO, createdFromIntPtr.IsAsync);
}
}
@@ -134,7 +134,7 @@ public static async Task SafeFileHandle_CreateAnonymousPipe_SetsIsAsyncAndTransf
await Task.WhenAll(writeTask, readTask);
Assert.Equal(message, buffer);
- // Now let's test the other direction,
+ // Now let's test a different order,
// which is going to test the E_WOULDBLOCK code path on Unix.
buffer.AsSpan().Reverse();
diff --git a/src/native/libs/System.Native/pal_io.c b/src/native/libs/System.Native/pal_io.c
index 89a2a56950b0e9..57003a962a8578 100644
--- a/src/native/libs/System.Native/pal_io.c
+++ b/src/native/libs/System.Native/pal_io.c
@@ -560,6 +560,20 @@ int32_t SystemNative_CloseDir(DIR* dir)
return result;
}
+static int32_t SetNonBlocking(int fd)
+{
+ int fdFlags;
+ while ((fdFlags = fcntl(fd, F_GETFL)) < 0 && errno == EINTR);
+ if (fdFlags < 0)
+ {
+ return -1;
+ }
+
+ int result;
+ while ((result = fcntl(fd, F_SETFL, fdFlags | O_NONBLOCK)) < 0 && errno == EINTR);
+ return result;
+}
+
int32_t SystemNative_Pipe(int32_t pipeFds[2], int32_t flags)
{
#ifdef TARGET_WASM
@@ -569,7 +583,7 @@ int32_t SystemNative_Pipe(int32_t pipeFds[2], int32_t flags)
errno = ENOTSUP;
return -1;
#else // TARGET_WASM
- if ((flags & ~(PAL_O_CLOEXEC | PAL_O_ASYNC_READ | PAL_O_ASYNC_WRITE)) != 0)
+ if ((flags & ~(PAL_O_CLOEXEC | PAL_O_NONBLOCK_READ | PAL_O_NONBLOCK_WRITE)) != 0)
{
assert_msg(false, "Unknown pipe flag", (int)flags);
errno = EINVAL;
@@ -617,34 +631,16 @@ int32_t SystemNative_Pipe(int32_t pipeFds[2], int32_t flags)
result = -1;
#endif /* HAVE_PIPE */
- if (result == 0 && ((flags & (PAL_O_ASYNC_READ | PAL_O_ASYNC_WRITE)) != 0))
+ if (result == 0 && ((flags & (PAL_O_NONBLOCK_READ | PAL_O_NONBLOCK_WRITE)) != 0))
{
- if ((flags & PAL_O_ASYNC_READ) != 0)
+ if ((flags & PAL_O_NONBLOCK_READ) != 0)
{
- int fdFlags;
- while ((fdFlags = fcntl(pipeFds[0], F_GETFL)) < 0 && errno == EINTR);
- if (fdFlags < 0)
- {
- result = -1;
- }
- else
- {
- while ((result = fcntl(pipeFds[0], F_SETFL, fdFlags | O_NONBLOCK)) < 0 && errno == EINTR);
- }
+ result = SetNonBlocking(pipeFds[0]);
}
- if (result == 0 && (flags & PAL_O_ASYNC_WRITE) != 0)
+ if (result == 0 && (flags & PAL_O_NONBLOCK_WRITE) != 0)
{
- int fdFlags;
- while ((fdFlags = fcntl(pipeFds[1], F_GETFL)) < 0 && errno == EINTR);
- if (fdFlags < 0)
- {
- result = -1;
- }
- else
- {
- while ((result = fcntl(pipeFds[1], F_SETFL, fdFlags | O_NONBLOCK)) < 0 && errno == EINTR);
- }
+ result = SetNonBlocking(pipeFds[1]);
}
if (result != 0)
diff --git a/src/native/libs/System.Native/pal_io.h b/src/native/libs/System.Native/pal_io.h
index e3ad34ff9f783e..8c66bcef207227 100644
--- a/src/native/libs/System.Native/pal_io.h
+++ b/src/native/libs/System.Native/pal_io.h
@@ -180,8 +180,8 @@ enum
PAL_O_TRUNC = 0x0080, // Truncate file to length 0 if it already exists
PAL_O_SYNC = 0x0100, // Block writes call will block until physically written
PAL_O_NOFOLLOW = 0x0200, // Fails to open the target if it's a symlink, parent symlinks are allowed
- PAL_O_ASYNC_READ = 0x0400, // Set O_NONBLOCK on the read end of a pipe
- PAL_O_ASYNC_WRITE = 0x0800, // Set O_NONBLOCK on the write end of a pipe
+ PAL_O_NONBLOCK_READ = 0x0400, // Set O_NONBLOCK on the read end of a pipe
+ PAL_O_NONBLOCK_WRITE = 0x0800, // Set O_NONBLOCK on the write end of a pipe
};
/**
@@ -445,7 +445,7 @@ PALEXPORT int32_t SystemNative_CloseDir(DIR* dir);
* Returns 0 for success, -1 for failure. Sets errno on failure.
*/
PALEXPORT int32_t SystemNative_Pipe(int32_t pipefd[2], // [out] pipefds[0] gets read end, pipefd[1] gets write end.
- int32_t flags); // 0 for defaults. Use PAL_O_CLOEXEC, PAL_O_ASYNC_READ, and PAL_O_ASYNC_WRITE for additional behavior.
+ int32_t flags); // 0 for defaults. Use PAL_O_CLOEXEC, PAL_O_NONBLOCK_READ, and PAL_O_NONBLOCK_WRITE for additional behavior.
// NOTE: Rather than a general fcntl shim, we opt to export separate functions
// for each command. This allows use to have strongly typed arguments and saves
From 2d387d3d7f0feb121606ff9761d5bd8cd28a9daa Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 10 Mar 2026 11:00:38 +0000
Subject: [PATCH 47/58] Fix test failures: use ReadExactlyAsync, skip
Browser/WASM, mark SendPacketsElement FileStream tests as Windows-only
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../tests/FunctionalTests/SendPacketsElementTest.cs | 6 ++++++
.../tests/System.IO.FileSystem.Tests/File/OpenHandle.cs | 5 ++++-
2 files changed, 10 insertions(+), 1 deletion(-)
diff --git a/src/libraries/System.Net.Sockets/tests/FunctionalTests/SendPacketsElementTest.cs b/src/libraries/System.Net.Sockets/tests/FunctionalTests/SendPacketsElementTest.cs
index 1c791e67c99ac2..552cbf05b4f4f7 100644
--- a/src/libraries/System.Net.Sockets/tests/FunctionalTests/SendPacketsElementTest.cs
+++ b/src/libraries/System.Net.Sockets/tests/FunctionalTests/SendPacketsElementTest.cs
@@ -618,6 +618,7 @@ public void FileStreamCtorNull_Throws()
}
[Fact]
+ [PlatformSpecific(TestPlatforms.Windows)] // FileStream.IsAsync is always false on Unix for regular files
[ActiveIssue("https://github.com/dotnet/runtime/issues/85690", TestPlatforms.Wasi)]
public void FileStreamCtorNormal_Success()
{
@@ -636,6 +637,7 @@ public void FileStreamCtorNormal_Success()
}
[Fact]
+ [PlatformSpecific(TestPlatforms.Windows)] // FileStream.IsAsync is always false on Unix for regular files
[ActiveIssue("https://github.com/dotnet/runtime/issues/85690", TestPlatforms.Wasi)]
public void FileStreamCtorZeroCountLength_Success()
{
@@ -664,6 +666,7 @@ public void FileStreamCtorZeroCountLength_Success()
}
[Fact]
+ [PlatformSpecific(TestPlatforms.Windows)] // FileStream.IsAsync is always false on Unix for regular files
[ActiveIssue("https://github.com/dotnet/runtime/issues/85690", TestPlatforms.Wasi)]
public void FileStreamCtorNegOffset_ArgumentOutOfRangeException()
{
@@ -689,6 +692,7 @@ public void FileStreamCtorNegOffset_ArgumentOutOfRangeException()
}
[Fact]
+ [PlatformSpecific(TestPlatforms.Windows)] // FileStream.IsAsync is always false on Unix for regular files
[ActiveIssue("https://github.com/dotnet/runtime/issues/85690", TestPlatforms.Wasi)]
public void FileStreamCtorNegCount_ArgumentOutOfRangeException()
{
@@ -729,6 +733,7 @@ public void FileStreamCtorSynchronous_ArgumentException()
// File lengths are validated on send
[Fact]
+ [PlatformSpecific(TestPlatforms.Windows)] // FileStream.IsAsync is always false on Unix for regular files
[ActiveIssue("https://github.com/dotnet/runtime/issues/85690", TestPlatforms.Wasi)]
public void FileStreamCtorEndOfBufferTrue_Success()
{
@@ -770,6 +775,7 @@ public void FileStreamCtorEndOfBufferTrue_Success()
}
[Fact]
+ [PlatformSpecific(TestPlatforms.Windows)] // FileStream.IsAsync is always false on Unix for regular files
[ActiveIssue("https://github.com/dotnet/runtime/issues/85690", TestPlatforms.Wasi)]
public void FileStreamCtorEndOfBufferFalse_Success()
{
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
index 90aea5376d985b..31c3cc8b3c11a0 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
@@ -87,6 +87,7 @@ public void SafeFileHandle_IsAsync_ReturnsCorrectInformation_ForRegularFiles(Fil
[Theory]
[InlineData(true)]
[InlineData(false)]
+ [SkipOnPlatform(TestPlatforms.Browser, "Pipes are not supported on browser")]
public void SafeFileHandle_IsAsync_ReturnsCorrectInformation_ForPipes(bool useAsync)
{
SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, asyncRead: useAsync, asyncWrite: useAsync);
@@ -113,6 +114,7 @@ static void Verify(SafeFileHandle fileHandle, bool useAsyncIO)
[InlineData(false, true)]
[InlineData(true, false)]
[InlineData(true, true)]
+ [SkipOnPlatform(TestPlatforms.Browser, "Pipes are not supported on browser")]
public static async Task SafeFileHandle_CreateAnonymousPipe_SetsIsAsyncAndTransfersData(bool asyncRead, bool asyncWrite)
{
byte[] message = "Hello, Pipe!"u8.ToArray();
@@ -138,7 +140,7 @@ public static async Task SafeFileHandle_CreateAnonymousPipe_SetsIsAsyncAndTransf
// which is going to test the E_WOULDBLOCK code path on Unix.
buffer.AsSpan().Reverse();
- readTask = readStream.ReadAsync(buffer, 0, buffer.Length);
+ readTask = readStream.ReadExactlyAsync(buffer).AsTask();
writeTask = writeStream.WriteAsync(message, 0, message.Length);
await Task.WhenAll(readTask, writeTask);
Assert.Equal(message, buffer);
@@ -166,6 +168,7 @@ static SafePipeHandle TransferOwnershipToPipeHandle(SafeFileHandle handle)
[Fact]
[PlatformSpecific(TestPlatforms.AnyUnix)]
+ [SkipOnPlatform(TestPlatforms.Browser, "Pipes are not supported on browser")]
public void AsyncHandleOnUnix_FileStream_ctor_Throws()
{
// Currently SafeFileHandle.CreateAnonymousPipe is the only public API that allows creating async handles on Unix
From caab93eb6f764beb1bab6e5253f540d2c386c8cf Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 10 Mar 2026 11:08:33 +0000
Subject: [PATCH 48/58] Replace SetNonBlocking helper with existing
SystemNative_FcntlSetIsNonBlocking
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
src/native/libs/System.Native/pal_io.c | 18 ++----------------
1 file changed, 2 insertions(+), 16 deletions(-)
diff --git a/src/native/libs/System.Native/pal_io.c b/src/native/libs/System.Native/pal_io.c
index 57003a962a8578..8495923fd42388 100644
--- a/src/native/libs/System.Native/pal_io.c
+++ b/src/native/libs/System.Native/pal_io.c
@@ -560,20 +560,6 @@ int32_t SystemNative_CloseDir(DIR* dir)
return result;
}
-static int32_t SetNonBlocking(int fd)
-{
- int fdFlags;
- while ((fdFlags = fcntl(fd, F_GETFL)) < 0 && errno == EINTR);
- if (fdFlags < 0)
- {
- return -1;
- }
-
- int result;
- while ((result = fcntl(fd, F_SETFL, fdFlags | O_NONBLOCK)) < 0 && errno == EINTR);
- return result;
-}
-
int32_t SystemNative_Pipe(int32_t pipeFds[2], int32_t flags)
{
#ifdef TARGET_WASM
@@ -635,12 +621,12 @@ int32_t SystemNative_Pipe(int32_t pipeFds[2], int32_t flags)
{
if ((flags & PAL_O_NONBLOCK_READ) != 0)
{
- result = SetNonBlocking(pipeFds[0]);
+ result = SystemNative_FcntlSetIsNonBlocking((intptr_t)pipeFds[0], 1);
}
if (result == 0 && (flags & PAL_O_NONBLOCK_WRITE) != 0)
{
- result = SetNonBlocking(pipeFds[1]);
+ result = SystemNative_FcntlSetIsNonBlocking((intptr_t)pipeFds[1], 1);
}
if (result != 0)
From 41ae7f9a6cd039f9b2c164cef74357cba70e7134 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 10 Mar 2026 11:33:15 +0000
Subject: [PATCH 49/58] Revert SendPacketsElementTest.cs changes, fix IsAsync
check in SendPacketsElement.cs
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../src/System/Net/Sockets/SendPacketsElement.cs | 2 +-
.../tests/FunctionalTests/SendPacketsElementTest.cs | 6 ------
2 files changed, 1 insertion(+), 7 deletions(-)
diff --git a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SendPacketsElement.cs b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SendPacketsElement.cs
index ce6254df95fed6..02f7905021de55 100644
--- a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SendPacketsElement.cs
+++ b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SendPacketsElement.cs
@@ -49,7 +49,7 @@ public SendPacketsElement(FileStream fileStream, long offset, int count, bool en
{
ArgumentNullException.ThrowIfNull(fileStream);
- if (!fileStream.IsAsync)
+ if (!fileStream.IsAsync && OperatingSystem.IsWindows())
{
throw new ArgumentException(SR.net_sockets_sendpackelement_FileStreamMustBeAsync, nameof(fileStream));
}
diff --git a/src/libraries/System.Net.Sockets/tests/FunctionalTests/SendPacketsElementTest.cs b/src/libraries/System.Net.Sockets/tests/FunctionalTests/SendPacketsElementTest.cs
index 552cbf05b4f4f7..1c791e67c99ac2 100644
--- a/src/libraries/System.Net.Sockets/tests/FunctionalTests/SendPacketsElementTest.cs
+++ b/src/libraries/System.Net.Sockets/tests/FunctionalTests/SendPacketsElementTest.cs
@@ -618,7 +618,6 @@ public void FileStreamCtorNull_Throws()
}
[Fact]
- [PlatformSpecific(TestPlatforms.Windows)] // FileStream.IsAsync is always false on Unix for regular files
[ActiveIssue("https://github.com/dotnet/runtime/issues/85690", TestPlatforms.Wasi)]
public void FileStreamCtorNormal_Success()
{
@@ -637,7 +636,6 @@ public void FileStreamCtorNormal_Success()
}
[Fact]
- [PlatformSpecific(TestPlatforms.Windows)] // FileStream.IsAsync is always false on Unix for regular files
[ActiveIssue("https://github.com/dotnet/runtime/issues/85690", TestPlatforms.Wasi)]
public void FileStreamCtorZeroCountLength_Success()
{
@@ -666,7 +664,6 @@ public void FileStreamCtorZeroCountLength_Success()
}
[Fact]
- [PlatformSpecific(TestPlatforms.Windows)] // FileStream.IsAsync is always false on Unix for regular files
[ActiveIssue("https://github.com/dotnet/runtime/issues/85690", TestPlatforms.Wasi)]
public void FileStreamCtorNegOffset_ArgumentOutOfRangeException()
{
@@ -692,7 +689,6 @@ public void FileStreamCtorNegOffset_ArgumentOutOfRangeException()
}
[Fact]
- [PlatformSpecific(TestPlatforms.Windows)] // FileStream.IsAsync is always false on Unix for regular files
[ActiveIssue("https://github.com/dotnet/runtime/issues/85690", TestPlatforms.Wasi)]
public void FileStreamCtorNegCount_ArgumentOutOfRangeException()
{
@@ -733,7 +729,6 @@ public void FileStreamCtorSynchronous_ArgumentException()
// File lengths are validated on send
[Fact]
- [PlatformSpecific(TestPlatforms.Windows)] // FileStream.IsAsync is always false on Unix for regular files
[ActiveIssue("https://github.com/dotnet/runtime/issues/85690", TestPlatforms.Wasi)]
public void FileStreamCtorEndOfBufferTrue_Success()
{
@@ -775,7 +770,6 @@ public void FileStreamCtorEndOfBufferTrue_Success()
}
[Fact]
- [PlatformSpecific(TestPlatforms.Windows)] // FileStream.IsAsync is always false on Unix for regular files
[ActiveIssue("https://github.com/dotnet/runtime/issues/85690", TestPlatforms.Wasi)]
public void FileStreamCtorEndOfBufferFalse_Success()
{
From d4c3d0624c79827436efa1a4579b57f68bb927ce Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 10 Mar 2026 12:45:48 +0000
Subject: [PATCH 50/58] Add [PlatformSpecific(TestPlatforms.Windows)] to
FileStreamCtorSynchronous_ArgumentException test
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../tests/FunctionalTests/SendPacketsElementTest.cs | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/libraries/System.Net.Sockets/tests/FunctionalTests/SendPacketsElementTest.cs b/src/libraries/System.Net.Sockets/tests/FunctionalTests/SendPacketsElementTest.cs
index 1c791e67c99ac2..59bb692db2c5bd 100644
--- a/src/libraries/System.Net.Sockets/tests/FunctionalTests/SendPacketsElementTest.cs
+++ b/src/libraries/System.Net.Sockets/tests/FunctionalTests/SendPacketsElementTest.cs
@@ -714,6 +714,7 @@ public void FileStreamCtorNegCount_ArgumentOutOfRangeException()
}
[Fact]
+ [PlatformSpecific(TestPlatforms.Windows)] // FileStream.IsAsync is always false on Unix for regular files
[ActiveIssue("https://github.com/dotnet/runtime/issues/85690", TestPlatforms.Wasi)]
public void FileStreamCtorSynchronous_ArgumentException()
{
From f65079600490336f924808442b3d537df779175e Mon Sep 17 00:00:00 2001
From: Adam Sitnik
Date: Tue, 10 Mar 2026 16:15:39 +0100
Subject: [PATCH 51/58] Apply suggestions from code review
---
.../tests/System.IO.FileSystem.Tests/File/OpenHandle.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
index 31c3cc8b3c11a0..2fc5209e5d30eb 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
@@ -132,7 +132,7 @@ public static async Task SafeFileHandle_CreateAnonymousPipe_SetsIsAsyncAndTransf
using (Stream writeStream = CreatePipeStream(writeHandle, FileAccess.Write, asyncWrite))
{
Task writeTask = writeStream.WriteAsync(message, 0, message.Length);
- Task readTask = readStream.ReadAsync(buffer, 0, buffer.Length);
+ Task readTask = readStream.ReadExactlyAsync(buffer).AsTask();
await Task.WhenAll(writeTask, readTask);
Assert.Equal(message, buffer);
From d0b94a01be8d7f56027c6fcd120709dfb0b50a26 Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Mar 2026 14:29:41 +0100
Subject: [PATCH 52/58] Implement async handle support on Unix for FileStream
(#125377)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
Co-authored-by: Adam Sitnik
---
.../Unix/System.Native/Interop.Read.cs | 3 +
.../Unix/System.Native/Interop.Write.cs | 3 +
.../src/System/IO/FileStream.cs | 4 --
.../src/System/IO/RandomAccess.Unix.cs | 12 +++-
.../File/OpenHandle.cs | 38 ++++++++----
src/native/libs/System.Native/entrypoints.c | 2 +
src/native/libs/System.Native/pal_io.c | 59 +++++++++++++++++++
src/native/libs/System.Native/pal_io.h | 16 +++++
8 files changed, 120 insertions(+), 17 deletions(-)
diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Read.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Read.cs
index 76f21fc80496e0..66d3913cdc7ae9 100644
--- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Read.cs
+++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Read.cs
@@ -19,5 +19,8 @@ internal static partial class Sys
///
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_Read", SetLastError = true)]
internal static unsafe partial int Read(SafeHandle fd, byte* buffer, int count);
+
+ [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_ReadFromNonblocking", SetLastError = true)]
+ internal static unsafe partial int ReadFromNonblocking(SafeHandle fd, byte* buffer, int count);
}
}
diff --git a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Write.cs b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Write.cs
index 749b34b2e0ca72..059debf52c7684 100644
--- a/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Write.cs
+++ b/src/libraries/Common/src/Interop/Unix/System.Native/Interop.Write.cs
@@ -22,5 +22,8 @@ internal static partial class Sys
[LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_Write", SetLastError = true)]
internal static unsafe partial int Write(IntPtr fd, byte* buffer, int bufferSize);
+
+ [LibraryImport(Libraries.SystemNative, EntryPoint = "SystemNative_WriteToNonblocking", SetLastError = true)]
+ internal static unsafe partial int WriteToNonblocking(SafeHandle fd, byte* buffer, int bufferSize);
}
}
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs
index 9404c2c2d45b27..50f15b4165c117 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/FileStream.cs
@@ -82,10 +82,6 @@ private static void ValidateHandle(SafeFileHandle handle, FileAccess access, int
{
ThrowHelper.ThrowObjectDisposedException_FileClosed();
}
- else if (!OperatingSystem.IsWindows() && handle.IsAsync)
- {
- throw new ArgumentException(SR.Arg_InvalidHandle, nameof(handle));
- }
}
public FileStream(SafeFileHandle handle, FileAccess access)
diff --git a/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs
index 147e3815812d0c..1fb4140feac36c 100644
--- a/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/System/IO/RandomAccess.Unix.cs
@@ -30,7 +30,11 @@ internal static unsafe int ReadAtOffset(SafeFileHandle handle, Span buffer
// isn't seekable. We do the same manually with PRead vs Read, in order to enable
// the function to be used by FileStream for all the same situations.
int result;
- if (handle.SupportsRandomAccess)
+ if (handle.IsAsync)
+ {
+ result = Interop.Sys.ReadFromNonblocking(handle, bufPtr, buffer.Length);
+ }
+ else if (handle.SupportsRandomAccess)
{
// Try pread for seekable files.
result = Interop.Sys.PRead(handle, bufPtr, buffer.Length, fileOffset);
@@ -108,7 +112,11 @@ internal static unsafe void WriteAtOffset(SafeFileHandle handle, ReadOnlySpan("handle", () => new FileStream(readHandle, FileAccess.ReadWrite));
- AssertExtensions.Throws("handle", () => new FileStream(writeHandle, FileAccess.ReadWrite));
+ readTask = readStream.ReadExactlyAsync(buffer).AsTask();
+ writeTask = writeStream.WriteAsync(message, 0, message.Length);
+ await Task.WhenAll(readTask, writeTask);
+ Assert.Equal(message, buffer);
}
}
diff --git a/src/native/libs/System.Native/entrypoints.c b/src/native/libs/System.Native/entrypoints.c
index 428c06603470a8..458980b89bd75e 100644
--- a/src/native/libs/System.Native/entrypoints.c
+++ b/src/native/libs/System.Native/entrypoints.c
@@ -104,11 +104,13 @@ static const Entry s_sysNative[] =
DllImportEntry(SystemNative_PosixFAdvise)
DllImportEntry(SystemNative_FAllocate)
DllImportEntry(SystemNative_Read)
+ DllImportEntry(SystemNative_ReadFromNonblocking)
DllImportEntry(SystemNative_ReadLink)
DllImportEntry(SystemNative_Rename)
DllImportEntry(SystemNative_RmDir)
DllImportEntry(SystemNative_Sync)
DllImportEntry(SystemNative_Write)
+ DllImportEntry(SystemNative_WriteToNonblocking)
DllImportEntry(SystemNative_CopyFile)
DllImportEntry(SystemNative_INotifyInit)
DllImportEntry(SystemNative_INotifyAddWatch)
diff --git a/src/native/libs/System.Native/pal_io.c b/src/native/libs/System.Native/pal_io.c
index 8495923fd42388..e04f95fb08e4b7 100644
--- a/src/native/libs/System.Native/pal_io.c
+++ b/src/native/libs/System.Native/pal_io.c
@@ -1232,6 +1232,65 @@ int32_t SystemNative_Read(intptr_t fd, void* buffer, int32_t bufferSize)
return Common_Read(fd, buffer, bufferSize);
}
+int32_t SystemNative_ReadFromNonblocking(intptr_t fd, void* buffer, int32_t bufferSize)
+{
+ int32_t result = Common_Read(fd, buffer, bufferSize);
+ if (result == -1 && (errno == EAGAIN || errno == EWOULDBLOCK))
+ {
+ // The fd is non-blocking and no data is available yet.
+ // Block (on a thread pool thread) until data arrives or the pipe/socket is closed.
+ PollEvent pollEvent = { .FileDescriptor = (int32_t)fd, .Events = PAL_POLLIN, .TriggeredEvents = 0 };
+ uint32_t triggered = 0;
+ int32_t pollResult = Common_Poll(&pollEvent, 1, -1, &triggered);
+ if (pollResult != Error_SUCCESS)
+ {
+ errno = ConvertErrorPalToPlatform(pollResult);
+ return -1;
+ }
+
+ if ((pollEvent.TriggeredEvents & (PAL_POLLHUP | PAL_POLLERR)) != 0 &&
+ (pollEvent.TriggeredEvents & PAL_POLLIN) == 0)
+ {
+ // The pipe/socket was closed with no data available (EOF).
+ return 0;
+ }
+
+ result = Common_Read(fd, buffer, bufferSize);
+ }
+
+ return result;
+}
+
+int32_t SystemNative_WriteToNonblocking(intptr_t fd, const void* buffer, int32_t bufferSize)
+{
+ int32_t result = Common_Write(fd, buffer, bufferSize);
+ if (result == -1 && (errno == EAGAIN || errno == EWOULDBLOCK))
+ {
+ // The fd is non-blocking and the write buffer is full.
+ // Block (on a thread pool thread) until space is available or the pipe/socket is closed.
+ PollEvent pollEvent = { .FileDescriptor = (int32_t)fd, .Events = PAL_POLLOUT, .TriggeredEvents = 0 };
+ uint32_t triggered = 0;
+ int32_t pollResult = Common_Poll(&pollEvent, 1, -1, &triggered);
+ if (pollResult != Error_SUCCESS)
+ {
+ errno = ConvertErrorPalToPlatform(pollResult);
+ return -1;
+ }
+
+ if ((pollEvent.TriggeredEvents & (PAL_POLLHUP | PAL_POLLERR)) != 0 &&
+ (pollEvent.TriggeredEvents & PAL_POLLOUT) == 0)
+ {
+ // The pipe/socket was closed.
+ errno = EPIPE;
+ return -1;
+ }
+
+ result = Common_Write(fd, buffer, bufferSize);
+ }
+
+ return result;
+}
+
int32_t SystemNative_ReadLink(const char* path, char* buffer, int32_t bufferSize)
{
assert(buffer != NULL || bufferSize == 0);
diff --git a/src/native/libs/System.Native/pal_io.h b/src/native/libs/System.Native/pal_io.h
index 8c66bcef207227..bc6c108828d256 100644
--- a/src/native/libs/System.Native/pal_io.h
+++ b/src/native/libs/System.Native/pal_io.h
@@ -705,6 +705,14 @@ PALEXPORT int32_t SystemNative_FAllocate(intptr_t fd, int64_t offset, int64_t le
*/
PALEXPORT int32_t SystemNative_Read(intptr_t fd, void* buffer, int32_t bufferSize);
+/**
+ * Reads the number of bytes specified into the provided buffer from the specified, opened non-blocking file descriptor.
+ * If no data is currently available, polls the file descriptor until data arrives or the pipe/socket is closed.
+ *
+ * Returns the number of bytes read on success; 0 on EOF; otherwise, -1 is returned and errno is set.
+ */
+PALEXPORT int32_t SystemNative_ReadFromNonblocking(intptr_t fd, void* buffer, int32_t bufferSize);
+
/**
* Takes a path to a symbolic link and attempts to place the link target path into the buffer. If the buffer is too
* small, the path will be truncated. No matter what, the buffer will not be null terminated.
@@ -740,6 +748,14 @@ PALEXPORT void SystemNative_Sync(void);
*/
PALEXPORT int32_t SystemNative_Write(intptr_t fd, const void* buffer, int32_t bufferSize);
+/**
+ * Writes the specified buffer to the provided open non-blocking file descriptor.
+ * If the write buffer is currently full, polls the file descriptor until space is available or the pipe/socket is closed.
+ *
+ * Returns the number of bytes written on success; otherwise, returns -1 and sets errno.
+ */
+PALEXPORT int32_t SystemNative_WriteToNonblocking(intptr_t fd, const void* buffer, int32_t bufferSize);
+
/**
* Copies all data from the source file descriptor to the destination file descriptor.
*
From 870b62b8969890e64bfaa7756d54c7305595ebd1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Mar 2026 13:53:23 +0000
Subject: [PATCH 53/58] Adopt SafeFileHandle.CreateAnonymousPipe in
Process.Windows.cs and replace GetStdHandle with Console.OpenStandard*Handle
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../src/System.Diagnostics.Process.csproj | 7 +--
.../src/System/Diagnostics/Process.Windows.cs | 58 ++++++-------------
2 files changed, 20 insertions(+), 45 deletions(-)
diff --git a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj
index f89dbf1fab2d31..1a4cb17a51ccb4 100644
--- a/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj
+++ b/src/libraries/System.Diagnostics.Process/src/System.Diagnostics.Process.csproj
@@ -105,8 +105,6 @@
Link="Common\Interop\Windows\Kernel32\Interop.GetProcessTimes.cs" />
-
-
-
+
diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs
index cebd7469d43667..07a4e18ffe38bd 100644
--- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs
+++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs
@@ -428,10 +428,10 @@ private void SetWorkingSetLimitsCore(IntPtr? newMin, IntPtr? newMax, out IntPtr
private unsafe bool StartWithCreateProcess(ProcessStartInfo startInfo)
{
// See knowledge base article Q190351 for an explanation of the following code. Noteworthy tricky points:
- // * The handles are duplicated as non-inheritable before they are passed to CreateProcess so
- // that the child process can not close them
+ // * The handles are duplicated as inheritable before they are passed to CreateProcess so
+ // that the child process can use them
// * CreateProcess allows you to redirect all or none of the standard IO handles, so we use
- // GetStdHandle for the handles that are not being redirected
+ // Console.OpenStandard*Handle for the handles that are not being redirected
var commandLine = new ValueStringBuilder(stackalloc char[256]);
BuildCommandLine(startInfo, ref commandLine);
@@ -468,7 +468,7 @@ private unsafe bool StartWithCreateProcess(ProcessStartInfo startInfo)
}
else
{
- childInputPipeHandle = new SafeFileHandle(Interop.Kernel32.GetStdHandle(Interop.Kernel32.HandleTypes.STD_INPUT_HANDLE), false);
+ childInputPipeHandle = Console.OpenStandardInputHandle();
}
if (startInfo.RedirectStandardOutput)
@@ -477,7 +477,7 @@ private unsafe bool StartWithCreateProcess(ProcessStartInfo startInfo)
}
else
{
- childOutputPipeHandle = new SafeFileHandle(Interop.Kernel32.GetStdHandle(Interop.Kernel32.HandleTypes.STD_OUTPUT_HANDLE), false);
+ childOutputPipeHandle = Console.OpenStandardOutputHandle();
}
if (startInfo.RedirectStandardError)
@@ -486,7 +486,7 @@ private unsafe bool StartWithCreateProcess(ProcessStartInfo startInfo)
}
else
{
- childErrorPipeHandle = new SafeFileHandle(Interop.Kernel32.GetStdHandle(Interop.Kernel32.HandleTypes.STD_ERROR_HANDLE), false);
+ childErrorPipeHandle = Console.OpenStandardErrorHandle();
}
startupInfo.hStdInput = childInputPipeHandle.DangerousGetHandle();
@@ -800,15 +800,6 @@ private SafeProcessHandle GetProcessHandle(int access, bool throwIfExited = true
}
}
- private static void CreatePipeWithSecurityAttributes(out SafeFileHandle hReadPipe, out SafeFileHandle hWritePipe, ref Interop.Kernel32.SECURITY_ATTRIBUTES lpPipeAttributes, int nSize)
- {
- bool ret = Interop.Kernel32.CreatePipe(out hReadPipe, out hWritePipe, ref lpPipeAttributes, nSize);
- if (!ret || hReadPipe.IsInvalid || hWritePipe.IsInvalid)
- {
- throw new Win32Exception();
- }
- }
-
// Using synchronous Anonymous pipes for process input/output redirection means we would end up
// wasting a worker threadpool thread per pipe instance. Overlapped pipe IO is desirable, since
// it will take advantage of the NT IO completion port infrastructure. But we can't really use
@@ -818,35 +809,24 @@ private static void CreatePipeWithSecurityAttributes(out SafeFileHandle hReadPip
// for synchronous I/O and hence they can work fine with ReadFile/WriteFile synchronously!
private static void CreatePipe(out SafeFileHandle parentHandle, out SafeFileHandle childHandle, bool parentInputs)
{
- Interop.Kernel32.SECURITY_ATTRIBUTES securityAttributesParent = default;
- securityAttributesParent.bInheritHandle = Interop.BOOL.TRUE;
+ SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle);
+
+ // parentInputs=true: parent writes to pipe, child reads (stdin redirect).
+ // parentInputs=false: parent reads from pipe, child writes (stdout/stderr redirect).
+ parentHandle = parentInputs ? writeHandle : readHandle;
+ SafeFileHandle hTmpChild = parentInputs ? readHandle : writeHandle;
- SafeFileHandle? hTmp = null;
try
{
- if (parentInputs)
- {
- CreatePipeWithSecurityAttributes(out childHandle, out hTmp, ref securityAttributesParent, 0);
- }
- else
- {
- CreatePipeWithSecurityAttributes(out hTmp,
- out childHandle,
- ref securityAttributesParent,
- 0);
- }
- // Duplicate the parent handle to be non-inheritable so that the child process
- // doesn't have access. This is done for correctness sake, exact reason is unclear.
- // One potential theory is that child process can do something brain dead like
- // closing the parent end of the pipe and there by getting into a blocking situation
- // as parent will not be draining the pipe at the other end anymore.
+ // Duplicate the child handle to be inheritable so that the child process
+ // has access. The original non-inheritable handle is closed afterwards.
IntPtr currentProcHandle = Interop.Kernel32.GetCurrentProcess();
if (!Interop.Kernel32.DuplicateHandle(currentProcHandle,
- hTmp,
+ hTmpChild,
currentProcHandle,
- out parentHandle,
+ out childHandle,
0,
- false,
+ true,
Interop.Kernel32.HandleOptions.DUPLICATE_SAME_ACCESS))
{
throw new Win32Exception();
@@ -854,9 +834,9 @@ private static void CreatePipe(out SafeFileHandle parentHandle, out SafeFileHand
}
finally
{
- if (hTmp != null && !hTmp.IsInvalid)
+ if (!hTmpChild.IsInvalid)
{
- hTmp.Dispose();
+ hTmpChild.Dispose();
}
}
}
From f38367aa4ca927896bfde369e4acf34035919352 Mon Sep 17 00:00:00 2001
From: Adam Sitnik
Date: Wed, 11 Mar 2026 16:44:47 +0100
Subject: [PATCH 54/58] address my own feedback:
- increase test coverage
- add conformance tests
- extend comments
---
.../tests/PipeStreamConformanceTests.cs | 54 ++++++++++
.../Win32/SafeHandles/SafeFileHandle.cs | 13 ++-
.../File/OpenHandle.cs | 99 +------------------
.../FileStream/FileStreamConformanceTests.cs | 40 ++++++++
4 files changed, 107 insertions(+), 99 deletions(-)
diff --git a/src/libraries/System.IO.Pipes/tests/PipeStreamConformanceTests.cs b/src/libraries/System.IO.Pipes/tests/PipeStreamConformanceTests.cs
index 867e12206528de..ab5b0d2cd81124 100644
--- a/src/libraries/System.IO.Pipes/tests/PipeStreamConformanceTests.cs
+++ b/src/libraries/System.IO.Pipes/tests/PipeStreamConformanceTests.cs
@@ -7,6 +7,7 @@
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
+using Microsoft.Win32.SafeHandles;
using Xunit;
namespace System.IO.Pipes.Tests
@@ -782,6 +783,59 @@ protected override (AnonymousPipeServerStream Server, AnonymousPipeClientStream
}
}
+ public abstract class AnonymousPipeTest_SafeFileHandle_CreateAnonymousPipe : AnonymousPipeStreamConformanceTests
+ {
+ protected abstract bool AsyncReads { get; }
+ protected abstract bool AsyncWrites { get; }
+
+ protected override (AnonymousPipeServerStream Server, AnonymousPipeClientStream Client) CreateServerAndClientStreams()
+ {
+ SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, asyncRead: AsyncReads, asyncWrite: AsyncWrites);
+
+ SafePipeHandle readPipeHandle = TransferOwnershipToPipeHandle(readHandle);
+ SafePipeHandle writePipeHandle = TransferOwnershipToPipeHandle(writeHandle);
+
+ AnonymousPipeServerStream server = new(PipeDirection.Out, serverSafePipeHandle: writePipeHandle, clientSafePipeHandle: readPipeHandle);
+ AnonymousPipeClientStream client = new(PipeDirection.In, server.ClientSafePipeHandle);
+ return (server, client);
+ }
+
+ private static SafePipeHandle TransferOwnershipToPipeHandle(SafeFileHandle handle)
+ {
+ SafePipeHandle pipeHandle = new(handle.DangerousGetHandle(), ownsHandle: true);
+ handle.SetHandleAsInvalid();
+ handle.Dispose();
+ return pipeHandle;
+ }
+ }
+
+ public sealed class AnonymousPipeTest_SafeFileHandle_CreateAnonymousPipe_SyncRead_SyncWrite : AnonymousPipeTest_SafeFileHandle_CreateAnonymousPipe
+ {
+ protected override bool AsyncReads => false;
+ protected override bool AsyncWrites => false;
+ }
+
+ [PlatformSpecific(TestPlatforms.AnyUnix)] // AnonymousPipeStreams don't support async I/O on Windows
+ public sealed class AnonymousPipeTest_SafeFileHandle_CreateAnonymousPipe_AsyncRead_SyncWrite : AnonymousPipeTest_SafeFileHandle_CreateAnonymousPipe
+ {
+ protected override bool AsyncReads => true;
+ protected override bool AsyncWrites => false;
+ }
+
+ [PlatformSpecific(TestPlatforms.AnyUnix)] // AnonymousPipeStreams don't support async I/O on Windows
+ public sealed class AnonymousPipeTest_SafeFileHandle_CreateAnonymousPipe_SyncRead_AsyncWrite : AnonymousPipeTest_SafeFileHandle_CreateAnonymousPipe
+ {
+ protected override bool AsyncReads => false;
+ protected override bool AsyncWrites => true;
+ }
+
+ [PlatformSpecific(TestPlatforms.AnyUnix)] // AnonymousPipeStreams don't support async I/O on Windows
+ public sealed class AnonymousPipeTest_SafeFileHandle_CreateAnonymousPipe_AsyncRead_AsyncWrite : AnonymousPipeTest_SafeFileHandle_CreateAnonymousPipe
+ {
+ protected override bool AsyncReads => true;
+ protected override bool AsyncWrites => true;
+ }
+
public abstract class NamedPipeTest_ServerOut_ClientIn : NamedPipeStreamConformanceTests
{
protected override NamedPipeServerStream CreateServerStream(string pipeName, int maxInstances = 1) =>
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
index 31c23450cd6ce3..f59caa56cba951 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
@@ -15,8 +15,17 @@ public sealed partial class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid
///
/// When this method returns, contains the read end of the pipe.
/// When this method returns, contains the write end of the pipe.
- /// to enable asynchronous operations for the read end of the pipe; otherwise, .
- /// to enable asynchronous operations for the write end of the pipe; otherwise, .
+ /// to enable asynchronous IO for the read end of the pipe; otherwise, .
+ /// to enable asynchronous IO for the write end of the pipe; otherwise, .
+ ///
+ ///
+ /// The created handles are not inheritable by design to avoid accidental handle leaks to child processes.
+ ///
+ ///
+ /// On Windows, async handles are created with the FILE_FLAG_OVERLAPPED flag.
+ /// On Unix, async handles are created with the O_NONBLOCK flag.
+ ///
+ ///
public static unsafe partial void CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead = false, bool asyncWrite = false);
///
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
index 02768b7f185eb8..5b2bd244570c57 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/File/OpenHandle.cs
@@ -1,8 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
-using System.IO.Pipes;
-using System.Threading.Tasks;
using Microsoft.Win32.SafeHandles;
using Xunit;
@@ -102,105 +100,12 @@ public void SafeFileHandle_IsAsync_ReturnsCorrectInformation_ForPipes(bool useAs
static void Verify(SafeFileHandle fileHandle, bool useAsyncIO)
{
Assert.Equal(useAsyncIO, fileHandle.IsAsync);
+ Assert.Equal(FileHandleType.Pipe, fileHandle.Type);
// The following code exercises the code path where the information is fetched from OS.
using SafeFileHandle createdFromIntPtr = new(fileHandle.DangerousGetHandle(), ownsHandle: false);
Assert.Equal(useAsyncIO, createdFromIntPtr.IsAsync);
- }
- }
-
- [Theory]
- [InlineData(false, false)]
- [InlineData(false, true)]
- [InlineData(true, false)]
- [InlineData(true, true)]
- [SkipOnPlatform(TestPlatforms.Browser, "Pipes are not supported on browser")]
- public static async Task SafeFileHandle_CreateAnonymousPipe_SetsIsAsyncAndTransfersData(bool asyncRead, bool asyncWrite)
- {
- byte[] message = "Hello, Pipe!"u8.ToArray();
- byte[] buffer = new byte[message.Length];
-
- SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, asyncRead, asyncWrite);
- Assert.Equal(asyncRead, readHandle.IsAsync);
- Assert.Equal(asyncWrite, writeHandle.IsAsync);
- Assert.Equal(FileHandleType.Pipe, readHandle.Type);
- Assert.Equal(FileHandleType.Pipe, writeHandle.Type);
-
- using (readHandle)
- using (writeHandle)
- using (Stream readStream = CreatePipeStream(readHandle, FileAccess.Read, asyncRead))
- using (Stream writeStream = CreatePipeStream(writeHandle, FileAccess.Write, asyncWrite))
- {
- Task writeTask = writeStream.WriteAsync(message, 0, message.Length);
- Task readTask = readStream.ReadExactlyAsync(buffer).AsTask();
- await Task.WhenAll(writeTask, readTask);
- Assert.Equal(message, buffer);
-
- // Now let's test a different order,
- // which is going to test the E_WOULDBLOCK code path on Unix.
- buffer.AsSpan().Reverse();
-
- readTask = readStream.ReadExactlyAsync(buffer).AsTask();
- writeTask = writeStream.WriteAsync(message, 0, message.Length);
- await Task.WhenAll(readTask, writeTask);
- Assert.Equal(message, buffer);
- }
- }
-
- private static Stream CreatePipeStream(SafeFileHandle readHandle, FileAccess access, bool asyncIO)
- {
- if (!OperatingSystem.IsWindows() && asyncIO)
- {
- PipeDirection direction = access == FileAccess.Read ? PipeDirection.In : PipeDirection.Out;
- return new AnonymousPipeClientStream(direction, TransferOwnershipToPipeHandle(readHandle));
- }
-
- return new FileStream(readHandle, access, 1, asyncIO);
-
- static SafePipeHandle TransferOwnershipToPipeHandle(SafeFileHandle handle)
- {
- SafePipeHandle pipeHandle = new SafePipeHandle(handle.DangerousGetHandle(), ownsHandle: true);
- handle.SetHandleAsInvalid();
- handle.Dispose();
- return pipeHandle;
- }
- }
-
- [Theory]
- [InlineData(false, false)]
- [InlineData(false, true)]
- [InlineData(true, false)]
- [InlineData(true, true)]
- [SkipOnPlatform(TestPlatforms.Browser, "Pipes are not supported on browser")]
- public static async Task SafeFileHandle_CreateAnonymousPipe_FileStream_SetsIsAsyncAndTransfersData(bool asyncRead, bool asyncWrite)
- {
- byte[] message = "Hello, Pipe!"u8.ToArray();
- byte[] buffer = new byte[message.Length];
-
- SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, asyncRead, asyncWrite);
- Assert.Equal(asyncRead, readHandle.IsAsync);
- Assert.Equal(asyncWrite, writeHandle.IsAsync);
- Assert.Equal(FileHandleType.Pipe, readHandle.Type);
- Assert.Equal(FileHandleType.Pipe, writeHandle.Type);
-
- using (readHandle)
- using (writeHandle)
- using (FileStream readStream = new FileStream(readHandle, FileAccess.Read, 1, asyncRead))
- using (FileStream writeStream = new FileStream(writeHandle, FileAccess.Write, 1, asyncWrite))
- {
- Task writeTask = writeStream.WriteAsync(message, 0, message.Length);
- Task readTask = readStream.ReadExactlyAsync(buffer).AsTask();
- await Task.WhenAll(writeTask, readTask);
- Assert.Equal(message, buffer);
-
- // Now let's test a different order,
- // which is going to test the E_WOULDBLOCK code path on Unix.
- buffer.AsSpan().Reverse();
-
- readTask = readStream.ReadExactlyAsync(buffer).AsTask();
- writeTask = writeStream.WriteAsync(message, 0, message.Length);
- await Task.WhenAll(readTask, writeTask);
- Assert.Equal(message, buffer);
+ Assert.Equal(FileHandleType.Pipe, createdFromIntPtr.Type);
}
}
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/FileStreamConformanceTests.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/FileStreamConformanceTests.cs
index 812bb775f69687..6717de9a0bc980 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/FileStreamConformanceTests.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/FileStreamConformanceTests.cs
@@ -246,6 +246,46 @@ protected override Task CreateConnectedStreamsAsync()
protected override bool SupportsConcurrentBidirectionalUse => false;
}
+ public abstract class AnonymousPipeFileStream_SafeFileHandle_CreateAnonymousPipe : AnonymousPipeFileStreamConnectedConformanceTests
+ {
+ protected abstract bool AsyncReads { get; }
+ protected abstract bool AsyncWrites { get; }
+
+ protected override Task CreateConnectedStreamsAsync()
+ {
+ SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, asyncRead: AsyncReads, asyncWrite: AsyncWrites);
+
+ FileStream readStream = new(writeHandle, FileAccess.Write);
+ FileStream writeStream = new(readHandle, FileAccess.Read);
+
+ return Task.FromResult((readStream, writeStream));
+ }
+ }
+
+ public class AnonymousPipeFileStreamConnectedConformanceTests_SyncRead_SyncWrite : AnonymousPipeFileStream_SafeFileHandle_CreateAnonymousPipe
+ {
+ protected override bool AsyncReads => false;
+ protected override bool AsyncWrites => false;
+ }
+
+ public class AnonymousPipeFileStreamConnectedConformanceTests_AsyncRead_SyncWrite : AnonymousPipeFileStream_SafeFileHandle_CreateAnonymousPipe
+ {
+ protected override bool AsyncReads => true;
+ protected override bool AsyncWrites => false;
+ }
+
+ public class AnonymousPipeFileStreamConnectedConformanceTests_SyncRead_AsyncWrite : AnonymousPipeFileStream_SafeFileHandle_CreateAnonymousPipe
+ {
+ protected override bool AsyncReads => false;
+ protected override bool AsyncWrites => true;
+ }
+
+ public class AnonymousPipeFileStreamConnectedConformanceTests_AsyncRead_AsyncWrite : AnonymousPipeFileStream_SafeFileHandle_CreateAnonymousPipe
+ {
+ protected override bool AsyncReads => true;
+ protected override bool AsyncWrites => true;
+ }
+
public class NamedPipeFileStreamConnectedConformanceTests : ConnectedStreamConformanceTests
{
protected override async Task CreateConnectedStreamsAsync()
From 5c1886e74caab4ec21651e5eae8c88dcbe0ecd3b Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 11 Mar 2026 17:35:28 +0000
Subject: [PATCH 55/58] Fix swapped variable names in
FileStreamConformanceTests
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../FileStream/FileStreamConformanceTests.cs | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/FileStreamConformanceTests.cs b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/FileStreamConformanceTests.cs
index 6717de9a0bc980..f02846332251d0 100644
--- a/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/FileStreamConformanceTests.cs
+++ b/src/libraries/System.Runtime/tests/System.IO.FileSystem.Tests/FileStream/FileStreamConformanceTests.cs
@@ -255,10 +255,10 @@ protected override Task CreateConnectedStreamsAsync()
{
SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, asyncRead: AsyncReads, asyncWrite: AsyncWrites);
- FileStream readStream = new(writeHandle, FileAccess.Write);
- FileStream writeStream = new(readHandle, FileAccess.Read);
+ FileStream writeStream = new(writeHandle, FileAccess.Write);
+ FileStream readStream = new(readHandle, FileAccess.Read);
- return Task.FromResult((readStream, writeStream));
+ return Task.FromResult((writeStream, readStream));
}
}
From 2432f807de3bd4ffbbda7cecb884e4fec0633467 Mon Sep 17 00:00:00 2001
From: Adam Sitnik
Date: Wed, 11 Mar 2026 20:29:06 +0100
Subject: [PATCH 56/58] PipeStreamConformanceTests does not support mixed async
IO (like sync reads and async writes)
---
.../tests/PipeStreamConformanceTests.cs | 31 +++++--------------
1 file changed, 7 insertions(+), 24 deletions(-)
diff --git a/src/libraries/System.IO.Pipes/tests/PipeStreamConformanceTests.cs b/src/libraries/System.IO.Pipes/tests/PipeStreamConformanceTests.cs
index ab5b0d2cd81124..d6851a092874db 100644
--- a/src/libraries/System.IO.Pipes/tests/PipeStreamConformanceTests.cs
+++ b/src/libraries/System.IO.Pipes/tests/PipeStreamConformanceTests.cs
@@ -785,12 +785,11 @@ protected override (AnonymousPipeServerStream Server, AnonymousPipeClientStream
public abstract class AnonymousPipeTest_SafeFileHandle_CreateAnonymousPipe : AnonymousPipeStreamConformanceTests
{
- protected abstract bool AsyncReads { get; }
- protected abstract bool AsyncWrites { get; }
+ protected abstract bool AsyncIO { get; }
protected override (AnonymousPipeServerStream Server, AnonymousPipeClientStream Client) CreateServerAndClientStreams()
{
- SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, asyncRead: AsyncReads, asyncWrite: AsyncWrites);
+ SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, asyncRead: AsyncIO, asyncWrite: AsyncIO);
SafePipeHandle readPipeHandle = TransferOwnershipToPipeHandle(readHandle);
SafePipeHandle writePipeHandle = TransferOwnershipToPipeHandle(writeHandle);
@@ -809,31 +808,15 @@ private static SafePipeHandle TransferOwnershipToPipeHandle(SafeFileHandle handl
}
}
- public sealed class AnonymousPipeTest_SafeFileHandle_CreateAnonymousPipe_SyncRead_SyncWrite : AnonymousPipeTest_SafeFileHandle_CreateAnonymousPipe
+ public sealed class AnonymousPipeTest_SafeFileHandle_CreateAnonymousPipe_Synchronous : AnonymousPipeTest_SafeFileHandle_CreateAnonymousPipe
{
- protected override bool AsyncReads => false;
- protected override bool AsyncWrites => false;
+ protected override bool AsyncIO => false;
}
- [PlatformSpecific(TestPlatforms.AnyUnix)] // AnonymousPipeStreams don't support async I/O on Windows
- public sealed class AnonymousPipeTest_SafeFileHandle_CreateAnonymousPipe_AsyncRead_SyncWrite : AnonymousPipeTest_SafeFileHandle_CreateAnonymousPipe
+ [ActiveIssue("https://github.com/dotnet/runtime/issues/125451", TestPlatforms.Windows)]
+ public sealed class AnonymousPipeTest_SafeFileHandle_CreateAnonymousPipe_Asynchronous : AnonymousPipeTest_SafeFileHandle_CreateAnonymousPipe
{
- protected override bool AsyncReads => true;
- protected override bool AsyncWrites => false;
- }
-
- [PlatformSpecific(TestPlatforms.AnyUnix)] // AnonymousPipeStreams don't support async I/O on Windows
- public sealed class AnonymousPipeTest_SafeFileHandle_CreateAnonymousPipe_SyncRead_AsyncWrite : AnonymousPipeTest_SafeFileHandle_CreateAnonymousPipe
- {
- protected override bool AsyncReads => false;
- protected override bool AsyncWrites => true;
- }
-
- [PlatformSpecific(TestPlatforms.AnyUnix)] // AnonymousPipeStreams don't support async I/O on Windows
- public sealed class AnonymousPipeTest_SafeFileHandle_CreateAnonymousPipe_AsyncRead_AsyncWrite : AnonymousPipeTest_SafeFileHandle_CreateAnonymousPipe
- {
- protected override bool AsyncReads => true;
- protected override bool AsyncWrites => true;
+ protected override bool AsyncIO => true;
}
public abstract class NamedPipeTest_ServerOut_ClientIn : NamedPipeStreamConformanceTests
From fbeb1ce05e3c01a04ed4eca0e6768b164ba1024c Mon Sep 17 00:00:00 2001
From: Adam Sitnik
Date: Wed, 11 Mar 2026 23:38:29 +0100
Subject: [PATCH 57/58] Limit AnonymousPipeStreamConformanceTests to sync
handles only
---
.../tests/PipeStreamConformanceTests.cs | 17 ++---------------
1 file changed, 2 insertions(+), 15 deletions(-)
diff --git a/src/libraries/System.IO.Pipes/tests/PipeStreamConformanceTests.cs b/src/libraries/System.IO.Pipes/tests/PipeStreamConformanceTests.cs
index d6851a092874db..dfe4408e39b800 100644
--- a/src/libraries/System.IO.Pipes/tests/PipeStreamConformanceTests.cs
+++ b/src/libraries/System.IO.Pipes/tests/PipeStreamConformanceTests.cs
@@ -783,13 +783,11 @@ protected override (AnonymousPipeServerStream Server, AnonymousPipeClientStream
}
}
- public abstract class AnonymousPipeTest_SafeFileHandle_CreateAnonymousPipe : AnonymousPipeStreamConformanceTests
+ public class AnonymousPipeTest_SafeFileHandle_CreateAnonymousPipe : AnonymousPipeStreamConformanceTests
{
- protected abstract bool AsyncIO { get; }
-
protected override (AnonymousPipeServerStream Server, AnonymousPipeClientStream Client) CreateServerAndClientStreams()
{
- SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, asyncRead: AsyncIO, asyncWrite: AsyncIO);
+ SafeFileHandle.CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle);
SafePipeHandle readPipeHandle = TransferOwnershipToPipeHandle(readHandle);
SafePipeHandle writePipeHandle = TransferOwnershipToPipeHandle(writeHandle);
@@ -808,17 +806,6 @@ private static SafePipeHandle TransferOwnershipToPipeHandle(SafeFileHandle handl
}
}
- public sealed class AnonymousPipeTest_SafeFileHandle_CreateAnonymousPipe_Synchronous : AnonymousPipeTest_SafeFileHandle_CreateAnonymousPipe
- {
- protected override bool AsyncIO => false;
- }
-
- [ActiveIssue("https://github.com/dotnet/runtime/issues/125451", TestPlatforms.Windows)]
- public sealed class AnonymousPipeTest_SafeFileHandle_CreateAnonymousPipe_Asynchronous : AnonymousPipeTest_SafeFileHandle_CreateAnonymousPipe
- {
- protected override bool AsyncIO => true;
- }
-
public abstract class NamedPipeTest_ServerOut_ClientIn : NamedPipeStreamConformanceTests
{
protected override NamedPipeServerStream CreateServerStream(string pipeName, int maxInstances = 1) =>
From ea4f9869c2efe8adeaa94d097a6f6490d293b4a1 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 12 Mar 2026 14:14:04 +0000
Subject: [PATCH 58/58] Address review feedback: loop on EAGAIN, fix
Process.Windows.cs error handling, remove unsafe from signatures, apply pipe
name format, add const, add comment
Co-authored-by: adamsitnik <6011991+adamsitnik@users.noreply.github.com>
---
.../src/System/Diagnostics/Process.Windows.cs | 37 ++++++++-----------
.../System/Net/Sockets/SendPacketsElement.cs | 2 +
.../Win32/SafeHandles/SafeFileHandle.Unix.cs | 25 ++++++++-----
.../SafeHandles/SafeFileHandle.Windows.cs | 6 +--
.../Win32/SafeHandles/SafeFileHandle.cs | 2 +-
src/native/libs/System.Native/pal_io.c | 26 +++++++------
6 files changed, 52 insertions(+), 46 deletions(-)
diff --git a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs
index 07a4e18ffe38bd..8a7e2b914c4850 100644
--- a/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs
+++ b/src/libraries/System.Diagnostics.Process/src/System/Diagnostics/Process.Windows.cs
@@ -816,29 +816,24 @@ private static void CreatePipe(out SafeFileHandle parentHandle, out SafeFileHand
parentHandle = parentInputs ? writeHandle : readHandle;
SafeFileHandle hTmpChild = parentInputs ? readHandle : writeHandle;
- try
- {
- // Duplicate the child handle to be inheritable so that the child process
- // has access. The original non-inheritable handle is closed afterwards.
- IntPtr currentProcHandle = Interop.Kernel32.GetCurrentProcess();
- if (!Interop.Kernel32.DuplicateHandle(currentProcHandle,
- hTmpChild,
- currentProcHandle,
- out childHandle,
- 0,
- true,
- Interop.Kernel32.HandleOptions.DUPLICATE_SAME_ACCESS))
- {
- throw new Win32Exception();
- }
- }
- finally
+ // Duplicate the child handle to be inheritable so that the child process
+ // has access. The original non-inheritable handle is closed afterwards.
+ IntPtr currentProcHandle = Interop.Kernel32.GetCurrentProcess();
+ if (!Interop.Kernel32.DuplicateHandle(currentProcHandle,
+ hTmpChild,
+ currentProcHandle,
+ out childHandle,
+ 0,
+ bInheritHandle: true,
+ Interop.Kernel32.HandleOptions.DUPLICATE_SAME_ACCESS))
{
- if (!hTmpChild.IsInvalid)
- {
- hTmpChild.Dispose();
- }
+ int lastError = Marshal.GetLastWin32Error();
+ parentHandle.Dispose();
+ hTmpChild.Dispose();
+ throw new Win32Exception(lastError);
}
+
+ hTmpChild.Dispose();
}
private static string GetEnvironmentVariablesBlock(DictionaryWrapper sd)
diff --git a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SendPacketsElement.cs b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SendPacketsElement.cs
index 02f7905021de55..038863b0a7f728 100644
--- a/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SendPacketsElement.cs
+++ b/src/libraries/System.Net.Sockets/src/System/Net/Sockets/SendPacketsElement.cs
@@ -49,6 +49,8 @@ public SendPacketsElement(FileStream fileStream, long offset, int count, bool en
{
ArgumentNullException.ThrowIfNull(fileStream);
+ // Async IO for regular files is only supported on Windows. On Unix, FileStream.IsAsync is always
+ // false for regular files, because Unix does not support O_NONBLOCK for regular files.
if (!fileStream.IsAsync && OperatingSystem.IsWindows())
{
throw new ArgumentException(SR.net_sockets_sendpackelement_FileStreamMustBeAsync, nameof(fileStream));
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
index 72ccda1e671201..725ef15dffd72e 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Unix.cs
@@ -180,13 +180,12 @@ public override bool IsInvalid
}
}
- public static unsafe partial void CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite)
+ public static partial void CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite)
{
// Allocate the handles first, so in case of OOM we don't leak any handles.
SafeFileHandle tempReadHandle = new();
SafeFileHandle tempWriteHandle = new();
- int* fds = stackalloc int[2];
Interop.Sys.PipeFlags flags = Interop.Sys.PipeFlags.O_CLOEXEC;
if (asyncRead)
{
@@ -198,18 +197,26 @@ public static unsafe partial void CreateAnonymousPipe(out SafeFileHandle readHan
flags |= Interop.Sys.PipeFlags.O_NONBLOCK_WRITE;
}
- if (Interop.Sys.Pipe(fds, flags) != 0)
+ int readFd, writeFd;
+ unsafe
{
- Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo();
- tempReadHandle.Dispose();
- tempWriteHandle.Dispose();
- throw Interop.GetExceptionForIoErrno(error);
+ int* fds = stackalloc int[2];
+ if (Interop.Sys.Pipe(fds, flags) != 0)
+ {
+ Interop.ErrorInfo error = Interop.Sys.GetLastErrorInfo();
+ tempReadHandle.Dispose();
+ tempWriteHandle.Dispose();
+ throw Interop.GetExceptionForIoErrno(error);
+ }
+
+ readFd = fds[Interop.Sys.ReadEndOfPipe];
+ writeFd = fds[Interop.Sys.WriteEndOfPipe];
}
- tempReadHandle.SetHandle(fds[Interop.Sys.ReadEndOfPipe]);
+ tempReadHandle.SetHandle(readFd);
tempReadHandle.IsAsync = asyncRead;
- tempWriteHandle.SetHandle(fds[Interop.Sys.WriteEndOfPipe]);
+ tempWriteHandle.SetHandle(writeFd);
tempWriteHandle.IsAsync = asyncWrite;
readHandle = tempReadHandle;
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
index 83a4211e9a6b45..3d5f8d22db02ba 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.Windows.cs
@@ -22,7 +22,7 @@ public SafeFileHandle() : base(true)
{
}
- public static unsafe partial void CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite)
+ public static partial void CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead, bool asyncWrite)
{
Interop.Kernel32.SECURITY_ATTRIBUTES securityAttributes = default;
SafeFileHandle? tempReadHandle;
@@ -46,7 +46,7 @@ public static unsafe partial void CreateAnonymousPipe(out SafeFileHandle readHan
else
{
// When one or both ends are async, use named pipes to support async I/O.
- string pipeName = $@"\\.\pipe\dotnet_{Guid.NewGuid()}";
+ string pipeName = $@"\\.\pipe\dotnet_{Guid.NewGuid():N}";
// Security: we don't need to specify a security descriptor, because
// we allow only for 1 instance of the pipe and immediately open the write end,
@@ -62,7 +62,7 @@ public static unsafe partial void CreateAnonymousPipe(out SafeFileHandle readHan
openMode |= Interop.Kernel32.FileOperations.FILE_FLAG_OVERLAPPED; // Asynchronous I/O
}
- int pipeMode = (int)(Interop.Kernel32.PipeOptions.PIPE_TYPE_BYTE | Interop.Kernel32.PipeOptions.PIPE_READMODE_BYTE); // Data is read from the pipe as a stream of bytes
+ const int pipeMode = (int)(Interop.Kernel32.PipeOptions.PIPE_TYPE_BYTE | Interop.Kernel32.PipeOptions.PIPE_READMODE_BYTE); // Data is read from the pipe as a stream of bytes
// We could consider specifying a larger buffer size.
tempReadHandle = Interop.Kernel32.CreateNamedPipeFileHandle(pipeName, openMode, pipeMode, 1, 0, 0, 0, ref securityAttributes);
diff --git a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
index f59caa56cba951..81401661763eac 100644
--- a/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
+++ b/src/libraries/System.Private.CoreLib/src/Microsoft/Win32/SafeHandles/SafeFileHandle.cs
@@ -26,7 +26,7 @@ public sealed partial class SafeFileHandle : SafeHandleZeroOrMinusOneIsInvalid
/// On Unix, async handles are created with the O_NONBLOCK flag.
///
///
- public static unsafe partial void CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead = false, bool asyncWrite = false);
+ public static partial void CreateAnonymousPipe(out SafeFileHandle readHandle, out SafeFileHandle writeHandle, bool asyncRead = false, bool asyncWrite = false);
///
/// Creates a around a file handle.
diff --git a/src/native/libs/System.Native/pal_io.c b/src/native/libs/System.Native/pal_io.c
index e04f95fb08e4b7..cd488c01c2c339 100644
--- a/src/native/libs/System.Native/pal_io.c
+++ b/src/native/libs/System.Native/pal_io.c
@@ -1234,9 +1234,14 @@ int32_t SystemNative_Read(intptr_t fd, void* buffer, int32_t bufferSize)
int32_t SystemNative_ReadFromNonblocking(intptr_t fd, void* buffer, int32_t bufferSize)
{
- int32_t result = Common_Read(fd, buffer, bufferSize);
- if (result == -1 && (errno == EAGAIN || errno == EWOULDBLOCK))
+ while (1)
{
+ int32_t result = Common_Read(fd, buffer, bufferSize);
+ if (result != -1 || (errno != EAGAIN && errno != EWOULDBLOCK))
+ {
+ return result;
+ }
+
// The fd is non-blocking and no data is available yet.
// Block (on a thread pool thread) until data arrives or the pipe/socket is closed.
PollEvent pollEvent = { .FileDescriptor = (int32_t)fd, .Events = PAL_POLLIN, .TriggeredEvents = 0 };
@@ -1254,18 +1259,19 @@ int32_t SystemNative_ReadFromNonblocking(intptr_t fd, void* buffer, int32_t buff
// The pipe/socket was closed with no data available (EOF).
return 0;
}
-
- result = Common_Read(fd, buffer, bufferSize);
}
-
- return result;
}
int32_t SystemNative_WriteToNonblocking(intptr_t fd, const void* buffer, int32_t bufferSize)
{
- int32_t result = Common_Write(fd, buffer, bufferSize);
- if (result == -1 && (errno == EAGAIN || errno == EWOULDBLOCK))
+ while (1)
{
+ int32_t result = Common_Write(fd, buffer, bufferSize);
+ if (result != -1 || (errno != EAGAIN && errno != EWOULDBLOCK))
+ {
+ return result;
+ }
+
// The fd is non-blocking and the write buffer is full.
// Block (on a thread pool thread) until space is available or the pipe/socket is closed.
PollEvent pollEvent = { .FileDescriptor = (int32_t)fd, .Events = PAL_POLLOUT, .TriggeredEvents = 0 };
@@ -1284,11 +1290,7 @@ int32_t SystemNative_WriteToNonblocking(intptr_t fd, const void* buffer, int32_t
errno = EPIPE;
return -1;
}
-
- result = Common_Write(fd, buffer, bufferSize);
}
-
- return result;
}
int32_t SystemNative_ReadLink(const char* path, char* buffer, int32_t bufferSize)