-
Notifications
You must be signed in to change notification settings - Fork 4.9k
Expose PipeOptions.CurrentUserOnly and add implementation when flag is passed #26395
Conversation
@@ -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) |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
@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" /> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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'"> |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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_"); |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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().
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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:
- Assume same user for all steps;
- Create server without current user only flag;
- 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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is being passed through the security attributes, see: https://github.com/dotnet/corefx/pull/26395/files/c50bb469733b32ab61607be7dbe9ae3c41111445#diff-a7e95545fe2b63c35aaee6c4c3de3cffR70
CC @agocke |
if (remoteOwnerSid != currentUserSid) | ||
{ | ||
State = PipeState.Closed; | ||
throw new UnauthorizedAccessException(); |
There was a problem hiding this comment.
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."
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. |
Ok, I'll wait to update this PR until we get a decision on what we want to do regarding this dependency. |
What is preventing you from adding tests? |
There was a problem hiding this 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; |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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(); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
@weshaggard could you please review: a57cbdf which is moving types down to System.IO.Pipes? |
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. |
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:
I believe there was an offline email about this issue. |
Thanks @weshaggard I forgot about that... for my learning where did you find the error? I guess I am looking at the wrong logs... |
I opened the All configuration build log and searched for error and found it. |
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" /> |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
49e610b
to
c282a2b
Compare
I will be adding negative tests next week for this feature. |
…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
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