Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions src/coreclr/nativeaot/Runtime/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,12 @@ set(COMMON_RUNTIME_SOURCES
${CLR_SRC_NATIVE_DIR}/minipal/xoshiro128pp.c
)

if (CLR_CMAKE_TARGET_UNIX AND NOT CLR_CMAKE_TARGET_ARCH_WASM)
list(APPEND COMMON_RUNTIME_SOURCES
${RUNTIME_DIR}/asyncsafethreadmap.cpp
)
endif()

set(SERVER_GC_SOURCES
${GC_DIR}/gceesvr.cpp
${GC_DIR}/gcsvr.cpp
Expand Down
20 changes: 20 additions & 0 deletions src/coreclr/nativeaot/Runtime/threadstore.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
#include "TargetPtrs.h"
#include "yieldprocessornormalized.h"
#include <minipal/time.h>
#include <minipal/thread.h>
#include "asyncsafethreadmap.h"

#include "slist.inl"

Expand Down Expand Up @@ -143,6 +145,14 @@ void ThreadStore::AttachCurrentThread(bool fAcquireThreadStoreLock)
pAttachingThread->m_ThreadStateFlags = Thread::TSF_Attached;

pTS->m_ThreadList.PushHead(pAttachingThread);

#ifdef TARGET_UNIX
if (!InsertThreadIntoAsyncSafeMap(pAttachingThread->m_threadId, pAttachingThread))
{
ASSERT_UNCONDITIONALLY("Failed to insert thread into async-safe map due to OOM.");
RhFailFast();
}
#endif
}

// static
Expand Down Expand Up @@ -188,6 +198,9 @@ void ThreadStore::DetachCurrentThread()
pTS->m_ThreadList.RemoveFirst(pDetachingThread);
// tidy up GC related stuff (release allocation context, etc..)
pDetachingThread->Detach();
#ifdef TARGET_UNIX
RemoveThreadFromAsyncSafeMap(pDetachingThread->m_threadId, pDetachingThread);
#endif
}

// post-mortem clean up.
Expand Down Expand Up @@ -352,6 +365,13 @@ EXTERN_C RuntimeThreadLocals* RhpGetThread()
return &tls_CurrentThread;
}

#ifdef TARGET_UNIX
Thread * ThreadStore::GetCurrentThreadIfAvailableAsyncSafe()
{
return (Thread*)FindThreadInAsyncSafeMap(minipal_get_current_thread_id_no_cache());
}
#endif // TARGET_UNIX

#endif // !DACCESS_COMPILE

#ifdef _WIN32
Expand Down
1 change: 1 addition & 0 deletions src/coreclr/nativeaot/Runtime/threadstore.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class ThreadStore
static Thread * RawGetCurrentThread();
static Thread * GetCurrentThread();
static Thread * GetCurrentThreadIfAvailable();
static Thread * GetCurrentThreadIfAvailableAsyncSafe();
static PTR_Thread GetSuspendingThread();
static void AttachCurrentThread();
static void AttachCurrentThread(bool fAcquireThreadStoreLock);
Expand Down
2 changes: 2 additions & 0 deletions src/coreclr/nativeaot/Runtime/threadstore.inl
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ inline Thread * ThreadStore::RawGetCurrentThread()
return (Thread *) &tls_CurrentThread;
}

#if defined(TARGET_UNIX) && !defined(DACCESS_COMPILE)
// static
inline Thread * ThreadStore::GetCurrentThread()
{
Expand All @@ -24,6 +25,7 @@ inline Thread * ThreadStore::GetCurrentThread()
ASSERT(pCurThread->IsInitialized());
return pCurThread;
}
#endif // TARGET_UNIX && !DACCESS_COMPILE

// static
inline Thread * ThreadStore::GetCurrentThreadIfAvailable()
Expand Down
2 changes: 1 addition & 1 deletion src/coreclr/nativeaot/Runtime/unix/PalUnix.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1032,7 +1032,7 @@ static void ActivationHandler(int code, siginfo_t* siginfo, void* context)
errno = savedErrNo;
}

