Skip to content

gh-151722: Defer GC tracking of frozendict to end of construction#151740

Open
corona10 wants to merge 5 commits into
python:mainfrom
corona10:gh-151722
Open

gh-151722: Defer GC tracking of frozendict to end of construction#151740
corona10 wants to merge 5 commits into
python:mainfrom
corona10:gh-151722

Conversation

@corona10

@corona10 corona10 commented Jun 19, 2026

Copy link
Copy Markdown
Member

@corona10

corona10 commented Jun 19, 2026

Copy link
Copy Markdown
Member Author

TSAN script

import threading, gc

N = 4000
stop = threading.Event()
box = []

def reader():
    while not stop.is_set():
        if box:
            o = box[0]
            try:
                len(o); hash(o); repr(o)
            except Exception:
                pass

class Evil:
    def __init__(self):
        self.n = 0
    def keys(self):
        return [f"k{i}" for i in range(N)]
    def __getitem__(self, k):
        self.n += 1
        if self.n == 10 and not box:        # one scan/construction; k0..k8 present
            for o in gc.get_objects():
                if type(o) is frozendict and "k0" in o and len(o) < N:
                    box.append(o)
                    break
        return 1

t = threading.Thread(target=reader, daemon=True)
t.start()
for _ in range(40):
    box.clear()
    frozendict(Evil())
stop.set()
t.join(timeout=2)
print("observed half-built:", bool(box))

AS-IS

➜  cpython git:(main) ✗ TSAN_OPTIONS="halt_on_error=0" ./python.exe repro_151722.py
==================
WARNING: ThreadSanitizer: data race (pid=56255)
  Atomic write of size 8 at 0x00012a0e63d8 by main thread:
    #0 insert_combined_dict dictobject.c:1906 (python.exe:arm64+0x1000cca38)
    #1 insertdict dictobject.c:2005 (python.exe:arm64+0x1000bcb84)
    #2 setitem_take2_lock_held dictobject.c:2785 (python.exe:arm64+0x1000bbf38)
    #3 dict_merge dictobject.c:4280 (python.exe:arm64+0x1000bfd00)
    #4 frozendict_vectorcall dictobject.c:5331 (python.exe:arm64+0x1000cc248)
    #5 PyObject_Vectorcall call.c:327 (python.exe:arm64+0x100061c10)

  Previous read of size 8 at 0x00012a0e63d8 by thread T1:
    #0 _PyDict_Next dictobject.c:3166 (python.exe:arm64+0x1000be214)
    #1 frozendict_hash dictobject.c:8339 (python.exe:arm64+0x1000cbef4)
    #2 builtin_hash bltinmodule.c:1869 (python.exe:arm64+0x1001c792c)
    #3 pythread_wrapper thread_pthread.h:234 (python.exe:arm64+0x10027a4ec)

  Thread T1 (tid=4632788, running) created by main thread at:
    #0 pthread_create <null> (libclang_rt.tsan_osx_dynamic.dylib:arm64e+0x33834)
    #1 do_start_joinable_thread thread_pthread.h:281 (python.exe:arm64+0x100279b6c)
    #2 do_start_new_thread _threadmodule.c:1919 (python.exe:arm64+0x100332da4)
    #3 thread_PyThread_start_joinable_thread _threadmodule.c:2042 (python.exe:arm64+0x100331f0c)
    #4 cfunction_call methodobject.c:564 (python.exe:arm64+0x1000de0b8)

