Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.
/ corefx Public archive

Commit

Permalink
Implement NamedPipe*Stream on Unix on Unix domain sockets
Browse files Browse the repository at this point in the history
Today, NamedPipeServer/ClientStream are implemented on top of FIFOs, aka named pipes.  There are some unfortunate limitations to this that are causing problems for typical usage of NamedPipeServer/ClientStream:
- FIFOs can be constructed as both read and write (i.e. InOut), but a writer can then immediately read what was written.  In other words, whereas one might expect there two be two data channels, one in each direction, there's only one that everyone can read and write from.
- FIFOs can be connected to by any number of readers and writers, which has the effect of meaning a reader doesn't unblock when a writer disconnects, because there may be another writer in the future.

To address these significant limitations, this commit moves NamedPipeServer/ClientStream to be built instead on top of Unix domain sockets.  Unix domain sockets are an IPC mechanism for local communication using the sockets model, and we have support for it via System.Net.Sockets.

Both of the aforementioned problems are addressed by this change.  A few other positive outcomes:
- The participants on each side of the connection can get the credentials of the other side, so GetImpersonatedUserName is now implemented.
- Our Socket implementation has a good async implementation, so we can use it for all of the async operations exposed rather than queueing a work item that then blocks doing the synchronous operation.

There are a few downsides, but they're worth the tradeoff:
- FIFOs have the same general blocking behavior as anonymous pipes and as named pipes on Windows: a call to Write doesn't complete until an associated Read comes along.  That's not the case with Unix domain sockets, where a Write typically just deposits the data into a buffer.  Writes on a socket only block when the buffer is full.
- Unix domain sockets are considered to be a bit slower than FIFOs.
- Changing the buffer size of a FIFO impacts the whole FIFO, whereas changing the buffer size for a socket impacts just that end of the socket.

There continue to be several features that we don't support, e.g. message transmission, max number of servers, etc.
  • Loading branch information
