Skip to content

Commit 561dbdd

Browse files
committed
Add the actual C backtrace printing logic
1 parent 958faf8 commit 561dbdd

File tree

3 files changed

+67
-12
lines changed

3 files changed

+67
-12
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.vscode/
22
build
33
*.egg-info
4-
*.so
4+
*.so
5+
__pycache__

cfaulthandler.c

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#include <Python.h>
22

3+
#include <execinfo.h>
34
#include <object.h>
45
#include <signal.h>
56
#include <signal.h>
@@ -254,6 +255,28 @@ get_thread_state(void)
254255
return tstate;
255256
}
256257

258+
#define CFAULTHANDLER_MAX_C_FRAMES 50
259+
260+
static void
261+
cfaulthandler_dump_c_traceback(int fd)
262+
{
263+
PUTS(fd, "\nCurrent thread's C call stack (most recent call first):\n");
264+
265+
/* We need a buffer to fit the C backtrace. But malloc() isn't safe to call
266+
from a signal handler; and we can't use a global buffer because multiple
267+
threads could theoretically be crashing at the same time. So we allocate it
268+
on the stack. Assuming 64-bit pointers, this would be 400 bytes. SIGSTKSZ
269+
is 8192 bytes (on my machine, at least) so 400 shouldn't blow the stack. */
270+
void *frames[CFAULTHANDLER_MAX_C_FRAMES];
271+
272+
int n_frames = backtrace(&frames, CFAULTHANDLER_MAX_C_FRAMES);
273+
backtrace_symbols_fd(frames, n_frames, fd);
274+
275+
if (n_frames == CFAULTHANDLER_MAX_C_FRAMES) {
276+
PUTS(fd, "<truncated>\n");
277+
}
278+
}
279+
257280
static void
258281
cfaulthandler_dump_traceback(int fd, int all_threads,
259282
PyInterpreterState *interp)
@@ -284,6 +307,8 @@ cfaulthandler_dump_traceback(int fd, int all_threads,
284307
_Py_DumpTraceback(fd, tstate);
285308
}
286309

310+
cfaulthandler_dump_c_traceback(fd);
311+
287312
reentrant = 0;
288313
}
289314

@@ -322,6 +347,8 @@ cfaulthandler_dump_traceback_py(PyObject *self,
322347
_Py_DumpTraceback(fd, tstate);
323348
}
324349

350+
cfaulthandler_dump_c_traceback(fd);
351+
325352
if (PyErr_CheckSignals())
326353
return NULL;
327354

@@ -664,6 +691,9 @@ cfaulthandler_thread(void *unused)
664691
errmsg = _Py_DumpTracebackThreads(thread.fd, thread.interp, NULL);
665692
ok = (errmsg == NULL);
666693

694+
/* Note, we can't dump other threads' C backtraces from the side
695+
thread; we only dump the Python backtraces. */
696+
667697
if (thread.exit)
668698
_exit(1);
669699
} while (ok && thread.repeat);

test_cfaulthandler.py

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,18 @@
2727
MS_WINDOWS = (os.name == 'nt')
2828

2929

30-
def expected_traceback(lineno1, lineno2, header, min_count=1):
30+
def expected_traceback(lineno1, lineno2, header, min_count=1, *, c_call_stack=True):
3131
regex = header
3232
regex += ' File "<string>", line %s in func\n' % lineno1
3333
regex += ' File "<string>", line %s in <module>' % lineno2
3434
if 1 < min_count:
35+
assert not c_call_stack
3536
return '^' + (regex + '\n') * (min_count - 1) + regex
3637
else:
37-
return '^' + regex + '$'
38+
if c_call_stack:
39+
return '^' + regex + "\n\nCurrent thread's C call stack"
40+
else:
41+
return '^' + regex + '$'
3842

