From 8b182d8afcb4a904bda840e099270c215c26d865 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Jul 2026 13:41:56 +0000 Subject: [PATCH 1/5] Initial plan From e30c110abf8628fc4afe221db8f6f7b9d6f8d31a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Jul 2026 13:45:59 +0000 Subject: [PATCH 2/5] Notify debugger when lazily creating exposed Thread object Co-authored-by: kotlarmilos <11523312+kotlarmilos@users.noreply.github.com> --- src/coreclr/vm/threads.cpp | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/coreclr/vm/threads.cpp b/src/coreclr/vm/threads.cpp index b290e205367523..6817cba4fe5470 100644 --- a/src/coreclr/vm/threads.cpp +++ b/src/coreclr/vm/threads.cpp @@ -3028,6 +3028,9 @@ OBJECTREF Thread::GetExposedObject() // Take a lock to make sure that only one thread creates the object. ThreadStoreLockHolder tsHolder(fNeedThreadStore); + // Track whether this call is the one that created the exposed object. + BOOL fCreatedExposedObject = FALSE; + // Check to see if another thread has not already created the exposed object. if (ObjectFromHandle(m_ExposedObject) == NULL) { @@ -3059,6 +3062,8 @@ OBJECTREF Thread::GetExposedObject() exposedHolder.SuppressRelease(); strongHolder.SuppressRelease(); + + fCreatedExposedObject = TRUE; } else { @@ -3066,6 +3071,21 @@ OBJECTREF Thread::GetExposedObject() } GCPROTECT_END(); + +#ifdef DEBUGGING_SUPPORTED + // The managed Thread object is created lazily, only the first time managed code on the + // thread reads Thread.CurrentThread. The debugger's CreateThread announcement, however, + // fires as soon as the native thread first reaches managed code, so for native-origin + // threads (such as the main thread or natively attached threads) the exposed object is + // still NULL when the debugger processes CreateThread and the thread shows up with no name. + // Now that this thread has created its own exposed object, notify an attached debugger so + // it re-queries the thread, reusing the same NameChangeEvent path that Thread.Name uses. + if (fCreatedExposedObject && pCurThread == this && CORDebuggerAttached()) + { + _ASSERTE(g_pDebugInterface != NULL); + g_pDebugInterface->NameChangeEvent(NULL, this); + } +#endif // DEBUGGING_SUPPORTED } return ObjectFromHandle(m_ExposedObject); } From 22c9235624da5ba98164480d377ce4b28df13fbe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Jul 2026 13:51:31 +0000 Subject: [PATCH 3/5] Declare exposed-object flag before GCPROTECT scope so it is visible after release Co-authored-by: kotlarmilos <11523312+kotlarmilos@users.noreply.github.com> --- src/coreclr/vm/threads.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/coreclr/vm/threads.cpp b/src/coreclr/vm/threads.cpp index 6817cba4fe5470..37267ebbd69267 100644 --- a/src/coreclr/vm/threads.cpp +++ b/src/coreclr/vm/threads.cpp @@ -3017,6 +3017,10 @@ OBJECTREF Thread::GetExposedObject() { // Allocate the exposed thread object. THREADBASEREF attempt = (THREADBASEREF) AllocateObject(g_pThreadClass); + + // Track whether this call is the one that created the exposed object. + BOOL fCreatedExposedObject = FALSE; + GCPROTECT_BEGIN(attempt); // The exposed object keeps us alive until it is GC'ed. This @@ -3028,9 +3032,6 @@ OBJECTREF Thread::GetExposedObject() // Take a lock to make sure that only one thread creates the object. ThreadStoreLockHolder tsHolder(fNeedThreadStore); - // Track whether this call is the one that created the exposed object. - BOOL fCreatedExposedObject = FALSE; - // Check to see if another thread has not already created the exposed object. if (ObjectFromHandle(m_ExposedObject) == NULL) { From a46e593f467cf4105b18adb414464ab290e9f820 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Jul 2026 14:08:38 +0000 Subject: [PATCH 4/5] Drop explanatory comments per review feedback Co-authored-by: kotlarmilos <11523312+kotlarmilos@users.noreply.github.com> --- src/coreclr/vm/threads.cpp | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/coreclr/vm/threads.cpp b/src/coreclr/vm/threads.cpp index 37267ebbd69267..dcf470f5dc4727 100644 --- a/src/coreclr/vm/threads.cpp +++ b/src/coreclr/vm/threads.cpp @@ -3018,7 +3018,6 @@ OBJECTREF Thread::GetExposedObject() // Allocate the exposed thread object. THREADBASEREF attempt = (THREADBASEREF) AllocateObject(g_pThreadClass); - // Track whether this call is the one that created the exposed object. BOOL fCreatedExposedObject = FALSE; GCPROTECT_BEGIN(attempt); @@ -3074,13 +3073,6 @@ OBJECTREF Thread::GetExposedObject() GCPROTECT_END(); #ifdef DEBUGGING_SUPPORTED - // The managed Thread object is created lazily, only the first time managed code on the - // thread reads Thread.CurrentThread. The debugger's CreateThread announcement, however, - // fires as soon as the native thread first reaches managed code, so for native-origin - // threads (such as the main thread or natively attached threads) the exposed object is - // still NULL when the debugger processes CreateThread and the thread shows up with no name. - // Now that this thread has created its own exposed object, notify an attached debugger so - // it re-queries the thread, reusing the same NameChangeEvent path that Thread.Name uses. if (fCreatedExposedObject && pCurThread == this && CORDebuggerAttached()) { _ASSERTE(g_pDebugInterface != NULL); From 03ef8e4b2af6a70a6eae7954801347f235bc7a23 Mon Sep 17 00:00:00 2001 From: Milos Kotlar Date: Mon, 6 Jul 2026 11:32:06 +0200 Subject: [PATCH 5/5] Name the underlying native thread on all Apple platforms Extend the startup native-thread naming path from TARGET_OSX to TARGET_APPLE so iOS, tvOS and MacCatalyst also name the underlying native thread, where only the thread itself can set its own name. This affects the thread name shown by native debuggers and tools. Also drop the earlier managed-debugger NameChangeEvent notification, which did not address the managed "No Name" case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/threads.cpp | 13 ------------- .../src/System/Threading/Thread.cs | 4 ++-- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/src/coreclr/vm/threads.cpp b/src/coreclr/vm/threads.cpp index dcf470f5dc4727..b290e205367523 100644 --- a/src/coreclr/vm/threads.cpp +++ b/src/coreclr/vm/threads.cpp @@ -3017,9 +3017,6 @@ OBJECTREF Thread::GetExposedObject() { // Allocate the exposed thread object. THREADBASEREF attempt = (THREADBASEREF) AllocateObject(g_pThreadClass); - - BOOL fCreatedExposedObject = FALSE; - GCPROTECT_BEGIN(attempt); // The exposed object keeps us alive until it is GC'ed. This @@ -3062,8 +3059,6 @@ OBJECTREF Thread::GetExposedObject() exposedHolder.SuppressRelease(); strongHolder.SuppressRelease(); - - fCreatedExposedObject = TRUE; } else { @@ -3071,14 +3066,6 @@ OBJECTREF Thread::GetExposedObject() } GCPROTECT_END(); - -#ifdef DEBUGGING_SUPPORTED - if (fCreatedExposedObject && pCurThread == this && CORDebuggerAttached()) - { - _ASSERTE(g_pDebugInterface != NULL); - g_pDebugInterface->NameChangeEvent(NULL, this); - } -#endif // DEBUGGING_SUPPORTED } return ObjectFromHandle(m_ExposedObject); } diff --git a/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.cs b/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.cs index da04bf1a2a79c5..ea1a24b625f4fd 100644 --- a/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.cs +++ b/src/libraries/System.Private.CoreLib/src/System/Threading/Thread.cs @@ -73,10 +73,10 @@ private void RunWorker() try { -#if TARGET_OSX || NATIVEAOT +#if TARGET_APPLE || NATIVEAOT // On other platforms, when the underlying native thread is created, // the thread name is set to the name of the managed thread by another thread. - // However, on OS X and NativeAOT (across all OSes), only the thread itself can set its name. + // However, on Apple platforms and NativeAOT (across all OSes), only the thread itself can set its name. // Therefore, by this point the native thread is still unnamed as it has not started yet. Thread thread = Thread.CurrentThread; if (!string.IsNullOrEmpty(thread.Name))