Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions Lib/test/test_traceback.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
25 changes: 25 additions & 0 deletions Python/suggestions.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -329,15 +336,23 @@ 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;
}

// Add a trailer ". Did you mean: (...)?"
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);
Expand All @@ -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;
}
Expand All @@ -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;
Expand Down