"Attempted to perform an unauthorized operation" when calling NamedPipeServerStream.SetAccessControl

I just had the same issue and tried to track it down.

TL;DR

The current status (Feb-2019) is sad but true: it just does not work with the classes that are given in today's NET Standard.

Ticket references

  • issue 30170 “unauthorized operation” on NamedPipeServerStream.SetAccessControl
  • issue 31190 System.IO.Pipes.AccessControl package does not work
  • issue 24040 NamedPipeServerStream: Provide support for WRITE_DAC

Also interesting in this context could be the *nix-related

  • issue 34400 mechanism for lower privileged user to connect to a privileged user's pipe

Possible workaround

One still can use native API calls to get security set up as desired, but that's not for the faint of heart. One basically needs to implement these steps:

  • set up a security descriptor in binary representation
  • connect it to the SECURITY_ATTRIBUTES structure
  • create the pipe via Windows API CreateNamedPipe()
  • wrap the resulting handle into a SafeHandle
  • and feed that into the right NamedPipeServerStream CTOR variant

PS: At least we now can look it up in the code where it is hitting the wall. Just imagine having that problem 20 years ago ...


Recently I encountered the same problem when translating the project to .Net Core.

I added a nuget package to help with the transition: https://www.nuget.org/packages/NamedPipeServerStream.NetFrameworkVersion/

  • The package targets .Net Standard 2.0 and contains the original constructor from the .Net Framework (which supports PipeSecurity, HandleInheritability, and PipeAccessRights).
  • I restored it from decompiled code without making any changes.
  • Exceptions are fully supported, no code has been lost.
  • Has a strong name.
  • The source code is also available.
Install-Package NamedPipeServerStream.NetFrameworkVersion
using System.IO.Pipes;

var pipeSecurity = new PipeSecurity();
pipeSecurity.AddAccessRule(new PipeAccessRule(WindowsIdentity.GetCurrent().Owner, PipeAccessRights.ReadWrite, AccessControlType.Allow));

using var serverStream = NamedPipeServerStreamConstructors.New(pipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.WriteThrough, 0, 0, pipeSecurity);

