Skip to content

Commit ae9529a

Browse files
kouvelmatouskozak
authored andcommitted
Fix class construction cycle in Lock on NativeAOT (dotnet#100374)
When a thread reenters class construction through accessing NativeRuntimeEventSource.Log, Log would return null. Checks on IsFullyInitialized were added to ensure that the normal path would not be taken in that case, to avoid null checks in several places and in different files. That doesn't work when a different thread sees the initialization stage as Complete, as it would try to initialize NativeRuntimeEventSource and run into a class construction cycle. Fixed by removing the IsFullyInitialized checks, introducing a new initialization stage PartiallyCompelte, and not setting the stage to Complete until it has been verified that Log does not return null. When the stage is PartiallyCompelte, a thread would retry the relevant initialization. This again guarantees that there would be at most one attempt at initialization through Lock at any given time, and prevents the class construction cycle. Fixes dotnet#99663
1 parent 470e7b4 commit ae9529a

File tree

1 file changed

+43
-32
lines changed

1 file changed

+43
-32
lines changed

src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Lock.NativeAot.cs

+43-32
Original file line numberDiff line numberDiff line change
@@ -92,18 +92,6 @@ internal void Reenter(uint previousRecursionCount)
9292
_recursionCount = previousRecursionCount;
9393
}
9494

95-
private static bool IsFullyInitialized
96-
{
97-
get
98-
{
99-
// If NativeRuntimeEventSource is already being class-constructed by this thread earlier in the stack, Log can
100-
// be null. This property is used to avoid going down the wait path in that case to avoid null checks in several
101-
// other places.
102-
Debug.Assert((StaticsInitializationStage)s_staticsInitializationStage == StaticsInitializationStage.Complete);
103-
return NativeRuntimeEventSource.Log != null;
104-
}
105-
}
106-
10795
[MethodImpl(MethodImplOptions.AggressiveInlining)]
10896
private TryLockResult LazyInitializeOrEnter()
10997
{
@@ -113,10 +101,6 @@ private TryLockResult LazyInitializeOrEnter()
113101
case StaticsInitializationStage.Complete:
114102
if (_spinCount == SpinCountNotInitialized)
115103
{
116-
if (!IsFullyInitialized)
117-
{
118-
goto case StaticsInitializationStage.Started;
119-
}
120104
_spinCount = s_maxSpinCount;
121105
}
122106
return TryLockResult.Spin;
@@ -137,7 +121,7 @@ private TryLockResult LazyInitializeOrEnter()
137121
}
138122

139123
stage = (StaticsInitializationStage)Volatile.Read(ref s_staticsInitializationStage);
140-
if (stage == StaticsInitializationStage.Complete && IsFullyInitialized)
124+
if (stage == StaticsInitializationStage.Complete)
141125
{
142126
goto case StaticsInitializationStage.Complete;
143127
}
@@ -155,7 +139,9 @@ private TryLockResult LazyInitializeOrEnter()
155139
}
156140

157141
default:
158-
Debug.Assert(stage == StaticsInitializationStage.NotStarted);
142+
Debug.Assert(
143+
stage == StaticsInitializationStage.NotStarted ||
144+
stage == StaticsInitializationStage.PartiallyComplete);
159145
if (TryInitializeStatics())
160146
{
161147
goto case StaticsInitializationStage.Complete;
@@ -169,29 +155,49 @@ private static bool TryInitializeStatics()
169155
{
170156
// Since Lock is used to synchronize class construction, and some of the statics initialization may involve class
171157
// construction, update the stage first to avoid infinite recursion
172-
switch (
173-
(StaticsInitializationStage)
174-
Interlocked.CompareExchange(
175-
ref s_staticsInitializationStage,
176-
(int)StaticsInitializationStage.Started,
177-
(int)StaticsInitializationStage.NotStarted))
158+
var oldStage = (StaticsInitializationStage)s_staticsInitializationStage;
159+
while (true)
178160
{
179-
case StaticsInitializationStage.Started:
180-
return false;
181-
case StaticsInitializationStage.Complete:
161+
if (oldStage == StaticsInitializationStage.Complete)
162+
{
182163
return true;
164+
}
165+
166+
var stageBeforeUpdate =
167+
(StaticsInitializationStage)Interlocked.CompareExchange(
168+
ref s_staticsInitializationStage,
169+
(int)StaticsInitializationStage.Started,
170+
(int)oldStage);
171+
if (stageBeforeUpdate == StaticsInitializationStage.Started)
172+
{
173+
return false;
174+
}
175+
if (stageBeforeUpdate == oldStage)
176+
{
177+
Debug.Assert(
178+
oldStage == StaticsInitializationStage.NotStarted ||
179+
oldStage == StaticsInitializationStage.PartiallyComplete);
180+
break;
181+
}
182+
183+
oldStage = stageBeforeUpdate;
183184
}
184185

185186
bool isFullyInitialized;
186187
try
187188
{
188-
s_isSingleProcessor = Environment.IsSingleProcessor;
189-
s_maxSpinCount = DetermineMaxSpinCount();
190-
s_minSpinCount = DetermineMinSpinCount();
189+
if (oldStage == StaticsInitializationStage.NotStarted)
190+
{
191+
// If the stage is PartiallyComplete, these will have already been initialized
192+
s_isSingleProcessor = Environment.IsSingleProcessor;
193+
s_maxSpinCount = DetermineMaxSpinCount();
194+
s_minSpinCount = DetermineMinSpinCount();
195+
}
191196

192197
// Also initialize some types that are used later to prevent potential class construction cycles. If
193198
// NativeRuntimeEventSource is already being class-constructed by this thread earlier in the stack, Log can be
194-
// null. Avoid going down the wait path in that case to avoid null checks in several other places.
199+
// null. Avoid going down the wait path in that case to avoid null checks in several other places. If not fully
200+
// initialized, the stage will also be set to PartiallyComplete to try again.
195201
isFullyInitialized = NativeRuntimeEventSource.Log != null;
196202
}
197203
catch
@@ -200,7 +206,11 @@ private static bool TryInitializeStatics()
200206
throw;
201207
}
202208

203-
Volatile.Write(ref s_staticsInitializationStage, (int)StaticsInitializationStage.Complete);
209+
Volatile.Write(
210+
ref s_staticsInitializationStage,
211+
isFullyInitialized
212+
? (int)StaticsInitializationStage.Complete
213+
: (int)StaticsInitializationStage.PartiallyComplete);
204214
return isFullyInitialized;
205215
}
206216

@@ -242,6 +252,7 @@ private enum StaticsInitializationStage
242252
{
243253
NotStarted,
244254
Started,
255+
PartiallyComplete,
245256
Complete
246257
}
247258
}

0 commit comments

Comments
 (0)