Description
memoryview.cast() accepts a mutable list for shape. copy_shape() reads a borrowed list element and then type-checks and converts it without holding the list's critical section or first taking an owned reference. Concurrent slot replacement can destroy that element before conversion.
Observed Behavior
On a free-threaded ASan/debug build, the reproducer crashes in under one second in PyLong_AsSsize_t(), called by copy_shape() at Objects/memoryobject.c:1353. The GIL-enabled ASan/debug control completes normally.
Affected Version
CPython 3.14.7 at commit 823f0323ee6ec1402088b73bce1a38473cac36dc, built with --disable-gil --with-pydebug --with-address-sanitizer.
Reproduction
Run:
ASAN_OPTIONS=detect_leaks=0:halt_on_error=1 PYTHON_GIL=0 python3.14 poc/reproduce.py --seconds 20 --width 64
PoC Source Code
poc/reproduce.py:
#!/usr/bin/env python3
"""Race memoryview.cast() against replacement in its shape list."""
import argparse
import threading
class Box(int):
"""A non-immortal integer whose last list reference can be removed."""
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--seconds", type=float, default=20.0)
parser.add_argument("--width", type=int, default=64)
args = parser.parse_args()
view = memoryview(b"x")
shape = [Box(1) for _ in range(args.width)]
stop = threading.Event()
start = threading.Barrier(2)
counts = [0, 0]
errors: list[tuple[int, int]] = []
def caster() -> None:
start.wait()
while not stop.is_set():
try:
result = view.cast("B", shape)
if result.nbytes != 1 or result.ndim != args.width:
errors.append((result.nbytes, result.ndim))
stop.set()
return
counts[0] += 1
except (OverflowError, TypeError, ValueError):
# A racing snapshot can be rejected, but it must remain safe.
pass
def mutator() -> None:
start.wait()
index = 0
while not stop.is_set():
shape[index] = Box(1)
index = (index + 1) % args.width
counts[1] += 1
reader = threading.Thread(target=caster, name="cast-shape")
writer = threading.Thread(target=mutator, name="replace-shape")
reader.start()
writer.start()
stop.wait(args.seconds)
stop.set()
reader.join()
writer.join()
if errors:
raise AssertionError(errors)
print({"casts": counts[0], "mutations": counts[1]})
if __name__ == "__main__":
main()
Description
memoryview.cast()accepts a mutable list forshape.copy_shape()reads a borrowed list element and then type-checks and converts it without holding the list's critical section or first taking an owned reference. Concurrent slot replacement can destroy that element before conversion.Observed Behavior
On a free-threaded ASan/debug build, the reproducer crashes in under one second in
PyLong_AsSsize_t(), called bycopy_shape()atObjects/memoryobject.c:1353. The GIL-enabled ASan/debug control completes normally.Affected Version
CPython 3.14.7 at commit
823f0323ee6ec1402088b73bce1a38473cac36dc, built with--disable-gil --with-pydebug --with-address-sanitizer.Reproduction
Run:
PoC Source Code
poc/reproduce.py: