Skip to content

dict.update() can use freed pair elements during source-list mutation #157087

Description

@Nievesjyl

Description

The generic PyDict_MergeFromSeq2() path locks the destination but not the source sequence. For an exact list pair, PySequence_Fast() returns the same mutable list. merge_from_seq2_lock_held() then loads fast[0] and fast[1] as borrowed references with PySequence_Fast_GET_ITEM() before separately incrementing them. Concurrent replacement can destroy either object between its load and increment, after which the stale key or value is passed to dictionary insertion.

Observed Behavior

Eight threads repeatedly updated dictionaries from one shared two-element list while another thread replaced its key and value. The free-threaded ASan/assert build crashed in under one second in PyObject_Hash() from merge_from_seq2_lock_held() at Objects/dictobject.c:3848. With the GIL enabled, the same binary completed more than 1.45 million merges in five seconds without a sanitizer 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()
pair = [("seed" * 64), bytearray(4096)]
counts = [0] * 9


def mutate():
    i = 0
    while not stop.is_set():
        pair[0] = ("key" * 64) + str(i)
        pair[1] = bytearray(4096)
        i += 1
        counts[0] = i


def merge(slot):
    target = {}
    while not stop.is_set():
        target.update([pair])
        target.clear()
        counts[slot] += 1


threads = [threading.Thread(target=mutate)]
threads += [threading.Thread(target=merge, 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