From d0e5519e1b773ec6817204f3a051a3c4396ad281 Mon Sep 17 00:00:00 2001 From: lazerg Date: Sat, 10 Oct 2026 11:42:38 +0500 Subject: [PATCH 1/2] gh-159098: Keep the type alive during generic attribute lookup --- Lib/test/test_descr.py | 34 +++++++++++++++++++ ...-10-10-06-40-25.gh-issue-159098.dT2h-w.rst | 2 ++ Objects/object.c | 2 ++ 3 files changed, 38 insertions(+) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-10-10-06-40-25.gh-issue-159098.dT2h-w.rst diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index a6ebda5bf4e5df7..face087a579b735 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -5052,6 +5052,40 @@ def __getattribute__(self, name): self.assertRaises(AttributeError, getattr, EvilGetattribute(), "attr") + def test_getattr_name_changes_class(self): + # gh-159098: The type could be freed while hashing or comparing + # the attribute name. + class Replacement: + pass + + def make_obj(): + class Victim: + pass + return Victim() + + class HashChangesClass(str): + def __hash__(self): + obj.__class__ = Replacement + gc.collect() + return super().__hash__() + + obj = make_obj() + with self.assertRaises(AttributeError): + getattr(obj, HashChangesClass("missing")) + + class EqChangesClass(str): + def __hash__(self): + return hash("pad") + def __eq__(self, other): + obj.__class__ = Replacement + gc.collect() + return False + + obj = make_obj() + obj.pad = None + with self.assertRaises(AttributeError): + getattr(obj, EqChangesClass("missing")) + def test_type___getattribute__(self): self.assertRaises(TypeError, type.__getattribute__, list, type) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-10-06-40-25.gh-issue-159098.dT2h-w.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-10-06-40-25.gh-issue-159098.dT2h-w.rst new file mode 100644 index 000000000000000..16d314e9d38410f --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-10-06-40-25.gh-issue-159098.dT2h-w.rst @@ -0,0 +1,2 @@ +Fix a crash in :func:`getattr` when hashing or comparing a :class:`str` +subclass used as the attribute name changes the object's class. diff --git a/Objects/object.c b/Objects/object.c index e334097d31c4325..28d879eeed65453 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -1912,6 +1912,7 @@ _PyObject_GenericGetAttrWithDict(PyObject *obj, PyObject *name, } Py_INCREF(name); + _Py_INCREF_TYPE(tp); PyThreadState *tstate = _PyThreadState_GET(); _PyCStackRef cref; @@ -2016,6 +2017,7 @@ _PyObject_GenericGetAttrWithDict(PyObject *obj, PyObject *name, } done: _PyThreadState_PopCStackRef(tstate, &cref); + _Py_DECREF_TYPE(tp); Py_DECREF(name); return res; } From c82e5092b903819339aa53d4834e31a3c478e259 Mon Sep 17 00:00:00 2001 From: lazerg Date: Sat, 10 Oct 2026 12:19:58 +0500 Subject: [PATCH 2/2] Also keep the type alive in the method lookup paths --- Lib/test/test_descr.py | 13 +++++++++++++ ...2026-10-10-06-40-25.gh-issue-159098.dT2h-w.rst | 4 ++-- Objects/object.c | 15 +++++++++++++++ 3 files changed, 30 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index face087a579b735..4873830e61ddc70 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -5086,6 +5086,19 @@ def __eq__(self, other): with self.assertRaises(AttributeError): getattr(obj, EqChangesClass("missing")) + class KeyChangesClass(str): + def __hash__(self): + return hash("missing") + def __eq__(self, other): + obj.__class__ = Replacement + gc.collect() + return False + + obj = make_obj() + obj.__dict__[KeyChangesClass("pad")] = None + with self.assertRaises(AttributeError): + obj.missing() + def test_type___getattribute__(self): self.assertRaises(TypeError, type.__getattribute__, list, type) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-10-06-40-25.gh-issue-159098.dT2h-w.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-10-06-40-25.gh-issue-159098.dT2h-w.rst index 16d314e9d38410f..3e9668cca28bd8e 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2026-10-10-06-40-25.gh-issue-159098.dT2h-w.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-10-10-06-40-25.gh-issue-159098.dT2h-w.rst @@ -1,2 +1,2 @@ -Fix a crash in :func:`getattr` when hashing or comparing a :class:`str` -subclass used as the attribute name changes the object's class. +Fix a crash in attribute and method lookup when hashing or comparing a +:class:`str` subclass changes the object's class. diff --git a/Objects/object.c b/Objects/object.c index 28d879eeed65453..130783f1131333f 100644 --- a/Objects/object.c +++ b/Objects/object.c @@ -1709,12 +1709,14 @@ _PyObject_GetMethod(PyObject *obj, PyObject *name, PyObject **method) dict = NULL; } } + _Py_INCREF_TYPE(tp); if (dict != NULL) { Py_INCREF(dict); if (PyDict_GetItemRef(dict, name, method) != 0) { // found or error Py_DECREF(dict); Py_XDECREF(descr); + _Py_DECREF_TYPE(tp); return 0; } // not found @@ -1723,23 +1725,27 @@ _PyObject_GetMethod(PyObject *obj, PyObject *name, PyObject **method) if (meth_found) { *method = descr; + _Py_DECREF_TYPE(tp); return 1; } if (f != NULL) { *method = f(descr, obj, (PyObject *)Py_TYPE(obj)); Py_DECREF(descr); + _Py_DECREF_TYPE(tp); return 0; } if (descr != NULL) { *method = descr; + _Py_DECREF_TYPE(tp); return 0; } PyErr_Format(PyExc_AttributeError, "'%.100s' object has no attribute '%U'", tp->tp_name, name); + _Py_DECREF_TYPE(tp); _PyObject_SetAttributeErrorContext(obj, name); return 0; @@ -1823,22 +1829,26 @@ _PyObject_GetMethodStackRef(PyThreadState *ts, _PyStackRef *self, dict = NULL; } } + _Py_INCREF_TYPE(tp); if (dict != NULL) { assert(PyUnicode_CheckExact(name)); int found = _PyDict_GetMethodStackRef((PyDictObject *)dict, name, method); if (found < 0) { assert(PyStackRef_IsNull(*method)); PyStackRef_CLEAR(*self); + _Py_DECREF_TYPE(tp); return -1; } else if (found) { PyStackRef_CLEAR(*self); + _Py_DECREF_TYPE(tp); return 0; } } if (meth_found) { assert(!PyStackRef_IsNull(*method)); + _Py_DECREF_TYPE(tp); return 1; } @@ -1847,17 +1857,20 @@ _PyObject_GetMethodStackRef(PyThreadState *ts, _PyStackRef *self, PyObject *callable = _PyClassMethod_GetFunc(descr); PyStackRef_XSETREF(*method, PyStackRef_FromPyObjectNew(callable)); PyStackRef_XSETREF(*self, PyStackRef_FromPyObjectNew((PyObject *)tp)); + _Py_DECREF_TYPE(tp); return 1; } else if (Py_IS_TYPE(descr, &PyStaticMethod_Type)) { PyObject *callable = _PyStaticMethod_GetFunc(descr); PyStackRef_XSETREF(*method, PyStackRef_FromPyObjectNew(callable)); PyStackRef_CLEAR(*self); + _Py_DECREF_TYPE(tp); return 0; } PyObject *value = f(descr, obj, (PyObject *)tp); PyStackRef_CLEAR(*method); PyStackRef_CLEAR(*self); + _Py_DECREF_TYPE(tp); if (value) { *method = PyStackRef_FromPyObjectSteal(value); return 0; @@ -1868,12 +1881,14 @@ _PyObject_GetMethodStackRef(PyThreadState *ts, _PyStackRef *self, if (descr != NULL) { assert(!PyStackRef_IsNull(*method)); PyStackRef_CLEAR(*self); + _Py_DECREF_TYPE(tp); return 0; } PyErr_Format(PyExc_AttributeError, "'%.100s' object has no attribute '%U'", tp->tp_name, name); + _Py_DECREF_TYPE(tp); _PyObject_SetAttributeErrorContext(obj, name); assert(PyStackRef_IsNull(*method));