`t_currentThread == null` assertion failure in NativeAOT threading under heavy emulation stress
- Dominant language
- C#
- Stars
- 18.3k
- Forks
- 5.6k
- PR merge metrics
- PR metrics pending
Description
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:
```sh
>>> 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.<$>g__RunTest|0_0(Func`1, String) in /runtime/src/tests/nativeaot/SmokeTests/UnitTests/Main.cs:line 25
at Program.$(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.
Contributor guide
Research direction
Start by reproducing BasicThreading.Run under the stated linux-musl-riscv64 QEMU setup, then inspect src/coreclr/nativeaot/System.Private.CoreLib/src/System/Threading/Thread.NativeAot.cs at line 91 and SmokeTests/UnitTests/BasicThreading.cs around line 453. Done means establishing whether the assertion is a runtime race or a QEMU-specific artifact and documenting the evidence or a focused regression path.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp, linux
- Domain
- operating-systems, testing-qa
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Needs clarification
- Newbie friendliness
- 35/100