diff --git a/src/libraries/System.IO.Pipes/ref/System.IO.Pipes.cs b/src/libraries/System.IO.Pipes/ref/System.IO.Pipes.cs index a7432dbc5a217..f7b404b93a502 100644 --- a/src/libraries/System.IO.Pipes/ref/System.IO.Pipes.cs +++ b/src/libraries/System.IO.Pipes/ref/System.IO.Pipes.cs @@ -95,6 +95,7 @@ public enum PipeOptions None = 0, CurrentUserOnly = 536870912, Asynchronous = 1073741824, + FirstPipeInstance = 524288 } public abstract partial class PipeStream : System.IO.Stream { diff --git a/src/libraries/System.IO.Pipes/src/Resources/Strings.resx b/src/libraries/System.IO.Pipes/src/Resources/Strings.resx index ce20624ce4334..8910eb4d7ac45 100644 --- a/src/libraries/System.IO.Pipes/src/Resources/Strings.resx +++ b/src/libraries/System.IO.Pipes/src/Resources/Strings.resx @@ -1,17 +1,17 @@  - diff --git a/src/libraries/System.IO.Pipes/src/System/IO/Pipes/NamedPipeServerStream.Unix.cs b/src/libraries/System.IO.Pipes/src/System/IO/Pipes/NamedPipeServerStream.Unix.cs index 8544186c1c049..2d8e934030ab1 100644 --- a/src/libraries/System.IO.Pipes/src/System/IO/Pipes/NamedPipeServerStream.Unix.cs +++ b/src/libraries/System.IO.Pipes/src/System/IO/Pipes/NamedPipeServerStream.Unix.cs @@ -6,7 +6,6 @@ using System.Diagnostics; using System.Net.Sockets; using System.Runtime.InteropServices; -using System.Security; using System.Threading; using System.Threading.Tasks; @@ -43,7 +42,7 @@ private void Create(string pipeName, PipeDirection direction, int maxNumberOfSer // in that the second process to come along and create a stream will find the pipe already in existence and will fail. _instance = SharedServer.Get( GetPipePath(".", pipeName), - (maxNumberOfServerInstances == MaxAllowedServerInstances) ? int.MaxValue : maxNumberOfServerInstances); + (maxNumberOfServerInstances == MaxAllowedServerInstances) ? int.MaxValue : maxNumberOfServerInstances, options); _direction = direction; _options = options; @@ -249,7 +248,7 @@ private sealed class SharedServer /// The concurrent number of concurrent streams using this instance. private int _currentCount; - internal static SharedServer Get(string path, int maxCount) + internal static SharedServer Get(string path, int maxCount, PipeOptions pipeOptions) { Debug.Assert(!string.IsNullOrEmpty(path)); Debug.Assert(maxCount >= 1); @@ -257,6 +256,7 @@ internal static SharedServer Get(string path, int maxCount) lock (s_servers) { SharedServer? server; + bool isFirstPipeInstance = (pipeOptions & PipeOptions.FirstPipeInstance) != 0; if (s_servers.TryGetValue(path, out server)) { // On Windows, if a subsequent server stream is created for the same pipe and with a different @@ -268,7 +268,7 @@ internal static SharedServer Get(string path, int maxCount) { throw new IOException(SR.IO_AllPipeInstancesAreBusy); } - else if (server._currentCount == maxCount) + else if (server._currentCount == maxCount || isFirstPipeInstance) { throw new UnauthorizedAccessException(SR.Format(SR.UnauthorizedAccess_IODenied_Path, path)); } @@ -276,7 +276,7 @@ internal static SharedServer Get(string path, int maxCount) else { // No instance exists yet for this path. Create one a new. - server = new SharedServer(path, maxCount); + server = new SharedServer(path, maxCount, isFirstPipeInstance); s_servers.Add(path, server); } @@ -311,20 +311,30 @@ internal void Dispose(bool disposing) } } - private SharedServer(string path, int maxCount) + private SharedServer(string path, int maxCount, bool isFirstPipeInstance) { - // Binding to an existing path fails, so we need to remove anything left over at this location. - // There's of course a race condition here, where it could be recreated by someone else between this - // deletion and the bind below, in which case we'll simply let the bind fail and throw. - Interop.Sys.Unlink(path); // ignore any failures + if (!isFirstPipeInstance) + { + // Binding to an existing path fails, so we need to remove anything left over at this location. + // There's of course a race condition here, where it could be recreated by someone else between this + // deletion and the bind below, in which case we'll simply let the bind fail and throw. + Interop.Sys.Unlink(path); // ignore any failures + } + bool isSocketBound = false; // Start listening for connections on the path. var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); try { socket.Bind(new UnixDomainSocketEndPoint(path)); + isSocketBound = true; socket.Listen(int.MaxValue); } + catch (SocketException) when (isFirstPipeInstance && !isSocketBound) + { + socket.Dispose(); + throw new UnauthorizedAccessException(SR.Format(SR.UnauthorizedAccess_IODenied_Path, path)); + } catch { socket.Dispose(); diff --git a/src/libraries/System.IO.Pipes/src/System/IO/Pipes/NamedPipeServerStream.cs b/src/libraries/System.IO.Pipes/src/System/IO/Pipes/NamedPipeServerStream.cs index 069ff0e0a930d..04e61475ecc8e 100644 --- a/src/libraries/System.IO.Pipes/src/System/IO/Pipes/NamedPipeServerStream.cs +++ b/src/libraries/System.IO.Pipes/src/System/IO/Pipes/NamedPipeServerStream.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.Diagnostics.CodeAnalysis; using System.Threading; using System.Threading.Tasks; using Microsoft.Win32.SafeHandles; @@ -60,10 +59,9 @@ public NamedPipeServerStream(string pipeName, PipeDirection direction, int maxNu /// Win32 note: this gets used for dwPipeMode. CreateNamedPipe allows you to specify PIPE_TYPE_BYTE/MESSAGE /// and PIPE_READMODE_BYTE/MESSAGE independently, but this sets type and readmode to match. /// - /// PipeOption enum: None, Asynchronous, or Write-through + /// PipeOption enum: None, Asynchronous, Write-through, or FirstPipeInstance /// Win32 note: this gets passed in with dwOpenMode to CreateNamedPipe. Asynchronous corresponds to - /// FILE_FLAG_OVERLAPPED option. PipeOptions enum doesn't expose FIRST_PIPE_INSTANCE option because - /// this sets that automatically based on the number of instances specified. + /// FILE_FLAG_OVERLAPPED option. /// /// Incoming buffer size, 0 or higher. /// Note: this size is always advisory; OS uses a suggestion. @@ -103,7 +101,7 @@ private void ValidateParameters( { throw new ArgumentOutOfRangeException(nameof(transmissionMode), SR.ArgumentOutOfRange_TransmissionModeByteOrMsg); } - if ((options & ~(PipeOptions.WriteThrough | PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly)) != 0) + if ((options & ~(PipeOptions.WriteThrough | PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly | PipeOptions.FirstPipeInstance)) != 0) { throw new ArgumentOutOfRangeException(nameof(options), SR.ArgumentOutOfRange_OptionsInvalid); } @@ -161,7 +159,7 @@ public Task WaitForConnectionAsync() return WaitForConnectionAsync(CancellationToken.None); } - public System.IAsyncResult BeginWaitForConnection(AsyncCallback? callback, object? state) => + public IAsyncResult BeginWaitForConnection(AsyncCallback? callback, object? state) => TaskToAsyncResult.Begin(WaitForConnectionAsync(), callback, state); public void EndWaitForConnection(IAsyncResult asyncResult) => diff --git a/src/libraries/System.IO.Pipes/src/System/IO/Pipes/PipeOptions.cs b/src/libraries/System.IO.Pipes/src/System/IO/Pipes/PipeOptions.cs index 493ee5f675329..28a65b2f9f550 100644 --- a/src/libraries/System.IO.Pipes/src/System/IO/Pipes/PipeOptions.cs +++ b/src/libraries/System.IO.Pipes/src/System/IO/Pipes/PipeOptions.cs @@ -1,6 +1,8 @@ // 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.Versioning; + namespace System.IO.Pipes { [Flags] @@ -9,6 +11,7 @@ public enum PipeOptions None = 0x0, WriteThrough = unchecked((int)0x80000000), Asynchronous = unchecked((int)0x40000000), // corresponds to FILE_FLAG_OVERLAPPED - CurrentUserOnly = unchecked((int)0x20000000) + CurrentUserOnly = unchecked((int)0x20000000), + FirstPipeInstance = unchecked((int)0x00080000) } } diff --git a/src/libraries/System.IO.Pipes/tests/NamedPipeTests/NamedPipeTest.CreateServer.cs b/src/libraries/System.IO.Pipes/tests/NamedPipeTests/NamedPipeTest.CreateServer.cs index d8afc8094a3de..6d7e83b4f0499 100644 --- a/src/libraries/System.IO.Pipes/tests/NamedPipeTests/NamedPipeTest.CreateServer.cs +++ b/src/libraries/System.IO.Pipes/tests/NamedPipeTests/NamedPipeTest.CreateServer.cs @@ -52,7 +52,8 @@ public static void ReservedPipeName_Throws_ArgumentOutOfRangeException(PipeDirec AssertExtensions.Throws("pipeName", () => new NamedPipeServerStream(reservedName, direction, 1)); AssertExtensions.Throws("pipeName", () => new NamedPipeServerStream(reservedName, direction, 1, PipeTransmissionMode.Byte)); AssertExtensions.Throws("pipeName", () => new NamedPipeServerStream(reservedName, direction, 1, PipeTransmissionMode.Byte, PipeOptions.None)); - AssertExtensions.Throws("pipeName", () => new NamedPipeServerStream(reservedName, direction, 1, PipeTransmissionMode.Byte, PipeOptions.None, 0, 0));} + AssertExtensions.Throws("pipeName", () => new NamedPipeServerStream(reservedName, direction, 1, PipeTransmissionMode.Byte, PipeOptions.None, 0, 0)); + } [Fact] [SkipOnPlatform(TestPlatforms.LinuxBionic, "SElinux blocks UNIX sockets in our CI environment")] @@ -254,5 +255,16 @@ public static void Windows_ServerCloneWithDifferentDirection_Throws_Unauthorized Assert.Throws(() => new NamedPipeServerStream(uniqueServerName, PipeDirection.Out)); } } + + [Fact] + public static void PipeOptions_FirstPipeInstanceWithSameNameReuse_Throws_UnauthorizedAccessException() + { + string uniqueServerName = PipeStreamConformanceTests.GetUniquePipeName(); + using (NamedPipeServerStream server = new NamedPipeServerStream(uniqueServerName, PipeDirection.In, 2, PipeTransmissionMode.Byte, PipeOptions.FirstPipeInstance)) + { + Assert.Throws(() => new NamedPipeServerStream(uniqueServerName, PipeDirection.In, 2, PipeTransmissionMode.Byte, PipeOptions.FirstPipeInstance)); + } + } + } } diff --git a/src/libraries/System.IO.Pipes/tests/NamedPipeTests/NamedPipeTest.CrossProcess.cs b/src/libraries/System.IO.Pipes/tests/NamedPipeTests/NamedPipeTest.CrossProcess.cs index b94f6771f07d4..e01f42f9d27e5 100644 --- a/src/libraries/System.IO.Pipes/tests/NamedPipeTests/NamedPipeTest.CrossProcess.cs +++ b/src/libraries/System.IO.Pipes/tests/NamedPipeTests/NamedPipeTest.CrossProcess.cs @@ -103,6 +103,22 @@ public async Task PingPong_Async() } } + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + [SkipOnPlatform(TestPlatforms.LinuxBionic, "SElinux blocks UNIX sockets in our CI environment")] + public void NamedPipeOptionsFirstPipeInstance_Throws_WhenNameIsUsedAcrossProcesses() + { + var uniqueServerName = PipeStreamConformanceTests.GetUniquePipeName(); + using (var firstServer = new NamedPipeServerStream(uniqueServerName, PipeDirection.In, 2, PipeTransmissionMode.Byte, PipeOptions.FirstPipeInstance)) + { + RemoteExecutor.Invoke(new Action(CreateFirstPipeInstance_OtherProcess), uniqueServerName).Dispose(); + } + } + + private static void CreateFirstPipeInstance_OtherProcess(string uniqueServerName) + { + Assert.Throws(() => new NamedPipeServerStream(uniqueServerName, PipeDirection.In, 2, PipeTransmissionMode.Byte, PipeOptions.FirstPipeInstance)); + } + private static void PingPong_OtherProcess(string inName, string outName) { // Create pipes with the supplied names