diff --git a/Lib/test/test_free_threading/test_memoryview.py b/Lib/test/test_free_threading/test_memoryview.py new file mode 100644 index 000000000000000..6416f0f278b6f09 --- /dev/null +++ b/Lib/test/test_free_threading/test_memoryview.py @@ -0,0 +1,36 @@ +import threading +import unittest + +from test.support import threading_helper + + +@threading_helper.requires_working_threading() +class TestMemoryViewSliceRace(unittest.TestCase): + def test_concurrent_slicing_keeps_export_count(self): + # gh-155606: slicing registers a new view on the shared managed buffer, + # and mbuf_add_view() bumped that buffer's export count with a plain + # ++. Concurrent slices of a single memoryview therefore lost + # increments, the count reached zero while views were still alive, and + # the underlying buffer was released early. + # + # The slices are created concurrently but only dropped afterwards, on + # one thread, so this covers the increment on its own. + mv = memoryview(bytes(2 ** 16)) + slices = [] + lock = threading.Lock() + + def make_slices(): + local = [mv[0:64] for _ in range(2000)] + with lock: + slices.extend(local) + + threading_helper.run_concurrently(make_slices, nthreads=8) + del slices + + # An early release makes this raise "operation forbidden on released + # memoryview object". + self.assertEqual(bytes(mv[0:4]), b"\x00" * 4) + + +if __name__ == "__main__": + unittest.main() diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-15-16-20-11.gh-issue-155606.Kv3Nqp.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-15-16-20-11.gh-issue-155606.Kv3Nqp.rst new file mode 100644 index 000000000000000..59b50cbea611ff8 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-15-16-20-11.gh-issue-155606.Kv3Nqp.rst @@ -0,0 +1,5 @@ +Fix a data race on free-threaded builds where slicing a shared +:class:`memoryview` from several threads could release the underlying buffer +while views were still alive, raising ``ValueError: operation forbidden on +released memoryview object``. The managed buffer's export count is now +incremented atomically. diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index 0bcd0b6596be776..fa7b84eb7d2b2ce 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -699,7 +699,7 @@ mbuf_add_view(_PyManagedBufferObject *mbuf, const Py_buffer *src) init_flags(mv); mv->mbuf = (_PyManagedBufferObject*)Py_NewRef(mbuf); - mbuf->exports++; + FT_ATOMIC_ADD_SSIZE(mbuf->exports, 1); return (PyObject *)mv; } @@ -729,7 +729,7 @@ mbuf_add_incomplete_view(_PyManagedBufferObject *mbuf, const Py_buffer *src, init_shared_values(dest, src); mv->mbuf = (_PyManagedBufferObject*)Py_NewRef(mbuf); - mbuf->exports++; + FT_ATOMIC_ADD_SSIZE(mbuf->exports, 1); return (PyObject *)mv; }