I've managed to make the possible workaround JensG mentioned. I used this sample to build it: https://code.msdn.microsoft.com/CSNamedPipeServer-4c760c2c/sourcecode?fileId=21684&pathId=1498714400

    public static class NativeNamedPipeServer
    {
        public static SafePipeHandle CreateNamedPipeServer(string pipeName, string sddl)
        {
            return NativeMethod.CreateNamedPipe(
                @"\\.\pipe\" + pipeName, // The unique pipe name.
                PipeOpenMode.PIPE_ACCESS_DUPLEX | PipeOpenMode.ASYNCHRONOUS,
                PipeMode.PIPE_TYPE_BYTE,
                1, // Max server instances
                1024 * 16, // Output buffer size
                1024 * 16, // Input buffer size
                NMPWAIT_USE_DEFAULT_WAIT, // Time-out interval
                CreateNativePipeSecurity(sddl) // Pipe security attributes
            );
        }

        /// <summary>
        /// The CreateNativePipeSecurity function creates and initializes a new 
        /// SECURITY_ATTRIBUTES object to allow Authenticated Users read and 
        /// write access to a pipe, and to allow the Administrators group full 
        /// access to the pipe.
        /// </summary>
        /// <returns>
        /// A SECURITY_ATTRIBUTES object that allows Authenticated Users read and 
        /// write access to a pipe, and allows the Administrators group full 
        /// access to the pipe.
        /// </returns>
        /// <see cref="http://msdn.microsoft.com/en-us/library/aa365600(VS.85).aspx"/>
        private static SECURITY_ATTRIBUTES CreateNativePipeSecurity(string sddl)
        {
            if (!NativeMethod.ConvertStringSecurityDescriptorToSecurityDescriptor(
                sddl, 1, out var pSecurityDescriptor, IntPtr.Zero))
            {
                throw new Win32Exception();
            }

            SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES();
            sa.nLength = Marshal.SizeOf(sa);
            sa.lpSecurityDescriptor = pSecurityDescriptor;
            sa.bInheritHandle = false;
            return sa;
        }


        #region Native API Signatures and Types

        /// <summary>
        /// Named Pipe Open Modes
        /// http://msdn.microsoft.com/en-us/library/aa365596.aspx
        /// </summary>
        [Flags]
        internal enum PipeOpenMode : uint
        {
            PIPE_ACCESS_INBOUND = 0x00000001, // Inbound pipe access.
            PIPE_ACCESS_OUTBOUND = 0x00000002, // Outbound pipe access.
            PIPE_ACCESS_DUPLEX = 0x00000003, // Duplex pipe access.

            // added from C# PipeOptions.cs
            WRITE_THROUGH = 0x80000000,
            ASYNCHRONOUS = 0x40000000,
            CURRENT_USER_ONLY = 0x20000000
        }

        /// <summary>
        /// Named Pipe Type, Read, and Wait Modes
        /// http://msdn.microsoft.com/en-us/library/aa365605.aspx
        /// </summary>
        [Flags]
        internal enum PipeMode : uint
        {
            // Type Mode
            PIPE_TYPE_BYTE = 0x00000000, // Byte pipe type.
            PIPE_TYPE_MESSAGE = 0x00000004, // Message pipe type.

            // Read Mode
            PIPE_READMODE_BYTE = 0x00000000, // Read mode of type Byte.
            PIPE_READMODE_MESSAGE = 0x00000002, // Read mode of type Message.

            // Wait Mode
            PIPE_WAIT = 0x00000000, // Pipe blocking mode.
            PIPE_NOWAIT = 0x00000001 // Pipe non-blocking mode.
        }

        /// <summary>
        /// Uses the default time-out specified in a call to the 
        /// CreateNamedPipe method.
        /// </summary>
        internal const uint NMPWAIT_USE_DEFAULT_WAIT = 0x00000000;


        /// <summary>
        /// The SECURITY_ATTRIBUTES structure contains the security descriptor for 
        /// an object and specifies whether the handle retrieved by specifying 
        /// this structure is inheritable. This structure provides security 
        /// settings for objects created by various functions, such as CreateFile, 
        /// CreateNamedPipe, CreateProcess, RegCreateKeyEx, or RegSaveKeyEx.
        /// </summary>
        [StructLayout(LayoutKind.Sequential)]
        internal class SECURITY_ATTRIBUTES
        {
            public int nLength;
            public SafeLocalMemHandle lpSecurityDescriptor;
            public bool bInheritHandle;
        }


        /// <summary>
        /// Represents a wrapper class for a local memory pointer. 
        /// </summary>
        [SuppressUnmanagedCodeSecurity,
         HostProtection(SecurityAction.LinkDemand, MayLeakOnAbort = true)]
        internal sealed class SafeLocalMemHandle : SafeHandleZeroOrMinusOneIsInvalid
        {
            public SafeLocalMemHandle() : base(true)
            {
            }

            public SafeLocalMemHandle(IntPtr preexistingHandle, bool ownsHandle)
                : base(ownsHandle)
            {
                base.SetHandle(preexistingHandle);
            }

            [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success),
             DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            private static extern IntPtr LocalFree(IntPtr hMem);

            protected override bool ReleaseHandle()
            {
                return (LocalFree(base.handle) == IntPtr.Zero);
            }
        }


        /// <summary>
        /// The class exposes Windows APIs to be used in this code sample.
        /// </summary>
        [SuppressUnmanagedCodeSecurity]
        internal class NativeMethod
        {
            /// <summary>
            /// Creates an instance of a named pipe and returns a handle for 
            /// subsequent pipe operations.
            /// </summary>
            /// <param name="pipeName">Pipe name</param>
            /// <param name="openMode">Pipe open mode</param>
            /// <param name="pipeMode">Pipe-specific modes</param>
            /// <param name="maxInstances">Maximum number of instances</param>
            /// <param name="outBufferSize">Output buffer size</param>
            /// <param name="inBufferSize">Input buffer size</param>
            /// <param name="defaultTimeout">Time-out interval</param>
            /// <param name="securityAttributes">Security attributes</param>
            /// <returns>If the function succeeds, the return value is a handle 
            /// to the server end of a named pipe instance.</returns>
            [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            public static extern SafePipeHandle CreateNamedPipe(string pipeName,
                PipeOpenMode openMode, PipeMode pipeMode, int maxInstances,
                int outBufferSize, int inBufferSize, uint defaultTimeout,
                SECURITY_ATTRIBUTES securityAttributes);


            /// <summary>
            /// The ConvertStringSecurityDescriptorToSecurityDescriptor function 
            /// converts a string-format security descriptor into a valid, 
            /// functional security descriptor.
            /// </summary>
            /// <param name="sddlSecurityDescriptor">
            /// A string containing the string-format security descriptor (SDDL) 
            /// to convert.
            /// </param>
            /// <param name="sddlRevision">
            /// The revision level of the sddlSecurityDescriptor string. 
            /// Currently this value must be 1.
            /// </param>
            /// <param name="pSecurityDescriptor">
            /// A pointer to a variable that receives a pointer to the converted 
            /// security descriptor.
            /// </param>
            /// <param name="securityDescriptorSize">
            /// A pointer to a variable that receives the size, in bytes, of the 
            /// converted security descriptor. This parameter can be IntPtr.Zero.
            /// </param>
            /// <returns>
            /// If the function succeeds, the return value is true.
            /// </returns>
            [return: MarshalAs(UnmanagedType.Bool)]
            [DllImport("advapi32.dll", CharSet = CharSet.Auto, SetLastError = true)]
            public static extern bool ConvertStringSecurityDescriptorToSecurityDescriptor(
                string sddlSecurityDescriptor, int sddlRevision,
                out SafeLocalMemHandle pSecurityDescriptor,
                IntPtr securityDescriptorSize);
        }

        #endregion
    }

The create:

            var safePipeHandle = NativeNamedPipeServer.CreateNamedPipeServer(_pipeName, 
                pipeSecurity.GetSecurityDescriptorSddlForm(AccessControlSections.Access));
            var stream = new NamedPipeServerStream(PipeDirection.InOut, true, false, safePipeHandle);

The trickiest part was to make asynchronous work as the original source did not have PipeOpenMode.ASYNCHRONOUS flag. Figured that out by checking .NET Core 3.0 code. Strangely enough, they have all the pipe security code in there, but just not the constructor for it. So an alternative way might be actually reflection.