Skip to content

Commit 1a9b776

Browse files
committed
gh-142731: fix re-entrant __hash__ UAF in setattr/delattr paths
1 parent e779777 commit 1a9b776

4 files changed

Lines changed: 84 additions & 0 deletions

File tree

Lib/test/test_builtin.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -643,6 +643,32 @@ def test_delattr(self):
643643
msg = r"^attribute name must be string, not 'int'$"
644644
self.assertRaisesRegex(TypeError, msg, delattr, sys, 1)
645645

646+
def test_delattr_reentrant_name_hash_does_not_crash(self):
647+
code = dedent("""
648+
import gc
649+
650+
class Victim:
651+
pass
652+
653+
class Evil(str):
654+
def __hash__(self):
655+
old = target.__dict__
656+
target.__dict__ = {}
657+
del old
658+
for _ in range(32):
659+
dict.fromkeys(range(4), 1)
660+
gc.collect()
661+
return hash(str(self))
662+
663+
for _ in range(2000):
664+
target = Victim()
665+
try:
666+
delattr(target, Evil("missing"))
667+
except AttributeError:
668+
pass
669+
""")
670+
assert_python_ok("-c", code)
671+
646672
def test_dir(self):
647673
# dir(wrong number of arguments)
648674
self.assertRaises(TypeError, dir, 42, 42)
@@ -1994,6 +2020,29 @@ def test_setattr(self):
19942020
msg = r"^attribute name must be string, not 'int'$"
19952021
self.assertRaisesRegex(TypeError, msg, setattr, sys, 1, 'spam')
19962022

2023+
def test_setattr_reentrant_name_hash_does_not_crash(self):
2024+
code = dedent("""
2025+
import gc
2026+
2027+
class Victim:
2028+
pass
2029+
2030+
class Evil(str):
2031+
def __hash__(self):
2032+
old = target.__dict__
2033+
target.__dict__ = {}
2034+
del old
2035+
for _ in range(32):
2036+
dict.fromkeys(range(4), 1)
2037+
gc.collect()
2038+
return hash(str(self))
2039+
2040+
for _ in range(2000):
2041+
target = Victim()
2042+
setattr(target, Evil("name"), 1)
2043+
""")
2044+
assert_python_ok("-c", code)
2045+
19972046
# test_str(): see test_str.py and test_bytes.py for str() tests.
19982047

19992048
def test_sum(self):

Lib/test/test_descr.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4691,6 +4691,33 @@ def test_carloverre(self):
46914691
else:
46924692
self.fail("Carlo Verre __delattr__ succeeded!")
46934693

4694+
def test_object_setattr_delattr_reentrant_name_hash_does_not_crash(self):
4695+
code = textwrap.dedent("""
4696+
import gc
4697+
4698+
class Victim:
4699+
pass
4700+
4701+
class Evil(str):
4702+
def __hash__(self):
4703+
old = target.__dict__
4704+
target.__dict__ = {}
4705+
del old
4706+
for _ in range(32):
4707+
dict.fromkeys(range(4), 1)
4708+
gc.collect()
4709+
return hash(str(self))
4710+
4711+
for _ in range(2000):
4712+
target = Victim()
4713+
object.__setattr__(target, Evil("name"), 1)
4714+
try:
4715+
object.__delattr__(target, Evil("missing"))
4716+
except AttributeError:
4717+
pass
4718+
""")
4719+
assert_python_ok("-c", code)
4720+
46944721
def test_carloverre_multi_inherit_valid(self):
46954722
class A(type):
46964723
def __setattr__(cls, key, value):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Fix a use-after-free in instance attribute setting/deletion when a str subclass
2+
attribute name has a re-entrant ``__hash__`` that replaces ``__dict__``.

Objects/dictobject.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7108,6 +7108,8 @@ store_instance_attr_dict(PyObject *obj, PyDictObject *dict, PyObject *name, PyOb
71087108
{
71097109
PyDictValues *values = _PyObject_InlineValues(obj);
71107110
int res;
7111+
// Keep the dict alive across potentially re-entrant hashing of 'name'.
7112+
Py_INCREF(dict);
71117113
Py_BEGIN_CRITICAL_SECTION(dict);
71127114
if (dict->ma_values == values) {
71137115
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
71167118
res = _PyDict_SetItem_LockHeld(dict, name, value);
71177119
}
71187120
Py_END_CRITICAL_SECTION();
7121+
Py_DECREF(dict);
71197122
return res;
71207123
}
71217124

@@ -7696,10 +7699,13 @@ _PyObjectDict_SetItem(PyTypeObject *tp, PyObject *obj, PyObject **dictptr,
76967699
return -1;
76977700
}
76987701

7702+
// Keep the dict alive across potentially re-entrant hashing of 'key'.
7703+
Py_INCREF(dict);
76997704
Py_BEGIN_CRITICAL_SECTION(dict);
77007705
res = _PyDict_SetItem_LockHeld((PyDictObject *)dict, key, value);
77017706
ASSERT_CONSISTENT(dict);
77027707
Py_END_CRITICAL_SECTION();
7708+
Py_DECREF(dict);
77037709
return res;
77047710
}
77057711

0 commit comments

Comments
 (0)