diff --git a/Lib/test/test_builtin.py b/Lib/test/test_builtin.py index 7b69374b1868d1..13eb34424a444a 100644 --- a/Lib/test/test_builtin.py +++ b/Lib/test/test_builtin.py @@ -643,6 +643,32 @@ def test_delattr(self): msg = r"^attribute name must be string, not 'int'$" self.assertRaisesRegex(TypeError, msg, delattr, sys, 1) + def test_delattr_reentrant_name_hash_does_not_crash(self): + code = dedent(""" + import gc + + class Victim: + pass + + class Evil(str): + def __hash__(self): + old = target.__dict__ + target.__dict__ = {} + del old + for _ in range(32): + dict.fromkeys(range(4), 1) + gc.collect() + return hash(str(self)) + + for _ in range(2000): + target = Victim() + try: + delattr(target, Evil("missing")) + except AttributeError: + pass + """) + assert_python_ok("-c", code) + def test_dir(self): # dir(wrong number of arguments) self.assertRaises(TypeError, dir, 42, 42) @@ -1994,6 +2020,29 @@ def test_setattr(self): msg = r"^attribute name must be string, not 'int'$" self.assertRaisesRegex(TypeError, msg, setattr, sys, 1, 'spam') + def test_setattr_reentrant_name_hash_does_not_crash(self): + code = dedent(""" + import gc + + class Victim: + pass + + class Evil(str): + def __hash__(self): + old = target.__dict__ + target.__dict__ = {} + del old + for _ in range(32): + dict.fromkeys(range(4), 1) + gc.collect() + return hash(str(self)) + + for _ in range(2000): + target = Victim() + setattr(target, Evil("name"), 1) + """) + assert_python_ok("-c", code) + # test_str(): see test_str.py and test_bytes.py for str() tests. def test_sum(self): diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index d6e3719479a214..d4d0ab92ddadb7 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -4691,6 +4691,33 @@ def test_carloverre(self): else: self.fail("Carlo Verre __delattr__ succeeded!") + def test_object_setattr_delattr_reentrant_name_hash_does_not_crash(self): + code = textwrap.dedent(""" + import gc + + class Victim: + pass + + class Evil(str): + def __hash__(self): + old = target.__dict__ + target.__dict__ = {} + del old + for _ in range(32): + dict.fromkeys(range(4), 1) + gc.collect() + return hash(str(self)) + + for _ in range(2000): + target = Victim() + object.__setattr__(target, Evil("name"), 1) + try: + object.__delattr__(target, Evil("missing")) + except AttributeError: + pass + """) + assert_python_ok("-c", code) + def test_carloverre_multi_inherit_valid(self): class A(type): def __setattr__(cls, key, value): diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-02-17-20-30-00.gh-issue-142731.kM4pQ2.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-17-20-30-00.gh-issue-142731.kM4pQ2.rst new file mode 100644 index 00000000000000..7b513d73214afa --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-02-17-20-30-00.gh-issue-142731.kM4pQ2.rst @@ -0,0 +1,2 @@ +Fix a use-after-free in instance attribute setting/deletion when a str subclass +attribute name has a re-entrant ``__hash__`` that replaces ``__dict__``. diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 0959e2c78a3289..e59474c73832a2 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -7108,6 +7108,8 @@ store_instance_attr_dict(PyObject *obj, PyDictObject *dict, PyObject *name, PyOb { PyDictValues *values = _PyObject_InlineValues(obj); int res; + // Keep the dict alive across potentially re-entrant hashing of 'name'. + Py_INCREF(dict); Py_BEGIN_CRITICAL_SECTION(dict); if (dict->ma_values == values) { res = store_instance_attr_lock_held(obj, values, name, value); @@ -7116,6 +7118,7 @@ store_instance_attr_dict(PyObject *obj, PyDictObject *dict, PyObject *name, PyOb res = _PyDict_SetItem_LockHeld(dict, name, value); } Py_END_CRITICAL_SECTION(); + Py_DECREF(dict); return res; } @@ -7696,10 +7699,13 @@ _PyObjectDict_SetItem(PyTypeObject *tp, PyObject *obj, PyObject **dictptr, return -1; } + // Keep the dict alive across potentially re-entrant hashing of 'key'. + Py_INCREF(dict); Py_BEGIN_CRITICAL_SECTION(dict); res = _PyDict_SetItem_LockHeld((PyDictObject *)dict, key, value); ASSERT_CONSISTENT(dict); Py_END_CRITICAL_SECTION(); + Py_DECREF(dict); return res; }