stephentoub committed Mar 11, 2016
1 parent 7299ed5 commit 4f5dad8
Show file tree
Hide file tree
Showing 29 changed files with 500 additions and 511 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ internal static partial class Interop
{
internal static partial class Sys
{
internal static class Fcntl
internal static partial class Fcntl
{
internal static readonly bool CanGetSetPipeSz = (FcntlCanGetSetPipeSz() != 0);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ internal static partial class Interop
{
internal static partial class Sys
{
internal static class Fcntl
internal static partial class Fcntl
{
[DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_FcntlSetCloseOnExec", SetLastError=true)]
internal static extern int SetCloseOnExec(SafeFileHandle fd);
internal static extern int SetCloseOnExec(SafeHandle fd);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System;
using System.Diagnostics;
using System.Runtime.InteropServices;

internal static partial class Interop
{
internal static partial class Sys
{
[DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_MkFifo", SetLastError = true)]
internal static extern int MkFifo(string path, int mode);
[DllImport(Libraries.SystemNative, EntryPoint = "SystemNative_GetPeerUserName", SetLastError = true)]
internal unsafe static extern string GetPeerUserName(SafeHandle socket);
}
}
1 change: 1 addition & 0 deletions src/Common/src/Interop/Unix/System.Native/Interop.Stat.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ internal static class FileTypes
internal const int S_IFDIR = 0x4000;
internal const int S_IFREG = 0x8000;
internal const int S_IFLNK = 0xA000;
internal const int S_IFSOCK = 0xC000;
}

[Flags]
Expand Down
1 change: 1 addition & 0 deletions src/Native/Common/pal_config.h.in
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
#cmakedefine01 HAVE_FCOPYFILE
#cmakedefine01 HAVE_GETHOSTBYNAME_R
#cmakedefine01 HAVE_GETHOSTBYADDR_R
#cmakedefine01 HAVE_GETPEEREID
#cmakedefine01 HAVE_SUPPORT_FOR_DUAL_MODE_IPV4_PACKET_INFO
#cmakedefine01 HAVE_THREAD_SAFE_GETHOSTBYNAME_AND_GETHOSTBYADDR
#cmakedefine01 HAVE_TCGETATTR
Expand Down
8 changes: 1 addition & 7 deletions src/Native/System.Native/pal_io.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ static_assert(PAL_S_IFCHR == S_IFCHR, "");
static_assert(PAL_S_IFDIR == S_IFDIR, "");
static_assert(PAL_S_IFREG == S_IFREG, "");
static_assert(PAL_S_IFLNK == S_IFLNK, "");
static_assert(PAL_S_IFSOCK == S_IFSOCK, "");

// Validate that our enum for inode types is the same as what is
// declared by the dirent.h header on the local system.
Expand Down Expand Up @@ -501,13 +502,6 @@ extern "C" int32_t SystemNative_FChMod(intptr_t fd, int32_t mode)
return result;
}

extern "C" int32_t SystemNative_MkFifo(const char* path, int32_t mode)
{
int32_t result;
while (CheckInterrupted(result = mkfifo(path, static_cast<mode_t>(mode))));
return result;
}

extern "C" int32_t SystemNative_FSync(intptr_t fd)
{
int32_t result;
Expand Down
8 changes: 1 addition & 7 deletions src/Native/System.Native/pal_io.h
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ enum
PAL_S_IFDIR = 0x4000, // Directory
PAL_S_IFREG = 0x8000, // Regular file
PAL_S_IFLNK = 0xA000, // Symbolic link
PAL_S_IFSOCK = 0xC000, // Socket
};

/**
Expand Down Expand Up @@ -437,13 +438,6 @@ extern "C" int32_t SystemNative_ChMod(const char* path, int32_t mode);
*/
extern "C" int32_t SystemNative_FChMod(intptr_t fd, int32_t mode);

/**
* Create a FIFO (named pipe). Implemented as a shim to mkfifo(3).
*
* Returns 0 for success, -1 for failure. Sets errno for failure.
*/
extern "C" int32_t SystemNative_MkFifo(const char* path, int32_t mode);

/**
* Flushes all modified data and attribtues of the specified File Descriptor to the storage medium.
*
Expand Down
61 changes: 61 additions & 0 deletions src/Native/System.Native/pal_networking.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
#endif
#include <unistd.h>
#include <vector>
#include <pwd.h>

#if HAVE_KQUEUE
#if KEVENT_HAS_VOID_UDATA
Expand Down Expand Up @@ -2678,3 +2679,63 @@ extern "C" int32_t SystemNative_PlatformSupportsDualModeIPv4PacketInfo()
return 0;
#endif
}

static char* GetNameFromUid(uid_t uid)
{
size_t bufferLength = 512;
while (true)
{
char *buffer = reinterpret_cast<char*>(malloc(bufferLength));
if (buffer == nullptr)
return nullptr;

struct passwd pw;
struct passwd* result;
if (getpwuid_r(uid, &pw, buffer, bufferLength, &result) == 0)
{
if (result == nullptr)
{
errno = ENOENT;
free(buffer);
return nullptr;
}
else
{
char* name = strdup(pw.pw_name);
free(buffer);
return name;
}
}

free(buffer);
if (errno == ERANGE)
{
bufferLength *= 2;
}
else
{
return nullptr;
}
}
}

extern "C" char* SystemNative_GetPeerUserName(intptr_t socket)
{
int fd = ToFileDescriptor(socket);
#ifdef SO_PEERCRED
struct ucred creds;
socklen_t len = sizeof(creds);
return getsockopt(fd, SOL_SOCKET, SO_PEERCRED, &creds, &len) == 0 ?
GetNameFromUid(creds.uid) :
nullptr;
#elif HAVE_GETPEEREID
uid_t euid, egid;
return getpeereid(fd, &euid, &egid) == 0 ?
GetNameFromUid(euid) :
nullptr;
#else
(void)fd;
errno = ENOTSUP;
return nullptr;
#endif
}
2 changes: 2 additions & 0 deletions src/Native/System.Native/pal_networking.h
Original file line number Diff line number Diff line change
Expand Up @@ -418,3 +418,5 @@ extern "C" Error SystemNative_TryChangeSocketEventRegistration(
extern "C" Error SystemNative_WaitForSocketEvents(int32_t port, SocketEvent* buffer, int32_t* count);

extern "C" int32_t SystemNative_PlatformSupportsDualModeIPv4PacketInfo();

extern "C" char* SystemNative_GetPeerUserName(intptr_t socket);
4 changes: 4 additions & 0 deletions src/Native/configure.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,10 @@ check_include_files(
linux/rtnetlink.h
HAVE_LINUX_RTNETLINK_H)

check_function_exists(
getpeereid
HAVE_GETPEEREID)

# getdomainname on OSX takes an 'int' instead of a 'size_t'
# check if compiling with 'size_t' would cause a warning
set (PREVIOUS_CMAKE_REQUIRED_FLAGS ${CMAKE_REQUIRED_FLAGS})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

using System;
using System.Diagnostics;
using System.Net.Sockets;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security;

Expand All @@ -13,34 +15,64 @@ public sealed partial class SafePipeHandle : SafeHandle
{
private const int DefaultInvalidHandle = -1;

/// <summary>Opens the specified file with the requested flags and mode.</summary>
/// <param name="path">The path to the file.</param>
/// <param name="flags">The flags with which to open the file.</param>
/// <param name="mode">The mode for opening the file.</param>
/// <returns>A SafeFileHandle for the opened file.</returns>
internal static SafePipeHandle Open(string path, Interop.Sys.OpenFlags flags, int mode)
// For anonymous pipes, SafePipeHandle.handle is the file descriptor of the pipe, and the
// _named* fields remain null. For named pipes, SafePipeHandle.handle is a copy of the file descriptor
// extracted from the Socket's SafeHandle, and the _named* fields are the socket and its safe handle.
// This allows operations related to file descriptors to be performed directly on the SafePipeHandle,
// and operations that should go through the Socket to be done via _namedPipeSocket. We keep the
// Socket's SafeHandle alive as long as this SafeHandle is alive.

private Socket _namedPipeSocket;
private SafeHandle _namedPipeSocketHandle;

internal SafePipeHandle(Socket namedPipeSocket) : base((IntPtr)DefaultInvalidHandle, ownsHandle: true)
{
// Ideally this would be a constrained execution region, but we don't have access to PrepareConstrainedRegions.
SafePipeHandle handle = Interop.CheckIo(Interop.Sys.OpenPipe(path, flags, mode));
Debug.Assert(namedPipeSocket != null);
_namedPipeSocket = namedPipeSocket;

// TODO: Issue https://github.com/dotnet/corefx/issues/6807
// This is unfortunately the only way of getting at the Socket's file descriptor right now, until #6807 is implemented.
_namedPipeSocketHandle = (SafeHandle)typeof(Socket).GetTypeInfo().GetDeclaredProperty("SafeHandle")?.GetValue(namedPipeSocket, null);

bool ignored = false;
_namedPipeSocketHandle.DangerousAddRef(ref ignored);
SetHandle(_namedPipeSocketHandle.DangerousGetHandle());
}

Debug.Assert(!handle.IsInvalid);
internal Socket NamedPipeSocket => _namedPipeSocket;
internal SafeHandle NamedPipeSocketHandle => _namedPipeSocketHandle;

return handle;
protected override void Dispose(bool disposing)
{
base.Dispose(disposing); // must be called before trying to Dispose the socket
if (disposing && _namedPipeSocket != null)
{
_namedPipeSocket.Dispose();
_namedPipeSocket = null;
}
}

protected override bool ReleaseHandle()
{
// Close the handle. Although close is documented to potentially fail with EINTR, we never want
// to retry, as the descriptor could actually have been closed, been subsequently reassigned, and
// be in use elsewhere in the process. Instead, we simply check whether the call was successful.
Debug.Assert(!this.IsInvalid);
return Interop.Sys.Close(handle) == 0;
Debug.Assert(!IsInvalid);

if (_namedPipeSocketHandle != null)
{
SetHandle(DefaultInvalidHandle);
_namedPipeSocketHandle.DangerousRelease();
_namedPipeSocketHandle = null;
return true;
}

return (long)handle >= 0 ?
Interop.Sys.Close(handle) == 0 :
true;
}

public override bool IsInvalid
{
[SecurityCritical]
get { return (long)handle < 0; }
get { return (long)handle < 0 && _namedPipeSocket == null; }
}
}
}
16 changes: 13 additions & 3 deletions src/System.IO.Pipes/src/System.IO.Pipes.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@
<ProjectJson>win/project.json</ProjectJson>
<ProjectLockJson>win/project.lock.json</ProjectLockJson>
</PropertyGroup>
<PropertyGroup Condition=" '$(TargetsUnix)' == 'true' ">
<ProjectJson>unix/project.json</ProjectJson>
<ProjectLockJson>unix/project.lock.json</ProjectLockJson>
</PropertyGroup>
<!-- Help VS understand available configurations -->
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Unix_Debug|AnyCPU'" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)' == 'Unix_Release|AnyCPU'" />
Expand Down Expand Up @@ -170,18 +174,21 @@
<Compile Include="$(CommonPath)\Interop\Unix\System.Native\Interop.Fcntl.Pipe.cs">
<Link>Common\Interop\Unix\Interop.Fcntl.Pipe.cs</Link>
</Compile>
<Compile Include="$(CommonPath)\Interop\Unix\System.Native\Interop.Fcntl.SetCloseOnExec.cs">
<Link>Common\Interop\Unix\Interop.Fcntl.SetCloseOnExec.cs</Link>
</Compile>
<Compile Include="$(CommonPath)\Interop\Unix\System.Native\Interop.FLock.cs">
<Link>Common\Interop\Unix\Interop.FLock.cs</Link>
</Compile>
<Compile Include="$(CommonPath)\Interop\Unix\System.Native\Interop.GetHostName.cs">
<Link>Common\Interop\Unix\Interop.GetHostName.cs</Link>
</Compile>
<Compile Include="$(CommonPath)\Interop\Unix\System.Native\Interop.GetPeerUserName.cs">
<Link>Common\Interop\Unix\Interop.GetPeerUserName.cs</Link>
</Compile>
<Compile Include="$(CommonPath)\Interop\Unix\System.Native\Interop.MkDir.cs">
<Link>Common\Interop\Unix\Interop.MkDir.cs</Link>
</Compile>
<Compile Include="$(CommonPath)\Interop\Unix\System.Native\Interop.MkFifo.cs">
<Link>Common\Interop\Unix\Interop.MkFifo.cs</Link>
</Compile>
<Compile Include="$(CommonPath)\Interop\Unix\System.Native\Interop.Open.cs">
<Link>Common\Interop\Unix\Interop.Open.cs</Link>
</Compile>
Expand Down Expand Up @@ -221,6 +228,9 @@
<Compile Include="$(CommonPath)\System\Threading\Tasks\ForceAsyncAwaiter.cs">
<Link>Common\System\Threading\Tasks\ForceAsyncAwaiter.cs</Link>
</Compile>
<Compile Include="$(CommonPath)\System\Net\Sockets\UnixDomainSocketEndPoint.cs">
<Link>Common\System\Net\UnixDomainSocketEndPoint.cs</Link>
</Compile>
</ItemGroup>
<ItemGroup Condition="'$(TargetGroup)' == 'net46'">
<TargetingPackReference Include="mscorlib" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using Microsoft.Win32.SafeHandles;
using System.Diagnostics.CodeAnalysis;
using System.Net.Sockets;
using System.Security;
using System.Threading;

Expand All @@ -17,30 +19,35 @@ public sealed partial class NamedPipeClientStream : PipeStream
[SecurityCritical]
private bool TryConnect(int timeout, CancellationToken cancellationToken)
{
// timeout and cancellationToken are currently ignored: [ActiveIssue(812, PlatformID.AnyUnix)]
// We should figure out if there's a good way to cancel calls to Open, such as
// by sending a signal that causes an EINTR, and then in handling the EINTR result
// poll the cancellationToken to see if cancellation was requested.

// timeout and cancellationToken aren't used as Connect will be very fast,
// either succeeding immediately if the server is listening or failing
// immediately if it isn't. The only delay will be between the time the server
// has called Bind and the time it's subsequently called Accept.
try
{
// Open the file. For In or Out, this will block until a client has connected.
// Unfortunately for InOut it won't, which is different from the Windows behavior;
// on Unix it won't block for InOut until it actually performs a read or write operation.
var clientHandle = Microsoft.Win32.SafeHandles.SafePipeHandle.Open(
_normalizedPipePath,
TranslateFlags(_direction, _pipeOptions, _inheritability),
(int)Interop.Sys.Permissions.S_IRWXU);
var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified);
socket.Connect(new UnixDomainSocketEndPoint(_normalizedPipePath));
var clientHandle = new SafePipeHandle(socket);
ConfigureSocket(socket, clientHandle, _direction, 0, 0, _inheritability);

// Pipe successfully opened. Store our client handle.
InitializeHandle(clientHandle, isExposed: false, isAsync: (_pipeOptions & PipeOptions.Asynchronous) != 0);
State = PipeState.Connected;
return true;
}
catch (FileNotFoundException)
catch (SocketException e)
{
// The FIFO file doesn't yet exist.
return false;
switch (e.SocketErrorCode)
{
// Retryable errors
case SocketError.AddressAlreadyInUse:
case SocketError.AddressNotAvailable:
case SocketError.ConnectionRefused:
return false;

// Non-retryable errors
default:
throw;
}
}
}

Expand Down
Loading

0 comments on commit 4f5dad8

Please sign in to comment.