Skip to content

Segfault from _PyDict_NewKeysForClass under memory pressure #153182

Description

@stestagg

Crash report

What happened?

import _testcapi

def make_class():
    class C:
        def f(self):
            self.x = 1

    return C

for start in range(100):
    _testcapi.set_nomemory(start, start + 1)
    try:
        x = make_class()
    except MemoryError:
        pass
fish: Job 1, './python.exe staticattrs.py' terminated by signal SIGSEGV (Address boundary error)

If an allocation failure happens when defining a class that has a non-empty __static_attributes__, then you can get a null pointer deref.

Root cause

Early during the class build process, __static_attributes__ is correctly populated with the list of detected static attributes.

Then later during PyType_Ready(): type_ready_managed_dict is called to make the shared key cache.
This calls _PyDict_NewKeysForClass and this line:

PyDictKeysObject *keys = new_keys_object(NEXT_LOG2_SHARED_KEYS_MAX_SIZE, 1);

Tries to allocate a keys object, and if that allocation fails, just clears the MemoryError and continues, skipping out some keys setup steps.

However, later on in that _PyDict_NewKeysForClass it fetches the __static_attributes__ tuple and tries to populate them into the keys object:

cpython/Objects/dictobject.c

Lines 7232 to 7238 in 9f9787d

for (Py_ssize_t i = 0; i < PyTuple_GET_SIZE(attrs); i++) {
PyObject *key = PyTuple_GET_ITEM(attrs, i);
Py_hash_t hash;
if (PyUnicode_CheckExact(key) && (hash = unicode_get_hash(key)) != -1) {
if (insert_split_key(keys, key, hash) == DKIX_EMPTY) {
break;
}

And insert_split_key reasonably assumes that keys is a valid Keys object, resulting in a relative pointer from NULL dereferenec.

Stacktrace

frame #0  _DK_ENTRIES(dk=<unavailable>) at pycore_dict.h:269
frame #1  do_lookup(mp=0x0, dk=0x0, ...) at dictobject.c:1090
frame #2  unicodekeys_lookup_unicode(dk=0x0, ...) at dictobject.c:1183
frame #3  insert_split_key(keys=0x0, ...) at dictobject.c:1931
frame #4  _PyDict_NewKeysForClass(cls=0x0000aaaaabfffa70) at dictobject.c:7236
frame #5  type_ready_managed_dict(type=0x0000aaaaabfffa70) at typeobject.c:9473
frame #6  type_ready(...) at typeobject.c:9578
frame #7  PyType_Ready(...) at typeobject.c:9622

Interestingly bisect failed here, because before this change, there was a different allocation failure segfault from class definition, so either this one is shadowing the other on (I'll check separately) or the other issue has already/accidentally been fixed.

Suggested fix.

Given that _PyDict_NewKeysForClass returns keys at the end anyway, it seems reasonable to just return NULL from the keys == NULL branch here:

cpython/Objects/dictobject.c

Lines 7220 to 7222 in 9f9787d

if (keys == NULL) {
PyErr_Clear();
}

CPython versions tested on:

CPython main branch

Operating systems tested on:

Linux

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

Python 3.16.0a0 (heads/main-dirty:639a552, Jul 4 2026, 21:13:52) [Clang 22.1.6 ]

Linked PRs

Metadata

Metadata

Assignees

No one assigned

    Labels

    interpreter-core(Objects, Python, Grammar, and Parser dirs)type-crashA hard crash of the interpreter, possibly with a core dump

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions