Skip to content

[BUG] array.fromlist() can dereference a replaced list element in free-threaded builds #157072

Description

@Nievesjyl

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)

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