SUMMARY: ThreadSanitizer: data race dictobject.c:1906 in insert_combined_dict
==================
==================
WARNING: ThreadSanitizer: data race (pid=56255)
  Read of size 8 at 0x00012a0e6488 by thread T1:
    #0 _PyDict_Next dictobject.c:3171 (python.exe:arm64+0x1000be260)
    #1 anydict_repr_impl dictobject.c:3621 (python.exe:arm64+0x1000ce668)
    #2 frozendict_repr dictobject.c:8271 (python.exe:arm64+0x1000cbdb0)
    #3 PyObject_Repr object.c:784 (python.exe:arm64+0x1000e46a0)
    #4 pythread_wrapper thread_pthread.h:234 (python.exe:arm64+0x10027a4ec)

  Previous atomic write of size 8 at 0x00012a0e6488 by main thread:
    #0 insert_combined_dict dictobject.c (python.exe:arm64+0x1000cc9e8)
    #1 insertdict dictobject.c:2005 (python.exe:arm64+0x1000bcb84)
    #2 setitem_take2_lock_held dictobject.c:2785 (python.exe:arm64+0x1000bbf38)
    #3 dict_merge dictobject.c:4280 (python.exe:arm64+0x1000bfd00)
    #4 frozendict_vectorcall dictobject.c:5331 (python.exe:arm64+0x1000cc248)
    #5 PyObject_Vectorcall call.c:327 (python.exe:arm64+0x100061c10)

  Thread T1 (tid=4632788, running) created by main thread at:
    #0 pthread_create <null> (libclang_rt.tsan_osx_dynamic.dylib:arm64e+0x33834)
    #1 do_start_joinable_thread thread_pthread.h:281 (python.exe:arm64+0x100279b6c)
    #2 do_start_new_thread _threadmodule.c:1919 (python.exe:arm64+0x100332da4)
    #3 thread_PyThread_start_joinable_thread _threadmodule.c:2042 (python.exe:arm64+0x100331f0c)
    #4 cfunction_call methodobject.c:564 (python.exe:arm64+0x1000de0b8)

SUMMARY: ThreadSanitizer: data race dictobject.c:3171 in _PyDict_Next
==================
==================
WARNING: ThreadSanitizer: data race (pid=56255)
  Atomic write of size 8 at 0x00012a74ff80 by main thread:
    #0 dictresize dictobject.c:2279 (python.exe:arm64+0x1000cd9b4)
    #1 insert_combined_dict dictobject.c:1881 (python.exe:arm64+0x1000cc84c)
    #2 insertdict dictobject.c:2005 (python.exe:arm64+0x1000bcb84)
    #3 setitem_take2_lock_held dictobject.c:2785 (python.exe:arm64+0x1000bbf38)
    #4 dict_merge dictobject.c:4280 (python.exe:arm64+0x1000bfd00)
    #5 frozendict_vectorcall dictobject.c:5331 (python.exe:arm64+0x1000cc248)
    #6 PyObject_Vectorcall call.c:327 (python.exe:arm64+0x100061c10)

  Previous read of size 8 at 0x00012a74ff80 by thread T1:
    #0 _PyDict_Next dictobject.c:3166 (python.exe:arm64+0x1000be208)
    #1 anydict_repr_impl dictobject.c:3621 (python.exe:arm64+0x1000ce668)
    #2 frozendict_repr dictobject.c:8271 (python.exe:arm64+0x1000cbdb0)
    #3 PyObject_Repr object.c:784 (python.exe:arm64+0x1000e46a0)
    #4 pythread_wrapper thread_pthread.h:234 (python.exe:arm64+0x10027a4ec)

  Thread T1 (tid=4632788, running) created by main thread at:
    #0 pthread_create <null> (libclang_rt.tsan_osx_dynamic.dylib:arm64e+0x33834)
    #1 do_start_joinable_thread thread_pthread.h:281 (python.exe:arm64+0x100279b6c)
    #2 do_start_new_thread _threadmodule.c:1919 (python.exe:arm64+0x100332da4)
    #3 thread_PyThread_start_joinable_thread _threadmodule.c:2042 (python.exe:arm64+0x100331f0c)
    #4 cfunction_call methodobject.c:564 (python.exe:arm64+0x1000de0b8)

SUMMARY: ThreadSanitizer: data race dictobject.c:2279 in dictresize
==================
==================
WARNING: ThreadSanitizer: data race (pid=56255)
  Read of size 8 at 0x00012a0e6480 by thread T1:
    #0 _PyDict_Next dictobject.c:3177 (python.exe:arm64+0x1000be2b8)
    #1 anydict_repr_impl dictobject.c:3621 (python.exe:arm64+0x1000ce668)
    #2 frozendict_repr dictobject.c:8271 (python.exe:arm64+0x1000cbdb0)
    #3 PyObject_Repr object.c:784 (python.exe:arm64+0x1000e46a0)
    #4 pythread_wrapper thread_pthread.h:234 (python.exe:arm64+0x10027a4ec)

  Previous atomic write of size 8 at 0x00012a0e6480 by main thread:
    #0 insert_combined_dict dictobject.c:1895 (python.exe:arm64+0x1000cc9a0)
    #1 insertdict dictobject.c:2005 (python.exe:arm64+0x1000bcb84)
    #2 setitem_take2_lock_held dictobject.c:2785 (python.exe:arm64+0x1000bbf38)
    #3 dict_merge dictobject.c:4280 (python.exe:arm64+0x1000bfd00)
    #4 frozendict_vectorcall dictobject.c:5331 (python.exe:arm64+0x1000cc248)
    #5 PyObject_Vectorcall call.c:327 (python.exe:arm64+0x100061c10)

  Thread T1 (tid=4632788, running) created by main thread at:
    #0 pthread_create <null> (libclang_rt.tsan_osx_dynamic.dylib:arm64e+0x33834)
    #1 do_start_joinable_thread thread_pthread.h:281 (python.exe:arm64+0x100279b6c)
    #2 do_start_new_thread _threadmodule.c:1919 (python.exe:arm64+0x100332da4)
    #3 thread_PyThread_start_joinable_thread _threadmodule.c:2042 (python.exe:arm64+0x100331f0c)
    #4 cfunction_call methodobject.c:564 (python.exe:arm64+0x1000de0b8)

SUMMARY: ThreadSanitizer: data race dictobject.c:3177 in _PyDict_Next
==================
==================
WARNING: ThreadSanitizer: data race (pid=56255)
  Read of size 8 at 0x00012a81c018 by thread T1:
    #0 _PyDict_Next dictobject.c:3166 (python.exe:arm64+0x1000be214)
    #1 anydict_repr_impl dictobject.c:3621 (python.exe:arm64+0x1000ce668)
    #2 frozendict_repr dictobject.c:8271 (python.exe:arm64+0x1000cbdb0)
    #3 PyObject_Repr object.c:784 (python.exe:arm64+0x1000e46a0)
    #4 pythread_wrapper thread_pthread.h:234 (python.exe:arm64+0x10027a4ec)

  Previous atomic write of size 8 at 0x00012a81c018 by main thread:
    #0 insert_combined_dict dictobject.c:1906 (python.exe:arm64+0x1000cca38)
    #1 insertdict dictobject.c:2005 (python.exe:arm64+0x1000bcb84)
    #2 setitem_take2_lock_held dictobject.c:2785 (python.exe:arm64+0x1000bbf38)
    #3 dict_merge dictobject.c:4280 (python.exe:arm64+0x1000bfd00)
    #4 frozendict_vectorcall dictobject.c:5331 (python.exe:arm64+0x1000cc248)
    #5 PyObject_Vectorcall call.c:327 (python.exe:arm64+0x100061c10)

  Thread T1 (tid=4632788, running) created by main thread at:
    #0 pthread_create <null> (libclang_rt.tsan_osx_dynamic.dylib:arm64e+0x33834)
    #1 do_start_joinable_thread thread_pthread.h:281 (python.exe:arm64+0x100279b6c)
    #2 do_start_new_thread _threadmodule.c:1919 (python.exe:arm64+0x100332da4)
    #3 thread_PyThread_start_joinable_thread _threadmodule.c:2042 (python.exe:arm64+0x100331f0c)
    #4 cfunction_call methodobject.c:564 (python.exe:arm64+0x1000de0b8)

SUMMARY: ThreadSanitizer: data race dictobject.c:3166 in _PyDict_Next
==================
==================
WARNING: ThreadSanitizer: data race (pid=56255)
  Atomic write of size 8 at 0x00012a0e6918 by main thread:
    #0 insert_combined_dict dictobject.c:1906 (python.exe:arm64+0x1000cca38)
    #1 insertdict dictobject.c:2005 (python.exe:arm64+0x1000bcb84)
    #2 setitem_take2_lock_held dictobject.c:2785 (python.exe:arm64+0x1000bbf38)
    #3 dict_merge dictobject.c:4280 (python.exe:arm64+0x1000bfd00)
    #4 frozendict_vectorcall dictobject.c:5331 (python.exe:arm64+0x1000cc248)
    #5 _PyEval_EvalFrameDefault generated_cases.c.h:2418 (python.exe:arm64+0x1001d0048)

  Previous read of size 8 at 0x00012a0e6918 by thread T1:
    #0 _PyDict_Next dictobject.c:3166 (python.exe:arm64+0x1000be214)
    #1 anydict_repr_impl dictobject.c:3621 (python.exe:arm64+0x1000ce668)
    #2 frozendict_repr dictobject.c:8271 (python.exe:arm64+0x1000cbdb0)
    #3 PyObject_Repr object.c:784 (python.exe:arm64+0x1000e46a0)
    #4 pythread_wrapper thread_pthread.h:234 (python.exe:arm64+0x10027a4ec)

  Thread T1 (tid=4632788, running) created by main thread at:
    #0 pthread_create <null> (libclang_rt.tsan_osx_dynamic.dylib:arm64e+0x33834)
    #1 do_start_joinable_thread thread_pthread.h:281 (python.exe:arm64+0x100279b6c)
    #2 do_start_new_thread _threadmodule.c:1919 (python.exe:arm64+0x100332da4)
    #3 thread_PyThread_start_joinable_thread _threadmodule.c:2042 (python.exe:arm64+0x100331f0c)
    #4 cfunction_call methodobject.c:564 (python.exe:arm64+0x1000de0b8)

SUMMARY: ThreadSanitizer: data race dictobject.c:1906 in insert_combined_dict
==================
==================
WARNING: ThreadSanitizer: data race (pid=56255)
  Atomic write of size 8 at 0x00012a74ffc0 by main thread:
    #0 insertdict dictobject.c:2008 (python.exe:arm64+0x1000bcba4)
    #1 setitem_take2_lock_held dictobject.c:2785 (python.exe:arm64+0x1000bbf38)
    #2 dict_merge dictobject.c:4280 (python.exe:arm64+0x1000bfd00)
    #3 frozendict_vectorcall dictobject.c:5331 (python.exe:arm64+0x1000cc248)
    #4 _PyEval_EvalFrameDefault generated_cases.c.h:2418 (python.exe:arm64+0x1001d0048)

  Previous read of size 8 at 0x00012a74ffc0 by thread T1:
    #0 frozendict_length dictobject.c:3701 (python.exe:arm64+0x1000d243c)
    #1 _PyEval_EvalFrameDefault generated_cases.c.h:3880 (python.exe:arm64+0x1001d1d98)
    #2 pythread_wrapper thread_pthread.h:234 (python.exe:arm64+0x10027a4ec)

  Thread T1 (tid=4632788, running) created by main thread at:
    #0 pthread_create <null> (libclang_rt.tsan_osx_dynamic.dylib:arm64e+0x33834)
    #1 do_start_joinable_thread thread_pthread.h:281 (python.exe:arm64+0x100279b6c)
    #2 do_start_new_thread _threadmodule.c:1919 (python.exe:arm64+0x100332da4)
    #3 thread_PyThread_start_joinable_thread _threadmodule.c:2042 (python.exe:arm64+0x100331f0c)
    #4 cfunction_call methodobject.c:564 (python.exe:arm64+0x1000de0b8)

SUMMARY: ThreadSanitizer: data race dictobject.c:2008 in insertdict
==================
==================
WARNING: ThreadSanitizer: data race (pid=56255)
  Read of size 8 at 0x00012a0e69c8 by thread T1:
    #0 _PyDict_Next dictobject.c:3171 (python.exe:arm64+0x1000be260)
    #1 anydict_repr_impl dictobject.c:3621 (python.exe:arm64+0x1000ce668)
    #2 frozendict_repr dictobject.c:8271 (python.exe:arm64+0x1000cbdb0)
    #3 PyObject_Repr object.c:784 (python.exe:arm64+0x1000e46a0)
    #4 pythread_wrapper thread_pthread.h:234 (python.exe:arm64+0x10027a4ec)

  Previous atomic write of size 8 at 0x00012a0e69c8 by main thread:
    #0 insert_combined_dict dictobject.c (python.exe:arm64+0x1000cc9e8)
    #1 insertdict dictobject.c:2005 (python.exe:arm64+0x1000bcb84)
    #2 setitem_take2_lock_held dictobject.c:2785 (python.exe:arm64+0x1000bbf38)
    #3 dict_merge dictobject.c:4280 (python.exe:arm64+0x1000bfd00)
    #4 frozendict_vectorcall dictobject.c:5331 (python.exe:arm64+0x1000cc248)
    #5 _PyEval_EvalFrameDefault generated_cases.c.h:2418 (python.exe:arm64+0x1001d0048)

  Thread T1 (tid=4632788, running) created by main thread at:
    #0 pthread_create <null> (libclang_rt.tsan_osx_dynamic.dylib:arm64e+0x33834)
    #1 do_start_joinable_thread thread_pthread.h:281 (python.exe:arm64+0x100279b6c)
    #2 do_start_new_thread _threadmodule.c:1919 (python.exe:arm64+0x100332da4)
    #3 thread_PyThread_start_joinable_thread _threadmodule.c:2042 (python.exe:arm64+0x100331f0c)
    #4 cfunction_call methodobject.c:564 (python.exe:arm64+0x1000de0b8)

SUMMARY: ThreadSanitizer: data race dictobject.c:3171 in _PyDict_Next
==================
==================
WARNING: ThreadSanitizer: data race (pid=56255)
  Atomic write of size 8 at 0x00012a74ffd0 by main thread:
    #0 dictresize dictobject.c:2279 (python.exe:arm64+0x1000cd9b4)
    #1 insert_combined_dict dictobject.c:1881 (python.exe:arm64+0x1000cc84c)
    #2 insertdict dictobject.c:2005 (python.exe:arm64+0x1000bcb84)
    #3 setitem_take2_lock_held dictobject.c:2785 (python.exe:arm64+0x1000bbf38)
    #4 dict_merge dictobject.c:4280 (python.exe:arm64+0x1000bfd00)
    #5 frozendict_vectorcall dictobject.c:5331 (python.exe:arm64+0x1000cc248)
    #6 _PyEval_EvalFrameDefault generated_cases.c.h:2418 (python.exe:arm64+0x1001d0048)

  Previous read of size 8 at 0x00012a74ffd0 by thread T1:
    #0 _PyDict_Next dictobject.c:3166 (python.exe:arm64+0x1000be208)
    #1 anydict_repr_impl dictobject.c:3621 (python.exe:arm64+0x1000ce668)
    #2 frozendict_repr dictobject.c:8271 (python.exe:arm64+0x1000cbdb0)
    #3 PyObject_Repr object.c:784 (python.exe:arm64+0x1000e46a0)
    #4 pythread_wrapper thread_pthread.h:234 (python.exe:arm64+0x10027a4ec)

  Thread T1 (tid=4632788, running) created by main thread at:
    #0 pthread_create <null> (libclang_rt.tsan_osx_dynamic.dylib:arm64e+0x33834)
    #1 do_start_joinable_thread thread_pthread.h:281 (python.exe:arm64+0x100279b6c)
    #2 do_start_new_thread _threadmodule.c:1919 (python.exe:arm64+0x100332da4)
    #3 thread_PyThread_start_joinable_thread _threadmodule.c:2042 (python.exe:arm64+0x100331f0c)
    #4 cfunction_call methodobject.c:564 (python.exe:arm64+0x1000de0b8)

SUMMARY: ThreadSanitizer: data race dictobject.c:2279 in dictresize
==================
==================
WARNING: ThreadSanitizer: data race (pid=56255)
  Read of size 8 at 0x00012a0e69c0 by thread T1:
    #0 _PyDict_Next dictobject.c:3177 (python.exe:arm64+0x1000be2b8)
    #1 anydict_repr_impl dictobject.c:3621 (python.exe:arm64+0x1000ce668)
    #2 frozendict_repr dictobject.c:8271 (python.exe:arm64+0x1000cbdb0)
    #3 PyObject_Repr object.c:784 (python.exe:arm64+0x1000e46a0)
    #4 pythread_wrapper thread_pthread.h:234 (python.exe:arm64+0x10027a4ec)

  Previous atomic write of size 8 at 0x00012a0e69c0 by main thread:
    #0 insert_combined_dict dictobject.c:1895 (python.exe:arm64+0x1000cc9a0)
    #1 insertdict dictobject.c:2005 (python.exe:arm64+0x1000bcb84)
    #2 setitem_take2_lock_held dictobject.c:2785 (python.exe:arm64+0x1000bbf38)
    #3 dict_merge dictobject.c:4280 (python.exe:arm64+0x1000bfd00)
    #4 frozendict_vectorcall dictobject.c:5331 (python.exe:arm64+0x1000cc248)
    #5 _PyEval_EvalFrameDefault generated_cases.c.h:2418 (python.exe:arm64+0x1001d0048)

  Thread T1 (tid=4632788, running) created by main thread at:
    #0 pthread_create <null> (libclang_rt.tsan_osx_dynamic.dylib:arm64e+0x33834)
    #1 do_start_joinable_thread thread_pthread.h:281 (python.exe:arm64+0x100279b6c)
    #2 do_start_new_thread _threadmodule.c:1919 (python.exe:arm64+0x100332da4)
    #3 thread_PyThread_start_joinable_thread _threadmodule.c:2042 (python.exe:arm64+0x100331f0c)
    #4 cfunction_call methodobject.c:564 (python.exe:arm64+0x1000de0b8)

