Skip to content

Commit 331b4b8

Browse files
[3.14] gh-140815: Fix faulthandler for invalid/freed frame (GH-140921) (#140981)
gh-140815: Fix faulthandler for invalid/freed frame (GH-140921) faulthandler now detects if a frame or a code object is invalid or freed. Add helper functions: * _PyCode_SafeAddr2Line() * _PyFrame_SafeGetCode() * _PyFrame_SafeGetLasti() _PyMem_IsPtrFreed() now detects pointers in [-0xff, 0xff] range as freed. (cherry picked from commit a84181c) Co-authored-by: Victor Stinner <[email protected]>
1 parent a9c964f commit 331b4b8

File tree

6 files changed

+127
-32
lines changed

6 files changed

+127
-32
lines changed

Include/internal/pycore_code.h

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -274,8 +274,13 @@ extern void _PyLineTable_InitAddressRange(
274274
/** API for traversing the line number table. */
275275
extern int _PyLineTable_NextAddressRange(PyCodeAddressRange *range);
276276
extern int _PyLineTable_PreviousAddressRange(PyCodeAddressRange *range);
277-
// This is used in dump_frame() in traceback.c without an attached tstate.
278-
extern int _PyCode_Addr2LineNoTstate(PyCodeObject *co, int addr);
277+
278+
// Similar to PyCode_Addr2Line(), but return -1 if the code object is invalid
279+
// and can be called without an attached tstate. Used by dump_frame() in
280+
// Python/traceback.c. The function uses heuristics to detect freed memory,
281+
// it's not 100% reliable.
282+
extern int _PyCode_SafeAddr2Line(PyCodeObject *co, int addr);
283+
279284

280285
/** API for executors */
281286
extern void _PyCode_Clear_Executors(PyCodeObject *code);

Include/internal/pycore_interpframe.h

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,36 @@ static inline PyCodeObject *_PyFrame_GetCode(_PyInterpreterFrame *f) {
2424
return (PyCodeObject *)executable;
2525
}
2626

27+
// Similar to _PyFrame_GetCode(), but return NULL if the frame is invalid or
28+
// freed. Used by dump_frame() in Python/traceback.c. The function uses
29+
// heuristics to detect freed memory, it's not 100% reliable.
30+
static inline PyCodeObject*
31+
_PyFrame_SafeGetCode(_PyInterpreterFrame *f)
32+
{
33+
// globals and builtins may be NULL on a legit frame, but it's unlikely.
34+
// It's more likely that it's a sign of an invalid frame.
35+
if (f->f_globals == NULL || f->f_builtins == NULL) {
36+
return NULL;
37+
}
38+
39+
if (PyStackRef_IsNull(f->f_executable)) {
40+
return NULL;
41+
}
42+
void *ptr;
43+
memcpy(&ptr, &f->f_executable, sizeof(f->f_executable));
44+
if (_PyMem_IsPtrFreed(ptr)) {
45+
return NULL;
46+
}
47+
PyObject *executable = PyStackRef_AsPyObjectBorrow(f->f_executable);
48+
if (_PyObject_IsFreed(executable)) {
49+
return NULL;
50+
}
51+
if (!PyCode_Check(executable)) {
52+
return NULL;
53+
}
54+
return (PyCodeObject *)executable;
55+
}
56+
2757
static inline _Py_CODEUNIT *
2858
_PyFrame_GetBytecode(_PyInterpreterFrame *f)
2959
{
@@ -37,6 +67,31 @@ _PyFrame_GetBytecode(_PyInterpreterFrame *f)
3767
#endif
3868
}
3969

70+
// Similar to PyUnstable_InterpreterFrame_GetLasti(), but return NULL if the
71+
// frame is invalid or freed. Used by dump_frame() in Python/traceback.c. The
72+
// function uses heuristics to detect freed memory, it's not 100% reliable.
73+
static inline int
74+
_PyFrame_SafeGetLasti(struct _PyInterpreterFrame *f)
75+
{
76+
// Code based on _PyFrame_GetBytecode() but replace _PyFrame_GetCode()
77+
// with _PyFrame_SafeGetCode().
78+
PyCodeObject *co = _PyFrame_SafeGetCode(f);
79+
if (co == NULL) {
80+
return -1;
81+
}
82+
83+
_Py_CODEUNIT *bytecode;
84+
#ifdef Py_GIL_DISABLED
85+
_PyCodeArray *tlbc = _PyCode_GetTLBCArray(co);
86+
assert(f->tlbc_index >= 0 && f->tlbc_index < tlbc->size);
87+
bytecode = (_Py_CODEUNIT *)tlbc->entries[f->tlbc_index];
88+
#else
89+
bytecode = _PyCode_CODE(co);
90+
#endif
91+
92+
return (int)(f->instr_ptr - bytecode) * sizeof(_Py_CODEUNIT);
93+
}
94+
4095
static inline PyFunctionObject *_PyFrame_GetFunction(_PyInterpreterFrame *f) {
4196
PyObject *func = PyStackRef_AsPyObjectBorrow(f->f_funcobj);
4297
assert(PyFunction_Check(func));

Include/internal/pycore_pymem.h

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,17 @@ static inline int _PyMem_IsPtrFreed(const void *ptr)
5454
{
5555
uintptr_t value = (uintptr_t)ptr;
5656
#if SIZEOF_VOID_P == 8
57-
return (value == 0
57+
return (value <= 0xff // NULL, 0x1, 0x2, ..., 0xff
5858
|| value == (uintptr_t)0xCDCDCDCDCDCDCDCD
5959
|| value == (uintptr_t)0xDDDDDDDDDDDDDDDD
60-
|| value == (uintptr_t)0xFDFDFDFDFDFDFDFD);
60+
|| value == (uintptr_t)0xFDFDFDFDFDFDFDFD
61+
|| value >= (uintptr_t)0xFFFFFFFFFFFFFF00); // -0xff, ..., -2, -1
6162
#elif SIZEOF_VOID_P == 4
62-
return (value == 0
63+
return (value <= 0xff
6364
|| value == (uintptr_t)0xCDCDCDCD
6465
|| value == (uintptr_t)0xDDDDDDDD
65-
|| value == (uintptr_t)0xFDFDFDFD);
66+
|| value == (uintptr_t)0xFDFDFDFD
67+
|| value >= (uintptr_t)0xFFFFFF00);
6668
#else
6769
# error "unknown pointer size"
6870
#endif
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:mod:`faulthandler` now detects if a frame or a code object is invalid or
2+
freed. Patch by Victor Stinner.

Objects/codeobject.c

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1013,8 +1013,8 @@ PyCode_NewEmpty(const char *filename, const char *funcname, int firstlineno)
10131013
* source location tracking (co_lines/co_positions)
10141014
******************/
10151015

1016-
int
1017-
_PyCode_Addr2LineNoTstate(PyCodeObject *co, int addrq)
1016+
static int
1017+
_PyCode_Addr2Line(PyCodeObject *co, int addrq)
10181018
{
10191019
if (addrq < 0) {
10201020
return co->co_firstlineno;
@@ -1028,12 +1028,29 @@ _PyCode_Addr2LineNoTstate(PyCodeObject *co, int addrq)
10281028
return _PyCode_CheckLineNumber(addrq, &bounds);
10291029
}
10301030

1031+
int
1032+
_PyCode_SafeAddr2Line(PyCodeObject *co, int addrq)
1033+
{
1034+
if (addrq < 0) {
1035+
return co->co_firstlineno;
1036+
}
1037+
if (co->_co_monitoring && co->_co_monitoring->lines) {
1038+
return _Py_Instrumentation_GetLine(co, addrq/sizeof(_Py_CODEUNIT));
1039+
}
1040+
if (!(addrq >= 0 && addrq < _PyCode_NBYTES(co))) {
1041+
return -1;
1042+
}
1043+
PyCodeAddressRange bounds;
1044+
_PyCode_InitAddressRange(co, &bounds);
1045+
return _PyCode_CheckLineNumber(addrq, &bounds);
1046+
}
1047+
10311048
int
10321049
PyCode_Addr2Line(PyCodeObject *co, int addrq)
10331050
{
10341051
int lineno;
10351052
Py_BEGIN_CRITICAL_SECTION(co);
1036-
lineno = _PyCode_Addr2LineNoTstate(co, addrq);
1053+
lineno = _PyCode_Addr2Line(co, addrq);
10371054
Py_END_CRITICAL_SECTION();
10381055
return lineno;
10391056
}

Python/traceback.c

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -976,44 +976,61 @@ _Py_DumpASCII(int fd, PyObject *text)
976976

977977
/* Write a frame into the file fd: "File "xxx", line xxx in xxx".
978978
979-
This function is signal safe. */
979+
This function is signal safe.
980980
981-
static void
981+
Return 0 on success. Return -1 if the frame is invalid. */
982+
983+
static int
982984
dump_frame(int fd, _PyInterpreterFrame *frame)
983985
{
984-
assert(frame->owner < FRAME_OWNED_BY_INTERPRETER);
986+
if (frame->owner == FRAME_OWNED_BY_INTERPRETER) {
987+
/* Ignore trampoline frame */
988+
return 0;
989+
}
985990

986-
PyCodeObject *code =_PyFrame_GetCode(frame);
991+
PyCodeObject *code = _PyFrame_SafeGetCode(frame);
992+
if (code == NULL) {
993+
return -1;
994+
}
995+
996+
int res = 0;
987997
PUTS(fd, " File ");
988998
if (code->co_filename != NULL
989999
&& PyUnicode_Check(code->co_filename))
9901000
{
9911001
PUTS(fd, "\"");
9921002
_Py_DumpASCII(fd, code->co_filename);
9931003
PUTS(fd, "\"");
994-
} else {
1004+
}
1005+
else {
9951006
PUTS(fd, "???");
1007+
res = -1;
9961008
}
997-
int lasti = PyUnstable_InterpreterFrame_GetLasti(frame);
998-
int lineno = _PyCode_Addr2LineNoTstate(code, lasti);
1009+
9991010
PUTS(fd, ", line ");
1011+
int lasti = _PyFrame_SafeGetLasti(frame);
1012+
int lineno = -1;
1013+
if (lasti >= 0) {
1014+
lineno = _PyCode_SafeAddr2Line(code, lasti);
1015+
}
10001016
if (lineno >= 0) {
10011017
_Py_DumpDecimal(fd, (size_t)lineno);
10021018
}
10031019
else {
10041020
PUTS(fd, "???");
1021+
res = -1;
10051022
}
1006-
PUTS(fd, " in ");
10071023

1008-
if (code->co_name != NULL
1009-
&& PyUnicode_Check(code->co_name)) {
1024+
PUTS(fd, " in ");
1025+
if (code->co_name != NULL && PyUnicode_Check(code->co_name)) {
10101026
_Py_DumpASCII(fd, code->co_name);
10111027
}
10121028
else {
10131029
PUTS(fd, "???");
1030+
res = -1;
10141031
}
1015-
10161032
PUTS(fd, "\n");
1033+
return res;
10171034
}
10181035

10191036
static int
@@ -1056,17 +1073,6 @@ dump_traceback(int fd, PyThreadState *tstate, int write_header)
10561073

10571074
unsigned int depth = 0;
10581075
while (1) {
1059-
if (frame->owner == FRAME_OWNED_BY_INTERPRETER) {
1060-
/* Trampoline frame */
1061-
frame = frame->previous;
1062-
if (frame == NULL) {
1063-
break;
1064-
}
1065-
1066-
/* Can't have more than one shim frame in a row */
1067-
assert(frame->owner != FRAME_OWNED_BY_INTERPRETER);
1068-
}
1069-
10701076
if (MAX_FRAME_DEPTH <= depth) {
10711077
if (MAX_FRAME_DEPTH < depth) {
10721078
PUTS(fd, "plus ");
@@ -1076,7 +1082,15 @@ dump_traceback(int fd, PyThreadState *tstate, int write_header)
10761082
break;
10771083
}
10781084

1079-
dump_frame(fd, frame);
1085+
if (_PyMem_IsPtrFreed(frame)) {
1086+
PUTS(fd, " <freed frame>\n");
1087+
break;
1088+
}
1089+
if (dump_frame(fd, frame) < 0) {
1090+
PUTS(fd, " <invalid frame>\n");
1091+
break;
1092+
}
1093+
10801094
frame = frame->previous;
10811095
if (frame == NULL) {
10821096
break;

0 commit comments

Comments
 (0)