Skip to content

Commit d8bcff8

Browse files
committed
Merge branch 'main' into dump-traceback
2 parents bce96a1 + c2642e2 commit d8bcff8

File tree

12 files changed

+115
-77
lines changed

12 files changed

+115
-77
lines changed

Doc/c-api/exceptions.rst

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1346,3 +1346,67 @@ Tracebacks
13461346
13471347
This function returns ``0`` on success, and returns ``-1`` with an
13481348
exception set on failure.
1349+
1350+
.. c:function:: const char* PyUnstable_DumpTraceback(int fd, PyThreadState *tstate)
1351+
1352+
Write a trace of the Python stack in *tstate* into the file *fd*. The format
1353+
looks like::
1354+
1355+
Traceback (most recent call first):
1356+
File "xxx", line xxx in <xxx>
1357+
File "xxx", line xxx in <xxx>
1358+
...
1359+
File "xxx", line xxx in <xxx>
1360+
1361+
This function is meant to debug situations such as segfaults, fatal errors,
1362+
and similar. The file and function names it outputs are encoded to ASCII with
1363+
backslashreplace and truncated to 500 characters. It writes only the first
1364+
100 frames; further frames are truncated with the line ``...``.
1365+
1366+
This function will return ``NULL`` on success, or an error message on error.
1367+
1368+
This function is intended for use in crash scenarios such as signal handlers
1369+
for SIGSEGV, where the interpreter may be in an inconsistent state. Given
1370+
that it reads interpreter data structures that may be partially modified, the
1371+
function might produce incomplete output or it may even crash itself.
1372+
1373+
The caller does not need to hold an :term:`attached thread state`, nor does
1374+
*tstate* need to be attached.
1375+
1376+
.. versionadded:: next
1377+
1378+
.. c:function:: const char* PyUnstable_DumpTracebackThreads(int fd, PyInterpreterState *interp, PyThreadState *current_tstate)
1379+
1380+
Write the traces of all Python threads in *interp* into the file *fd*.
1381+
1382+
If *interp* is ``NULL`` then this function will try to identify the current
1383+
interpreter using thread-specific storage. If it cannot, it will return an
1384+
error.
1385+
1386+
If *current_tstate* is not ``NULL`` then it will be used to identify what the
1387+
current thread is in the written output. If it is ``NULL`` then this function
1388+
will identify the current thread using thread-specific storage. It is not an
1389+
error if the function is unable to get the current Python thread state.
1390+
1391+
This function will return ``NULL`` on success, or an error message on error.
1392+
It will also write this error message to *fd*.
1393+
1394+
This function is meant to debug debug situations such as segfaults, fatal
1395+
errors, and similar. It calls :c:func:`PyUnstable_DumpTraceback` for each
1396+
thread. It only writes the tracebacks of the first 100 threads, further
1397+
output is truncated with the line ``...``.
1398+
1399+
This function is intended for use in crash scenarios such as signal handlers
1400+
for SIGSEGV, where the interpreter may be in an inconsistent state. Given
1401+
that it reads interpreter data structures that may be partially modified, the
1402+
function might produce incomplete output or it may even crash itself.
1403+
1404+
The caller does not need to hold an :term:`attached thread state`, nor does
1405+
*current_tstate* need to be attached.
1406+
1407+
.. warning::
1408+
On the :term:`free-threaded build`, this function is not thread-safe. If
1409+
another thread deletes its :term:`thread state` while this function is being
1410+
called, the process will likely crash.
1411+
1412+
.. versionadded:: next

Doc/whatsnew/3.15.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1788,6 +1788,11 @@ New features
17881788
Python 3.14.
17891789
(Contributed by Victor Stinner in :gh:`142417`.)
17901790

1791+
* Add :c:func:`PyUnstable_DumpTraceback` and
1792+
:c:func:`PyUnstable_DumpTracebackThreads` functions to safely output Python
1793+
stacktraces.
1794+
(Contributed by Alex Malyshev in :gh:`145559`.)
1795+
17911796
Changed C APIs
17921797
--------------
17931798

Include/cpython/traceback.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,10 @@ struct _traceback {
1111
int tb_lasti;
1212
int tb_lineno;
1313
};
14+
15+
PyAPI_FUNC(const char*) PyUnstable_DumpTraceback(int fd, PyThreadState *tstate);
16+
17+
PyAPI_FUNC(const char*) PyUnstable_DumpTracebackThreads(
18+
int fd,
19+
PyInterpreterState *interp,
20+
PyThreadState *current_tstate);

Include/internal/pycore_traceback.h

