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()
Description
The list path in
_jsonreads a borrowed element withPySequence_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 inencoder_listencode_list(). The GIL-enabled ASan control completes normally.Affected Version
CPython 3.14.7 at commit
823f0323ee6ec1402088b73bce1a38473cac36dc, built with--disable-gil --with-address-sanitizerand run withPYTHON_GIL=0.Reproduction
Run the stress reproducer; the schedule is probabilistic:
PoC Source Code
poc/reproduce.py: