Description
In a free-threaded build, array.fromlist() obtains source-list elements with PyList_GET_ITEM() and passes the borrowed pointer to the array descriptor's conversion function without owning the element or locking the list. A concurrent same-length replacement can destroy the old integer before conversion dereferences it. The later length check does not detect element replacement.
Observed Behavior
An ASan run fails in under one second with a high-address read in PyLong_AsUnsignedLongLong(), called through QQ_setitem() and array_array_fromlist_impl(). The process exits with status 134. With PYTHON_GIL=1, the same binary completes a five-second control run with 697,145 fromlist() calls and no sanitizer failure.
Affected Version
CPython 3.14.7 at commit 823f0323ee6ec1402088b73bce1a38473cac36dc, using a free-threaded ASan/assert build.
Reproduction
Run from a directory containing poc/reproduce.py:
ASAN_OPTIONS=abort_on_error=1:detect_leaks=0 PYTHON_GIL=0 \
CONCURDEP_DURATION=20 python3.14 poc/reproduce.py
The GIL-enabled control is:
ASAN_OPTIONS=abort_on_error=1:detect_leaks=0 PYTHON_GIL=1 \
CONCURDEP_DURATION=5 python3.14 poc/reproduce.py
PoC Source Code
poc/reproduce.py
import array
import os
import threading
import time
duration = float(os.environ.get("CONCURDEP_DURATION", "20"))
stop = threading.Event()
shared = [10**30 + i for i in range(64)]
counts = [0] * 9
def mutate():
i = 0
while not stop.is_set():
for index in range(len(shared)):
shared[index] = (i << 64) + index
i += 1
counts[0] = i
def extend(slot):
target = array.array("Q")
while not stop.is_set():
try:
target.fromlist(shared)
except OverflowError:
pass
del target[:]
counts[slot] += 1
threads = [threading.Thread(target=mutate)]
threads += [threading.Thread(target=extend, 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
In a free-threaded build,
array.fromlist()obtains source-list elements withPyList_GET_ITEM()and passes the borrowed pointer to the array descriptor's conversion function without owning the element or locking the list. A concurrent same-length replacement can destroy the old integer before conversion dereferences it. The later length check does not detect element replacement.Observed Behavior
An ASan run fails in under one second with a high-address read in
PyLong_AsUnsignedLongLong(), called throughQQ_setitem()andarray_array_fromlist_impl(). The process exits with status 134. WithPYTHON_GIL=1, the same binary completes a five-second control run with 697,145fromlist()calls and no sanitizer failure.Affected Version
CPython 3.14.7 at commit
823f0323ee6ec1402088b73bce1a38473cac36dc, using a free-threaded ASan/assert build.Reproduction
Run from a directory containing
poc/reproduce.py:The GIL-enabled control is:
PoC Source Code
poc/reproduce.py