Skip to content

Direct class dict modification through gc.get_referrers() leads to an invalid type cache and a segfault #113631

@jstasiak

Description

@jstasiak

Crash report

What happened?

Over at Eventlet an interesting crash has been discovered: eventlet/eventlet#864

I managed to narrow it down to something like this

# crash.py
import gc

class Cls:
    var = []

for ref in gc.get_referrers(Cls.var):
    del ref['var']

# Depending on the environment only a subset of these calls is needed
# to trigger the segfault:
gc.collect()
print(Cls.var)
gc.collect()
print(Cls.var)

It crashes with all currently supported Python versions (3.8-3.12) and with the current main branch:

% python3 crash.py
[]
zsh: segmentation fault  python3 crash.py

I'm not familiar with this code but I think the problem is caused by the type cache being unaware of the class' dictionary being modified. If I disable the type cache lookup in _PyType_Lookup like so

diff --git a/Objects/typeobject.c b/Objects/typeobject.c
index ea29a38d74ae..c6e65593b454 100644
--- a/Objects/typeobject.c
+++ b/Objects/typeobject.c
@@ -4759,13 +4759,13 @@ _PyType_Lookup(PyTypeObject *type, PyObject *name)
     unsigned int h = MCACHE_HASH_METHOD(type, name);
     struct type_cache *cache = get_type_cache();
     struct type_cache_entry *entry = &cache->hashtable[h];
-    if (entry->version == type->tp_version_tag &&
-        entry->name == name) {
-        assert(_PyType_HasFeature(type, Py_TPFLAGS_VALID_VERSION_TAG));
-        OBJECT_STAT_INC_COND(type_cache_hits, !is_dunder_name(name));
-        OBJECT_STAT_INC_COND(type_cache_dunder_hits, is_dunder_name(name));
-        return entry->value;
-    }
+    // if (entry->version == type->tp_version_tag &&
+    //     entry->name == name) {
+    //     assert(_PyType_HasFeature(type, Py_TPFLAGS_VALID_VERSION_TAG));
+    //     OBJECT_STAT_INC_COND(type_cache_hits, !is_dunder_name(name));
+    //     OBJECT_STAT_INC_COND(type_cache_dunder_hits, is_dunder_name(name));
+    //     return entry->value;
+    // }
     OBJECT_STAT_INC_COND(type_cache_misses, !is_dunder_name(name));
     OBJECT_STAT_INC_COND(type_cache_dunder_misses, is_dunder_name(name));

the issue goes away:

% ~/projects/cpython/python.exe crash.py 
Traceback (most recent call last):
  File "/Users/user/crash.py", line 12, in <module>
    print(Cls.var)
          ^^^^^^^
AttributeError: type object 'Cls' has no attribute 'var'

I don't feel confident enough to produce a good fix for this at this stage.

CPython versions tested on:

3.8, 3.9, 3.10, 3.11, 3.12, CPython main branch

Operating systems tested on:

Linux, macOS

Output from running 'python -VV' on the command line:

No response

Metadata

Metadata

Assignees

No one assigned

    Labels

    type-crashA hard crash of the interpreter, possibly with a core dump

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions