Skip to content

Crash due to racy read in dictobject do_lookup under free threading #132869

Open
@hawkinsp

Description

@hawkinsp

Crash report

What happened?

Repro:

import concurrent.futures
import functools
import threading
import typing

def closure(b, c):
  b.wait()
  for i in range(10):
    getattr(c, str(i), None)
    setattr(c, str(i), 99)

with concurrent.futures.ThreadPoolExecutor(max_workers=32) as executor:
  for _ in range(100):
    class MyClass:
      pass
    b = threading.Barrier(32)
    o = MyClass()
    for j in range(32):
      executor.submit(functools.partial(closure, b, o))

with an interpreter build with free threading and --with-pydebug this will eventually crash with:

python: Objects/dictobject.c:1139: int compare_unicode_unicode(PyDictObject *, PyDictKeysObject *, void *, Py_ssize_t, PyObject *, Py_hash_t): Assertion `ep_key != NULL' failed.

From a JAX CI job that exhibited the problem, I have this stack trace:

    #5 __tsan::CallUserSignalHandler(__tsan::ThreadState*, bool, bool, int, __sanitizer::__sanitizer_siginfo*, void*) tsan_interceptors_posix.cpp.o (python3.13+0xe5db5) (BuildId: 9b1024b81603c5cc913dc4b031a6f189f128dafb)
    #6 _PyDictKeys_StringLookup /usr/local/google/home/phawkins/p/cpython/Objects/dictobject.c:1209:12 (python3.13+0x2f4fc5) (BuildId: 9b1024b81603c5cc913dc4b031a6f189f128dafb)
    #7 _PyObject_TryGetInstanceAttribute /usr/local/google/home/phawkins/p/cpython/Objects/dictobject.c:6975:21 (python3.13+0x2f4fc5)
    #8 _PyObject_GenericGetAttrWithDict /usr/local/google/home/phawkins/p/cpython/Objects/object.c:1676:17 (python3.13+0x320157) (BuildId: 9b1024b81603c5cc913dc4b031a6f189f128dafb)
    #9 PyObject_GenericGetAttr /usr/local/google/home/phawkins/p/cpython/Objects/object.c:1751:12 (python3.13+0x31ff14) (BuildId: 9b1024b81603c5cc913dc4b031a6f189f128dafb)
    #10 PyObject_GetAttr /usr/local/google/home/phawkins/p/cpython/Objects/object.c:1261:18 (python3.13+0x31f44a) (BuildId: 9b1024b81603c5cc913dc4b031a6f189f128dafb)
    #11 _PyEval_EvalFrameDefault /usr/local/google/home/phawkins/p/cpython/Python/generated_cases.c.h:3770:28 (python3.13+0x4c00a0) (BuildId: 9b1024b81603c5cc913dc4b031a6f189f128dafb)
    #12 _PyEval_EvalFrame /usr/local/google/home/phawkins/p/cpython/./Include/internal/pycore_ceval.h:119:16 (python3.13+0x4a8283) (BuildId: 9b1024b81603c5cc913dc4b031a6f189f128dafb)
    #13 _PyEval_Vector /usr/local/google/home/phawkins/p/cpython/Python/ceval.c:1816:12 (python3.13+0x4a8283)
    #14 _PyFunction_Vectorcall /usr/local/google/home/phawkins/p/cpython/Objects/call.c (python3.13+0x2595dc) (BuildId: 9b1024b81603c5cc913dc4b031a6f189f128dafb)
    #15 _PyObject_VectorcallDictTstate /usr/local/google/home/phawkins/p/cpython/Objects/call.c:146:15 (python3.13+0x257864) (BuildId: 9b1024b81603c5cc913dc4b031a6f189f128dafb)
    #16 _PyObject_Call_Prepend /usr/local/google/home/phawkins/p/cpython/Objects/call.c:504:24 (python3.13+0x259a42) (BuildId: 9b1024b81603c5cc913dc4b031a6f189f128dafb)

I think what is happening here is that do_lookup does an unlocked read of the index:

ix = dictkeys_get_index(dk, i);

and then the key:
PyObject *ep_key = FT_ATOMIC_LOAD_PTR_RELAXED(ep->me_key);

but a writer may set the index of a fresh entry before populating the key. For example, this writer would do that:

dictkeys_set_index(mp->ma_keys, hashpos, mp->ma_keys->dk_nentries);

(I don't know which writer is involved here.)

This is from the 3.13 branch at commit 341b86e .

CPython versions tested on:

3.13

Operating systems tested on:

Linux

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

Python 3.13.3+ experimental free-threading build (heads/3.13:341b86e095e, Apr 23 2025, 21:51:53) [Clang 18.1.8 (16+build1)]

Metadata

Metadata

Assignees

No one assigned

    Labels

    interpreter-core(Objects, Python, Grammar, and Parser dirs)topic-free-threadingtype-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