Skip to content

Commit 08322e8

Browse files
Fix GC hole with STOREIND of LCL_VAR_ADDR/LCL_FLD_ADDR (#45818)
* Fix GC hole with STOREIND of LCL_VAR_ADDR/LCL_FLD_ADDR We were updating the GC liveness of the ADDR node, but `genUpdateLife()` expects to get the parent node, so no liveness was ever updated. There were 4 SPMI GC info diffs in the libraries, all related to uses of `System.Collections.Immutable.ImmutableArray` where we struct promote fields who are themselves single-element gc ref structs that are kept on the stack and not in registers. In all cases, the liveness of the stack local was not reflected in codegen's GC sets, but it was reflected in the emitter's GC sets, so it was marked as a GC lifetime. However, that lifetime would get cut short if we hit a call site before the last use, as calls (sometimes) carry the full set of live variables across the call. So, variables not in this set (including the "accidental" emitter-created GC lifetimes here) would get killed, leaving a hole between the intermediate call and actual stack local last use. Fixes #45557 * Add unit test * Add comment to `emitInsLoadInd()`
1 parent c400049 commit 08322e8

File tree

3 files changed

+110
-2
lines changed

3 files changed

+110
-2
lines changed

src/coreclr/jit/emitxarch.cpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3054,7 +3054,12 @@ void emitter::emitInsLoadInd(instruction ins, emitAttr attr, regNumber dstReg, G
30543054
unsigned offset = varNode->GetLclOffs();
30553055
emitIns_R_S(ins, attr, dstReg, varNode->GetLclNum(), offset);
30563056

3057-
// Updating variable liveness after instruction was emitted
3057+
// Updating variable liveness after instruction was emitted.
3058+
// TODO-Review: it appears that this call to genUpdateLife does nothing because it
3059+
// returns quickly when passed GT_LCL_VAR_ADDR or GT_LCL_FLD_ADDR. Below, emitInsStoreInd
3060+
// had similar code that replaced `varNode` with `mem` (to fix a GC hole). It might be
3061+
// appropriate to do that here as well, but doing so showed no asm diffs, so it's not
3062+
// clear when this scenario gets hit, at least for GC refs.
30583063
codeGen->genUpdateLife(varNode);
30593064
return;
30603065
}
@@ -3116,7 +3121,7 @@ void emitter::emitInsStoreInd(instruction ins, emitAttr attr, GenTreeStoreInd* m
31163121
}
31173122

31183123
// Updating variable liveness after instruction was emitted
3119-
codeGen->genUpdateLife(varNode);
3124+
codeGen->genUpdateLife(mem);
31203125
return;
31213126
}
31223127

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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+
// Regression test for https://github.com/dotnet/runtime/issues/45557,
5+
// derived from Roslyn failure case.
6+
//
7+
// Bug was a GC hole with STOREIND of LCL_VAR_ADDR/LCL_FLD_ADDR.
8+
// We were updating the GC liveness of the ADDR node, but
9+
// genUpdateLife() expects to get the parent node, so no
10+
// liveness was ever updated.
11+
//
12+
// The bad code cases in the libraries were related to uses of
13+
// System.Collections.Immutable.ImmutableArray
14+
// where we struct promote fields who are themselves single-element
15+
// gc ref structs that are kept on the stack and not in registers.
16+
// In all cases, the liveness of the stack local was not reflected
17+
// in codegen's GC sets, but it was reflected in the emitter's GC
18+
// sets, so it was marked as a GC lifetime. However, that lifetime
19+
// would get cut short if we hit a call site before the last use,
20+
// as calls (sometimes) carry the full set of live variables across
21+
// the call. So, variables not in this set (including the
22+
// "accidental" emitter-created GC lifetimes here) would get killed,
23+
// leaving a hole between the intermediate call and actual stack
24+
// local last use.
25+
26+
using System;
27+
using System.Collections.Generic;
28+
using System.Collections.Immutable;
29+
using System.Runtime.CompilerServices;
30+
31+
namespace Runtime_45557
32+
{
33+
internal readonly struct ObjectBinderSnapshot
34+
{
35+
private readonly Dictionary<Type, int> _typeToIndex;
36+
private readonly ImmutableArray<Type> _types;
37+
private readonly ImmutableArray<Func<Object, Object>> _typeReaders;
38+
39+
[MethodImpl(MethodImplOptions.AggressiveInlining)] // this needs to get inlined to cause the failure
40+
public ObjectBinderSnapshot(
41+
Dictionary<Type, int> typeToIndex,
42+
List<Type> types,
43+
List<Func<Object, Object>> typeReaders)
44+
{
45+
_typeToIndex = new Dictionary<Type, int>(typeToIndex);
46+
_types = types.ToImmutableArray(); // stack variable here would go live
47+
_typeReaders = typeReaders.ToImmutableArray(); // it would get erroneously killed in GC info here
48+
GC.Collect(); // try to cause a crash by collecting the variable
49+
Console.WriteLine($"{_types.Length}"); // use the collected variable; should crash (most of the time, depending on GC behavior)
50+
}
51+
52+
public string SomeValue => _types.ToString();
53+
}
54+
55+
internal static class ObjectBinder
56+
{
57+
private static readonly object s_gate = new();
58+
59+
private static ObjectBinderSnapshot? s_lastSnapshot = null;
60+
61+
private static readonly Dictionary<Type, int> s_typeToIndex = new();
62+
private static readonly List<Type> s_types = new();
63+
private static readonly List<Func<Object, Object>> s_typeReaders = new();
64+
65+
[MethodImpl(MethodImplOptions.NoInlining)]
66+
public static ObjectBinderSnapshot GetSnapshot()
67+
{
68+
lock (s_gate)
69+
{
70+
if (s_lastSnapshot == null)
71+
{
72+
s_lastSnapshot = new ObjectBinderSnapshot(s_typeToIndex, s_types, s_typeReaders);
73+
}
74+
75+
return s_lastSnapshot.Value;
76+
}
77+
}
78+
}
79+
80+
class Program
81+
{
82+
static int Main(string[] args)
83+
{
84+
ObjectBinderSnapshot o = ObjectBinder.GetSnapshot();
85+
Console.WriteLine($"Test output: {o.SomeValue}");
86+
87+
return 100; // success if we got here without crashing
88+
}
89+
}
90+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<OutputType>Exe</OutputType>
4+
<CLRTestPriority>0</CLRTestPriority>
5+
</PropertyGroup>
6+
<PropertyGroup>
7+
<DebugType>None</DebugType>
8+
<Optimize>True</Optimize>
9+
</PropertyGroup>
10+
<ItemGroup>
11+
<Compile Include="Runtime_45557.cs" />
12+
</ItemGroup>
13+
</Project>

0 commit comments

Comments
 (0)