Skip to content

Commit 29f4c75

Browse files
committed
Backport python#128159 to Python 3.13.
1 parent 8183fa5 commit 29f4c75

File tree

10 files changed

+339
-65
lines changed

10 files changed

+339
-65
lines changed

Include/internal/pycore_faulthandler.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ struct _faulthandler_runtime_state {
5656
#ifdef MS_WINDOWS
5757
void *exc_handler;
5858
#endif
59+
int c_stack;
5960
} fatal_error;
6061

6162
struct {

Include/internal/pycore_traceback.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,9 @@ extern int _PyTraceBack_Print(
100100
extern int _Py_WriteIndentedMargin(int, const char*, PyObject *);
101101
extern int _Py_WriteIndent(int, PyObject *);
102102

103+
// Export for the faulthandler module
104+
PyAPI_FUNC(void) _Py_DumpStack(int fd);
105+
103106
#ifdef __cplusplus
104107
}
105108
#endif

Lib/test/test_faulthandler.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ def temporary_filename():
4545
finally:
4646
os_helper.unlink(filename)
4747

48+
49+
ADDRESS_EXPR = "0x[0-9a-f]+"
50+
C_STACK_REGEX = [
51+
r"Current thread's C stack trace \(most recent call first\):",
52+
fr'( Binary file ".+"(, at .*(\+|-){ADDRESS_EXPR})? \[{ADDRESS_EXPR}\])|(<.+>)'
53+
]
54+
4855
class FaultHandlerTests(unittest.TestCase):
4956

5057
def get_output(self, code, filename=None, fd=None):
@@ -93,6 +100,7 @@ def check_error(self, code, lineno, fatal_error, *,
93100
fd=None, know_current_thread=True,
94101
py_fatal_error=False,
95102
garbage_collecting=False,
103+
c_stack=True,
96104
function='<module>'):
97105
"""
98106
Check that the fault handler for fatal errors is enabled and check the
@@ -115,6 +123,8 @@ def check_error(self, code, lineno, fatal_error, *,
115123
if garbage_collecting:
116124
regex.append(' Garbage-collecting')
117125
regex.append(fr' File "<string>", line {lineno} in {function}')
126+
if c_stack:
127+
regex.extend(C_STACK_REGEX)
118128
regex = '\n'.join(regex)
119129

120130
if other_regex:
@@ -599,6 +609,50 @@ def test_dump_traceback_threads_file(self):
599609
with temporary_filename() as filename:
600610
self.check_dump_traceback_threads(filename)
601611

612+
def check_c_stack(self, *, filename=None, fd=None):
613+
"""
614+
Explicitly call dump_c_stack() function and check its output.
615+
Raise an error if the output doesn't match the expected format.
616+
"""
617+
code = """
618+
import faulthandler
619+
620+
filename = {filename!r}
621+
fd = {fd}
622+
623+
def funcB():
624+
if filename:
625+
with open(filename, "wb") as fp:
626+
faulthandler.dump_c_stack(fp)
627+
elif fd is not None:
628+
faulthandler.dump_c_stack(fd)
629+
else:
630+
faulthandler.dump_c_stack()
631+
632+
def funcA():
633+
funcB()
634+
635+
funcA()
636+
"""
637+
code = code.format(
638+
filename=filename,
639+
fd=fd,
640+
)
641+
trace, exitcode = self.get_output(code, filename, fd)
642+
trace = '\n'.join(trace)
643+
# The C stack trace should contain the header
644+
self.assertRegex(trace, r"Current thread's C stack trace \(most recent call first\):")
645+
# And at least one frame with an address
646+
self.assertRegex(trace, r'\[0x[0-9a-f]+\]')
647+
self.assertEqual(exitcode, 0)
648+
649+
def test_dump_c_stack(self):
650+
self.check_c_stack()
651+
652+
def test_dump_c_stack_file(self):
653+
with temporary_filename() as filename:
654+
self.check_c_stack(filename=filename)
655+
602656
def check_dump_traceback_later(self, repeat=False, cancel=False, loops=1,
603657
*, filename=None, fd=None):
604658
"""

Lib/test/test_inspect/test_inspect.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6228,7 +6228,7 @@ def test_errno_module_has_signatures(self):
62286228

62296229
def test_faulthandler_module_has_signatures(self):
62306230
import faulthandler
6231-
unsupported_signature = {'dump_traceback', 'dump_traceback_later', 'enable'}
6231+
unsupported_signature = {'dump_c_stack', 'dump_traceback', 'dump_traceback_later', 'enable'}
62326232
unsupported_signature |= {name for name in ['register']
62336233
if hasattr(faulthandler, name)}
62346234
self._test_module_has_signatures(faulthandler, unsupported_signature=unsupported_signature)

Modules/faulthandler.c

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,25 @@ faulthandler_dump_traceback(int fd, int all_threads,
218218
reentrant = 0;
219219
}
220220

221+
static void
222+
faulthandler_dump_c_stack(int fd)
223+
{
224+
static volatile int reentrant = 0;
225+
226+
if (reentrant) {
227+
return;
228+
}
229+
230+
reentrant = 1;
231+
232+
if (fatal_error.c_stack) {
233+
PUTS(fd, "\n");
234+
_Py_DumpStack(fd);
235+
}
236+
237+
reentrant = 0;
238+
}
239+
221240
static PyObject*
222241
faulthandler_dump_traceback_py(PyObject *self,
223242
PyObject *args, PyObject *kwargs)
@@ -268,6 +287,33 @@ faulthandler_dump_traceback_py(PyObject *self,
268287
Py_RETURN_NONE;
269288
}
270289

290+
static PyObject *
291+
faulthandler_dump_c_stack_py(PyObject *self,
292+
PyObject *args, PyObject *kwargs)
293+
{
294+
static char *kwlist[] = {"file", NULL};
295+
PyObject *file = NULL;
296+
297+
if (!PyArg_ParseTupleAndKeywords(args, kwargs,
298+
"|O:dump_c_stack", kwlist,
299+
&file)) {
300+
return NULL;
301+
}
302+
303+
int fd = faulthandler_get_fileno(&file);
304+
if (fd < 0) {
305+
return NULL;
306+
}
307+
308+
_Py_DumpStack(fd);
309+
310+
if (PyErr_CheckSignals()) {
311+
return NULL;
312+
}
313+
314+
Py_RETURN_NONE;
315+
}
316+
271317
static void
272318
faulthandler_disable_fatal_handler(fault_handler_t *handler)
273319
{
@@ -337,6 +383,7 @@ faulthandler_fatal_error(int signum)
337383

338384
faulthandler_dump_traceback(fd, fatal_error.all_threads,
339385
fatal_error.interp);
386+
faulthandler_dump_c_stack(fd);
340387

341388
_Py_DumpExtensionModules(fd, fatal_error.interp);
342389

@@ -413,6 +460,7 @@ faulthandler_exc_handler(struct _EXCEPTION_POINTERS *exc_info)
413460

414461
faulthandler_dump_traceback(fd, fatal_error.all_threads,
415462
fatal_error.interp);
463+
faulthandler_dump_c_stack(fd);
416464

417465
/* call the next exception handler */
418466
return EXCEPTION_CONTINUE_SEARCH;
@@ -507,14 +555,15 @@ faulthandler_enable(void)
507555
static PyObject*
508556
faulthandler_py_enable(PyObject *self, PyObject *args, PyObject *kwargs)
509557
{
510-
static char *kwlist[] = {"file", "all_threads", NULL};
558+
static char *kwlist[] = {"file", "all_threads", "c_stack", NULL};
511559
PyObject *file = NULL;
512560
int all_threads = 1;
513561
int fd;
562+
int c_stack = 1;
514563
PyThreadState *tstate;
515564

516565
if (!PyArg_ParseTupleAndKeywords(args, kwargs,
517-
"|Op:enable", kwlist, &file, &all_threads))
566+
"|Opp:enable", kwlist, &file, &all_threads, &c_stack))
518567
return NULL;
519568

520569
fd = faulthandler_get_fileno(&file);
@@ -531,6 +580,7 @@ faulthandler_py_enable(PyObject *self, PyObject *args, PyObject *kwargs)
531580
fatal_error.fd = fd;
532581
fatal_error.all_threads = all_threads;
533582
fatal_error.interp = PyThreadState_GetInterpreter(tstate);
583+
fatal_error.c_stack = c_stack;
534584

535585
if (faulthandler_enable() < 0) {
536586
return NULL;
@@ -1226,6 +1276,10 @@ static PyMethodDef module_methods[] = {
12261276
PyDoc_STR("dump_traceback($module, /, file=sys.stderr, all_threads=True)\n--\n\n"
12271277
"Dump the traceback of the current thread, or of all threads "
12281278
"if all_threads is True, into file.")},
1279+
{"dump_c_stack",
1280+
_PyCFunction_CAST(faulthandler_dump_c_stack_py), METH_VARARGS|METH_KEYWORDS,
1281+
PyDoc_STR("dump_c_stack($module, /, file=sys.stderr)\n--\n\n"
1282+
"Dump the C stack of the current thread.")},
12291283
{"dump_traceback_later",
12301284
_PyCFunction_CAST(faulthandler_dump_traceback_later), METH_VARARGS|METH_KEYWORDS,
12311285
PyDoc_STR("dump_traceback_later($module, /, timeout, repeat=False, file=sys.stderr, exit=False)\n--\n\n"

Python/traceback.c

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,25 @@
2121
#ifdef HAVE_UNISTD_H
2222
# include <unistd.h> // lseek()
2323
#endif
24+
#if defined(HAVE_EXECINFO_H) && defined(HAVE_DLFCN_H) && defined(HAVE_LINK_H)
25+
# include <execinfo.h> // backtrace(), backtrace_symbols()
26+
# include <dlfcn.h> // dladdr1()
27+
# include <link.h> // struct DL_info
28+
# if defined(HAVE_BACKTRACE) && defined(HAVE_BACKTRACE_SYMBOLS) && defined(HAVE_DLADDR1)
29+
# define CAN_C_BACKTRACE
30+
# endif
31+
#endif
2432

33+
#if defined(__STDC_NO_VLA__) && (__STDC_NO_VLA__ == 1)
34+
/* Use alloca() for VLAs. */
35+
# define VLA(type, name, size) type *name = alloca(size)
36+
#elif !defined(__STDC_NO_VLA__) || (__STDC_NO_VLA__ == 0)
37+
/* Use actual C VLAs.*/
38+
# define VLA(type, name, size) type name[size]
39+
#elif defined(CAN_C_BACKTRACE)
40+
/* VLAs are not possible. Disable C stack trace functions. */
41+
# undef CAN_C_BACKTRACE
42+
#endif
2543

2644
#define OFF(x) offsetof(PyTracebackObject, x)
2745
#define PUTS(fd, str) (void)_Py_write_noraise(fd, str, (int)strlen(str))
@@ -1110,3 +1128,94 @@ _Py_DumpTracebackThreads(int fd, PyInterpreterState *interp,
11101128
return NULL;
11111129
}
11121130

1131+
#ifdef CAN_C_BACKTRACE
1132+
/* Based on glibc's implementation of backtrace_symbols(), but only uses stack memory. */
1133+
void
1134+
_Py_backtrace_symbols_fd(int fd, void *const *array, Py_ssize_t size)
1135+
{
1136+
VLA(Dl_info, info, size);
1137+
VLA(int, status, size);
1138+
/* Fill in the information we can get from dladdr() */
1139+
for (Py_ssize_t i = 0; i < size; ++i) {
1140+
struct link_map *map;
1141+
status[i] = dladdr1(array[i], &info[i], (void **)&map, RTLD_DL_LINKMAP);
1142+
if (status[i] != 0
1143+
&& info[i].dli_fname != NULL
1144+
&& info[i].dli_fname[0] != '\0') {
1145+
/* The load bias is more useful to the user than the load
1146+
address. The use of these addresses is to calculate an
1147+
address in the ELF file, so its prelinked bias is not
1148+
something we want to subtract out */
1149+
info[i].dli_fbase = (void *) map->l_addr;
1150+
}
1151+
}
1152+
for (Py_ssize_t i = 0; i < size; ++i) {
1153+
if (status[i] == 0
1154+
|| info[i].dli_fname == NULL
1155+
|| info[i].dli_fname[0] == '\0'
1156+
) {
1157+
dprintf(fd, " Binary file '<unknown>' [%p]\n", array[i]);
1158+
continue;
1159+
}
1160+
1161+
if (info[i].dli_sname == NULL) {
1162+
/* We found no symbol name to use, so describe it as
1163+
relative to the file. */
1164+
info[i].dli_saddr = info[i].dli_fbase;
1165+
}
1166+
1167+
if (info[i].dli_sname == NULL
1168+
&& info[i].dli_saddr == 0) {
1169+
dprintf(fd, " Binary file \"%s\" [%p]\n",
1170+
info[i].dli_fname,
1171+
array[i]);
1172+
}
1173+
else {
1174+
char sign;
1175+
ptrdiff_t offset;
1176+
if (array[i] >= (void *) info[i].dli_saddr) {
1177+
sign = '+';
1178+
offset = array[i] - info[i].dli_saddr;
1179+
}
1180+
else {
1181+
sign = '-';
1182+
offset = info[i].dli_saddr - array[i];
1183+
}
1184+
const char *symbol_name = info[i].dli_sname != NULL ? info[i].dli_sname : "";
1185+
dprintf(fd, " Binary file \"%s\", at %s%c%#tx [%p]\n",
1186+
info[i].dli_fname,
1187+
symbol_name,
1188+
sign, offset, array[i]);
1189+
}
1190+
}
1191+
}
1192+
1193+
void
1194+
_Py_DumpStack(int fd)
1195+
{
1196+
#define BACKTRACE_SIZE 32
1197+
PUTS(fd, "Current thread's C stack trace (most recent call first):\n");
1198+
VLA(void *, callstack, BACKTRACE_SIZE);
1199+
int frames = backtrace(callstack, BACKTRACE_SIZE);
1200+
if (frames == 0) {
1201+
// Some systems won't return anything for the stack trace
1202+
PUTS(fd, " <system returned no stack trace>\n");
1203+
return;
1204+
}
1205+
1206+
_Py_backtrace_symbols_fd(fd, callstack, frames);
1207+
if (frames == BACKTRACE_SIZE) {
1208+
PUTS(fd, " <truncated rest of calls>\n");
1209+
}
1210+
1211+
#undef BACKTRACE_SIZE
1212+
}
1213+
#else
1214+
void
1215+
_Py_DumpStack(int fd)
1216+
{
1217+
PUTS(fd, "Current thread's C stack trace (most recent call first):\n");
1218+
PUTS(fd, " <cannot get C stack on this system>\n");
1219+
}
1220+
#endif
1221+

Tools/c-analyzer/cpython/ignored.tsv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ Python/sysmodule.c - _preinit_xoptions -
160160
# thread-safety
161161
# XXX need race protection?
162162
Modules/faulthandler.c faulthandler_dump_traceback reentrant -
163+
Modules/faulthandler.c faulthandler_dump_c_stack reentrant -
163164
Python/pylifecycle.c _Py_FatalErrorFormat reentrant -
164165
Python/pylifecycle.c fatal_error reentrant -
165166

0 commit comments

Comments
 (0)