Lines changed: 0 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -14,55 +14,6 @@ PyAPI_FUNC(int) _Py_DisplaySourceLine(PyObject *, PyObject *, int, int, int *, P
1414
// Export for 'pyexact' shared extension
1515
PyAPI_FUNC(void) _PyTraceback_Add(const char *, const char *, int);
1616

17-
/* Write the Python traceback into the file 'fd'. For example:
18-
19-
Traceback (most recent call first):
20-
File "xxx", line xxx in <xxx>
21-
File "xxx", line xxx in <xxx>
22-
...
23-
File "xxx", line xxx in <xxx>
24-
25-
This function is written for debug purpose only, to dump the traceback in
26-
the worst case: after a segmentation fault, at fatal error, etc. That's why,
27-
it is very limited. Strings are truncated to 100 characters and encoded to
28-
ASCII with backslashreplace. It doesn't write the source code, only the
29-
function name, filename and line number of each frame. Write only the first
30-
100 frames: if the traceback is truncated, write the line " ...".
31-
32-
This function is signal safe. */
33-
34-
extern void _Py_DumpTraceback(
35-
int fd,
36-
PyThreadState *tstate);
37-
38-
/* Write the traceback of all threads into the file 'fd'. current_thread can be
39-
NULL.
40-
41-
Return NULL on success, or an error message on error.
42-
43-
This function is written for debug purpose only. It calls
44-
_Py_DumpTraceback() for each thread, and so has the same limitations. It
45-
only write the traceback of the first 100 threads: write "..." if there are
46-
more threads.
47-
48-
If current_tstate is NULL, the function tries to get the Python thread state
49-
of the current thread. It is not an error if the function is unable to get
50-
the current Python thread state.
51-
52-
If interp is NULL, the function tries to get the interpreter state from
53-
the current Python thread state, or from
54-
_PyGILState_GetInterpreterStateUnsafe() in last resort.
55-
56-
It is better to pass NULL to interp and current_tstate, the function tries
57-
different options to retrieve this information.
58-
59-
This function is signal safe. */
60-
61-
extern const char* _Py_DumpTracebackThreads(
62-
int fd,
63-
PyInterpreterState *interp,
64-
PyThreadState *current_tstate);
65-
6617
/* Write a Unicode object into the file descriptor fd. Encode the string to
6718
ASCII using the backslashreplace error handler.
6819
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Rename ``_Py_DumpTraceback`` and ``_Py_DumpTracebackThreads`` to :c:func:`PyUnstable_DumpTraceback` and :c:func:`PyUnstable_DumpTracebackThreads`.

Modules/faulthandler.c

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
#include "pycore_runtime.h" // _Py_ID()
88
#include "pycore_signal.h" // Py_NSIG
99
#include "pycore_time.h" // _PyTime_FromSecondsObject()
10-
#include "pycore_traceback.h" // _Py_DumpTracebackThreads
10+
#include "pycore_traceback.h" // _Py_DumpStack()
1111
#ifdef HAVE_UNISTD_H
1212
# include <unistd.h> // _exit()
1313
#endif
@@ -205,14 +205,15 @@ faulthandler_dump_traceback(int fd, int all_threads,
205205
PyThreadState *tstate = PyGILState_GetThisThreadState();
206206

207207
if (all_threads == 1) {
208-
(void)_Py_DumpTracebackThreads(fd, NULL, tstate);
208+
(void)PyUnstable_DumpTracebackThreads(fd, NULL, tstate);
209209
}
210210
else {
211211
if (all_threads == FT_IGNORE_ALL_THREADS) {
212212
PUTS(fd, "<Cannot show all threads while the GIL is disabled>\n");
213213
}
214-
if (tstate != NULL)
215-
_Py_DumpTraceback(fd, tstate);
214+
if (tstate != NULL) {
215+
PyUnstable_DumpTraceback(fd, tstate);
216+
}
216217
}
217218

218219
reentrant = 0;
@@ -273,17 +274,18 @@ faulthandler_dump_traceback_py_impl(PyObject *module, PyObject *file,
273274
/* gh-128400: Accessing other thread states while they're running
274275
* isn't safe if those threads are running. */
275276
_PyEval_StopTheWorld(interp);
276-
errmsg = _Py_DumpTracebackThreads(fd, NULL, tstate);
277+
errmsg = PyUnstable_DumpTracebackThreads(fd, NULL, tstate);
277278
_PyEval_StartTheWorld(interp);
278-
if (errmsg != NULL) {
279-
PyErr_SetString(PyExc_RuntimeError, errmsg);
280-
Py_XDECREF(file);
281-
return NULL;
282-
}
283279
}
284280
else {
285-
_Py_DumpTraceback(fd, tstate);
281+
errmsg = PyUnstable_DumpTraceback(fd, tstate);
282+
}
283+
if (errmsg != NULL) {
284+
PyErr_SetString(PyExc_RuntimeError, errmsg);
285+
Py_XDECREF(file);
286+
return NULL;
286287
}
288+
287289
Py_XDECREF(file);
288290

289291
if (PyErr_CheckSignals())
@@ -703,7 +705,7 @@ faulthandler_thread(void *unused)
703705

704706
(void)_Py_write_noraise(thread.fd, thread.header, (int)thread.header_len);
705707

706-
errmsg = _Py_DumpTracebackThreads(thread.fd, thread.interp, NULL);
708+
errmsg = PyUnstable_DumpTracebackThreads(thread.fd, thread.interp, NULL);
707709
ok = (errmsg == NULL);
708710

709711
if (thread.exit)

