Skip to content

t_currentThread == null assertion failure in NativeAOT threading under heavy emulation stress #130999

Description

@am11

Hey @jakobbotsch, just wanted to share an interesting data point from some CI testing https://github.com/am11/CrossRepoCITesting/actions/runs/29607066033/job/87972760371. I'm just sharing an observation about how the runtime async initialization assumptions behave when put under a heavy timing vise.

While running BasicThreading.Run using QEMU emulation on a linux-musl-riscv64 target (Alpine base), I consistently hit this assertion failure:

>>> Running: /runtime/artifacts/tests/coreclr/linux.riscv64.Checked/nativeaot/SmokeTests/UnitTests/UnitTests/native/UnitTests
===== Running test BasicThreading.Run =====
Process terminated. Assertion failed.
t_currentThread == null
   at System.Diagnostics.DebugProvider.Fail(String, String) in /_/src/libraries/System.Private.CoreLib/src/System/Diagnostics/DebugProvider.cs:line 22
   at System.Diagnostics.Debug.Fail(String, String) in /_/src/libraries/System.Private.CoreLib/src/System/Diagnostics/Debug.cs:line 135
   at System.Threading.Thread.InitializeCurrentThread() in /_/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs:line 91
   at System.Threading.WaitSubsystem.ThreadWaitInfo.Wait(Int32, Boolean, Boolean, WaitSubsystem.LockHolder&) in /_/src/libraries/System.Private.CoreLib/src/System/Threading/WaitSubsystem.ThreadWaitInfo.Unix.cs:line 322
   at System.Threading.WaitSubsystem.Wait(WaitSubsystem.IWaitableObject, WaitSubsystem.ThreadWaitInfo, Int32, Boolean) in /_/src/libraries/System.Private.CoreLib/src/System/Threading/WaitSubsystem.HandleManager.Unix.cs:line 35
   at System.Threading.WaitHandle.WaitOneNoCheck(Int32, Boolean, Object, NativeRuntimeEventSource.WaitHandleWaitSourceMap) in /_/src/libraries/System.Private.CoreLib/src/System/Threading/WaitHandle.cs:line 182
   at System.Threading.Condition.Wait(Int32, Object) in /_/src/libraries/System.Private.CoreLib/src/System/Threading/Condition.cs:line 150
   at System.Threading.ManualResetEventSlim.Wait(Int32, CancellationToken) in /_/src/libraries/System.Private.CoreLib/src/System/Threading/ManualResetEventSlim.cs:line 601
   at System.Threading.Tasks.Task.WaitAllBlockingCore(List`1, Int32, CancellationToken) in /_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs:line 5263
   at System.Threading.Tasks.Task.WaitAllCore(ReadOnlySpan`1, Int32, CancellationToken) in /_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs:line 5181
   at System.Threading.Tasks.Task.WaitAll(Task[]) in /_/src/libraries/System.Private.CoreLib/src/System/Threading/Tasks/Task.cs:line 4897
   at ThreadTest.TestConcurrentIsBackgroundProperty() in /runtime/src/tests/nativeaot/SmokeTests/UnitTests/BasicThreading.cs:line 453
   at ThreadTest.Run() in /runtime/src/tests/nativeaot/SmokeTests/UnitTests/BasicThreading.cs:line 612
   at BasicThreading.Run() in /runtime/src/tests/nativeaot/SmokeTests/UnitTests/BasicThreading.cs:line 29
   at Program.<<Main>$>g__RunTest|0_0(Func`1, String) in /runtime/src/tests/nativeaot/SmokeTests/UnitTests/Main.cs:line 25
   at Program.<Main>$(String[] args) in /runtime/src/tests/nativeaot/SmokeTests/UnitTests/Main.cs:line 7
Aborted (core dumped)
/runtime/artifacts/tests/coreclr/linux.riscv64.Checked/nativeaot/SmokeTests/UnitTests/UnitTests/native/UnitTests failed with exit code 134

Context & Observations

  • Native Device: Passes 100% perfectly on bare-metal hardware.
  • Emulation Environment: Consistently reproduces under the warped scheduling timings of QEMU.

Looking closely at the trace and the recent changes in #117056 (CurrentThreadNoInit) and #117314 (disabling task-awaited inlining), it looks like an extreme timing/race condition edge case.

Under heavy task churn (Task.WaitAll), the emulated environment seems to cause managed state tracking/teardown to lag slightly behind native host kernel speed. On Linux, when a thread dies, its pthread_t ID gets instantly recycled. A brand new thread gets spawned, inherits that identical recycled ID, and runs through the optimized async path via CurrentThreadNoInit.

Because the asynchronous cleanup hasn't fully cleared the old thread's slot yet, it pulls dirty, stale data. When it eventually hits a blocking sync point and falls back to InitializeCurrentThread(), the safety gate triggers because t_currentThread isn't null.

Again, this is totally stable on native hardware, but I wanted to throw this your way since you've been deep in the async context state-saving mechanics. It might highlight a subtle "weakness" or timing assumption gap in how the runtime handles rapid thread identifier recycling when execution chains get artificially stretched out.

No urgent action expected on my end, just wanted to share the telemetry! Feel free to close if you think it's purely a QEMU artifact.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    • Status
      No status

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions