From 2ce7ac6a65b269437d63498a441902c13cad4700 Mon Sep 17 00:00:00 2001 From: Alishahryar1 <20476625+Alishahryar1@users.noreply.github.com> Date: Fri, 25 Sep 2026 18:42:28 -0700 Subject: [PATCH] gh-158200: Harden faulthandler code metadata reads --- Lib/test/test_faulthandler.py | 22 +++++++++ ...026-09-25-19-00-00.gh-issue-158200.uaf.rst | 3 ++ Objects/codeobject.c | 45 +++++++++++++++++-- Python/traceback.c | 39 +++++++++++----- 4 files changed, 93 insertions(+), 16 deletions(-) create mode 100644 Misc/NEWS.d/next/Library/2026-09-25-19-00-00.gh-issue-158200.uaf.rst diff --git a/Lib/test/test_faulthandler.py b/Lib/test/test_faulthandler.py index 5a493a4fd95680..b1fdded2cd475e 100644 --- a/Lib/test/test_faulthandler.py +++ b/Lib/test/test_faulthandler.py @@ -707,6 +707,28 @@ def test_dump_traceback_later(self): def test_dump_traceback_later_repeat(self): self.check_dump_traceback_later(repeat=True) + @threading_helper.requires_working_threading() + @support.requires_gil_enabled() + def test_dump_traceback_later_does_not_wait_for_gil(self): + code = dedent(""" + import faulthandler + import sys + + sys.setswitchinterval(3600.0) + faulthandler.dump_traceback_later(0.05, exit=True) + while True: + pass + """) + with support.SuppressCrashReport(): + process = subprocess.run( + [sys.executable, '-I', '-c', code], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + timeout=support.SHORT_TIMEOUT, + ) + self.assertEqual(process.returncode, 1) + self.assertIn(b'Timeout', process.stderr) + def test_dump_traceback_later_cancel(self): self.check_dump_traceback_later(cancel=True) diff --git a/Misc/NEWS.d/next/Library/2026-09-25-19-00-00.gh-issue-158200.uaf.rst b/Misc/NEWS.d/next/Library/2026-09-25-19-00-00.gh-issue-158200.uaf.rst new file mode 100644 index 00000000000000..4b90b932454447 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-09-25-19-00-00.gh-issue-158200.uaf.rst @@ -0,0 +1,3 @@ +Improve :func:`faulthandler.dump_traceback_later` when another thread +concurrently frees code metadata. The traceback walker now detects common +freed-memory states instead of dereferencing them. diff --git a/Objects/codeobject.c b/Objects/codeobject.c index 6b4bae40cc77de..3d4abb38342f1e 100644 --- a/Objects/codeobject.c +++ b/Objects/codeobject.c @@ -10,6 +10,7 @@ #include "pycore_interpframe.h" // FRAME_SPECIALS_SIZE #include "pycore_opcode_metadata.h" // _PyOpcode_Caches #include "pycore_opcode_utils.h" // RESUME_AT_FUNC_START +#include "pycore_object.h" // _PyObject_IsFreed() #include "pycore_optimizer.h" // _Py_ExecutorDetach #include "pycore_pymem.h" // _PyMem_FreeDelayed() #include "pycore_pystate.h" // _PyInterpreterState_GET() @@ -1035,17 +1036,53 @@ PyCode_Addr2Line(PyCodeObject *co, int addrq) int _PyCode_SafeAddr2Line(PyCodeObject *co, int addrq) { + if (_PyObject_IsFreed((PyObject *)co) || !PyCode_Check(co)) { + return -1; + } if (addrq < 0) { return co->co_firstlineno; } - if (co->_co_monitoring && co->_co_monitoring->lines) { - return _Py_Instrumentation_GetLine(co, co->_co_monitoring->lines, addrq/sizeof(_Py_CODEUNIT)); + + Py_ssize_t code_size = Py_SIZE(co); + if (code_size < 0 || addrq / (int)sizeof(_Py_CODEUNIT) >= code_size) { + return -1; + } + + _PyCoMonitoringData *monitoring = + _Py_atomic_load_ptr_acquire(&co->_co_monitoring); + if (monitoring != NULL) { + if (_PyMem_IsPtrFreed(monitoring)) { + return -1; + } + _PyCoLineInstrumentationData *lines = + _Py_atomic_load_ptr_acquire(&monitoring->lines); + if (lines != NULL) { + if (_PyMem_IsPtrFreed(lines) + || _PyObject_IsFreed((PyObject *)co) + || !PyCode_Check(co)) + { + return -1; + } + return _Py_Instrumentation_GetLine( + co, lines, addrq / sizeof(_Py_CODEUNIT)); + } + } + + if (_PyObject_IsFreed((PyObject *)co) || !PyCode_Check(co)) { + return -1; } - if (!(addrq >= 0 && addrq < _PyCode_NBYTES(co))) { + PyObject *linetable = co->co_linetable; + if (_PyObject_IsFreed(linetable) || !PyBytes_Check(linetable)) { return -1; } + int firstlineno = co->co_firstlineno; + PyCodeAddressRange bounds; - _PyCode_InitAddressRange(co, &bounds); + _PyLineTable_InitAddressRange( + PyBytes_AS_STRING(linetable), + PyBytes_GET_SIZE(linetable), + firstlineno, + &bounds); return _PyCode_CheckLineNumber(addrq, &bounds); } diff --git a/Python/traceback.c b/Python/traceback.c index ca94a735db5b8f..c0030a526b22c1 100644 --- a/Python/traceback.c +++ b/Python/traceback.c @@ -7,6 +7,7 @@ #include "pycore_frame.h" // PyFrameObject #include "pycore_interp.h" // PyInterpreterState.gc #include "pycore_interpframe.h" // _PyFrame_GetCode() +#include "pycore_object.h" // _PyObject_IsFreed() #include "pycore_pyerrors.h" // _PyErr_GetRaisedException() #include "pycore_pystate.h" // _PyThreadState_GET() #include "pycore_traceback.h" // EXCEPTION_TB_HEADER @@ -898,15 +899,17 @@ dump_char(int fd, char ch) void _Py_DumpASCII(int fd, PyObject *text) { - PyASCIIObject *ascii = _PyASCIIObject_CAST(text); Py_ssize_t i, size; int truncated; int kind; void *data = NULL; Py_UCS4 ch; - if (!PyUnicode_Check(text)) + if (_PyObject_IsFreed(text) || !PyUnicode_Check(text)) { return; + } + + PyASCIIObject *ascii = _PyASCIIObject_CAST(text); size = ascii->length; kind = ascii->state.kind; @@ -1043,19 +1046,26 @@ dump_frame(int fd, _PyInterpreterFrame *frame) int res = 0; PUTS(fd, " File "); - if (code->co_filename != NULL - && PyUnicode_Check(code->co_filename)) - { - PUTS(fd, "\""); - _Py_DumpASCII(fd, code->co_filename); - PUTS(fd, "\""); + if (_PyObject_IsFreed((PyObject *)code) || !PyCode_Check(code)) { + PUTS(fd, "???, line ??? in ???\n"); + return -1; } - else { + PyObject *filename = code->co_filename; + if (_PyObject_IsFreed(filename) || !PyUnicode_Check(filename)) { PUTS(fd, "???"); res = -1; } + else { + PUTS(fd, "\""); + _Py_DumpASCII(fd, filename); + PUTS(fd, "\""); + } PUTS(fd, ", line "); + if (_PyObject_IsFreed((PyObject *)code) || !PyCode_Check(code)) { + PUTS(fd, "??? in ???\n"); + return -1; + } int lasti = _PyFrame_SafeGetLasti(frame); int lineno = -1; if (lasti >= 0) { @@ -1070,13 +1080,18 @@ dump_frame(int fd, _PyInterpreterFrame *frame) } PUTS(fd, " in "); - if (code->co_name != NULL && PyUnicode_Check(code->co_name)) { - _Py_DumpASCII(fd, code->co_name); + if (_PyObject_IsFreed((PyObject *)code) || !PyCode_Check(code)) { + PUTS(fd, "???\n"); + return -1; } - else { + PyObject *name = code->co_name; + if (_PyObject_IsFreed(name) || !PyUnicode_Check(name)) { PUTS(fd, "???"); res = -1; } + else { + _Py_DumpASCII(fd, name); + } PUTS(fd, "\n"); return res; }