Platforms/emscripten/node_entry.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,6 @@ try {
5757
// Show JavaScript exception and traceback
5858
console.warn(e);
5959
// Show Python exception and traceback
60-
Module.__Py_DumpTraceback(2, Module._PyGILState_GetThisThreadState());
60+
Module.PyUnstable_DumpTraceback(2, Module._PyGILState_GetThisThreadState());
6161
process.exit(1);
6262
}

Platforms/emscripten/web_example_pyrepl_jspi/src.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,6 @@ try {
189189
// Show JavaScript exception and traceback
190190
console.warn(e);
191191
// Show Python exception and traceback
192-
Module.__Py_DumpTraceback(2, Module._PyGILState_GetThisThreadState());
192+
Module.PyUnstable_DumpTraceback(2, Module._PyGILState_GetThisThreadState());
193193
process.exit(1);
194194
}

Python/pylifecycle.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3261,9 +3261,9 @@ _Py_FatalError_DumpTracebacks(int fd, PyInterpreterState *interp,
32613261

32623262
/* display the current Python stack */
32633263
#ifndef Py_GIL_DISABLED
3264-
_Py_DumpTracebackThreads(fd, interp, tstate);
3264+
PyUnstable_DumpTracebackThreads(fd, interp, tstate);
32653265
#else
3266-
_Py_DumpTraceback(fd, tstate);
3266+
PyUnstable_DumpTraceback(fd, tstate);
32673267
#endif
32683268
}
32693269

Python/traceback.c

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1167,10 +1167,11 @@ dump_traceback(int fd, PyThreadState *tstate, int write_header)
11671167
11681168
The caller is responsible to call PyErr_CheckSignals() to call Python signal
11691169
handlers if signals were received. */
1170-
void
1171-
_Py_DumpTraceback(int fd, PyThreadState *tstate)
1170+
const char*
1171+
PyUnstable_DumpTraceback(int fd, PyThreadState *tstate)
11721172
{
11731173
dump_traceback(fd, tstate, 1);
1174+
return NULL;
11741175
}
11751176

11761177
#if defined(HAVE_PTHREAD_GETNAME_NP) || defined(HAVE_PTHREAD_GET_NAME_NP)
@@ -1257,18 +1258,26 @@ write_thread_id(int fd, PyThreadState *tstate, int is_current)
12571258
PUTS(fd, " (most recent call first):\n");
12581259
}
12591260

1261+
/* Write an error string and also return it at the same time. */
1262+
static const char*
1263+
dump_error(int fd, const char *msg)
1264+
{
1265+
PUTS(fd, msg);
1266+
return msg;
1267+
}
1268+
12601269
/* Dump the traceback of all Python threads into fd. Use write() to write the
12611270
traceback and retry if write() is interrupted by a signal (failed with
12621271
EINTR), but don't call the Python signal handler.
12631272
12641273
The caller is responsible to call PyErr_CheckSignals() to call Python signal
12651274
handlers if signals were received. */
12661275
const char* _Py_NO_SANITIZE_THREAD
1267-
_Py_DumpTracebackThreads(int fd, PyInterpreterState *interp,
1268-
PyThreadState *current_tstate)
1276+
PyUnstable_DumpTracebackThreads(int fd, PyInterpreterState *interp,
1277+
PyThreadState *current_tstate)
12691278
{
12701279
if (current_tstate == NULL) {
1271-
/* _Py_DumpTracebackThreads() is called from signal handlers by
1280+
/* PyUnstable_DumpTracebackThreads() is called from signal handlers by
12721281
faulthandler.
12731282
12741283
SIGSEGV, SIGFPE, SIGABRT, SIGBUS and SIGILL are synchronous signals
@@ -1283,15 +1292,15 @@ _Py_DumpTracebackThreads(int fd, PyInterpreterState *interp,
12831292
}
12841293

12851294
if (current_tstate != NULL && tstate_is_freed(current_tstate)) {
1286-
return "tstate is freed";
1295+
return dump_error(fd, "tstate is freed");
12871296
}
12881297

12891298
if (interp == NULL) {
12901299
if (current_tstate == NULL) {
12911300
interp = _PyGILState_GetInterpreterStateUnsafe();
12921301
if (interp == NULL) {
12931302
/* We need the interpreter state to get Python threads */
1294-
return "unable to get the interpreter state";
1303+
return dump_error(fd, "unable to get the interpreter state");
12951304
}
12961305
}
12971306
else {
@@ -1301,13 +1310,13 @@ _Py_DumpTracebackThreads(int fd, PyInterpreterState *interp,
13011310
assert(interp != NULL);
13021311

13031312
if (interp_is_freed(interp)) {
1304-
return "interp is freed";
1313+
return dump_error(fd, "interp is freed");
13051314
}
13061315

13071316
/* Get the current interpreter from the current thread */
13081317
PyThreadState *tstate = PyInterpreterState_ThreadHead(interp);
13091318
if (tstate == NULL)
1310-
return "unable to get the thread head state";
1319+
return dump_error(fd, "unable to get the thread head state");
13111320

13121321
/* Dump the traceback of each thread */
13131322
unsigned int nthreads = 0;

0 commit comments

Comments
 (0)