Skip to content

float(bytearray) can read unstable storage during concurrent resize #157096

Description

@Nievesjyl

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)

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions