Skip to content

json.dumps() can use freed list elements during concurrent mutation #157107

Description

@Nievesjyl

Description

The list path in _json reads a borrowed element with PySequence_Fast_GET_ITEM() and increments it without holding the source sequence's critical section. A concurrent replacement or removal can destroy the element between acquisition and ownership promotion.

Observed Behavior

On a free-threaded ASan build, racing json.dumps() with mutation of the encoded list produces a null SEGV in encoder_listencode_list(). The GIL-enabled ASan control completes normally.

Affected Version

CPython 3.14.7 at commit 823f0323ee6ec1402088b73bce1a38473cac36dc, built with --disable-gil --with-address-sanitizer and run with PYTHON_GIL=0.

Reproduction

Run the stress reproducer; the schedule is probabilistic:

ASAN_OPTIONS=detect_leaks=0:halt_on_error=1 PYTHON_GIL=0 python3.14 poc/reproduce.py --seconds 10

PoC Source Code

poc/reproduce.py:

#!/usr/bin/env python3
"""Race json.dumps() against mutation of the exact list it traverses."""

import argparse
import json
import threading
import time


def main() -> None:
    parser = argparse.ArgumentParser()
    parser.add_argument("--seconds", type=float, default=10.0)
    args = parser.parse_args()

    shared = [[index] for index in range(512)]
    stop = threading.Event()
    counts = [0, 0]

    def encode() -> None:
        while not stop.is_set():
            try:
                json.dumps(shared, check_circular=False)
            except (RuntimeError, ValueError, IndexError):
                pass
            counts[0] += 1

    def mutate() -> None:
        generation = 0
        while not stop.is_set():
            index = generation % 512
            shared[index] = [generation]
            if generation % 4 == 0:
                shared.pop()
                shared.append([generation + 1])
            generation += 1
            counts[1] += 1

    threads = [threading.Thread(target=encode), threading.Thread(target=mutate)]
    for thread in threads:
        thread.start()
    time.sleep(args.seconds)
    stop.set()
    for thread in threads:
        thread.join()
    print({"encode": counts[0], "mutate": counts[1]})


if __name__ == "__main__":
    main()

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