From 6ab3b49792bf96d8270e00e63f2398bad9d11cc5 Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Mon, 27 Oct 2025 18:41:18 +0100 Subject: [PATCH 1/2] gh-125434: Display thread name in faulthandler on Windows (#140675) (cherry picked from commit 313145eab5f6ebca21d2e3c80c130980d3bcdc88) --- Include/internal/pycore_traceback.h | 2 + ...-10-27-16-01-41.gh-issue-125434.qy0uRA.rst | 2 + Python/pylifecycle.c | 1 + Python/traceback.c | 89 ++++++++++++++++--- 4 files changed, 80 insertions(+), 14 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2025-10-27-16-01-41.gh-issue-125434.qy0uRA.rst diff --git a/Include/internal/pycore_traceback.h b/Include/internal/pycore_traceback.h index a4f125e073d3d1..8357cce9d899fb 100644 --- a/Include/internal/pycore_traceback.h +++ b/Include/internal/pycore_traceback.h @@ -103,6 +103,8 @@ extern int _Py_WriteIndent(int, PyObject *); PyAPI_FUNC(void) _Py_InitDumpStack(void); PyAPI_FUNC(void) _Py_DumpStack(int fd); +extern void _Py_DumpTraceback_Init(void); + #ifdef __cplusplus } #endif diff --git a/Misc/NEWS.d/next/Library/2025-10-27-16-01-41.gh-issue-125434.qy0uRA.rst b/Misc/NEWS.d/next/Library/2025-10-27-16-01-41.gh-issue-125434.qy0uRA.rst new file mode 100644 index 00000000000000..299e9f04df7c39 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2025-10-27-16-01-41.gh-issue-125434.qy0uRA.rst @@ -0,0 +1,2 @@ +Display thread name in :mod:`faulthandler` on Windows. Patch by Victor +Stinner. diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index f5fbdeedac34ad..fad69f82b60796 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -503,6 +503,7 @@ pycore_init_runtime(_PyRuntimeState *runtime, _PyRuntimeState_SetFinalizing(runtime, NULL); _Py_InitVersion(); + _Py_DumpTraceback_Init(); status = _Py_HashRandomization_Init(config); if (_PyStatus_EXCEPTION(status)) { diff --git a/Python/traceback.c b/Python/traceback.c index 5efa70fb676882..176fb73e9cd147 100644 --- a/Python/traceback.c +++ b/Python/traceback.c @@ -70,6 +70,13 @@ class traceback "PyTracebackObject *" "&PyTraceback_Type" #include "clinic/traceback.c.h" + +#ifdef MS_WINDOWS +typedef HRESULT (WINAPI *PF_GET_THREAD_DESCRIPTION)(HANDLE, PCWSTR*); +static PF_GET_THREAD_DESCRIPTION pGetThreadDescription = NULL; +#endif + + static PyObject * tb_create_raw(PyTracebackObject *next, PyFrameObject *frame, int lasti, int lineno) @@ -1122,23 +1129,12 @@ _Py_DumpTraceback(int fd, PyThreadState *tstate) # endif #endif -/* Write the thread identifier into the file 'fd': "Current thread 0xHHHH:\" if - is_current is true, "Thread 0xHHHH:\n" otherwise. - - This function is signal safe. */ +// Write the thread name static void -write_thread_id(int fd, PyThreadState *tstate, int is_current) +write_thread_name(int fd, PyThreadState *tstate) { - if (is_current) - PUTS(fd, "Current thread 0x"); - else - PUTS(fd, "Thread 0x"); - _Py_DumpHexadecimal(fd, - tstate->thread_id, - sizeof(unsigned long) * 2); - - // Write the thread name +#ifndef MS_WINDOWS #if defined(HAVE_PTHREAD_GETNAME_NP) || defined(HAVE_PTHREAD_GET_NAME_NP) char name[100]; pthread_t thread = (pthread_t)tstate->thread_id; @@ -1157,6 +1153,54 @@ write_thread_id(int fd, PyThreadState *tstate, int is_current) } } #endif +#else + // Windows implementation + if (pGetThreadDescription == NULL) { + return; + } + + HANDLE thread = OpenThread(THREAD_QUERY_LIMITED_INFORMATION, FALSE, tstate->thread_id); + if (thread == NULL) { + return; + } + + wchar_t *wname; + HRESULT hr = pGetThreadDescription(thread, &wname); + if (!FAILED(hr)) { + char *name = _Py_EncodeLocaleRaw(wname, NULL); + if (name != NULL) { + size_t len = strlen(name); + if (len) { + PUTS(fd, " ["); + (void)_Py_write_noraise(fd, name, len); + PUTS(fd, "]"); + } + PyMem_RawFree(name); + } + LocalFree(wname); + } + CloseHandle(thread); +#endif +} + + +/* Write the thread identifier into the file 'fd': "Current thread 0xHHHH:\" if + is_current is true, "Thread 0xHHHH:\n" otherwise. + + This function is signal safe (except on Windows). */ + +static void +write_thread_id(int fd, PyThreadState *tstate, int is_current) +{ + if (is_current) + PUTS(fd, "Current thread 0x"); + else + PUTS(fd, "Thread 0x"); + _Py_DumpHexadecimal(fd, + tstate->thread_id, + sizeof(unsigned long) * 2); + + write_thread_name(fd, tstate); PUTS(fd, " (most recent call first):\n"); } @@ -1351,3 +1395,20 @@ _Py_InitDumpStack(void) (void)backtrace(callstack, 1); #endif } + + +void +_Py_DumpTraceback_Init(void) +{ +#ifdef MS_WINDOWS + if (pGetThreadDescription != NULL) { + return; + } + + HMODULE kernelbase = GetModuleHandleW(L"kernelbase.dll"); + if (kernelbase != NULL) { + pGetThreadDescription = (PF_GET_THREAD_DESCRIPTION)GetProcAddress( + kernelbase, "GetThreadDescription"); + } +#endif +} From 41cde91a67d1bd2479b64974e209688fe20c5ecb Mon Sep 17 00:00:00 2001 From: Victor Stinner Date: Tue, 28 Oct 2025 14:41:51 +0100 Subject: [PATCH 2/2] gh-125434: Fix non-ASCII thread names in faulthandler on Windows (#140700) Add _Py_DumpWideString() function to dump a wide string as ASCII. It supports surrogate pairs. Replace _Py_EncodeLocaleRaw() with _Py_DumpWideString() in write_thread_name(). (cherry picked from commit 80f20f58b2b8368ed8451a0161036dda94d8d33a) --- Python/traceback.c | 65 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 53 insertions(+), 12 deletions(-) diff --git a/Python/traceback.c b/Python/traceback.c index 176fb73e9cd147..a46276f66b285b 100644 --- a/Python/traceback.c +++ b/Python/traceback.c @@ -981,6 +981,52 @@ _Py_DumpASCII(int fd, PyObject *text) } } + +#ifdef MS_WINDOWS +static void +_Py_DumpWideString(int fd, wchar_t *str) +{ + Py_ssize_t size = wcslen(str); + int truncated; + if (MAX_STRING_LENGTH < size) { + size = MAX_STRING_LENGTH; + truncated = 1; + } + else { + truncated = 0; + } + + for (Py_ssize_t i=0; i < size; i++) { + Py_UCS4 ch = str[i]; + if (' ' <= ch && ch <= 126) { + /* printable ASCII character */ + dump_char(fd, (char)ch); + } + else if (ch <= 0xff) { + PUTS(fd, "\\x"); + _Py_DumpHexadecimal(fd, ch, 2); + } + else if (Py_UNICODE_IS_HIGH_SURROGATE(ch) + && Py_UNICODE_IS_LOW_SURROGATE(str[i+1])) { + ch = Py_UNICODE_JOIN_SURROGATES(ch, str[i+1]); + i++; // Skip the low surrogate character + PUTS(fd, "\\U"); + _Py_DumpHexadecimal(fd, ch, 8); + } + else { + Py_BUILD_ASSERT(sizeof(wchar_t) == 2); + PUTS(fd, "\\u"); + _Py_DumpHexadecimal(fd, ch, 4); + } + } + + if (truncated) { + PUTS(fd, "..."); + } +} +#endif + + /* Write a frame into the file fd: "File "xxx", line xxx in xxx". This function is signal safe. @@ -1164,20 +1210,15 @@ write_thread_name(int fd, PyThreadState *tstate) return; } - wchar_t *wname; - HRESULT hr = pGetThreadDescription(thread, &wname); + wchar_t *name; + HRESULT hr = pGetThreadDescription(thread, &name); if (!FAILED(hr)) { - char *name = _Py_EncodeLocaleRaw(wname, NULL); - if (name != NULL) { - size_t len = strlen(name); - if (len) { - PUTS(fd, " ["); - (void)_Py_write_noraise(fd, name, len); - PUTS(fd, "]"); - } - PyMem_RawFree(name); + if (name[0] != 0) { + PUTS(fd, " ["); + _Py_DumpWideString(fd, name); + PUTS(fd, "]"); } - LocalFree(wname); + LocalFree(name); } CloseHandle(thread); #endif