Skip to content

Commit 2f02ce9

Browse files
committed
gh-155606: Increment the managed buffer export count atomically
mbuf_add_view() and mbuf_add_incomplete_view() registered a new view on the shared _PyManagedBufferObject with a plain mbuf->exports++. On free-threaded builds concurrent slices of one memoryview lose increments, so the count drops to zero while views are still alive and mbuf_release() frees the buffer early. The matching PyMemoryViewObject.exports counter already uses FT_ATOMIC_ADD_SSIZE; use it here too. On default builds the macro expands to a plain +=, so this is a no-op there.
1 parent 61818b6 commit 2f02ce9

3 files changed

Lines changed: 43 additions & 2 deletions

File tree

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import threading
2+
import unittest
3+
4+
from test.support import threading_helper
5+
6+
7+
@threading_helper.requires_working_threading()
8+
class TestMemoryViewSliceRace(unittest.TestCase):
9+
def test_concurrent_slicing_keeps_export_count(self):
10+
# gh-155606: slicing registers a new view on the shared managed buffer,
11+
# and mbuf_add_view() bumped that buffer's export count with a plain
12+
# ++. Concurrent slices of a single memoryview therefore lost
13+
# increments, the count reached zero while views were still alive, and
14+
# the underlying buffer was released early.
15+
#
16+
# The slices are created concurrently but only dropped afterwards, on
17+
# one thread, so this covers the increment on its own.
18+
mv = memoryview(bytes(2 ** 16))
19+
slices = []
20+
lock = threading.Lock()
21+
22+
def make_slices():
23+
local = [mv[0:64] for _ in range(2000)]
24+
with lock:
25+
slices.extend(local)
26+
27+
threading_helper.run_concurrently(make_slices, nthreads=8)
28+
del slices
29+
30+
# An early release makes this raise "operation forbidden on released
31+
# memoryview object".
32+
self.assertEqual(bytes(mv[0:4]), b"\x00" * 4)
33+
34+
35+
if __name__ == "__main__":
36+
unittest.main()
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
Fix a data race on free-threaded builds where slicing a shared
2+
:class:`memoryview` from several threads could release the underlying buffer
3+
while views were still alive, raising ``ValueError: operation forbidden on
4+
released memoryview object``. The managed buffer's export count is now
5+
incremented atomically.

Objects/memoryobject.c

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -699,7 +699,7 @@ mbuf_add_view(_PyManagedBufferObject *mbuf, const Py_buffer *src)
699699
init_flags(mv);
700700

701701
mv->mbuf = (_PyManagedBufferObject*)Py_NewRef(mbuf);
702-
mbuf->exports++;
702+
FT_ATOMIC_ADD_SSIZE(mbuf->exports, 1);
703703

704704
return (PyObject *)mv;
705705
}
@@ -729,7 +729,7 @@ mbuf_add_incomplete_view(_PyManagedBufferObject *mbuf, const Py_buffer *src,
729729
init_shared_values(dest, src);
730730

731731
mv->mbuf = (_PyManagedBufferObject*)Py_NewRef(mbuf);
732-
mbuf->exports++;
732+
FT_ATOMIC_ADD_SSIZE(mbuf->exports, 1);
733733

734734
return (PyObject *)mv;
735735
}

0 commit comments

Comments
 (0)