From fc1d6adc6d762936f3f777dcd6683523a99c7067 Mon Sep 17 00:00:00 2001 From: Alex Malyshev Date: Sun, 5 Apr 2026 17:36:22 -0400 Subject: [PATCH 1/6] Add PyUnstable_DumpTraceback() and PyUnstable_DumpTracebackThreads() Public versions of _Py_DumpTraceback() and _Py_DumpTracebackThreads(). --- Doc/c-api/exceptions.rst | 64 +++++++++++++++++++ Doc/whatsnew/3.15.rst | 5 ++ Include/cpython/traceback.h | 7 ++ Include/internal/pycore_traceback.h | 49 -------------- Modules/faulthandler.c | 26 ++++---- Platforms/emscripten/node_entry.mjs | 2 +- .../web_example_pyrepl_jspi/src.mjs | 2 +- Python/pylifecycle.c | 6 +- Python/traceback.c | 27 +++++--- configure | 3 +- configure.ac | 2 +- 11 files changed, 115 insertions(+), 78 deletions(-) diff --git a/Doc/c-api/exceptions.rst b/Doc/c-api/exceptions.rst index 8ecd7c62517104..576b58e6fbcb19 100644 --- a/Doc/c-api/exceptions.rst +++ b/Doc/c-api/exceptions.rst @@ -1346,3 +1346,67 @@ Tracebacks This function returns ``0`` on success, and returns ``-1`` with an exception set on failure. + +.. c:function:: const char* PyUnstable_DumpTraceback(int fd, PyThreadState *tstate) + + Write a trace of the Python stack in *tstate* into the file *fd*. The format + looks like:: + + Traceback (most recent call first): + File "xxx", line xxx in + File "xxx", line xxx in + ... + File "xxx", line xxx in + + This function is meant to debug situations such as segfaults, fatal errors, + and similar. The file and function names it outputs are encoded to ASCII with + backslashreplace and truncated to 500 characters. It writes only the first + 100 frames; further frames are truncated with the line ``...``. + + This function will return ``NULL`` on success, or an error message on error. + + This function is intended for use in crash scenarios such as signal handlers + for SIGSEGV, where the interpreter may be in an inconsistent state. Given + that it reads interpreter data structures that may be partially modified, the + function might produce incomplete output or it may even crash itself. + + The caller does not need to hold an :term:`attached thread state`, nor does + *tstate* need to be attached. + + .. versionadded:: next + +.. c:function:: const char* PyUnstable_DumpTracebackThreads(int fd, PyInterpreterState *interp, PyThreadState *current_tstate) + + Write the traces of all Python threads in *interp* into the file *fd*. + + If *interp* is ``NULL`` then this function will try to identify the current + interpreter using thread-specific storage. If it cannot, it will return an + error. + + If *current_tstate* is not ``NULL`` then it will be used to identify what the + current thread is in the written output. If it is ``NULL`` then this function + will identify the current thread using thread-specific storage. It is not an + error if the function is unable to get the current Python thread state. + + This function will return ``NULL`` on success, or an error message on error. + It will also write this error message to *fd*. + + This function is meant to debug debug situations such as segfaults, fatal + errors, and similar. It calls :c:func:`PyUnstable_DumpTraceback` for each + thread. It only writes the tracebacks of the first 100 threads, further + output is truncated with the line ``...``. + + This function is intended for use in crash scenarios such as signal handlers + for SIGSEGV, where the interpreter may be in an inconsistent state. Given + that it reads interpreter data structures that may be partially modified, the + function might produce incomplete output or it may even crash itself. + + The caller does not need to hold an :term:`attached thread state`, nor does + *current_tstate* need to be attached. + + .. warning:: + On the :term:`free-threaded build`, this function is not thread-safe. If + another thread deletes its :term:`thread state` while this function is being + called, the process will likely crash. + + .. versionadded:: next diff --git a/Doc/whatsnew/3.15.rst b/Doc/whatsnew/3.15.rst index d1d4b92bcf4e97..54679c60fc9370 100644 --- a/Doc/whatsnew/3.15.rst +++ b/Doc/whatsnew/3.15.rst @@ -1788,6 +1788,11 @@ New features Python 3.14. (Contributed by Victor Stinner in :gh:`142417`.) +* Add :c:func:`PyUnstable_DumpTraceback` and + :c:func:`PyUnstable_DumpTracebackThreads` functions to safely output Python + stacktraces. + (Contributed by Alex Malyshev in :gh:`145559`.) + Changed C APIs -------------- diff --git a/Include/cpython/traceback.h b/Include/cpython/traceback.h index 81c51944f136f2..c6a6f3814a2417 100644 --- a/Include/cpython/traceback.h +++ b/Include/cpython/traceback.h @@ -11,3 +11,10 @@ struct _traceback { int tb_lasti; int tb_lineno; }; + +PyAPI_FUNC(const char*) PyUnstable_DumpTraceback(int fd, PyThreadState *tstate); + +PyAPI_FUNC(const char*) PyUnstable_DumpTracebackThreads( + int fd, + PyInterpreterState *interp, + PyThreadState *current_tstate); diff --git a/Include/internal/pycore_traceback.h b/Include/internal/pycore_traceback.h index 8357cce9d899fb..e7729965b71769 100644 --- a/Include/internal/pycore_traceback.h +++ b/Include/internal/pycore_traceback.h @@ -14,55 +14,6 @@ PyAPI_FUNC(int) _Py_DisplaySourceLine(PyObject *, PyObject *, int, int, int *, P // Export for 'pyexact' shared extension PyAPI_FUNC(void) _PyTraceback_Add(const char *, const char *, int); -/* Write the Python traceback into the file 'fd'. For example: - - Traceback (most recent call first): - File "xxx", line xxx in - File "xxx", line xxx in - ... - File "xxx", line xxx in - - This function is written for debug purpose only, to dump the traceback in - the worst case: after a segmentation fault, at fatal error, etc. That's why, - it is very limited. Strings are truncated to 100 characters and encoded to - ASCII with backslashreplace. It doesn't write the source code, only the - function name, filename and line number of each frame. Write only the first - 100 frames: if the traceback is truncated, write the line " ...". - - This function is signal safe. */ - -extern void _Py_DumpTraceback( - int fd, - PyThreadState *tstate); - -/* Write the traceback of all threads into the file 'fd'. current_thread can be - NULL. - - Return NULL on success, or an error message on error. - - This function is written for debug purpose only. It calls - _Py_DumpTraceback() for each thread, and so has the same limitations. It - only write the traceback of the first 100 threads: write "..." if there are - more threads. - - If current_tstate is NULL, the function tries to get the Python thread state - of the current thread. It is not an error if the function is unable to get - the current Python thread state. - - If interp is NULL, the function tries to get the interpreter state from - the current Python thread state, or from - _PyGILState_GetInterpreterStateUnsafe() in last resort. - - It is better to pass NULL to interp and current_tstate, the function tries - different options to retrieve this information. - - This function is signal safe. */ - -extern const char* _Py_DumpTracebackThreads( - int fd, - PyInterpreterState *interp, - PyThreadState *current_tstate); - /* Write a Unicode object into the file descriptor fd. Encode the string to ASCII using the backslashreplace error handler. diff --git a/Modules/faulthandler.c b/Modules/faulthandler.c index bc7731c2588dc0..437575ea6b793f 100644 --- a/Modules/faulthandler.c +++ b/Modules/faulthandler.c @@ -7,7 +7,7 @@ #include "pycore_runtime.h" // _Py_ID() #include "pycore_signal.h" // Py_NSIG #include "pycore_time.h" // _PyTime_FromSecondsObject() -#include "pycore_traceback.h" // _Py_DumpTracebackThreads +#include "pycore_traceback.h" // _Py_DumpStack() #ifdef HAVE_UNISTD_H # include // _exit() #endif @@ -205,14 +205,15 @@ faulthandler_dump_traceback(int fd, int all_threads, PyThreadState *tstate = PyGILState_GetThisThreadState(); if (all_threads == 1) { - (void)_Py_DumpTracebackThreads(fd, NULL, tstate); + (void)PyUnstable_DumpTracebackThreads(fd, NULL, tstate); } else { if (all_threads == FT_IGNORE_ALL_THREADS) { PUTS(fd, "\n"); } - if (tstate != NULL) - _Py_DumpTraceback(fd, tstate); + if (tstate != NULL) { + PyUnstable_DumpTraceback(fd, tstate); + } } reentrant = 0; @@ -273,17 +274,18 @@ faulthandler_dump_traceback_py_impl(PyObject *module, PyObject *file, /* gh-128400: Accessing other thread states while they're running * isn't safe if those threads are running. */ _PyEval_StopTheWorld(interp); - errmsg = _Py_DumpTracebackThreads(fd, NULL, tstate); + errmsg = PyUnstable_DumpTracebackThreads(fd, NULL, tstate); _PyEval_StartTheWorld(interp); - if (errmsg != NULL) { - PyErr_SetString(PyExc_RuntimeError, errmsg); - Py_XDECREF(file); - return NULL; - } } else { - _Py_DumpTraceback(fd, tstate); + errmsg = PyUnstable_DumpTraceback(fd, tstate); + } + if (errmsg != NULL) { + PyErr_SetString(PyExc_RuntimeError, errmsg); + Py_XDECREF(file); + return NULL; } + Py_XDECREF(file); if (PyErr_CheckSignals()) @@ -703,7 +705,7 @@ faulthandler_thread(void *unused) (void)_Py_write_noraise(thread.fd, thread.header, (int)thread.header_len); - errmsg = _Py_DumpTracebackThreads(thread.fd, thread.interp, NULL); + errmsg = PyUnstable_DumpTracebackThreads(thread.fd, thread.interp, NULL); ok = (errmsg == NULL); if (thread.exit) diff --git a/Platforms/emscripten/node_entry.mjs b/Platforms/emscripten/node_entry.mjs index 9478b7714adbc8..110aadc5de1014 100644 --- a/Platforms/emscripten/node_entry.mjs +++ b/Platforms/emscripten/node_entry.mjs @@ -57,6 +57,6 @@ try { // Show JavaScript exception and traceback console.warn(e); // Show Python exception and traceback - Module.__Py_DumpTraceback(2, Module._PyGILState_GetThisThreadState()); + Module.PyUnstable_DumpTraceback(2, Module._PyGILState_GetThisThreadState()); process.exit(1); } diff --git a/Platforms/emscripten/web_example_pyrepl_jspi/src.mjs b/Platforms/emscripten/web_example_pyrepl_jspi/src.mjs index 5642372c9d2472..38a622117c2a50 100644 --- a/Platforms/emscripten/web_example_pyrepl_jspi/src.mjs +++ b/Platforms/emscripten/web_example_pyrepl_jspi/src.mjs @@ -189,6 +189,6 @@ try { // Show JavaScript exception and traceback console.warn(e); // Show Python exception and traceback - Module.__Py_DumpTraceback(2, Module._PyGILState_GetThisThreadState()); + Module.PyUnstable_DumpTraceback(2, Module._PyGILState_GetThisThreadState()); process.exit(1); } diff --git a/Python/pylifecycle.c b/Python/pylifecycle.c index 5da0f3e5be3a70..539a262039e16c 100644 --- a/Python/pylifecycle.c +++ b/Python/pylifecycle.c @@ -29,7 +29,7 @@ #include "pycore_setobject.h" // _PySet_NextEntry() #include "pycore_stats.h" // _PyStats_InterpInit() #include "pycore_sysmodule.h" // _PySys_ClearAttrString() -#include "pycore_traceback.h" // _Py_DumpTracebackThreads() +#include "pycore_traceback.h" // PyUnstable_TracebackThreads() #include "pycore_tuple.h" // _PyTuple_FromPair #include "pycore_typeobject.h" // _PyTypes_InitTypes() #include "pycore_typevarobject.h" // _Py_clear_generic_types() @@ -3261,9 +3261,9 @@ _Py_FatalError_DumpTracebacks(int fd, PyInterpreterState *interp, /* display the current Python stack */ #ifndef Py_GIL_DISABLED - _Py_DumpTracebackThreads(fd, interp, tstate); + PyUnstable_DumpTracebackThreads(fd, interp, tstate); #else - _Py_DumpTraceback(fd, tstate); + PyUnstable_DumpTraceback(fd, tstate); #endif } diff --git a/Python/traceback.c b/Python/traceback.c index 1e8c9c879f9aac..efdbb0e1af4c9e 100644 --- a/Python/traceback.c +++ b/Python/traceback.c @@ -1167,10 +1167,11 @@ dump_traceback(int fd, PyThreadState *tstate, int write_header) The caller is responsible to call PyErr_CheckSignals() to call Python signal handlers if signals were received. */ -void -_Py_DumpTraceback(int fd, PyThreadState *tstate) +const char* +PyUnstable_DumpTraceback(int fd, PyThreadState *tstate) { dump_traceback(fd, tstate, 1); + return NULL; } #if defined(HAVE_PTHREAD_GETNAME_NP) || defined(HAVE_PTHREAD_GET_NAME_NP) @@ -1257,6 +1258,14 @@ write_thread_id(int fd, PyThreadState *tstate, int is_current) PUTS(fd, " (most recent call first):\n"); } +/* Write an error string and also return it at the same time. */ +static const char* +dump_error(int fd, const char *msg) +{ + PUTS(fd, msg); + return msg; +} + /* Dump the traceback of all Python threads into fd. Use write() to write the traceback and retry if write() is interrupted by a signal (failed with EINTR), but don't call the Python signal handler. @@ -1264,11 +1273,11 @@ write_thread_id(int fd, PyThreadState *tstate, int is_current) The caller is responsible to call PyErr_CheckSignals() to call Python signal handlers if signals were received. */ const char* _Py_NO_SANITIZE_THREAD -_Py_DumpTracebackThreads(int fd, PyInterpreterState *interp, - PyThreadState *current_tstate) +PyUnstable_DumpTracebackThreads(int fd, PyInterpreterState *interp, + PyThreadState *current_tstate) { if (current_tstate == NULL) { - /* _Py_DumpTracebackThreads() is called from signal handlers by + /* PyUnstable_DumpTracebackThreads() is called from signal handlers by faulthandler. SIGSEGV, SIGFPE, SIGABRT, SIGBUS and SIGILL are synchronous signals @@ -1283,7 +1292,7 @@ _Py_DumpTracebackThreads(int fd, PyInterpreterState *interp, } if (current_tstate != NULL && tstate_is_freed(current_tstate)) { - return "tstate is freed"; + return dump_error(fd, "tstate is freed"); } if (interp == NULL) { @@ -1291,7 +1300,7 @@ _Py_DumpTracebackThreads(int fd, PyInterpreterState *interp, interp = _PyGILState_GetInterpreterStateUnsafe(); if (interp == NULL) { /* We need the interpreter state to get Python threads */ - return "unable to get the interpreter state"; + return dump_error(fd, "unable to get the interpreter state"); } } else { @@ -1301,13 +1310,13 @@ _Py_DumpTracebackThreads(int fd, PyInterpreterState *interp, assert(interp != NULL); if (interp_is_freed(interp)) { - return "interp is freed"; + return dump_error(fd, "interp is freed"); } /* Get the current interpreter from the current thread */ PyThreadState *tstate = PyInterpreterState_ThreadHead(interp); if (tstate == NULL) - return "unable to get the thread head state"; + return dump_error(fd, "unable to get the thread head state"); /* Dump the traceback of each thread */ unsigned int nthreads = 0; diff --git a/configure b/configure index 4726b4fe3102ac..1d070504d75021 100755 --- a/configure +++ b/configure @@ -9699,7 +9699,7 @@ fi as_fn_append LINKFORSHARED " -sFORCE_FILESYSTEM -lidbfs.js -lnodefs.js -lproxyfs.js -lworkerfs.js" as_fn_append LINKFORSHARED " -sEXPORTED_RUNTIME_METHODS=FS,callMain,ENV,HEAPU32,TTY,ERRNO_CODES" - as_fn_append LINKFORSHARED " -sEXPORTED_FUNCTIONS=_main,_Py_Version,__PyRuntime,_PyGILState_GetThisThreadState,__Py_DumpTraceback,__PyEM_EMSCRIPTEN_TRAMPOLINE_OFFSET" + as_fn_append LINKFORSHARED " -sEXPORTED_FUNCTIONS=_main,_Py_Version,__PyRuntime,_PyGILState_GetThisThreadState,__PyEM_EMSCRIPTEN_TRAMPOLINE_OFFSET" as_fn_append LINKFORSHARED " -sSTACK_SIZE=5MB" as_fn_append LINKFORSHARED " -sTEXTDECODER=2" @@ -36311,4 +36311,3 @@ if test "$ac_cv_header_stdatomic_h" != "yes"; then { printf "%s\n" "$as_me:${as_lineno-$LINENO}: Your compiler or platform does have a working C11 stdatomic.h. A future version of Python may require stdatomic.h." >&5 printf "%s\n" "$as_me: Your compiler or platform does have a working C11 stdatomic.h. A future version of Python may require stdatomic.h." >&6;} fi - diff --git a/configure.ac b/configure.ac index dd860292cc2058..44754fc0353600 100644 --- a/configure.ac +++ b/configure.ac @@ -2366,7 +2366,7 @@ AS_CASE([$ac_sys_system], dnl Include file system support AS_VAR_APPEND([LINKFORSHARED], [" -sFORCE_FILESYSTEM -lidbfs.js -lnodefs.js -lproxyfs.js -lworkerfs.js"]) AS_VAR_APPEND([LINKFORSHARED], [" -sEXPORTED_RUNTIME_METHODS=FS,callMain,ENV,HEAPU32,TTY,ERRNO_CODES"]) - AS_VAR_APPEND([LINKFORSHARED], [" -sEXPORTED_FUNCTIONS=_main,_Py_Version,__PyRuntime,_PyGILState_GetThisThreadState,__Py_DumpTraceback,__PyEM_EMSCRIPTEN_TRAMPOLINE_OFFSET"]) + AS_VAR_APPEND([LINKFORSHARED], [" -sEXPORTED_FUNCTIONS=_main,_Py_Version,__PyRuntime,_PyGILState_GetThisThreadState,__PyEM_EMSCRIPTEN_TRAMPOLINE_OFFSET"]) AS_VAR_APPEND([LINKFORSHARED], [" -sSTACK_SIZE=5MB"]) dnl Avoid bugs in JS fallback string decoding path AS_VAR_APPEND([LINKFORSHARED], [" -sTEXTDECODER=2"]) From d7c02c5fd113b3e62a6ebdf0194bc1bb0d2a1e2f Mon Sep 17 00:00:00 2001 From: Alex Malyshev Date: Sun, 5 Apr 2026 18:19:18 -0400 Subject: [PATCH 2/6] Generate NEWS.d entry with blurb --- .../next/C_API/2026-04-05-18-18-59.gh-issue-145559.qKJH9S.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Misc/NEWS.d/next/C_API/2026-04-05-18-18-59.gh-issue-145559.qKJH9S.rst diff --git a/Misc/NEWS.d/next/C_API/2026-04-05-18-18-59.gh-issue-145559.qKJH9S.rst b/Misc/NEWS.d/next/C_API/2026-04-05-18-18-59.gh-issue-145559.qKJH9S.rst new file mode 100644 index 00000000000000..e2e035d8ec142a --- /dev/null +++ b/Misc/NEWS.d/next/C_API/2026-04-05-18-18-59.gh-issue-145559.qKJH9S.rst @@ -0,0 +1,2 @@ +Rename `_Py_DumpTraceback` and `_Py_DumpTracebackThreads` to +`PyUnstable_DumpTraceback` and `PyUnstable_DumpTracebackThreads`. From d7d4720d70b574686f9ed514407c056dd2289b35 Mon Sep 17 00:00:00 2001 From: Alex Malyshev Date: Sun, 5 Apr 2026 18:21:37 -0400 Subject: [PATCH 3/6] Run `make regen-configure` --- configure | 1 + 1 file changed, 1 insertion(+) diff --git a/configure b/configure index 1d070504d75021..7c056315dbc532 100755 --- a/configure +++ b/configure @@ -36311,3 +36311,4 @@ if test "$ac_cv_header_stdatomic_h" != "yes"; then { printf "%s\n" "$as_me:${as_lineno-$LINENO}: Your compiler or platform does have a working C11 stdatomic.h. A future version of Python may require stdatomic.h." >&5 printf "%s\n" "$as_me: Your compiler or platform does have a working C11 stdatomic.h. A future version of Python may require stdatomic.h." >&6;} fi + From 7809d9ce3de96c34187c6d203daf3858f7f90e96 Mon Sep 17 00:00:00 2001 From: Alex Malyshev Date: Sun, 5 Apr 2026 18:26:21 -0400 Subject: [PATCH 4/6] Use backticks and roles properly in NEWS.d entry --- .../next/C_API/2026-04-05-18-18-59.gh-issue-145559.qKJH9S.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Misc/NEWS.d/next/C_API/2026-04-05-18-18-59.gh-issue-145559.qKJH9S.rst b/Misc/NEWS.d/next/C_API/2026-04-05-18-18-59.gh-issue-145559.qKJH9S.rst index e2e035d8ec142a..0165f5642b9c40 100644 --- a/Misc/NEWS.d/next/C_API/2026-04-05-18-18-59.gh-issue-145559.qKJH9S.rst +++ b/Misc/NEWS.d/next/C_API/2026-04-05-18-18-59.gh-issue-145559.qKJH9S.rst @@ -1,2 +1,2 @@ -Rename `_Py_DumpTraceback` and `_Py_DumpTracebackThreads` to -`PyUnstable_DumpTraceback` and `PyUnstable_DumpTracebackThreads`. +Rename ``_Py_DumpTraceback`` and ``_Py_DumpTracebackThreads`` to +c:func:`PyUnstable_DumpTraceback` and c:func`PyUnstable_DumpTracebackThreads`. From 0e0329280d8929399880852fdc27eed8c67c2112 Mon Sep 17 00:00:00 2001 From: Alex Malyshev Date: Sun, 5 Apr 2026 18:29:15 -0400 Subject: [PATCH 5/6] Fix missing colon --- .../next/C_API/2026-04-05-18-18-59.gh-issue-145559.qKJH9S.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/C_API/2026-04-05-18-18-59.gh-issue-145559.qKJH9S.rst b/Misc/NEWS.d/next/C_API/2026-04-05-18-18-59.gh-issue-145559.qKJH9S.rst index 0165f5642b9c40..dd2a556c171f1f 100644 --- a/Misc/NEWS.d/next/C_API/2026-04-05-18-18-59.gh-issue-145559.qKJH9S.rst +++ b/Misc/NEWS.d/next/C_API/2026-04-05-18-18-59.gh-issue-145559.qKJH9S.rst @@ -1,2 +1,2 @@ Rename ``_Py_DumpTraceback`` and ``_Py_DumpTracebackThreads`` to -c:func:`PyUnstable_DumpTraceback` and c:func`PyUnstable_DumpTracebackThreads`. +c:func:`PyUnstable_DumpTraceback` and c:func:`PyUnstable_DumpTracebackThreads`. From 4bcd445a97ce20a845acb42368adfdff1393e1aa Mon Sep 17 00:00:00 2001 From: Alex Malyshev Date: Sun, 5 Apr 2026 18:32:25 -0400 Subject: [PATCH 6/6] More sphinx-lint fixes --- .../next/C_API/2026-04-05-18-18-59.gh-issue-145559.qKJH9S.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/C_API/2026-04-05-18-18-59.gh-issue-145559.qKJH9S.rst b/Misc/NEWS.d/next/C_API/2026-04-05-18-18-59.gh-issue-145559.qKJH9S.rst index dd2a556c171f1f..9495d42160a9cd 100644 --- a/Misc/NEWS.d/next/C_API/2026-04-05-18-18-59.gh-issue-145559.qKJH9S.rst +++ b/Misc/NEWS.d/next/C_API/2026-04-05-18-18-59.gh-issue-145559.qKJH9S.rst @@ -1,2 +1,3 @@ Rename ``_Py_DumpTraceback`` and ``_Py_DumpTracebackThreads`` to -c:func:`PyUnstable_DumpTraceback` and c:func:`PyUnstable_DumpTracebackThreads`. +:c:func:`PyUnstable_DumpTraceback` and +:c:func:`PyUnstable_DumpTracebackThreads`.