Description
The bytearray special case in PyFloat_FromString() reads PyByteArray_AS_STRING(v) and PyByteArray_GET_SIZE(v) without an object lock, buffer export, or copy. It passes that address and length to _Py_string_to_number_with_underscores(), which scans the storage and expects a NUL byte at s[len]. Concurrent bytearray resizing can independently change the allocation, content, length, and terminator while parsing is in progress.
Observed Behavior
Eight threads called float() on one bytearray while another alternated it between a short float and a 2 MiB numeric string. The free-threaded ASan/assert build aborted almost immediately because s[orig_len] == '\0' failed in Python/pystrtod.c:354. With the GIL enabled, the same binary completed 390,767 conversions in five seconds without failure.
Affected Version
CPython 3.14.7 at commit 823f0323ee6ec1402088b73bce1a38473cac36dc, tested with a free-threaded ASan/assert build.
Reproduction
ASAN_OPTIONS=abort_on_error=1:detect_leaks=0 PYTHON_GIL=0 CONCURDEP_DURATION=20 python3.14 poc/reproduce.py
Setting PYTHON_GIL=1 CONCURDEP_DURATION=5 provides the GIL-enabled control.
PoC Source Code
poc/reproduce.py
import os
import threading
import time
duration = float(os.environ.get("CONCURDEP_DURATION", "20"))
stop = threading.Event()
source = bytearray(b"1" * 1048576 + b".25")
counts = [0] * 9
def mutate():
i = 0
while not stop.is_set():
if i & 1:
source[:] = b"1" * 2097152 + b".25"
else:
source[:] = b"3.5"
i += 1
counts[0] = i
def parse(slot):
while not stop.is_set():
try:
float(source)
except (ValueError, OverflowError):
pass
counts[slot] += 1
threads = [threading.Thread(target=mutate)]
threads += [threading.Thread(target=parse, 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
The bytearray special case in
PyFloat_FromString()readsPyByteArray_AS_STRING(v)andPyByteArray_GET_SIZE(v)without an object lock, buffer export, or copy. It passes that address and length to_Py_string_to_number_with_underscores(), which scans the storage and expects a NUL byte ats[len]. Concurrent bytearray resizing can independently change the allocation, content, length, and terminator while parsing is in progress.Observed Behavior
Eight threads called
float()on one bytearray while another alternated it between a short float and a 2 MiB numeric string. The free-threaded ASan/assert build aborted almost immediately becauses[orig_len] == '\0'failed inPython/pystrtod.c:354. With the GIL enabled, the same binary completed 390,767 conversions in five seconds without failure.Affected Version
CPython 3.14.7 at commit
823f0323ee6ec1402088b73bce1a38473cac36dc, tested with a free-threaded ASan/assert build.Reproduction
Setting
PYTHON_GIL=1 CONCURDEP_DURATION=5provides the GIL-enabled control.PoC Source Code
poc/reproduce.py