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

fix stack overflow when overriding HttpClientHandler.SendAsync() #367

Merged
merged 12 commits into from
Jun 3, 2019

Conversation

lucaspimentel
Copy link
Member

@lucaspimentel lucaspimentel commented May 31, 2019

Fix StackOverflowException when HttpClientHandler.SendAsync() is overridden by a method that calls the base method. See #351.

For example:

public class DerivedHandler : HttpClientHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
    {
        return await base.SendAsync(request, cancellationToken);
    }
}

This PR makes changes that affect all integrations:

  • profiler: push new argument (the original call's opcode) to execution stack when replacing a method call
  • integrations: add new opcode argument to every wrapper method to pop this value off the stack
  • most integration won't use this yet and default to CALLVIRT to keep the current behavior

Changes to HttpMessageHandlerIntegration:

  • split into separate wrappers for calls to HttpMessageHandler.SendAsync() and HttpClientHandler.SendAsync()
  • use the new opcode argument to emit the correct IL (CALL or CALLVIRT) when calling original method

@lucaspimentel lucaspimentel added type:bug area:automatic-instrumentation Automatic instrumentation managed C# code (Datadog.Trace.ClrProfiler.Managed) area:native-library Automatic instrumentation native C++ code (Datadog.Trace.ClrProfiler.Native) labels May 31, 2019
@lucaspimentel lucaspimentel added this to the 1.3 milestone May 31, 2019
@lucaspimentel lucaspimentel self-assigned this May 31, 2019
@lucaspimentel lucaspimentel force-pushed the lpimentel/httpclient-stackoverflow branch from 084b4f8 to 1e5e290 Compare May 31, 2019 20:09
@@ -329,18 +336,22 @@ internal static class DynamicMethodBuilder<TDelegate>
}
}

if (methodInfo.IsStatic)
if (callOpCode == OpCodeValue.Call || methodInfo.IsStatic)
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 is where we use the new opcode value when emitting IL. Most integrations will still pass the default opcode (CALLVIRT), so we still check if it's a static method and use CALL.

// additional arguments for the wrapper method
ILRewriterWrapper rewriter_wrapper(&rewriter);
rewriter_wrapper.SetILPosition(pInstr);
rewriter_wrapper.LoadInt32(pInstr->m_opcode);
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 is where we insert the original call's opcode value into the execution stack before replacing the method call.

@lucaspimentel lucaspimentel marked this pull request as ready for review May 31, 2019 20:40
@lucaspimentel lucaspimentel requested a review from a team as a code owner May 31, 2019 20:40
var tokenSource = cancellationTokenSource as CancellationTokenSource;
var cancellationToken = tokenSource?.Token ?? CancellationToken.None;
var callOpCode = (OpCodeValue)opCode;
var handlerType = typeof(HttpClientHandler); // Note the HttpClientHandler to match the method call we replaced
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you rename handlerType to httpClientHandlerType to be a little more specific about the meaning of the variable? I was confused initially because I thought handlerType meant handler.GetType()

Copy link
Member Author

Choose a reason for hiding this comment

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

Renamed that and a few more in feffd93.

Copy link
Contributor

Choose a reason for hiding this comment

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

That helps a lot, thanks!

var tokenSource = cancellationTokenSource as CancellationTokenSource;
var cancellationToken = tokenSource?.Token ?? CancellationToken.None;
var callOpCode = (OpCodeValue)opCode;
var handlerType = typeof(HttpMessageHandler); // Note the HttpMessageHandler to match the method call we replaced
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment here, can we do httpMessageHandlerType instead of just handlerType for the variable name?

Copy link
Member Author

Choose a reason for hiding this comment

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

Renamed in the same commit as the previous comment.

@colin-higgins
Copy link
Member

Moving forward with this then, our convention will be that all wrapper methods must have int opCode as the last argument?
Sounds like a good candidate for a test as well, something along the lines of:
wrapper_methods.Select(w => w.MethodInfo).All(m => m.Parameters.Last().GetType() == typeof(int));

@@ -17,7 +17,7 @@
</ItemGroup>

<ItemGroup>
<None Include="..\..\src\Datadog.Trace.ClrProfiler.Native\bin\$(Configuration)\$(Platform)\**"
<None Include="..\..\src\Datadog.Trace.ClrProfiler.Native\bin\$(Configuration)\$(Platform)\*.dll;..\..\src\Datadog.Trace.ClrProfiler.Native\bin\$(Configuration)\$(Platform)\*.so"
Copy link
Member

Choose a reason for hiding this comment

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

Doesn't this disregard pdb files?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch! I added the pdb files in 2f28ef3.

@lucaspimentel
Copy link
Member Author

@colin-higgins: (sorry, I couldn't reply directly to your comment)

our convention will be that all wrapper methods must have int opCode as the last argument?

Yes. As of this PR only the HttpClientHandler integration uses this new value, and the other integrations ignore it and keep using the old behavior. I will create a separate PR to change all the other integrations to use this value. Using the original opcode (CALL vs CALLVIRT) is the more correct behavior and we should do it across the board to avoid hitting this bug again if we instrument another overridden virtual method.

Sounds like a good candidate for a test as well

Good point! I added a test in ba059d4.

@lucaspimentel lucaspimentel merged commit cf8326c into develop Jun 3, 2019
@lucaspimentel lucaspimentel deleted the lpimentel/httpclient-stackoverflow branch June 3, 2019 16:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area:automatic-instrumentation Automatic instrumentation managed C# code (Datadog.Trace.ClrProfiler.Managed) area:native-library Automatic instrumentation native C++ code (Datadog.Trace.ClrProfiler.Native) type:bug
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants