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)