3943
def skip_segfault_on_android(test):
4044
# Issue #32138: Raising SIGSEGV on Android may not cause a crash.
@@ -89,6 +93,7 @@ def check_error(self, code, lineno, fatal_error, *,
8993
fd=None, know_current_thread=True,
9094
py_fatal_error=False,
9195
garbage_collecting=False,
96+
c_call_stack=True,
9297
function='<module>'):
9398
"""
9499
Check that the fault handler for fatal errors is enabled and check the
@@ -110,7 +115,19 @@ def check_error(self, code, lineno, fatal_error, *,
110115
regex.append(fr'{header} \(most recent call first\):')
111116
if garbage_collecting:
112117
regex.append(' Garbage-collecting')
113-
regex.append(fr' File "<string>", line {lineno} in {function}')
118+
regex.append(
119+
fr' File "<string>", line {lineno} in {function}'
120+
r'(?:\n File "<string>", line \d+ in .*)*'
121+
)
122+
if c_call_stack:
123+
regex.append('')
124+
regex.append(r"Current thread's C call stack \(most recent call first\):")
125+
regex.append(
126+
# First line should always be in the cfaulthandler shared library
127+
fr"{re.escape(cfaulthandler.__file__)}\(.*\+0x[0-9a-f]+\)\[0x[0-9a-f]+\]"
128+
# Remaining lines could be anywhere
129+
r"(?:\n.*(?:\(.*\+0x[0-9a-f]+\))?\[0x[0-9a-f]+\])+"
130+
)
114131
regex = '\n'.join(regex)
115132

116133
if other_regex:
@@ -212,7 +229,8 @@ def test_fatal_error_c_thread(self):
212229
'in new thread',
213230
know_current_thread=False,
214231
func='cfaulthandler_fatal_error_thread',
215-
py_fatal_error=True)
232+
py_fatal_error=True,
233+
c_call_stack=False)
216234

217235
def test_sigabrt(self):
218236
self.check_fatal_error("""
@@ -272,7 +290,8 @@ def check_fatal_error_func(self, release_gil):
272290
2,
273291
'xyz',
274292
func='test_fatal_error',
275-
py_fatal_error=True)
293+
py_fatal_error=True,
294+
c_call_stack=False)
276295

277296
def test_fatal_error(self):
278297
self.check_fatal_error_func(False)
@@ -451,14 +470,17 @@ def funcA():
451470
lineno = 11
452471
else:
453472
lineno = 14
454-
expected = [
473+
expected_start = [
455474
'Stack (most recent call first):',
456475
' File "<string>", line %s in funcB' % lineno,
457476
' File "<string>", line 17 in funcA',
458-
' File "<string>", line 19 in <module>'
477+
' File "<string>", line 19 in <module>',
478+
'',
479+
"Current thread's C call stack (most recent call first):",
459480
]
460481
trace, exitcode = self.get_output(code, filename, fd)
461-
self.assertEqual(trace, expected)
482+
self.assertEqual(trace[:len(expected_start)], expected_start)
483+
self.assertRegex(trace[len(expected_start)], fr"{re.escape(cfaulthandler.__file__)}\(.*\+0x[0-9a-f]+\)\[0x[0-9a-f]+\]")
462484
self.assertEqual(exitcode, 0)
463485

464486
def test_dump_traceback(self):
@@ -495,7 +517,7 @@ def {func_name}():
495517
' File "<string>", line 6 in <module>'
496518
]
497519
trace, exitcode = self.get_output(code)
498-
self.assertEqual(trace, expected)
520+
self.assertEqual(trace[:len(expected)], expected)
499521
self.assertEqual(exitcode, 0)
500522

501523
def check_dump_traceback_threads(self, filename):
@@ -551,7 +573,9 @@ def run(self):
551573
552574
Current thread 0x[0-9a-f]+ \(most recent call first\):
553575
File "<string>", line {lineno} in dump
554-
File "<string>", line 28 in <module>$
576+
File "<string>", line 28 in <module>
577+
578+
Current thread's C call stack \(most recent call first\):
555579
"""
556580
regex = dedent(regex.format(lineno=lineno)).strip()
557581
self.assertRegex(output, regex)
@@ -620,7 +644,7 @@ def func(timeout, repeat, cancel, file, loops):
620644
if repeat:
621645
count *= 2
622646
header = r'Timeout \(%s\)!\nThread 0x[0-9a-f]+ \(most recent call first\):\n' % timeout_str
623-
regex = expected_traceback(17, 26, header, min_count=count)
647+
regex = expected_traceback(17, 26, header, min_count=count, c_call_stack=False)
624648
self.assertRegex(trace, regex)
625649
else:
626650
self.assertEqual(trace, '')

0 commit comments

Comments
 (0)