Thread* pThread = ThreadStore::GetCurrentThreadIfAvailable();
Thread* pThread = ThreadStore::GetCurrentThreadIfAvailableAsyncSafe();
if (pThread)
{
pThread->SetActivationPending(false);
Expand Down
125 changes: 125 additions & 0 deletions src/coreclr/runtime/asyncsafethreadmap.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#include "common.h"

#include "asyncsafethreadmap.h"

// Async safe lock free thread map for use in signal handlers

struct ThreadEntry
{
size_t osThread;
void* pThread;
};

#define MAX_THREADS_IN_SEGMENT 256

struct ThreadSegment
{
ThreadEntry entries[MAX_THREADS_IN_SEGMENT];
ThreadSegment* pNext;
};

static ThreadSegment *s_pAsyncSafeThreadMapHead = NULL;

bool InsertThreadIntoAsyncSafeMap(size_t osThread, void* pThread)
{
size_t startIndex = osThread % MAX_THREADS_IN_SEGMENT;

ThreadSegment* pSegment = s_pAsyncSafeThreadMapHead;
ThreadSegment** ppSegment = &s_pAsyncSafeThreadMapHead;
while (true)
{
if (pSegment == NULL)
{
// Need to add a new segment
ThreadSegment* pNewSegment = new (nothrow) ThreadSegment();
if (pNewSegment == NULL)
{
// Memory allocation failed
return false;
}

memset(pNewSegment, 0, sizeof(ThreadSegment));
ThreadSegment* pExpected = NULL;
if (!__atomic_compare_exchange_n(
ppSegment,
&pExpected,
pNewSegment,
false /* weak */,
__ATOMIC_RELEASE /* success_memorder */,
__ATOMIC_RELAXED /* failure_memorder */))
{
// Another thread added the segment first
delete pNewSegment;
pNewSegment = *ppSegment;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of reaching back up and accessing a potentially changing value, this API writes the non-equal failure value back into pExpected. I think the more correct operation would be pNewSegment = *pExpected;

}

pSegment = pNewSegment;
}
for (size_t i = 0; i < MAX_THREADS_IN_SEGMENT; i++)
{
size_t index = (startIndex + i) % MAX_THREADS_IN_SEGMENT;

size_t expected = 0;
if (__atomic_compare_exchange_n(
&pSegment->entries[index].osThread,
&expected, osThread,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
&expected, osThread,
&expected,
osThread,

false /* weak */,
__ATOMIC_RELEASE /* success_memorder */,
__ATOMIC_RELAXED /* failure_memorder */))
{
// Successfully inserted
// Use atomic store with release to ensure proper ordering
__atomic_store_n(&pSegment->entries[index].pThread, pThread, __ATOMIC_RELEASE);
return true;
}
}

ppSegment = &pSegment->pNext;
pSegment = __atomic_load_n(&pSegment->pNext, __ATOMIC_ACQUIRE);
}
}

void RemoveThreadFromAsyncSafeMap(size_t osThread, void* pThread)
{
size_t startIndex = osThread % MAX_THREADS_IN_SEGMENT;

ThreadSegment* pSegment = s_pAsyncSafeThreadMapHead;
while (pSegment)
{
for (size_t i = 0; i < MAX_THREADS_IN_SEGMENT; i++)
{
size_t index = (startIndex + i) % MAX_THREADS_IN_SEGMENT;
if (pSegment->entries[index].pThread == pThread)
{
// Found the entry, remove it
pSegment->entries[index].pThread = NULL;
__atomic_exchange_n(&pSegment->entries[index].osThread, (size_t)0, __ATOMIC_RELEASE);
return;
}
}
pSegment = __atomic_load_n(&pSegment->pNext, __ATOMIC_ACQUIRE);
}
}

void *FindThreadInAsyncSafeMap(size_t osThread)
{
size_t startIndex = osThread % MAX_THREADS_IN_SEGMENT;
ThreadSegment* pSegment = s_pAsyncSafeThreadMapHead;
while (pSegment)
{
for (size_t i = 0; i < MAX_THREADS_IN_SEGMENT; i++)
{
size_t index = (startIndex + i) % MAX_THREADS_IN_SEGMENT;
// Use acquire to synchronize with release in insert_thread_to_async_safe_map
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment references insert_thread_to_async_safe_map but the actual function name is minipal_insert_thread_into_async_safe_map. This should be corrected for clarity and consistency.

Suggested change
// Use acquire to synchronize with release in insert_thread_to_async_safe_map
// Use acquire to synchronize with release in minipal_insert_thread_into_async_safe_map

Copilot uses AI. Check for mistakes.
if (__atomic_load_n(&pSegment->entries[index].osThread, __ATOMIC_ACQUIRE) == osThread)
{
return pSegment->entries[index].pThread;
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pThread field is read non-atomically at line 122, which can race with the non-atomic write at line 78 in minipal_insert_thread_into_async_safe_map. Even though the osThread field is read with acquire semantics, this doesn't protect the subsequent pThread read because it's a separate memory location.

The pThread read should use an atomic load with acquire semantics to properly synchronize with the insert operation:

void* result = __atomic_load_n(&pSegment->entries[index].pThread, __ATOMIC_ACQUIRE);
return result;

This ensures that if we observe a matching osThread value, we'll also observe the correctly written pThread value.

Suggested change
return pSegment->entries[index].pThread;
return __atomic_load_n(&pSegment->entries[index].pThread, __ATOMIC_ACQUIRE);

Copilot uses AI. Check for mistakes.
}
}
pSegment = __atomic_load_n(&pSegment->pNext, __ATOMIC_ACQUIRE);
}
return NULL;
}
27 changes: 27 additions & 0 deletions src/coreclr/runtime/asyncsafethreadmap.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#ifndef __ASYNCSAFETHREADMAP_H__
#define __ASYNCSAFETHREADMAP_H__

#if defined(TARGET_UNIX) && !defined(TARGET_WASM)

// Insert a thread into the async-safe map.
// * osThread - The OS thread ID to insert.
// * pThread - A pointer to the thread object to associate with the OS thread ID.
// * return true if the insertion was successful, false otherwise (OOM).
bool InsertThreadIntoAsyncSafeMap(size_t osThread, void* pThread);

// Remove a thread from the async-safe map.
// * osThread - The OS thread ID to remove.
// * pThread - A pointer to the thread object associated with the OS thread ID.
void RemoveThreadFromAsyncSafeMap(size_t osThread, void* pThread);

// Find a thread in the async-safe map.
// * osThread = The OS thread ID to search for.
// * return - A pointer to the thread object associated with the OS thread ID, or NULL if not found.
void* FindThreadInAsyncSafeMap(size_t osThread);

#endif // TARGET_UNIX && !TARGET_WASM

#endif // __ASYNCSAFETHREADMAP_H__
1 change: 1 addition & 0 deletions src/coreclr/vm/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ set(VM_SOURCES_WKS
assemblyspec.cpp
baseassemblyspec.cpp
bundle.cpp
${RUNTIME_DIR}/asyncsafethreadmap.cpp
${RUNTIME_DIR}/CachedInterfaceDispatch.cpp
CachedInterfaceDispatch_Coreclr.cpp
cachelinealloc.cpp
Expand Down
25 changes: 23 additions & 2 deletions src/coreclr/vm/codeman.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -858,7 +858,7 @@ IJitManager::IJitManager()
// been stopped when we suspend the EE so they won't be touching an element that is about to be deleted.
// However for pre-emptive mode threads, they could be stalled right on top of the element we want
// to delete, so we need to apply the reader lock to them and wait for them to drain.
ExecutionManager::ScanFlag ExecutionManager::GetScanFlags()
ExecutionManager::ScanFlag ExecutionManager::GetScanFlags(Thread *pThread)
{
CONTRACTL {
NOTHROW;
Expand All @@ -869,7 +869,10 @@ ExecutionManager::ScanFlag ExecutionManager::GetScanFlags()
#if !defined(DACCESS_COMPILE)


Thread *pThread = GetThreadNULLOk();
if (!pThread)
{
pThread = GetThreadNULLOk();
}

if (!pThread)
return ScanNoReaderLock;
Expand Down Expand Up @@ -5229,6 +5232,24 @@ BOOL ExecutionManager::IsManagedCode(PCODE currentPC)
return IsManagedCodeWorker(currentPC, &lockState);
}

//**************************************************************************
BOOL ExecutionManager::IsManagedCodeNoLock(PCODE currentPC)
{
CONTRACTL {
NOTHROW;
GC_NOTRIGGER;
} CONTRACTL_END;

if (currentPC == (PCODE)NULL)
return FALSE;

_ASSERTE(GetScanFlags() != ScanReaderLock);

// Since ScanReaderLock is not set, then we must assume that the ReaderLock is effectively taken.
RangeSectionLockState lockState = RangeSectionLockState::ReaderLocked;
return IsManagedCodeWorker(currentPC, &lockState);
}

//**************************************************************************
NOINLINE // Make sure that the slow path with lock won't affect the fast path
BOOL ExecutionManager::IsManagedCodeWithLock(PCODE currentPC)
Expand Down
6 changes: 5 additions & 1 deletion src/coreclr/vm/codeman.h
Original file line number Diff line number Diff line change
Expand Up @@ -2293,11 +2293,15 @@ class ExecutionManager
};

// Returns default scan flag for current thread
static ScanFlag GetScanFlags();
static ScanFlag GetScanFlags(Thread *pThread = NULL);

// Returns whether currentPC is in managed code. Returns false for jump stubs on WIN64.
static BOOL IsManagedCode(PCODE currentPC);

// Returns whether currentPC is in managed code. Returns false for jump stubs on WIN64.
// Does not acquire the reader lock. Caller must ensure it is safe.
static BOOL IsManagedCodeNoLock(PCODE currentPC);

// Returns true if currentPC is ready to run codegen
static BOOL IsReadyToRunCode(PCODE currentPC);

Expand Down
24 changes: 24 additions & 0 deletions src/coreclr/vm/threads.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
#include "vmholder.h"
#include "exceptmacros.h"
#include "minipal/time.h"
#include "minipal/thread.h"
#include "asyncsafethreadmap.h"

#ifdef FEATURE_COMINTEROP
#include "runtimecallablewrapper.h"
Expand Down Expand Up @@ -62,6 +64,13 @@
#include "interpexec.h"
#endif // FEATURE_INTERPRETER

#if defined(TARGET_UNIX) && !defined(TARGET_WASM)
Thread* GetThreadAsyncSafe()
{
return (Thread*)FindThreadInAsyncSafeMap(minipal_get_current_thread_id_no_cache());
}
#endif // TARGET_UNIX && !TARGET_WASM

static const PortableTailCallFrame g_sentinelTailCallFrame = { NULL, NULL };

TailCallTls::TailCallTls()
Expand Down Expand Up @@ -371,6 +380,21 @@ void SetThread(Thread* t)

// Clear or set the app domain to the one domain based on if the thread is being nulled out or set
t_CurrentThreadInfo.m_pAppDomain = t == NULL ? NULL : AppDomain::GetCurrentDomain();

#if defined(TARGET_UNIX) && !defined(TARGET_WASM)
if (t != NULL)
{
if (!InsertThreadIntoAsyncSafeMap(t->GetOSThreadId64(), t))
{
// TODO: can we handle this OOM more gracefully?
EEPOLICY_HANDLE_FATAL_ERROR_WITH_MESSAGE(COR_E_EXECUTIONENGINE, W("Failed to insert thread into async-safe map due to OOM."));
}
}
else if (origThread != NULL)
{
RemoveThreadFromAsyncSafeMap(origThread->GetOSThreadId64(), origThread);
}
#endif // TARGET_UNIX && !TARGET_WASM
}

BOOL Thread::Alert ()
Expand Down
4 changes: 4 additions & 0 deletions src/coreclr/vm/threads.h
Original file line number Diff line number Diff line change
Expand Up @@ -5512,6 +5512,10 @@ class StackWalkerWalkingThreadHolder
Thread* m_PreviousValue;
};

#ifdef TARGET_UNIX
EXTERN_C Thread* GetThreadAsyncSafe();
#endif

#ifndef DACCESS_COMPILE
#if defined(TARGET_WINDOWS) && defined(TARGET_AMD64)
EXTERN_C void STDCALL ClrRestoreNonvolatileContextWorker(PCONTEXT ContextRecord, DWORD64 ssp);
Expand Down
8 changes: 5 additions & 3 deletions src/coreclr/vm/threadsuspend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5731,16 +5731,18 @@ void ThreadSuspend::SuspendEE(SUSPEND_REASON reason)
// It is unsafe to use blocking APIs or allocate in this method.
BOOL CheckActivationSafePoint(SIZE_T ip)
{
Thread *pThread = GetThreadNULLOk();
Thread *pThread = GetThreadAsyncSafe();
_ASSERTE(pThread != NULL);

Comment on lines +5735 to 5736
Copy link

Copilot AI Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assertion _ASSERTE(pThread != NULL) on line 5735 conflicts with the subsequent null check on line 5739. If the assertion is correct and pThread can never be NULL, then the null checks on lines 5739 and 5745 are redundant and should be removed. However, if there are cases where GetThreadAsyncSafe() can return NULL (e.g., if the thread hasn't been registered yet), then the assertion is incorrect and should be changed to a null check with early return.

Given that this is called from an async signal handler where the thread might not be fully initialized, the safer approach would be to remove the assertion and handle the NULL case gracefully:

Thread *pThread = GetThreadAsyncSafe();
if (pThread == NULL)
{
    return FALSE;
}
Suggested change
_ASSERTE(pThread != NULL);
if (pThread == NULL)
{
return FALSE;
}

Copilot uses AI. Check for mistakes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like valid feedback. We can be seeing a signal sent by some other component (e.g. second NAOT runtime). We should forward the signal to the next component in that case.

Could you please do some ad-hoc stress testing to make sure that it works well? We should not be crashing, and we should not be eating signals that other components may expect.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This assert is a forgotten leftover from my local testing, it should not be there.

Could you please do some ad-hoc stress testing to make sure that it works well? We should not be crashing, and we should not be eating signals that other components may expect.

Yes, I'll do that.

// The criteria for safe activation is to be running managed code.
// Also we are not interested in handling interruption if we are already in preemptive mode nor if we are single stepping
BOOL isActivationSafePoint = pThread != NULL &&
(pThread->m_StateNC & Thread::TSNC_DebuggerIsStepping) == 0 &&
pThread->PreemptiveGCDisabled() &&
ExecutionManager::IsManagedCode(ip);
(ExecutionManager::GetScanFlags(pThread) != ExecutionManager::ScanReaderLock) &&
ExecutionManager::IsManagedCodeNoLock(ip);

if (!isActivationSafePoint)
if (!isActivationSafePoint && pThread != NULL)
{
pThread->m_hasPendingActivation = false;
}
Expand Down
Loading