Skip to content

Commit 7eb058b

Browse files
Fix silent bad codegen VSD possible tailcall converted to normal call (#49552)
The problem was when a VSD interface call returning a multi-byte struct in registers was initially considered to be a tailcall, but the tailcall was abandoned in morph due to not enough stack space to store outgoing arguments, in which case we create a new call return local to store the return struct, and re-morph the call. In doing so, we forget that we had already added VSD non-standard args, and re-add them, leaving the originally added arg as a "normal" arg that shouldn't be there. So, in summary, for a call A->B, to see this failure, we need: 1. The call is considered a potential tailcall (by the importer) 2. The call requires non-standard arguments that add call argument IR in fgInitArgInfo() (e.g., VSD call -- in this case, a generic interface call) 3. We reject the tailcall in fgMorphPotentialTailCall() (e.g., not enough incoming arg stack space in A to store B's outgoing args), in this case because the first arg is a large struct. We can't reject it earlier, due to things like address exposed locals -- we must get far enough through the checks to have called fgInitArgInfo() to add the extra non-standard arg. 4. B returns a struct in multiple registers (e.g., a 16-byte struct in Linux x64 ABI) The fix is to remove the previously-added non-standard VSD argument IR when we are preparing to re-morph a call. There was one other location that was already doing this. I'm a little worried that there are other places where the non-standard added IR is not being considered when we clear out the arg info before remorphing. It seems like there is some risk here. Probably, we should consider a better way of handling the non-standard arg IR given the need in some cases to re-morph calls. I commented out a few cases of: ``` assert(!fgCanFastTailCall(call, nullptr)); ``` because this function can call `fgInitArgInfo` which can alter the IR. That seems dangerous in an assert, which should have any side-effects like modifying the IR. Fixes #49078 No SPMI asm diffs.
1 parent e1655ab commit 7eb058b

File tree

5 files changed

+169
-8
lines changed

5 files changed

+169
-8
lines changed

src/coreclr/src/jit/gentree.cpp

+32
Original file line numberDiff line numberDiff line change
@@ -1205,6 +1205,38 @@ bool GenTreeCall::Equals(GenTreeCall* c1, GenTreeCall* c2)
12051205
return true;
12061206
}
12071207

1208+
//--------------------------------------------------------------------------
1209+
// ResetArgInfo: The argument info needs to be reset so it can be recomputed based on some change
1210+
// in conditions, such as changing the return type of a call due to giving up on doing a tailcall.
1211+
// If there is no fgArgInfo computed yet for this call, then there is nothing to reset.
1212+
//
1213+
void GenTreeCall::ResetArgInfo()
1214+
{
1215+
if (fgArgInfo == nullptr)
1216+
{
1217+
return;
1218+
}
1219+
1220+
// We would like to just set `fgArgInfo = nullptr`. But `fgInitArgInfo()` not
1221+
// only sets up fgArgInfo, it also adds non-standard args to the IR, and we need
1222+
// to remove that extra IR so it doesn't get added again.
1223+
//
1224+
// NOTE: this doesn't handle all possible cases. There might be cases where we
1225+
// should be removing non-standard arg IR but currently aren't.
1226+
CLANG_FORMAT_COMMENT_ANCHOR;
1227+
1228+
#if !defined(TARGET_X86)
1229+
if (IsVirtualStub())
1230+
{
1231+
JITDUMP("Removing VSD non-standard arg [%06u] to prepare for re-morphing call [%06u]\n",
1232+
Compiler::dspTreeID(gtCallArgs->GetNode()), gtTreeID);
1233+
gtCallArgs = gtCallArgs->GetNext();
1234+
}
1235+
#endif // !defined(TARGET_X86)
1236+
1237+
fgArgInfo = nullptr;
1238+
}
1239+
12081240
#if !defined(FEATURE_PUT_STRUCT_ARG_STK)
12091241
unsigned GenTreePutArgStk::getArgSize()
12101242
{

src/coreclr/src/jit/gentree.h

+2
Original file line numberDiff line numberDiff line change
@@ -4502,6 +4502,8 @@ struct GenTreeCall final : public GenTree
45024502
return (gtFlags & GTF_CALL_M_EXP_RUNTIME_LOOKUP) != 0;
45034503
}
45044504

4505+
void ResetArgInfo();
4506+
45054507
unsigned gtCallMoreFlags; // in addition to gtFlags
45064508

45074509
unsigned char gtCallType : 3; // value from the gtCallTypes enumeration

src/coreclr/src/jit/morph.cpp

+16-8
Original file line numberDiff line numberDiff line change
@@ -2390,6 +2390,9 @@ GenTree* Compiler::fgInsertCommaFormTemp(GenTree** ppTree, CORINFO_CLASS_HANDLE
23902390
// This method only computes the arg table and arg entries for the call (the fgArgInfo),
23912391
// and makes no modification of the args themselves.
23922392
//
2393+
// The IR for the call args can change for calls with non-standard arguments: some non-standard
2394+
// arguments add new call argument IR nodes.
2395+
//
23932396
void Compiler::fgInitArgInfo(GenTreeCall* call)
23942397
{
23952398
GenTreeCall::Use* args;
@@ -6596,6 +6599,9 @@ void Compiler::fgMorphCallInlineHelper(GenTreeCall* call, InlineResult* result)
65966599
// This function is target specific and each target will make the fastTailCall
65976600
// decision differently. See the notes below.
65986601
//
6602+
// This function calls fgInitArgInfo() to initialize the arg info table, which
6603+
// is used to analyze the argument. This function can alter the call arguments
6604+
// by adding argument IR nodes for non-standard arguments.
65996605
//
66006606
// Windows Amd64:
66016607
// A fast tail call can be made whenever the number of callee arguments
@@ -7698,7 +7704,10 @@ GenTree* Compiler::fgMorphTailCallViaHelpers(GenTreeCall* call, CORINFO_TAILCALL
76987704
// We come this route only for tail prefixed calls that cannot be dispatched as
76997705
// fast tail calls
77007706
assert(!call->IsImplicitTailCall());
7701-
assert(!fgCanFastTailCall(call, nullptr));
7707+
7708+
// We want to use the following assert, but it can modify the IR in some cases, so we
7709+
// can't do that in an assert.
7710+
// assert(!fgCanFastTailCall(call, nullptr));
77027711

77037712
bool virtualCall = call->IsVirtual();
77047713

@@ -7709,11 +7718,7 @@ GenTree* Compiler::fgMorphTailCallViaHelpers(GenTreeCall* call, CORINFO_TAILCALL
77097718
{
77107719
JITDUMP("This is a VSD\n");
77117720
#if FEATURE_FASTTAILCALL
7712-
// fgInitArgInfo has been called from fgCanFastTailCall and it added the stub address
7713-
// to the arg list. Remove it now.
7714-
call->gtCallArgs = call->gtCallArgs->GetNext();
7715-
// We changed args so recompute info.
7716-
call->fgArgInfo = nullptr;
7721+
call->ResetArgInfo();
77177722
#endif
77187723

77197724
call->gtFlags &= ~GTF_CALL_VIRT_STUB;
@@ -8328,7 +8333,10 @@ void Compiler::fgMorphTailCallViaJitHelper(GenTreeCall* call)
83288333
// We come this route only for tail prefixed calls that cannot be dispatched as
83298334
// fast tail calls
83308335
assert(!call->IsImplicitTailCall());
8331-
assert(!fgCanFastTailCall(call, nullptr));
8336+
8337+
// We want to use the following assert, but it can modify the IR in some cases, so we
8338+
// can't do that in an assert.
8339+
// assert(!fgCanFastTailCall(call, nullptr));
83328340

83338341
// First move the 'this' pointer (if any) onto the regular arg list. We do this because
83348342
// we are going to prepend special arguments onto the argument list (for non-x86 platforms),
@@ -8826,7 +8834,7 @@ GenTree* Compiler::fgMorphCall(GenTreeCall* call)
88268834
// ret temp
88278835

88288836
// Force re-evaluating the argInfo as the return argument has changed.
8829-
call->fgArgInfo = nullptr;
8837+
call->ResetArgInfo();
88308838

88318839
// Create a new temp.
88328840
unsigned tmpNum =
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
6+
// Regression test for GitHub issue 49078: https://github.com/dotnet/runtime/issues/49078
7+
//
8+
// The problem was when a VSD interface call returning a multi-byte struct in registers was initially considered
9+
// to be a tailcall, but the tailcall was abandoned in morph due to not enough stack space to store outgoing
10+
// arguments, in which case we create a new call return local to store the return struct, and re-morph the
11+
// call. In doing so, we forget that we had already added VSD non-standard args, and re-add them, leaving
12+
// the originally added arg as a "normal" arg that shouldn't be there.
13+
//
14+
// So, in summary, for a call A->B, to see this failure, we need:
15+
//
16+
// 1. The call is considered a potential tailcall (by the importer)
17+
// 2. The call requires non-standard arguments that add call argument IR in fgInitArgInfo()
18+
// (e.g., VSD call -- in this case, a generic interface call)
19+
// 3. We reject the tailcall in fgMorphPotentialTailCall() (e.g., not enough incoming arg stack space in A
20+
// to store B's outgoing args), in this case because the first arg is a large struct. We can't reject
21+
// it earlier, due to things like address exposed locals -- we must get far enough through the checks
22+
// to have called fgInitArgInfo() to add the extra non-standard arg.
23+
// 4. B returns a struct in multiple registers (e.g., a 16-byte struct in Linux x64 ABI)
24+
25+
namespace GitHub_49078
26+
{
27+
public struct S16
28+
{
29+
public IntPtr a;
30+
public uint b;
31+
}
32+
33+
public struct BigStruct
34+
{
35+
public IntPtr a, b, c, d, e, f, g, h, j, k, l, m;
36+
37+
public BigStruct(IntPtr a1)
38+
{
39+
a = b = c = d = e = f = g = h = j = k = l = m = a1;
40+
}
41+
}
42+
43+
public interface IFoo<T>
44+
{
45+
public S16 Foo(BigStruct b, int i, int j);
46+
}
47+
48+
public class CFoo<T> : IFoo<T>
49+
{
50+
public S16 Foo(BigStruct b, int i, int j)
51+
{
52+
S16 s16;
53+
s16.a = (IntPtr)i;
54+
s16.b = (uint)j;
55+
return s16;
56+
}
57+
}
58+
59+
class Test
60+
{
61+
IFoo<int> m_if = new CFoo<int>();
62+
BigStruct m_bs = new BigStruct((IntPtr)1);
63+
64+
public S16 Caller(int a)
65+
{
66+
// Add some computation so this is not inlineable (but don't mark it noinline,
67+
// which would prevent the tailcall consideration).
68+
int i = 7;
69+
try
70+
{
71+
for (int j = 0; j < a; j++)
72+
{
73+
i += j;
74+
}
75+
}
76+
finally
77+
{
78+
i += 2;
79+
}
80+
81+
return m_if.Foo(m_bs, i, a);
82+
}
83+
}
84+
85+
class Program
86+
{
87+
static int Main(string[] args)
88+
{
89+
Test t = new Test();
90+
S16 s = t.Caller(4);
91+
long l = (long)s.a + s.b;
92+
if (l == 19)
93+
{
94+
Console.WriteLine("Passed");
95+
return 100;
96+
}
97+
else
98+
{
99+
Console.WriteLine($"{s.a}, {s.b}, {l}");
100+
Console.WriteLine("Failed");
101+
return 101;
102+
}
103+
}
104+
}
105+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<OutputType>Exe</OutputType>
4+
<CLRTestPriority>1</CLRTestPriority>
5+
</PropertyGroup>
6+
<PropertyGroup>
7+
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
8+
<DebugType>None</DebugType>
9+
<Optimize>True</Optimize>
10+
</PropertyGroup>
11+
<ItemGroup>
12+
<Compile Include="$(MSBuildProjectName).cs" />
13+
</ItemGroup>
14+
</Project>

0 commit comments

Comments
 (0)