Skip to content

Tail-call stepping correctness in ReadyToRun assemblies #123673

@max-charlamb

Description

@max-charlamb

Description

The IsTailCall function in src/coreclr/debug/ee/controller.cpp assumes that given a call IP, we can immediately determine the destination MethodDesc that will be invoked. However, this assumption breaks for cross ReadyToRun (R2R) image tail calls.

Specifically, if ReadyToRun image A has method Foo1() which tail-calls to Foo2() in image B, then Foo1() may have a call to one of the tail-call helpers at the end that is indirected through an ExternalMethodFixup. The StubManager for ExternalMethodFixup does not immediately figure out the destination MethodDesc - it uses callbacks that pass content to TraceManager() to determine where execution will go next.

This means the debugger may not correctly handle stepping through tail calls that cross ReadyToRun image boundaries.

Background

This issue was discovered as part of PR #123640, which fixed an access violation (AV) in IsTailCall when trace.GetAddress() returns NULL. The AV was fixed by adding a NULL check before calling GetNativeCodeMethodDesc(), but the underlying stepping correctness issue remains.

Related changes:

These trace types set address = NULL by design because:

  1. TRACE_UNJITTED_METHOD - The MethodDesc field describes a method that will be jitted and executed in the future, code address not yet known.
  2. TRACE_MULTICAST_DELEGATE_HELPER and TRACE_EXTERNAL_METHOD_FIXUP - A callback can be turned on and the content of that callback can be passed to TraceManager() to determine where execution will go next.
  3. Other trace types - The address is some IP that will be executed in the future where a breakpoint can be placed and then further actions taken from there.

Current Behavior

When stepping through a tail call that crosses R2R image boundaries (via ExternalMethodFixup), the debugger may not correctly step into the target method because IsTailCall cannot determine the destination MethodDesc at the time of the call.

Expected Behavior

The debugger should correctly step through tail calls regardless of whether they cross ReadyToRun image boundaries.

Affected Code

  • src/coreclr/debug/ee/controller.cpp - IsTailCall function (lines ~5820-5860)
  • src/coreclr/vm/stubmgr.cpp - ExternalMethodFixupStubManager

Potential Solutions

This will require redesigning how IsTailCall determines the target method for calls that go through ExternalMethodFixup stubs. Possible approaches include:

  1. Enhancing the StubManager to provide target method information for ExternalMethodFixup
  2. Using the callback mechanism (TraceManager()) within IsTailCall to resolve the target
  3. Special-casing R2R cross-image tail calls in the stepping logic

References

Metadata

Metadata

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions