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.
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.Runusing QEMU emulation on alinux-musl-riscv64target (Alpine base), I consistently hit this assertion failure:Context & Observations
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, itspthread_tID gets instantly recycled. A brand new thread gets spawned, inherits that identical recycled ID, and runs through the optimized async path viaCurrentThreadNoInit.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 becauset_currentThreadisn'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.