SUMMARY: ThreadSanitizer: data race dictobject.c:3177 in _PyDict_Next
==================
==================
WARNING: ThreadSanitizer: data race (pid=56255)
  Read of size 8 at 0x00012a0ff198 by thread T1:
    #0 _PyDict_Next dictobject.c:3166 (python.exe:arm64+0x1000be214)
    #1 anydict_repr_impl dictobject.c:3621 (python.exe:arm64+0x1000ce448)
    #2 frozendict_repr dictobject.c:8271 (python.exe:arm64+0x1000cbdb0)
    #3 PyObject_Repr object.c:784 (python.exe:arm64+0x1000e46a0)
    #4 pythread_wrapper thread_pthread.h:234 (python.exe:arm64+0x10027a4ec)

  Previous write of size 8 at 0x00012a0ff198 by main thread:
    #0 new_keys_object dictobject.c:853 (python.exe:arm64+0x1000cc6d4)
    #1 dictresize dictobject.c:2174 (python.exe:arm64+0x1000cccd0)
    #2 insert_combined_dict dictobject.c:1881 (python.exe:arm64+0x1000cc84c)
    #3 insertdict dictobject.c:2005 (python.exe:arm64+0x1000bcb84)
    #4 setitem_take2_lock_held dictobject.c:2785 (python.exe:arm64+0x1000bbf38)
    #5 dict_merge dictobject.c:4280 (python.exe:arm64+0x1000bfd00)
    #6 frozendict_vectorcall dictobject.c:5331 (python.exe:arm64+0x1000cc248)
    #7 _PyEval_EvalFrameDefault generated_cases.c.h:2418 (python.exe:arm64+0x1001d0048)

  Thread T1 (tid=4632788, running) created by main thread at:
    #0 pthread_create <null> (libclang_rt.tsan_osx_dynamic.dylib:arm64e+0x33834)
    #1 do_start_joinable_thread thread_pthread.h:281 (python.exe:arm64+0x100279b6c)
    #2 do_start_new_thread _threadmodule.c:1919 (python.exe:arm64+0x100332da4)
    #3 thread_PyThread_start_joinable_thread _threadmodule.c:2042 (python.exe:arm64+0x100331f0c)
    #4 cfunction_call methodobject.c:564 (python.exe:arm64+0x1000de0b8)

SUMMARY: ThreadSanitizer: data race dictobject.c:3166 in _PyDict_Next
==================
==================
WARNING: ThreadSanitizer: data race (pid=56255)
  Read of size 8 at 0x00012a108118 by thread T1:
    #0 _PyDict_Next dictobject.c:3166 (python.exe:arm64+0x1000be214)
    #1 anydict_repr_impl dictobject.c:3621 (python.exe:arm64+0x1000ce668)
    #2 frozendict_repr dictobject.c:8271 (python.exe:arm64+0x1000cbdb0)
    #3 PyObject_Repr object.c:784 (python.exe:arm64+0x1000e46a0)
    #4 pythread_wrapper thread_pthread.h:234 (python.exe:arm64+0x10027a4ec)

  Previous atomic write of size 8 at 0x00012a108118 by main thread:
    #0 dictresize dictobject.c:2292 (python.exe:arm64+0x1000cdb28)
    #1 insert_combined_dict dictobject.c:1881 (python.exe:arm64+0x1000cc84c)
    #2 insertdict dictobject.c:2005 (python.exe:arm64+0x1000bcb84)
    #3 setitem_take2_lock_held dictobject.c:2785 (python.exe:arm64+0x1000bbf38)
    #4 dict_merge dictobject.c:4280 (python.exe:arm64+0x1000bfd00)
    #5 frozendict_vectorcall dictobject.c:5331 (python.exe:arm64+0x1000cc248)
    #6 _PyEval_EvalFrameDefault generated_cases.c.h:2418 (python.exe:arm64+0x1001d0048)

  Thread T1 (tid=4632788, running) created by main thread at:
    #0 pthread_create <null> (libclang_rt.tsan_osx_dynamic.dylib:arm64e+0x33834)
    #1 do_start_joinable_thread thread_pthread.h:281 (python.exe:arm64+0x100279b6c)
    #2 do_start_new_thread _threadmodule.c:1919 (python.exe:arm64+0x100332da4)
    #3 thread_PyThread_start_joinable_thread _threadmodule.c:2042 (python.exe:arm64+0x100331f0c)
    #4 cfunction_call methodobject.c:564 (python.exe:arm64+0x1000de0b8)

SUMMARY: ThreadSanitizer: data race dictobject.c:3166 in _PyDict_Next
==================
ThreadSanitizer:DEADLYSIGNAL
==56255==ERROR: ThreadSanitizer: SEGV on unknown address 0x000000000029 (pc 0x00010106faec bp 0x000170b8a540 sp 0x000170b8a500 T4632788)
==56255==The signal is caused by a READ memory access.
==56255==Hint: address points to the zero page.
    #0 __tsan_atomic64_load <null> (libclang_rt.tsan_osx_dynamic.dylib:arm64e+0x5faec)
    #1 _PyDict_Next dictobject.c (python.exe:arm64+0x1000be2c8)
    #2 anydict_repr_impl dictobject.c:3621 (python.exe:arm64+0x1000ce668)
    #3 frozendict_repr dictobject.c:8271 (python.exe:arm64+0x1000cbdb0)
    #4 PyObject_Repr object.c:784 (python.exe:arm64+0x1000e46a0)
    #5 _PyEval_EvalFrameDefault generated_cases.c.h:2712 (python.exe:arm64+0x1001d07c4)
    #6 _PyEval_Vector ceval.c:2141 (python.exe:arm64+0x1001cc028)
    #7 _PyObject_VectorcallPrepend call.c:855 (python.exe:arm64+0x100063290)
    #8 context_run context.c:728 (python.exe:arm64+0x1001fff94)
    #9 PyObject_Vectorcall call.c:327 (python.exe:arm64+0x100061c10)
    #10 _Py_VectorCallInstrumentation_StackRefSteal ceval.c:768 (python.exe:arm64+0x1001cc64c)
    #11 _PyEval_EvalFrameDefault generated_cases.c.h:1906 (python.exe:arm64+0x1001cf74c)
    #12 _PyEval_Vector ceval.c:2141 (python.exe:arm64+0x1001cc028)
    #13 _PyObject_VectorcallPrepend call.c:855 (python.exe:arm64+0x100063290)
    #14 thread_run _threadmodule.c:388 (python.exe:arm64+0x100333580)
    #15 pythread_wrapper thread_pthread.h:234 (python.exe:arm64+0x10027a4ec)
    #16 __tsan_thread_start_func <null> (libclang_rt.tsan_osx_dynamic.dylib:arm64e+0x337a0)
    #17 _pthread_start <null> (libsystem_pthread.dylib:arm64e+0x6c54)
    #18 thread_start <null> (libsystem_pthread.dylib:arm64e+0x1c18)

==56255==Register values:
 x[0] = 0x0000000101000000   x[1] = 0x0000100000000050   x[2] = 0x00000000cf7d1ffe   x[3] = 0x0000000000000008
 x[4] = 0x0000000000000003   x[5] = 0x0000000000000000   x[6] = 0x0000000000000000   x[7] = 0x0000000000000000
 x[8] = 0x0000000000000000   x[9] = 0x000000000000001f  x[10] = 0x00000000c0000000  x[11] = 0x0000000000000000
x[12] = 0x000000010b485628  x[13] = 0x0000100000000050  x[14] = 0x0000000000000003  x[15] = 0x0000000000004010
x[16] = 0x0000000000000000  x[17] = 0x00000001010c4a40  x[18] = 0x0000000000000000  x[19] = 0x0000000000000029
x[20] = 0x0000000101000000  x[21] = 0x00000001003c62cc  x[22] = 0x0000000000000000  x[23] = 0x0000000000000001
x[24] = 0x0000000000000000  x[25] = 0x00000000000007ee  x[26] = 0x0000000000010707  x[27] = 0x000000012ace6240
x[28] = 0x000000012c0e0190     fp = 0x0000000170b8a540     lr = 0x000000010106fadc     sp = 0x0000000170b8a500
ThreadSanitizer can not provide additional info.
SUMMARY: ThreadSanitizer: SEGV dictobject.c in _PyDict_Next
==56255==ABORTING
[1]    56255 abort      TSAN_OPTIONS="halt_on_error=0" ./python.exe repro_151722.py

TO-BE

➜  cpython git:(gh-151722) ✗ TSAN_OPTIONS="halt_on_error=0" ./python.exe repro_1
51722.py
observed half-built: False

@corona10

Copy link
Copy Markdown
Member Author

cc @tonghuaroot

@tonghuaroot

Copy link
Copy Markdown
Contributor

