Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add missing tests for NamedPipes #72956

Merged
merged 10 commits into from
Oct 20, 2022
Merged

Add missing tests for NamedPipes #72956

merged 10 commits into from
Oct 20, 2022

Conversation

jozkee
Copy link
Member

@jozkee jozkee commented Jul 27, 2022

Some tests were missing on PipeOptions.CurrentUserOnly scenarios.
This PR extend the test to account for the following (under the OS defaults, which allows readonly access on different users):

  • Same user asking for read access
  • Same user asking for read/write access
  • Different user asking for read access
  • Different user asking for read/write access

@jozkee jozkee added this to the 7.0.0 milestone Jul 27, 2022
@jozkee jozkee self-assigned this Jul 27, 2022
@ghost
Copy link

ghost commented Jul 27, 2022

Tagging subscribers to this area: @dotnet/area-system-io
See info in area-owners.md if you want to be subscribed.

Issue Details

Some tests were missing on PipeOptions.CurrentUserOnly scenarios.
This PR extend the test to account for the following (under the OS defaults, which allows readonly access on different users):

  • Same user asking for read access
  • Same user asking for read/write access
  • Different user asking for read access
  • Different user asking for read/write access
Author: Jozkee
Assignees: Jozkee
Labels:

area-System.IO

Milestone: 7.0.0

@danmoseley
Copy link
Member

My original comment on these tests (for Windows) so I can page back in:

These tests only run when the user is elevated. They create a new machine scope user and run the other half of the test impersonated as that user. The first test tests with and without PipeOptions.CurrentUserOnly on both ends, and verifies that the client connection is rejected in every case with UnauthorizedAccessException. This includes when both ends have PipeOptions.None, because the pipe is being created with the default ACL, which allows Read access to everyone, but not Write, and the client is being created with PipeDirection.InOut, so it passes GENERIC_WRITE to CreateFile, which then fails. The test below it verifies that client can connect to server when server is elevated (and a different user, in this case) and it succeeds because it's requesting PipeDirection.In. Because the first test fails even when PipeOptions.None is being passed on both sides, the PipeOptions.CurrentUser cases are not actually testing that PipeOptions.CurrentUser is implemented.
It seems we are missing several cases
• CurrentUserOnly on client, server, both, and neither end, with different users on other end, neither elevated, with the client requesting PipeDirection.In. Should throw UnauthorizedAccessException on client connection but pass when CurrentUserOnly is on neither end
• Same but with the same users on both ends, one or the other side elevated.

});
});

Thread.Sleep(1000); // give it a sec to allow above tasks to perform some work before this.
Copy link
Member

Choose a reason for hiding this comment

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

I've gotten pretty suspicious of sleeps in tests! I guess it's OK since the test won't fail on a slow machine, but the ideal pattern is to wait on some event set in the task eg after it's looped.

Copy link
Member

Choose a reason for hiding this comment

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

in fact, rather than having one task looping, you could consider kicking off N tasks all trying to connect once. That will give you some concurrency, and you can just do Task.WaitAll for them to complete, no sleep and no event.

Copy link
Member

Choose a reason for hiding this comment

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

the WaitAll would be after the await serverWait;.

As an improvement, I suggest to have the tasks all wait on an event before they try to connect, and then set that event on the line before you do client.Connect(). That will make it most likely they do their work concurrently with the succesful connect.

Copy link
Member

Choose a reason for hiding this comment

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

+1

Copy link
Member Author

Choose a reason for hiding this comment

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

With serverWait = server.WaitForConnectionAsync(), we can't do anything after awaiting it, the method blocks until a client connects; the event should be set before awaiting serverWait. Please check latest commit.

Copy link
Member

Choose a reason for hiding this comment

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

Not sure I follow. How is 100 better than 1? The ideal is to not have any guesses.

Copy link
Member Author

@jozkee jozkee Aug 16, 2022

Choose a reason for hiding this comment

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

I'n not very good explaining so, here's how the test would look like:

[OuterLoop]
[ConditionalFact(nameof(IsAdminOnSupportedWindowsVersions))]
public async Task Connection_UnderDifferentUsers_CurrentUserOnlyOnServer_InvalidClientConnectionAttempts_DoNotBlockSuccessfulClient()
{
    string name = PipeStreamConformanceTests.GetUniquePipeName();
    bool invalidClientShouldStop = false;
+    int invalidClientAttempts = 0;

    using var server = new NamedPipeServerStream(name, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);
    Task serverWait = server.WaitForConnectionAsync();

    Task invalidClient = Task.Run(() =>
    {
        // invalid non-current user tries to connect to server.
        _testAccountImpersonator.RunImpersonated(() =>
        {
            while (!Volatile.Read(ref invalidClientShouldStop))
            {
                using var client = new NamedPipeClientStream(".", name, PipeDirection.In, PipeOptions.Asynchronous);
                Assert.Throws<UnauthorizedAccessException>(() => client.Connect());
+                Volatile.Write(ref invalidClientAttempts, invalidClientAttempts + 1);
            }
        });
    });

-    Thread.Sleep(1000); // give it a sec to allow above tasks to perform some work before this.
+    while (Volatile.Read(ref invalidClientAttempts) < 100) ; // Wait until the invalid client has tried multiple times.

    Assert.False(serverWait.IsCompleted);

    // valid client tries to connect and succeeds.
    using var client = new NamedPipeClientStream(".", name, PipeDirection.In, PipeOptions.Asynchronous);
    client.Connect();
    await serverWait;
    Volatile.Write(ref invalidClientShouldStop, true);
}

This way we can at least ensure that the other two threads were running prior to the valid client thread, and it won't take 1 second, will take whatever 100 iterations take, which is probably less.
100 was just a placeholder, it can be 20 or 5 or 1.

Copy link
Member Author

Choose a reason for hiding this comment

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

What I am arguing is that we can have the loop with the event, and without the thread.sleep.

Copy link
Member

Choose a reason for hiding this comment

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

Yes to the loop without the sleep, but what I'm asking is why you need a counter. As long as it's gone through the loop once, you know the "interference" is in progress, right? We don't expect any new behavior to emerge after 100 times.

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 agree there's no need for 100 times.

@danmoseley
Copy link
Member

Should we also have Windows tests where there are different users on each end, and neither is elevated? this would probably be easiest to do by creating two different users.

});
});

Thread.Sleep(1000); // give it a sec to allow above tasks to perform some work before this.
Copy link
Member

Choose a reason for hiding this comment

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

+1

@jozkee
Copy link
Member Author

jozkee commented Jul 28, 2022

Should we also have Windows tests where there are different users on each end, and neither is elevated? this would probably be easiest to do by creating two different users.

It is doable, the _testAccountImpersonator is populated using xUnit's IClassFixture<TestAccountImpersonator>; I tried adding an extra parameter of the same type to the test class ctor expecting to get a new instance (i.e: a new user) but xUnit passes the same instance twice, so this probably requires switching IClassFixture<TestAccountImpersonator> to a IClassFixture<TestAccountImpersonator**Set**>, containing the two impersonators.

My point is that it may be too cumbersome for this PR. I also don't think there's too much value on it.

@jozkee
Copy link
Member Author

jozkee commented Jul 28, 2022

Actually, we can create the second TestAccountImpersonator inside the test method, that should be easier. We just need to make sure that the theory data does not create a new one everytime.

});

using var server = new NamedPipeServerStream(name, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous | PipeOptions.CurrentUserOnly);
@event.Set();
Copy link
Member

Choose a reason for hiding this comment

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

There's a very high liklihood that both Connects will be initiated before the server begins waiting. There's also a high liklihood that the valid client will connect before the invalid one does.

In the original test I'd proposed, the invalid client was looping, trying to connect over and over, and only once it started that loop would the valid client try to connect, the goal of the test being to ensure the invalid client couldn't starve out the valid client. That's no longer happening here, so I'm not sure what this test is actually testing. With the invalid client making a single connection attempt, eventually that attempt will end and the valid client can connect. And if that were broken, that would be caught by just a simple test with no client/client concurrency that simply tried to connect from an invalid client and then sequentially tried to connect from a valid client.

Copy link
Member Author

@jozkee jozkee Aug 5, 2022

Choose a reason for hiding this comment

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

I can bring the original test back and keep this version as well.
fwiw: this was the old version that issued invalid connects in a loop:
c94d060#diff-23b3a1e5a562e7712c4adff12019bbc8485dd0566627dc77808efa97efee2b30R134

Copy link
Member

Choose a reason for hiding this comment

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

I agree that this test is not worth keeping, because it will so rarely do anything useful. If you can tweak the test you brought back below, I think it will do it correctly, and you can delete this.

@jozkee jozkee removed this from the 7.0.0 milestone Aug 16, 2022
@jozkee jozkee added this to the 8.0.0 milestone Aug 16, 2022
Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

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

It's almost ready, thank you for improving the tests @jozkee !

{
bool isRoot = AdminHelpers.IsProcessElevated();
Copy link
Member

Choose a reason for hiding this comment

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

This test is executed only for "super user":

private static bool IsRemoteExecutorSupportedAndOnUnixAndSuperUser => RemoteExecutor.IsSupported && PlatformDetection.IsUnixAndSuperUser;
[ConditionalTheory(nameof(IsRemoteExecutorSupportedAndOnUnixAndSuperUser))]

public static bool IsSuperUser => IsBrowser || IsWindows ? false : libc.geteuid() == 0;

and the helper that checks whether the process is elevated performs same check:

public static unsafe bool IsProcessElevated()
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
uint userId = Interop.Sys.GetEUid();
return(userId == 0);

It seems that this check is redundant and adding PipeOptions.CurrentUserOnly test cases has no effect?

Copy link
Member Author

Choose a reason for hiding this comment

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

IIRC, the reason I did this was to complete the test matrix and have explicit behaviors of all combinations. But this could be represented by just having the theorydata where clientPipeOptions = CurrentUserOnly commented-out.

Copy link
Member

Choose a reason for hiding this comment

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

@jozkee IMO it would be better to remove the test cases that are always skipped and just add a comment to the test explaining why given scenarios are not covered.

Copy link
Member Author

Choose a reason for hiding this comment

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

Actually, the right thing to do is remove IsRemoteExecutorSupportedAndOnUnixAndSuperUser that way we can run all the inline data when dotnet is not running as sudo/root and skip the clientPipeOptions == PipeOptions.CurrentUserOnly when it is.

This worked as I just described on my local wsl, I hope it works in CI as well.

Copy link
Member

Choose a reason for hiding this comment

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

@jozkee but we need to keep the IsRemoteExecutorSupported condition as the test is using it.

@jozkee
Copy link
Member Author

jozkee commented Oct 13, 2022

/azp run runtime-libraries-coreclr outerloop

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@jozkee
Copy link
Member Author

jozkee commented Oct 18, 2022

CI errors are #76755

Copy link
Member

@adamsitnik adamsitnik left a comment

Choose a reason for hiding this comment

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

LGTM, thank you @jozkee !

@jozkee jozkee merged commit 88aea31 into dotnet:main Oct 20, 2022
@jozkee jozkee deleted the pipe_tests branch October 20, 2022 04:35
@ghost ghost locked as resolved and limited conversation to collaborators Nov 19, 2022
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.

5 participants