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

Expose PipeOptions.CurrentUserOnly and add implementation when flag is passed #26395

Merged
merged 15 commits into from
Feb 22, 2018

Conversation

safern
Copy link
Member

@safern safern commented Jan 17, 2018

This is the implementation for PipeOptions.CurrentUserOnly.

  • Server side (Windows): if the flag is on, remove the flag from options variable, and add all the current user as the owner and the only user with rights to access the pipe through ACL attributes. This handles the user's elevation as well.

  • Client side (windows and unix): if flag is on, remove it, set bool variable to true and then when trying to connect to the server, if the variable is true and the connection was successful, validate that the server we connected was created by the same user with the same elevation level by comparing the Sids. If this is not the case, set State as closed and throw.

Relates to: https://github.com/dotnet/corefx/issues/25427

@@ -412,6 +412,33 @@ internal static unsafe Interop.Kernel32.SECURITY_ATTRIBUTES GetSecAttrs(HandleIn
return secAttrs;
}

internal static unsafe Interop.Kernel32.SECURITY_ATTRIBUTES GetSecAttrs(HandleInheritability inheritability, PipeSecurity pipeSecurity, out object pinningHandle)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you change this to ref GCHandle to avoid the extra allocation?


if (pipeSecurity != null)
{
// no inherability flag was found so we need to initialize secAttrs structure.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

inherability [](start = 22, length = 12)

inheritability

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also this comment should go inside the "if" shouldnt it?

}
finally
{
if (pinningHandle != null)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we pass the GCHandle by ref to GetSecAttrs this can be changed to if (pinningHandle.IsAllocated) pinningHandle.Free();

@@ -10,5 +10,6 @@ public enum PipeOptions
None = 0x0,
WriteThrough = unchecked((int)0x80000000),
Asynchronous = unchecked((int)0x40000000), // corresponds to FILE_FLAG_OVERLAPPED
CurrentUserOnly = unchecked((int)0x20000000)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if Windows adds a new option to the CreateNamedPipe flags with this number? I guess that's OK, it's only our convention that the numbers currently match what we send to Windows.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was the proposed value in the original API proposal, from @stephentoub:

but for extra good measure we should also choose a value that's not currently accepted by CreateNamedPipe

@safern
Copy link
Member Author

safern commented Jan 18, 2018

@weshaggard this needs some stuff in System.IO.Pipes.AccessControl but that is shipped OOB and is not part of the closure, should I move types down and add typeforwards or what do you suggest?

@@ -254,6 +254,12 @@
<Reference Include="System.Threading.Overlapped" />
<Reference Include="System.Threading.Tasks" />
</ItemGroup>
<ItemGroup Condition="'$(TargetsWindows)' == 'true'">
<Reference Include="System.IO.Pipes.AccessControl" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets avoid this reference we intentionally wanted System.IO.Pipes.AccessControl to be a higher level component.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes I added it cause I missed it was OOB :) Removing and we can decide how much we want to move down to System.IO.Pipes.

@weshaggard
Copy link
Member

weshaggard commented Jan 18, 2018

@weshaggard this needs some stuff in System.IO.Pipes.AccessControl but that is shipped OOB and is not part of the closure, should I move types down and add typeforwards or what do you suggest?

How much is needed? @danmosemsft do we want to merge all of System.IO.Pipes.AccessControl into System.IO.Pipes? If we do that then we will have to do the same thing for AccessControl as well and make it throw on non-windows.

<Reference Include="System.Security.AccessControl" />
<Reference Include="System.Security.Principal.Windows" />
<Reference Include="System.Security.Claims" />
</ItemGroup>
<ItemGroup Condition="'$(TargetsUnix)' == 'true'">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are we going to do on unix?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm still reading about Unix security. Not yet what dependencies we're going to need. If any of the ones imported for windows is needed I will move it to the common ItemGroup.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Unix we will probably want to create a directory that's chmod'd to only be accessible by the current user, and then create the domain socket in that directory.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is the main thing still missing: right now there is enforcement on the client but nothing on the server side.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'm working on it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see what you're saying. What if for the UserOnly pipes instead of adding the corefx prefix in the pipe name we use that space that we're already taking, for the directory name?

private static readonly string s_pipePrefix = Path.Combine(Path.GetTempPath(), "CoreFxPipe_");

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could create a directory that contains the userid so that it is unique and related to this user and we don't generate random names, so maybe something like: tempdir/{euid}_fx/{pipename} where an euid is using the 16 bits. As the docs say:

The original Linux getuid() and geteuid() system calls supported only 16-bit user IDs. Subsequently, Linux 2.4 added getuid32() and geteuid32(), supporting 32-bit IDs. The glibc getuid() and geteuid() wrapper functions transparently deal with the variations across kernel versions.

Which at the end will be using less space than what CoreFxPipe_ prefix is already using on a regular named pipe.

@stephentoub thoughts?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stephentoub noting for posterity that on Roslyn they currently don't use this approach, they connect blindly then check GetPeerID() against GetEUid().

https://github.com/dotnet/roslyn/blob/c7a465fabaf688affbd5897d7348431eeb2059b5/src/Compilers/Shared/BuildServerConnection.cs#L490

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@stephentoub any reason to prefer one over the other? It seems like getpeerid() is for this exact purpose. https://linux.die.net/man/3/getpeereid

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@danmosemsft I am starting to look back at this to add the tests this week, and I think your point about getpeerid is valid. The only thing that cross my mind is that the existing API on the server side currently has to either throw an exception for each mismatched peer (after the socket connection) or simply ignore them - neither seems good (perhaps a new API in the future?). In this sense the directory is helpful because prevents connections before it reaches the server socket. OTOH it will introduce some small difference between Unix and Windows in at least some (mis)configurations:

  1. Assume same user for all steps;
  2. Create server without current user only flag;
  3. Create client with current user only flag;

On Windows: client will connect fine;

On Unix: client, depending on API, hangs or throws timeout exception (since the path for the named pipes is different between server and client)

All of this to say that let's keep the directory, but also add the getpeerid check also on the server side (extra precaution), it is already done on the client side.

if ((options & PipeOptions.CurrentUserOnly) != 0)
{
SecurityIdentifier identifier = GetCurrentUser();
PipeAccessRule rule = new PipeAccessRule(identifier, PipeAccessRights.ReadWrite, AccessControlType.Allow);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How is this setting up the security for the given Pipe? I don't see the pipe instance getting passed anywhere.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jaredpar
Copy link
Member

CC @agocke

if (remoteOwnerSid != currentUserSid)
{
State = PipeState.Closed;
throw new UnauthorizedAccessException();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You might want a custom message eg "Could not connect to the pipe because it was not owned by the current user."

@safern
Copy link
Member Author

safern commented Jan 18, 2018

How much is needed?

I just need PipeSecurity and whatever PipeSecurity needs from S.IO.Pipes.AccessControl.

@weshaggard
Copy link
Member

I just need PipeSecurity and whatever PipeSecurity needs from S.IO.Pipes.AccessControl.

Looks like that is most of that library. I originally though this reference would cause a cycle but it doesn't appear to so the dependency would be allowed but it would pull it into the .NET Core App. I don't have a strong opinion about pull in the library or just merging the library. I will leave that question up to @danmosemsft.

@safern
Copy link
Member Author

safern commented Jan 18, 2018

Ok, I'll wait to update this PR until we get a decision on what we want to do regarding this dependency.

@pjanotti
Copy link
Contributor

Testing will need to be manual

What is preventing you from adding tests?

Copy link
Contributor

@pjanotti pjanotti left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Small stuff, but I think it should be feasible to add tests to this feature.

@@ -3,6 +3,8 @@
// See the LICENSE file in the project root for more information.

using Microsoft.Win32.SafeHandles;
using System.Security.Principal;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these really needed by the new test?

pipeSecurity.AddAccessRule(rule);
pipeSecurity.SetOwner(identifier);

// We need to remove this flag from options because it is not a valid flag for windows PInvoke to create a pipe.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug.Assert this invariant after the if block.

if (secAttrs.Equals(default(Interop.Kernel32.SECURITY_ATTRIBUTES)))
{
secAttrs = new Interop.Kernel32.SECURITY_ATTRIBUTES();
secAttrs.nLength = (uint)sizeof(Interop.Kernel32.SECURITY_ATTRIBUTES);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that the old code only sets nLength when inheritability is set but does it cause any problem? I would expect not. Anyway, the logic here can be simply to create the struct and then set the fields, there is no need to used the old GetSecAttrs and compare against the default.

if ((options & PipeOptions.CurrentUserOnly) != 0)
{
SecurityIdentifier identifier = GetCurrentUser();
PipeAccessRule rule = new PipeAccessRule(identifier, PipeAccessRights.ReadWrite, AccessControlType.Allow);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Windows should be creating a default DACL for the named pipe if we don't specify one, can you compare the default one with the one created when the rule is added? Just to be double sure...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The default DACL will give full control to more users than the current one:

The ACLs in the default security descriptor for a named pipe grant full control to the LocalSystem account, administrators, and the creator owner. They also grant read access to members of the Everyone group and the anonymous account.

Also in the docs it says that if we want to prevent any other user to connect to the pipe we should add the Sid to the DACL:

To prevent remote users or users on a different terminal services session from accessing a named pipe, use the logon SID on the DACL for the pipe. The logon SID is used in run-as logons as well; it is the SID used to protect the per-session object namespace.

I just compared the rules with and without my change and that can be seen. Without my change it will have 4 rules involving the administrators, owner and others. With my change the Pipe will have only one rule set specifically to the owner's SID with ReadWrite access.

secAttrs.bInheritHandle = Interop.BOOL.TRUE;
}

byte[] securityDescriptor = pipeSecurity.GetSecurityDescriptorBinaryForm();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it is an "or" on the if statement you can hit this line if pipeSecurity is null.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hrgg forgot to add that if in the refactor. Thanks for catching it.

byte[] securityDescriptor = pipeSecurity.GetSecurityDescriptorBinaryForm();
pinningHandle = GCHandle.Alloc(securityDescriptor, GCHandleType.Pinned);
fixed (byte* pSecurityDescriptor = securityDescriptor)
if (pipeSecurity != null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick: it was already on the old code but you don't need the if statement on top, nor the new Interop.Kernel32.SECURITY_ATTRIBUTES(). Just use the one created on the top set its length (always) and let the other ifs do any of the extra work if needed.

@safern
Copy link
Member Author

safern commented Jan 24, 2018

@weshaggard could you please review: a57cbdf which is moving types down to System.IO.Pipes?

@safern safern changed the title WIP: Expose PipeOptions.CurrentUserOnly and add windows implementation when flag is passed WIP: Expose PipeOptions.CurrentUserOnly and add implementation when flag is passed Feb 2, 2018
@safern
Copy link
Member Author

safern commented Feb 2, 2018

I've added Unix implementation to the client side to validate we're connecting to a pipe created by the same user. Still figuring out what to do in the Unix server side and writing more tests.

@weshaggard
Copy link
Member

It is odd that msbuild isn't correctly reporting the errors, I suspect it is because the ILLinker task isn't logging them correctly but I do see the following error:

16:04:36 Fatal error in IL Linker
16:04:36 Mono.Cecil.AssemblyResolutionException: Failed to resolve assembly: 'System.Threading.Tasks, Version=4.1.1.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a'
16:04:36    at Mono.Linker.DirectoryAssemblyResolver.Resolve(AssemblyNameReference name, ReaderParameters parameters)
16:04:36    at Mono.Linker.AssemblyResolver.Resolve(AssemblyNameReference name, ReaderParameters parameters)
16:04:36    at Mono.Linker.DirectoryAssemblyResolver.Resolve(AssemblyNameReference name)
16:04:36    at Mono.Cecil.MetadataResolver.Resolve(TypeReference type)
16:04:36    at Mono.Cecil.TypeReference.Resolve()
16:04:36    at Mono.Linker.TypeReferenceExtensions.<GetMethods>d__4.MoveNext()
16:04:36    at Mono.Linker.Steps.TypeMapStep.TryMatchMethod(TypeReference type, MethodDefinition method)
16:04:36    at Mono.Linker.Steps.TypeMapStep.GetBaseMethodInTypeHierarchy(TypeDefinition type, MethodDefinition method)
16:04:36    at Mono.Linker.Steps.TypeMapStep.GetBaseMethodInTypeHierarchy(MethodDefinition method)
16:04:36    at Mono.Linker.Steps.TypeMapStep.MapVirtualBaseMethod(MethodDefinition method)
16:04:36    at Mono.Linker.Steps.TypeMapStep.MapVirtualMethod(MethodDefinition method)
16:04:36    at Mono.Linker.Steps.TypeMapStep.MapVirtualMethods(TypeDefinition type)
16:04:36    at Mono.Linker.Steps.TypeMapStep.MapType(TypeDefinition type)
16:04:36    at Mono.Linker.Steps.TypeMapStep.ProcessAssembly(AssemblyDefinition assembly)
16:04:36    at Mono.Linker.Steps.BaseStep.Process(LinkContext context)
16:04:36    at Mono.Linker.Pipeline.Process(LinkContext context)
16:04:36    at Mono.Linker.Driver.Run()
16:04:36    at Mono.Linker.Driver.Main(String[] args)

I believe there was an offline email about this issue.

@pjanotti
Copy link
Contributor

Thanks @weshaggard I forgot about that... for my learning where did you find the error? I guess I am looking at the wrong logs...

@weshaggard
Copy link
Member

I opened the All configuration build log and searched for error and found it.

@pjanotti
Copy link
Contributor

Ah I was looking that the Windows one...

<Reference Include="System.Security.AccessControl" />
<Reference Include="System.Security.Principal.Windows" />
</ItemGroup>
<ItemGroup Condition="'$(TargetGroup)' == 'netcoreapp' OR '$(TargetGroup)' == 'uap'">
<ReferenceFromRuntime Include="System.Private.Corelib" />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure you needed to switch to building as CoreLib based assembly? Since Pipes is System.Runtime based I would have expected it to be sufficient to merely add a ProjectReference to Pipes and keep everything else as a simple reference. It is very weird for something this high in the stack to have access to System.Private.CoreLib.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's what I mean: 51b4aeb

With that commit I was able to successfully build this without any changes to dependencies. @pjanotti

@pjanotti pjanotti changed the title WIP: Expose PipeOptions.CurrentUserOnly and add implementation when flag is passed Expose PipeOptions.CurrentUserOnly and add implementation when flag is passed Feb 22, 2018
@pjanotti pjanotti merged commit 29cd6a0 into dotnet:master Feb 22, 2018
@pjanotti
Copy link
Contributor

I will be adding negative tests next week for this feature.

@karelz karelz added this to the 2.1.0 milestone Mar 10, 2018
@safern safern deleted the NamedPipesAcl branch May 8, 2018 20:26
picenka21 pushed a commit to picenka21/runtime that referenced this pull request Feb 18, 2022
…s passed (dotnet/corefx#26395)

* Expose PipeOptions.CurrentUserOnly and add windows implementation when flag is passed

* PR Feedback

* Add missing if statement

* Move PipeSecurity types in implementation to System.IO.Pipes

* PR Feedback and move tests to netcoreapp file

* Add CurrentUserOnly implementation for NamedPipeClient in Unix

* PR Feedback

* Refactor to not have duplicated code

* Implement server side current option only in named pipes for unix

* More PR Feedback

* PR Feedback round 3

* Add more tests

* Fix build and add using to WindowsIdentity objects

* Fix packaging issues

* netstandard-Windows_NT needs to be built from sources


Commit migrated from dotnet/corefx@29cd6a0
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.