Thanks for the cc. Built the PR (--disable-gil --tsan) — confirms it closes the frozendict(mapping) path (TSan-clean where stock trips + crashes).

Two sibling creation paths the defer-track doesn't reach yet, both pre-existing + TSan-confirmed on this branch:

  1. frozendict.fromkeys(non_dict_iterable, v)_PyDict_FromKeys creates the result tracked, then fills it in the PyIter_Next loop, so it's observable half-built (clean TSan race: frozendict_length vs _PyDict_FromKeys/setitem_take2_lock_held). Small fix in your frozendict_vectorcall idiom (untrack the fill loop, re-track after) — I've built + verified it (race → 0, fast paths untouched, test_dict/test_free_threading green) plus a regression test. Happy to send as a PR against your branch or a standalone follow-up, whichever you prefer.
  2. frozendict subclass — a subclass gets a tracking tp_alloc, so the trailing _PyObject_GC_TRACK is a no-op; FD(mapping) is observable half-built. Flagging — that's the alloc path, your call.

Also no committed test yet — the fromkeys one above doubles as a test_racing_* for your path if useful.

Comment thread Objects/dictobject.c Outdated
if (self == NULL) {
return NULL;
}
if (!_PyObject_GC_IS_TRACKED(self)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this if statement needed?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we don't need it

@corona10

Copy link
Copy Markdown
Member Author

@tonghuaroot

  1. For frozendict.fromkey, you can submit the separate PR after this PR is merged.
  2. For frozendict subclas, I will take a look at this separately

Comment thread Objects/dictobject.c Outdated
}

static PyObject *
dict_new(PyTypeObject *type, PyObject *args, PyObject *kwds)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is strange. Why does it ignore its parameters?
Creating a dict with arguments should create a dict from those arguments.

This comment was marked as outdated.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cpython/Objects/dictobject.c

Lines 5245 to 5269 in aa5b164

dict_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
assert(type != NULL);
assert(type->tp_alloc != NULL);
// dict subclasses must implement the GC protocol
assert(_PyType_IS_GC(type));
PyObject *self = type->tp_alloc(type, 0);
if (self == NULL) {
return NULL;
}
PyDictObject *d = (PyDictObject *)self;
d->ma_used = 0;
d->_ma_watcher_tag = 0;
// We don't inc ref empty keys because they're immortal
assert((Py_EMPTY_KEYS)->dk_refcnt == _Py_DICT_IMMORTAL_INITIAL_REFCNT);
d->ma_keys = Py_EMPTY_KEYS;
d->ma_values = NULL;
ASSERT_CONSISTENT(d);
if (!_PyObject_GC_IS_TRACKED(d)) {
_PyObject_GC_TRACK(d);
}
return self;
}

The function already ignore those params, and it was done by dict_init

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed:

>>> dict.__new__(dict, (), a=1,b=2)
{}

but frozendict is inconsistent:

>>> frozendict.__new__(frozendict, (), a=1,b=2)
frozendict({'a': 1, 'b': 2})

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that than we need to modify frozenset also

>>> set.__new__(set, [1,2,3])
set()
>>> frozenset.__new__(frozenset, [1,2,3])
frozenset({1, 2, 3})

Let's handle this at separate issue.

@markshannon

Copy link
Copy Markdown
Member

See #151722 (comment) for the proper fix for the issue.

Having said that, deferring tracking of any object until the object is complete is a good idea.
No object can be part of an unreachable cycle during creation.

@corona10 corona10 changed the title gh-151722: Defer GC tracking of frozendict to end of construction [WIP] gh-151722: Defer GC tracking of frozendict to end of construction Jun 20, 2026
@corona10 corona10 changed the title [WIP] gh-151722: Defer GC tracking of frozendict to end of construction gh-151722: Defer GC tracking of frozendict to end of construction Jun 20, 2026
@corona10 corona10 requested a review from markshannon June 20, 2026 12:08
@corona10

Copy link
Copy Markdown
Member Author

@markshannon @methane I've updated the PR. PTAL :)

Comment thread Objects/dictobject.c
STORE_USED(mp, other->ma_used);
ASSERT_CONSISTENT(mp);

if (_PyObject_GC_IS_TRACKED(other) && !_PyObject_GC_IS_TRACKED(mp)) {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#127027

No more lazy track from here, we can remove this logic safely.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

awaiting core review needs backport to 3.15 pre-release feature fixes, bugs and security fixes topic-free-threading

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants