From f03febe26c753bb955fd4b59fbab385e9746507d Mon Sep 17 00:00:00 2001 From: nvnzno Date: Wed, 23 Sep 2026 03:20:28 +0200 Subject: [PATCH 1/2] [3.12] gh-157947: Keep exception fields alive during suggestion lookups get_suggestions_for_attribute_error(), offer_suggestions_for_name_error() and offer_suggestions_for_import_error() used borrowed references to exc->name/exc->obj/exc->name_from and the traceback frame across calls that can invoke arbitrary Python code (PyObject_Dir, PyImport_GetModule, _PyObject_LookupAttr). That code can rebind or clear the exception's attributes and free the referenced objects, leaving dangling pointers that calculate_suggestions() then dereferences -> use-after-free. Take strong references for the duration of the calls that may run arbitrary code. --- Lib/test/test_traceback.py | 21 +++++++++++++++++++++ Python/suggestions.c | 25 +++++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/Lib/test/test_traceback.py b/Lib/test/test_traceback.py index 8300e511cf4232..8eca38ae6e88e9 100644 --- a/Lib/test/test_traceback.py +++ b/Lib/test/test_traceback.py @@ -3220,6 +3220,27 @@ def __dir__(self): actual = self.get_suggestion(A(), 'blech') self.assertNotIn("Did you mean", actual) + def test_attribute_error_name_cleared_in_dir(self): + # gh-157947: PyObject_Dir() can run code that mutates exc.name/exc.obj + # while suggestion computation still holds borrowed references to them. + class Evil: + def __dir__(self): + exc = sys.exc_info()[1] + if isinstance(exc, AttributeError): + exc.name = "replaced" + exc.obj = None + return ["x"] + + def callable(): + raise AttributeError("boom", + name="X" * (4 * 1024 * 1024) + "!", + obj=Evil()) + + result_lines = self.get_exception( + callable, slice_start=-1, slice_end=None + ) + self.assertIn("AttributeError", result_lines[-1]) + def test_attribute_error_with_failing_dict(self): class T: bluch = 1 diff --git a/Python/suggestions.c b/Python/suggestions.c index ad58393490efc2..3bba7c4a83f3f3 100644 --- a/Python/suggestions.c +++ b/Python/suggestions.c @@ -190,12 +190,19 @@ get_suggestions_for_attribute_error(PyAttributeErrorObject *exc) return NULL; } + // PyObject_Dir() can invoke arbitrary code which may mutate or clear + // exc->name/exc->obj, so keep our own references alive across the call. + Py_INCREF(name); + Py_INCREF(obj); PyObject *dir = PyObject_Dir(obj); + Py_DECREF(obj); if (dir == NULL) { + Py_DECREF(name); return NULL; } PyObject *suggestions = calculate_suggestions(dir, name); + Py_DECREF(name); Py_DECREF(dir); return suggestions; } @@ -329,8 +336,15 @@ offer_suggestions_for_name_error(PyNameErrorObject *exc) PyFrameObject *frame = traceback->tb_frame; assert(frame != NULL); + // get_suggestions_for_name_error() can invoke arbitrary code which may + // mutate or clear exc->name/exc->traceback, so keep our own references + // alive across the call. + Py_INCREF(name); + Py_INCREF(frame); PyObject* suggestion = get_suggestions_for_name_error(name, frame); + Py_DECREF(frame); if (suggestion == NULL && PyErr_Occurred()) { + Py_DECREF(name); return NULL; } @@ -338,6 +352,7 @@ offer_suggestions_for_name_error(PyNameErrorObject *exc) PyObject* result = NULL; if (!is_name_stdlib_module(name)) { if (suggestion == NULL) { + Py_DECREF(name); return NULL; } result = PyUnicode_FromFormat(". Did you mean: %R?", suggestion); @@ -346,6 +361,7 @@ offer_suggestions_for_name_error(PyNameErrorObject *exc) } else { result = PyUnicode_FromFormat(". Did you mean: %R? Or did you forget to import %R?", suggestion, name); } + Py_DECREF(name); Py_XDECREF(suggestion); return result; } @@ -360,18 +376,27 @@ offer_suggestions_for_import_error(PyImportErrorObject *exc) return NULL; } + // PyImport_GetModule() and PyObject_Dir() can invoke arbitrary code which + // may mutate or clear exc->name/exc->name_from, so keep our own references + // alive across the calls. + Py_INCREF(name); + Py_INCREF(mod_name); PyObject* mod = PyImport_GetModule(mod_name); + Py_DECREF(mod_name); if (mod == NULL) { + Py_DECREF(name); return NULL; } PyObject *dir = PyObject_Dir(mod); Py_DECREF(mod); if (dir == NULL) { + Py_DECREF(name); return NULL; } PyObject *suggestion = calculate_suggestions(dir, name); + Py_DECREF(name); Py_DECREF(dir); if (!suggestion) { return NULL; From 32b4697e49b81141974f17b5876c5e86bc5f1d31 Mon Sep 17 00:00:00 2001 From: nvnzno Date: Wed, 23 Sep 2026 03:22:22 +0200 Subject: [PATCH 2/2] Add NEWS entry --- .../Security/2026-09-22-15-30-00.gh-issue-157947.vK2mQp.rst | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 Misc/NEWS.d/next/Security/2026-09-22-15-30-00.gh-issue-157947.vK2mQp.rst diff --git a/Misc/NEWS.d/next/Security/2026-09-22-15-30-00.gh-issue-157947.vK2mQp.rst b/Misc/NEWS.d/next/Security/2026-09-22-15-30-00.gh-issue-157947.vK2mQp.rst new file mode 100644 index 00000000000000..e3abbf8ede9d71 --- /dev/null +++ b/Misc/NEWS.d/next/Security/2026-09-22-15-30-00.gh-issue-157947.vK2mQp.rst @@ -0,0 +1,5 @@ +Fix use-after-free in exception "Did you mean" suggestions: the C +implementation held borrowed references to :exc:`AttributeError`, +:exc:`NameError` and :exc:`ImportError` fields across calls that can +invoke arbitrary Python code (``__dir__``, attribute lookups, module +imports) which may rebind or clear those fields.