Description
module_dir() retrieves a borrowed __dir__ value from the module dictionary and calls it without retaining the callable or holding the dictionary's critical section. A concurrent assignment can remove the dictionary's last reference to the selected callable before dispatch.
Observed Behavior
On a free-threaded ASan/assert build, the reproducer crashes in under one second during stale callable dispatch from _PyObject_CallNoArgs() at Objects/moduleobject.c:1196. The same executable completes normally with the compatibility GIL enabled.
Affected Version
CPython 3.14.7 at commit 823f0323ee6ec1402088b73bce1a38473cac36dc, built with ASan, assertions, and free-threading support.
Reproduction
Run:
ASAN_OPTIONS=abort_on_error=1:detect_leaks=0 PYTHON_GIL=0 CONCURDEP_DURATION=20 python3.14 poc/reproduce.py
For comparison, run the same command with PYTHON_GIL=1.
PoC Source Code
poc/reproduce.py:
import os
import threading
import time
import types
duration = float(os.environ.get("CONCURDEP_DURATION", "20"))
stop = threading.Event()
module = types.ModuleType("race_module")
counts = [0] * 9
class Callable:
__slots__ = ("padding",)
def __init__(self):
self.padding = bytearray(4096)
def __call__(self):
return ["value"]
module.__dir__ = Callable()
def mutate():
i = 0
while not stop.is_set():
module.__dir__ = Callable()
i += 1
counts[0] = i
def inspect(slot):
while not stop.is_set():
try:
dir(module)
except (TypeError, AttributeError):
pass
counts[slot] += 1
threads = [threading.Thread(target=mutate)]
threads += [threading.Thread(target=inspect, args=(i,)) for i in range(1, 9)]
for thread in threads:
thread.start()
time.sleep(duration)
stop.set()
for thread in threads:
thread.join()
print("counts", counts)
Description
module_dir()retrieves a borrowed__dir__value from the module dictionary and calls it without retaining the callable or holding the dictionary's critical section. A concurrent assignment can remove the dictionary's last reference to the selected callable before dispatch.Observed Behavior
On a free-threaded ASan/assert build, the reproducer crashes in under one second during stale callable dispatch from
_PyObject_CallNoArgs()atObjects/moduleobject.c:1196. The same executable completes normally with the compatibility GIL enabled.Affected Version
CPython 3.14.7 at commit
823f0323ee6ec1402088b73bce1a38473cac36dc, built with ASan, assertions, and free-threading support.Reproduction
Run:
For comparison, run the same command with
PYTHON_GIL=1.PoC Source Code
poc/reproduce.py: