Skip to content

Skip null data (fix NRE) in output data received handler #11448

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

Merged
merged 9 commits into from
Jan 10, 2020

Conversation

iSazonov
Copy link
Collaborator

@iSazonov iSazonov commented Dec 28, 2019

PR Summary

Add null check in OnOutputDataReceived() method.

PR Context

Fix #11445. The NRE comes from #11380.

PR Checklist

@iSazonov iSazonov added the CL-Engine Indicates that a PR should be marked as an engine change in the Change Log label Dec 28, 2019
@iSazonov iSazonov added this to the GA-consider milestone Dec 28, 2019
@@ -1217,6 +1219,11 @@ protected override void CleanupConnection()

private void OnOutputDataReceived(object sender, DataReceivedEventArgs e)
{
if (string.IsNullOrEmpty(e.Data))
Copy link
Contributor

Choose a reason for hiding this comment

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

Please remove this code, and add this null check to line 749 above.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

What should GetMessageGuid() return in the case? Guid.Empty?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes. Ensuing data processing will check for null or bad packets and do the appropriate thing. Thanks!

@ghost ghost added Waiting on Author The PR was reviewed and requires changes or comments from the author before being accept and removed Waiting on Author The PR was reviewed and requires changes or comments from the author before being accept labels Jan 6, 2020
Copy link
Contributor

@PaulHigin PaulHigin left a comment

Choose a reason for hiding this comment

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

LGTM

@iSazonov
Copy link
Collaborator Author

iSazonov commented Jan 6, 2020

@PaulHigin Make sense to optimize GetMessageGuid() method? Is it on hot path? I could exclude allocations (we don't use parsed GUID) and try-catch.

@PaulHigin
Copy link
Contributor

@iSazonov
My first thought was to let later packet processing check for null/empty packet string. But that is an invalid PSRP packet and nothing can be done with it. So now I am thinking we should do the null/empty check at protected void HandleOutputDataReceived() and simply return if string is null or empty.

I don't believe there will be any noticeable perf impact, but it feels cleaner.

To summarize, I think we should move the null check from line 749 to line 773.

Do you agree?

@iSazonov
Copy link
Collaborator Author

iSazonov commented Jan 6, 2020

invalid PSRP packet

If we silently drop the packet it seems we hide a bug. If I undenstand right the packet is created on server before the send code - and if we created a broken package this looks like a bug.
Although the original design allow this. Actually, it worked like that before #11380 - we sent such a empty package. The PR brings back this behavior. This is the best minimal change.
If we do not want to send empty packets, then we should rather eliminate the reason for their appearance. This is much harder to do. I don’t really know where this is going. If this is not critical, then perhaps we could stop on the current fix.

@iSazonov
Copy link
Collaborator Author

iSazonov commented Jan 6, 2020

I added an optimization commit to avoid extra allocations. If you don't like it I will revert.

@PaulHigin
Copy link
Contributor

@iSazonov
Packet processing already detects malformed packets and emits a proper error. But a null/empty data string indicates a problem in the transport, e.g., named pipe emitting a null packet because it closed or some reason. In this case the code simply ignores the packet and is the correct thing to do. I feel this check and return should be done in HandleOutputDataReceived() as I mentioned above.

@daxian-dbw daxian-dbw requested a review from PaulHigin January 6, 2020 20:28

private Guid GetMessageGuid(string data)
private bool ContainsMessageGuid(ReadOnlySpan<char> data)
{
// Perform quick scan for data packet for a GUID, ignoring any errors.
Copy link
Contributor

Choose a reason for hiding this comment

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

Please add try/catch. This method should not throw.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

With the new implementation the method can not throw. But I added explicit try-catch to avoid a regression if anybody change the method.

Copy link
Contributor

Choose a reason for hiding this comment

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

Both Slice() and IndexOf() methods can throw.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It seems ReadOnlySpan.IndexOf() never throw. Slice() could be but IndexOf() before it protects from this.
In any case I already added try-catch to exclude a regression in future..

@@ -775,15 +766,15 @@ protected void HandleOutputDataReceived(string data)
{
// Route protocol message based on whether it is a session or command message.
Copy link
Contributor

Choose a reason for hiding this comment

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

Please move string null/empty check to here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I added the explicit check.

@ghost ghost added Waiting on Author The PR was reviewed and requires changes or comments from the author before being accept and removed Waiting on Author The PR was reviewed and requires changes or comments from the author before being accept labels Jan 6, 2020
// Ignore any malformed packet errors here and return an empty Guid.
// Packet errors will be reported later during message processing.
var psGuidString = data.Slice(iTag + GUIDTAG.Length);
return Guid.TryParse(psGuidString, out _);
Copy link
Contributor

Choose a reason for hiding this comment

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

Using Span.Slice() is a good optimization. But this code is actually incorrect as is. All PSRP packets contain a Guid. But a session message guid is always an empty guid (which is why the original code compares to an empty Guid). A non-empty guid means the PSRP packet is a command message. This method is used to route the message to the appropriate processing thread. To fix this, please make the following changes;

private bool IsCommandMessage(ReadOnlySpan<char> data)
{
    try
    {
        // Perform quick scan for data packet GUID.
        var iTag = data.Index(GUIDTAG, StringComparison.OrdinalIgnoreCase);
        if (iTag > -1)
        {
            // An empty GUID value means this is a session message, otherwise it is a command message.
            var psGuidString = data.Slice(iTag + GUIDTag.Length, GUID_STR_LEN);
            if (Guid.TryParse(psGuidString out Guid psGuid))
            {
                return !Guid.Equals(psGuid, Guid.Empty);
            }
        }
    }
    catch
    {
        // The method should never throw.
    }

    // Missing Guid means an invalid packet.  Continue processing as session message.
    return false;
}

Copy link
Collaborator Author

@iSazonov iSazonov Jan 7, 2020

Choose a reason for hiding this comment

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

Ah, I did know. Thanks!

Do we know that the package integrity already was checked and GUID always present? If yes we could only search the const without parsing:

Suggested change
return Guid.TryParse(psGuidString, out _);
private const string GUIDTAG = "PSGuid=00000000-0000-0000-0000-000000000000'";

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, good point. We can simply search for an empty PSGuid element. All valid packets will have a PSGuid element, and if invalid we just treat as a session message.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think we can make it even simpler. I now agree that the method won't throw so we can remove the try/catch. Also empty Guid equates to session message so we should name the method correctly. I was thinking:

private const string SESSIONGUID = "PSGuid='00000000-0000-0000-0000-000000000000'";
private bool IsSessionMessage(string data)
{
    // Perform quick scan for an empty GUID element, which identifies a session message.
    return data.IndexOf(SESSIONGUID, StringComparison.OrdinalIgnoreCase) > -1;
}

Copy link
Contributor

Choose a reason for hiding this comment

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

Since this is only used for routing, any invalid packet can be routed to command thread ... it really shouldn't matter.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done.

// Route protocol message based on whether it is a session or command message.
// Session messages have empty Guid values.
if (Guid.Equals(GetMessageGuid(data), Guid.Empty))
if (ContainsMessageGuid(data))
Copy link
Contributor

Choose a reason for hiding this comment

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

Please change to use new IsCommandMessage().

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done.

// In this case we simply ignore the packet.
return;
}

// Route protocol message based on whether it is a session or command message.
// Session messages have empty Guid values.
Copy link
Contributor

Choose a reason for hiding this comment

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

Please remove this comment.
I mean please just remove this comment:
// Session messages have empty Guid values.
because it seems unnecessary now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done.

@ghost ghost added the Waiting on Author The PR was reviewed and requires changes or comments from the author before being accept label Jan 7, 2020
@@ -773,17 +771,25 @@ protected void HandleOutputDataReceived(string data)
{
try
{
if (string.IsNullOrEmpty(data))
Copy link
Contributor

Choose a reason for hiding this comment

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

Minor comment. The null check does not need to be within the try/catch.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done.

@ghost ghost removed the Waiting on Author The PR was reviewed and requires changes or comments from the author before being accept label Jan 7, 2020
Copy link
Contributor

@PaulHigin PaulHigin left a comment

Choose a reason for hiding this comment

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

LGTM


private Guid GetMessageGuid(string data)
private bool IsSessionMessage(ReadOnlySpan<char> data)
Copy link
Contributor

Choose a reason for hiding this comment

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

Just curious if we still need to pass as Span type. We can just scan a normal string type, but I am not sure if it really makes a difference.

Copy link
Collaborator Author

@iSazonov iSazonov Jan 7, 2020

Choose a reason for hiding this comment

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

:-) Yes, but we again would care about null to exclude a regression in future.

Copy link
Member

Choose a reason for hiding this comment

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

I think if you worry that a null value may be passed to this method, the intent is more clear by having a null check here:

private bool IsSessionMessage(string data)
{
    if (data == null) { return false; }
    return data.IndexOf(SESSIONDMESSAGETAG, StringComparison.OrdinalIgnoreCase) > -1;
}

Or, maybe this method should just be directly inlined in HandleOutputDataReceived given it's just one line of code and not used anywhere else.

Copy link
Contributor

Choose a reason for hiding this comment

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

I prefer having the private method since it makes clear what the purpose is. Null check is not needed.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Inlined the method.

Copy link
Collaborator Author

@iSazonov iSazonov Jan 7, 2020

Choose a reason for hiding this comment

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

Oops, @PaulHigin sorry I did not see your comment. Perhaps SESSIONDMESSAGETAG well says that the purpose is and inlining is goog? Thoughts?

Copy link
Member

Choose a reason for hiding this comment

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

@PaulHigin Would it be OK to add a comment to the inlined code to make the purpose clear?
I don't mind having it a separate private method, just thought using string data should be sufficient.

Copy link
Contributor

Choose a reason for hiding this comment

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

I assumed the compiler would inline but I am fine either way. These are minor issues and overall I am happy with the changes.

@daxian-dbw daxian-dbw merged commit 397756a into PowerShell:master Jan 10, 2020
@iSazonov iSazonov deleted the remoting-nre-in-handler branch January 10, 2020 03:34
@iSazonov
Copy link
Collaborator Author

@PaulHigin Thanks for help!

@PaulHigin
Copy link
Contributor

@iSazonov Thanks for fixing this!

@ghost
Copy link

ghost commented Jan 16, 2020

🎉v7.0.0-rc.2 has been released which incorporates this pull request.:tada:

Handy links:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
CL-Engine Indicates that a PR should be marked as an engine change in the Change Log MustHave
Projects
None yet
6 participants