From 8fa2daf18da79308d5321513e87a1bf7bb2032c7 Mon Sep 17 00:00:00 2001 From: Lu Xiaowei Date: Mon, 27 Jul 2026 20:28:10 +0800 Subject: [PATCH 01/12] gh-127716: make memoryview thread-safe Co-authored-by: ayaangazali Co-authored-by: Lu Xiaowei --- Doc/library/threadsafety.rst | 6 + Include/cpython/memoryobject.h | 2 +- .../test_free_threading/test_memoryview.py | 313 +++++++++ Lib/test/test_memoryview.py | 7 + ...-08-21-14-37-50.gh-issue-127716.KrPth8.rst | 9 + Objects/memoryobject.c | 637 +++++++++++++----- 6 files changed, 817 insertions(+), 157 deletions(-) create mode 100644 Lib/test/test_free_threading/test_memoryview.py create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2026-08-21-14-37-50.gh-issue-127716.KrPth8.rst diff --git a/Doc/library/threadsafety.rst b/Doc/library/threadsafety.rst index a529f7803affbc2..2ff44b0fabd6081 100644 --- a/Doc/library/threadsafety.rst +++ b/Doc/library/threadsafety.rst @@ -604,3 +604,9 @@ underlying object supports it: Resizing or reallocating the underlying object (such as calling :meth:`bytearray.resize`) while a memoryview is exported raises :exc:`BufferError`. This is enforced regardless of threading. + +In the :term:`free-threaded build`, :meth:`~memoryview.release` is +thread-safe. If another thread has an in-flight operation on the memoryview +when it is released, the underlying buffer may remain exported until that +operation completes. Restrictions on the exporter, such as prohibiting +resizing, remain in effect until then. diff --git a/Include/cpython/memoryobject.h b/Include/cpython/memoryobject.h index 961161b70f20580..419e57d1ad56a49 100644 --- a/Include/cpython/memoryobject.h +++ b/Include/cpython/memoryobject.h @@ -11,7 +11,7 @@ typedef struct { PyObject_HEAD int flags; /* state flags */ - Py_ssize_t exports; /* number of direct memoryview exports */ + Py_ssize_t exports; /* registered views and active operations */ Py_buffer master; /* snapshot buffer obtained from the original exporter */ } _PyManagedBufferObject; 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..8493f425d7b86b8 --- /dev/null +++ b/Lib/test/test_free_threading/test_memoryview.py @@ -0,0 +1,313 @@ +import pickle +import threading +import unittest + +from test.support import threading_helper + +NTHREADS = 8 + +# Fresh objects expose one-time races under ThreadSanitizer. +ROUNDS = 20 +ITERS = 20 + +HASH_DATA_TEMPLATE = bytes(range(256)) +HASH_DATA_REPEAT = 256 + +READ_OPS = [ + lambda mv: mv.tobytes(), + lambda mv: mv.hex(), + lambda mv: mv.tolist(), + lambda mv: mv.cast("B"), + lambda mv: mv.toreadonly(), + lambda mv: memoryview(mv), + lambda mv: bytes(mv), + lambda mv: pickle.PickleBuffer(mv).release(), + lambda mv: mv.obj, + lambda mv: mv.format, + lambda mv: mv.c_contiguous, + lambda mv: mv[0], + lambda mv: mv[0:4], + lambda mv: mv.count(0), + lambda mv: mv.index(0), + lambda mv: mv == mv, + lambda mv: list(mv), + lambda mv: len(mv), +] + + +def run_racy(func, *args): + try: + func(*args) + except (ValueError, BufferError): + pass + + +@threading_helper.requires_working_threading() +class TestMemoryViewRaces(unittest.TestCase): + def assert_exporter_free(self, buf): + buf.append(0) + del buf[-1] + + def test_concurrent_slicing_keeps_export_count(self): + for _ in range(ROUNDS): + mv = memoryview(bytes(64)) + slices = [] + lock = threading.Lock() + + def make_slices(): + local = [mv[0:4] for _ in range(ITERS)] + local += [memoryview(mv) for _ in range(ITERS)] + with lock: + slices.extend(local) + + threading_helper.run_concurrently(make_slices, nthreads=NTHREADS) + del slices + + self.assertEqual(bytes(mv[0:4]), b"\x00" * 4) + + def test_concurrent_release(self): + buf = bytearray(64) + + for _ in range(ROUNDS): + views = [memoryview(buf) for _ in range(NTHREADS)] + + def release(views=views): + for mv in views: + run_racy(mv.release) + + threading_helper.run_concurrently(release, nthreads=NTHREADS) + + self.assert_exporter_free(buf) + + def test_release_races_with_reads(self): + for _ in range(ROUNDS): + buf = bytearray(64) + cell = [memoryview(buf)] + lock = threading.Lock() + + def releaser(): + for _ in range(ITERS): + mv = cell[0] + run_racy(mv.release) + with lock: + cell[0] = memoryview(buf) + + def reader(): + for _ in range(ITERS): + mv = cell[0] + for op in READ_OPS: + run_racy(op, mv) + + threading_helper.run_concurrently( + [releaser] * (NTHREADS // 2) + [reader] * (NTHREADS // 2), + nthreads=NTHREADS, + ) + + cell[0].release() + self.assert_exporter_free(buf) + + def test_read_keeps_exporter_alive_after_release(self): + size = 1 << 20 + for _ in range(ROUNDS): + exporter = [bytearray(size)] + view = memoryview(exporter[0]) + stale = [] + + def reader(): + try: + for _ in range(4): + data = view.tobytes() + if data.count(0) != len(data): + stale.append(True) + return + except ValueError: + pass + + def releaser(): + view.release() + exporter.clear() + for _ in range(8): + bytearray(b"\xdb" * size) + + threading_helper.run_concurrently([reader, releaser]) + self.assertFalse(stale) + + def test_release_races_with_writes(self): + for _ in range(ROUNDS): + buf = bytearray(8 * NTHREADS) + cell = [memoryview(buf)] + lock = threading.Lock() + + def releaser(): + for _ in range(ITERS): + mv = cell[0] + run_racy(mv.release) + with lock: + cell[0] = memoryview(buf) + + def writer(slot): + start = slot * 8 + for _ in range(ITERS): + mv = cell[0] + run_racy(mv.__setitem__, start, 1) + run_racy(mv.__setitem__, slice(start, start + 4), b"abcd") + + workers = [releaser] * (NTHREADS // 2) + workers += [lambda s=s: writer(s) for s in range(NTHREADS // 2)] + threading_helper.run_concurrently(workers, nthreads=NTHREADS) + + cell[0].release() + self.assert_exporter_free(buf) + + def test_release_races_with_buffer_exports(self): + for _ in range(ROUNDS): + buf = bytearray(64) + mv = memoryview(buf) + + def exporter(): + for _ in range(ITERS): + run_racy(lambda: pickle.PickleBuffer(mv).release()) + + def releaser(): + for _ in range(ITERS): + run_racy(mv.release) + + threading_helper.run_concurrently( + [exporter] * (NTHREADS - 1) + [releaser], nthreads=NTHREADS + ) + + mv.release() + self.assert_exporter_free(buf) + + def test_release_with_live_export(self): + buf = bytearray(64) + + for _ in range(ROUNDS): + mv = memoryview(buf) + held = pickle.PickleBuffer(mv) + + def release(mv=mv): + try: + mv.release() + except BufferError: + pass + + threading_helper.run_concurrently(release, nthreads=NTHREADS) + + self.assertEqual(bytes(mv[0:4]), b"\x00" * 4) + held.release() + mv.release() + + self.assert_exporter_free(buf) + + def test_compare_two_views_races_with_release(self): + for _ in range(ROUNDS): + buf = bytearray(64) + cell = [memoryview(buf), memoryview(buf)] + lock = threading.Lock() + + def releaser(slot): + for _ in range(ITERS): + mv = cell[slot] + run_racy(mv.release) + with lock: + cell[slot] = memoryview(buf) + + def comparer(): + for _ in range(ITERS): + left, right = cell[0], cell[1] + run_racy(lambda: left == right) + run_racy(lambda: left != right) + + threading_helper.run_concurrently( + [lambda: releaser(0), lambda: releaser(1)] + [comparer] * 6, + nthreads=NTHREADS, + ) + + for mv in cell: + mv.release() + self.assert_exporter_free(buf) + + def test_release_parent_keeps_child_valid(self): + for _ in range(ROUNDS): + buf = bytearray(range(64)) + parent = memoryview(buf) + child = parent[0:32] + + def use_child(): + for _ in range(ITERS): + self.assertEqual(child[0], 0) + child.tobytes() + + def release_parent(): + parent.release() + + threading_helper.run_concurrently( + [release_parent] + [use_child] * (NTHREADS - 1), + nthreads=NTHREADS, + ) + + self.assertEqual(child.tobytes(), bytes(range(32))) + child.release() + self.assert_exporter_free(buf) + + def test_concurrent_iteration(self): + for _ in range(ROUNDS): + buf = bytearray(range(64)) + cell = [memoryview(buf)] + lock = threading.Lock() + + def releaser(): + for _ in range(ITERS): + mv = cell[0] + run_racy(mv.release) + with lock: + cell[0] = memoryview(buf) + + def iterator(): + for _ in range(ITERS): + run_racy(list, cell[0]) + + threading_helper.run_concurrently( + [releaser] * (NTHREADS // 2) + [iterator] * (NTHREADS // 2), + nthreads=NTHREADS, + ) + + cell[0].release() + self.assert_exporter_free(buf) + + def test_iterator_exhaustion_drops_last_reference(self): + def loop(): + for _ in range(ROUNDS * ITERS): + self.assertEqual(list(iter(memoryview(b"ab"))), [97, 98]) + + threading_helper.run_concurrently(loop, nthreads=NTHREADS) + + def test_concurrent_hash(self): + for _ in range(ROUNDS): + data = HASH_DATA_TEMPLATE * HASH_DATA_REPEAT + mv = memoryview(data) + results = [] + lock = threading.Lock() + + def hasher(): + local = {hash(mv) for _ in range(ITERS)} + with lock: + results.append(local) + + threading_helper.run_concurrently(hasher, nthreads=NTHREADS) + self.assertEqual({h for s in results for h in s}, {hash(data)}) + + def test_concurrent_hash_and_release(self): + for _ in range(ROUNDS): + mv = memoryview(HASH_DATA_TEMPLATE * HASH_DATA_REPEAT) + + def work(mv=mv): + run_racy(hash, mv) + run_racy(mv.release) + + threading_helper.run_concurrently(work, nthreads=NTHREADS) + + +if __name__ == "__main__": + unittest.main() diff --git a/Lib/test/test_memoryview.py b/Lib/test/test_memoryview.py index 3cb8a104faee5e6..172b0374cd15f9f 100644 --- a/Lib/test/test_memoryview.py +++ b/Lib/test/test_memoryview.py @@ -91,6 +91,13 @@ def test_iter(self): m = self._view(b) self.assertEqual(list(m), [m[i] for i in range(len(m))]) + def test_released_empty_iterator(self): + for tp in self._types: + m = self._view(tp(b"")) + iterator = iter(m) + m.release() + self.assertRaises(StopIteration, next, iterator) + def test_count(self): for tp in self._types: b = tp(self._source) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-21-14-37-50.gh-issue-127716.KrPth8.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-21-14-37-50.gh-issue-127716.KrPth8.rst new file mode 100644 index 000000000000000..731dd2bb83851c1 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-21-14-37-50.gh-issue-127716.KrPth8.rst @@ -0,0 +1,9 @@ +On the free-threaded build, :class:`memoryview` operations are now thread safe: +an operation that reads the buffer holds the managed buffer for its duration, +so a concurrent :meth:`~memoryview.release` can no longer free the buffer under +it; slicing a memoryview from several threads no longer loses buffer +registrations and raises ``ValueError: operation forbidden on released +memoryview object``; and :func:`hash` of a memoryview is no longer a data race. +As a result :meth:`~memoryview.release` may now return while an operation +started by another thread is still running, see +:ref:`thread-safety-memoryview`. diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index 0bcd0b6596be776..2bd65d36f54f8ea 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -103,13 +103,57 @@ _PyManagedBuffer_FromObject(PyObject *base, int flags) return (PyObject *)mbuf; } +static inline int +flags_test_and_set(int *flags, int bit) +{ +#ifdef Py_GIL_DISABLED + int prev = _Py_atomic_load_int_relaxed(flags); + while (!_Py_atomic_compare_exchange_int(flags, &prev, prev | bit)) { + // Compare-exchange updates prev. + } +#else + int prev = *flags; + *flags = prev | bit; +#endif + return (prev & bit) != 0; +} + +static inline int +mv_get_flags(PyMemoryViewObject *mv) +{ + return FT_ATOMIC_LOAD_INT(mv->flags); +} + +static inline int +mv_has_flag(PyMemoryViewObject *mv, int bits) +{ + return (mv_get_flags(mv) & bits) != 0; +} + +static inline int +mv_set_flag(PyMemoryViewObject *mv, int bit) +{ + return flags_test_and_set(&mv->flags, bit); +} + +static inline int +mbuf_has_flag(_PyManagedBufferObject *mbuf, int bits) +{ + return (FT_ATOMIC_LOAD_INT(mbuf->flags) & bits) != 0; +} + +static inline int +mbuf_set_flag(_PyManagedBufferObject *mbuf, int bit) +{ + return flags_test_and_set(&mbuf->flags, bit); +} + static void mbuf_release(_PyManagedBufferObject *self) { - if (self->flags&_Py_MANAGED_BUFFER_RELEASED) + if (mbuf_set_flag(self, _Py_MANAGED_BUFFER_RELEASED)) { return; - - self->flags |= _Py_MANAGED_BUFFER_RELEASED; + } /* PyBuffer_Release() decrements master->obj and sets it to NULL. */ _PyObject_GC_UNTRACK(self); @@ -122,7 +166,7 @@ mbuf_dealloc(PyObject *_self) _PyManagedBufferObject *self = (_PyManagedBufferObject *)_self; assert(self->exports == 0); mbuf_release(self); - if (self->flags&_Py_MANAGED_BUFFER_FREE_FORMAT) + if (mbuf_has_flag(self, _Py_MANAGED_BUFFER_FREE_FORMAT)) PyMem_Free(self->master.format); PyObject_GC_Del(self); } @@ -178,8 +222,8 @@ PyTypeObject _PyManagedBuffer_Type = { /* In the process of breaking reference cycles mbuf_release() can be called before memory_release(). */ #define BASE_INACCESSIBLE(mv) \ - (((PyMemoryViewObject *)mv)->flags&_Py_MEMORYVIEW_RELEASED || \ - ((PyMemoryViewObject *)mv)->mbuf->flags&_Py_MANAGED_BUFFER_RELEASED) + (mv_has_flag(_PyMemoryView_CAST(mv), _Py_MEMORYVIEW_RELEASED) || \ + mbuf_has_flag(_PyMemoryView_CAST(mv)->mbuf, _Py_MANAGED_BUFFER_RELEASED)) #define CHECK_RELEASED(mv) \ if (BASE_INACCESSIBLE(mv)) { \ @@ -196,17 +240,17 @@ PyTypeObject _PyManagedBuffer_Type = { } #define CHECK_RESTRICTED(mv) \ - if (((PyMemoryViewObject *)(mv))->flags & _Py_MEMORYVIEW_RESTRICTED) { \ - PyErr_SetString(PyExc_ValueError, \ - "cannot create new view on restricted memoryview"); \ - return NULL; \ + if (mv_has_flag(_PyMemoryView_CAST(mv), _Py_MEMORYVIEW_RESTRICTED)) { \ + PyErr_SetString(PyExc_ValueError, \ + "cannot create new view on restricted memoryview"); \ + return NULL; \ } #define CHECK_RESTRICTED_INT(mv) \ - if (((PyMemoryViewObject *)(mv))->flags & _Py_MEMORYVIEW_RESTRICTED) { \ - PyErr_SetString(PyExc_ValueError, \ - "cannot create new view on restricted memoryview"); \ - return -1; \ + if (mv_has_flag(_PyMemoryView_CAST(mv), _Py_MEMORYVIEW_RESTRICTED)) { \ + PyErr_SetString(PyExc_ValueError, \ + "cannot create new view on restricted memoryview"); \ + return -1; \ } /* See gh-92888. These macros signal that we need to check the memoryview @@ -699,7 +743,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,11 +773,76 @@ 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; } +#ifdef Py_GIL_DISABLED +static int +memoryview_add_export(PyMemoryViewObject *self) +{ + int result = 0; + + Py_BEGIN_CRITICAL_SECTION(self); + if (BASE_INACCESSIBLE(self)) { + PyErr_SetString(PyExc_ValueError, + "operation forbidden on released memoryview object"); + result = -1; + } + else { + FT_ATOMIC_ADD_SSIZE(self->exports, 1); + } + Py_END_CRITICAL_SECTION(); + + return result; +} +#endif + +static void +mbuf_drop_export(_PyManagedBufferObject *mbuf) +{ +#ifdef Py_GIL_DISABLED + Py_ssize_t prev_exports = _Py_atomic_add_ssize(&mbuf->exports, -1); +#else + Py_ssize_t prev_exports = mbuf->exports--; +#endif + assert(prev_exports > 0); + if (prev_exports == 1) { + mbuf_release(mbuf); + } +} + +static inline int +memoryview_pin(PyMemoryViewObject *self) +{ +#ifdef Py_GIL_DISABLED + FT_ATOMIC_ADD_SSIZE(self->mbuf->exports, 1); + if (BASE_INACCESSIBLE(self)) { + mbuf_drop_export(self->mbuf); + PyErr_SetString(PyExc_ValueError, + "operation forbidden on released memoryview object"); + return -1; + } + return 0; +#else + if (BASE_INACCESSIBLE(self)) { + PyErr_SetString(PyExc_ValueError, + "operation forbidden on released memoryview object"); + return -1; + } + return 0; +#endif +} + +static inline void +memoryview_unpin(PyMemoryViewObject *self) +{ +#ifdef Py_GIL_DISABLED + mbuf_drop_export(self->mbuf); +#endif +} + /* Expose a raw memory area as a view of contiguous bytes. flags can be PyBUF_READ or PyBUF_WRITE. view->format is set to "B" (unsigned bytes). The memoryview has complete buffer information. */ @@ -804,9 +913,20 @@ PyMemoryView_FromObjectAndFlags(PyObject *v, int flags) if (PyMemoryView_Check(v)) { PyMemoryViewObject *mv = (PyMemoryViewObject *)v; - CHECK_RELEASED(mv); - CHECK_RESTRICTED(mv); - return mbuf_add_view(mv->mbuf, &mv->view); + if (memoryview_pin(mv) < 0) { + return NULL; + } + PyObject *result; + if (mv_has_flag(mv, _Py_MEMORYVIEW_RESTRICTED)) { + PyErr_SetString(PyExc_ValueError, + "cannot create new view on restricted memoryview"); + result = NULL; + } + else { + result = mbuf_add_view(mv->mbuf, &mv->view); + } + memoryview_unpin(mv); + return result; } else if (PyObject_CheckBuffer(v)) { PyObject *ret; @@ -1089,11 +1209,7 @@ PyBuffer_ToContiguous(void *buf, const Py_buffer *src, Py_ssize_t len, char orde static inline Py_ssize_t get_exports(PyMemoryViewObject *buf) { -#ifdef Py_GIL_DISABLED - return _Py_atomic_load_ssize_relaxed(&buf->exports); -#else - return buf->exports; -#endif + return FT_ATOMIC_LOAD_SSIZE_RELAXED(buf->exports); } @@ -1109,14 +1225,11 @@ static void _memory_release(PyMemoryViewObject *self) { assert(get_exports(self) == 0); - if (self->flags & _Py_MEMORYVIEW_RELEASED) + if (mv_set_flag(self, _Py_MEMORYVIEW_RELEASED)) { return; - - self->flags |= _Py_MEMORYVIEW_RELEASED; - assert(self->mbuf->exports > 0); - if (--self->mbuf->exports == 0) { - mbuf_release(self->mbuf); } + + mbuf_drop_export(self->mbuf); } /*[clinic input] @@ -1129,22 +1242,26 @@ static PyObject * memoryview_release_impl(PyMemoryViewObject *self) /*[clinic end generated code: output=d0b7e3ba95b7fcb9 input=bc71d1d51f4a52f0]*/ { + PyObject *result = NULL; + + Py_BEGIN_CRITICAL_SECTION(self); Py_ssize_t exports = get_exports(self); if (exports == 0) { _memory_release(self); - Py_RETURN_NONE; + result = Py_NewRef(Py_None); } - - if (exports > 0) { + else if (exports > 0) { PyErr_Format(PyExc_BufferError, "memoryview has %zd exported buffer%s", exports, - exports==1 ? "" : "s"); - return NULL; + exports == 1 ? "" : "s"); + } + else { + PyErr_SetString(PyExc_SystemError, + "memoryview: negative export count"); } + Py_END_CRITICAL_SECTION(); - PyErr_SetString(PyExc_SystemError, - "memoryview: negative export count"); - return NULL; + return result; } static void @@ -1468,25 +1585,9 @@ zero_in_shape(PyMemoryViewObject *mv) All casts must result in views that will have the exact byte size of the original input. Otherwise, an error is raised. */ -/*[clinic input] -memoryview.cast - - format: unicode - shape: object = NULL - * - order: int(accept={str}) = 'C' - -Cast a memoryview to a new format or shape. - -With a multidimensional *shape*, *order* selects the result -layout: 'C' for C-contiguous (row-major, the default) or 'F' -for Fortran-contiguous (column-major). -[clinic start generated code]*/ - static PyObject * -memoryview_cast_impl(PyMemoryViewObject *self, PyObject *format, +memoryview_cast_pinned(PyMemoryViewObject *self, PyObject *format, PyObject *shape, int order) -/*[clinic end generated code: output=6410d87141f6bb56 input=4a1a2326c59caeb3]*/ { PyMemoryViewObject *mv = NULL; Py_ssize_t ndim = 1; @@ -1499,8 +1600,9 @@ memoryview_cast_impl(PyMemoryViewObject *self, PyObject *format, return NULL; } - if (!MV_C_CONTIGUOUS(self->flags)) { - if (shape || !MV_F_CONTIGUOUS(self->flags)) { + int self_flags = mv_get_flags(self); + if (!MV_C_CONTIGUOUS(self_flags)) { + if (shape || !MV_F_CONTIGUOUS(self_flags)) { PyErr_SetString(PyExc_TypeError, "memoryview: casts are restricted to contiguous views"); return NULL; @@ -1544,6 +1646,34 @@ memoryview_cast_impl(PyMemoryViewObject *self, PyObject *format, return NULL; } +/*[clinic input] +memoryview.cast + + format: unicode + shape: object = NULL + * + order: int(accept={str}) = 'C' + +Cast a memoryview to a new format or shape. + +With a multidimensional *shape*, *order* selects the result +layout: 'C' for C-contiguous (row-major, the default) or 'F' +for Fortran-contiguous (column-major). +[clinic start generated code]*/ + +static PyObject * +memoryview_cast_impl(PyMemoryViewObject *self, PyObject *format, + PyObject *shape, int order) +/*[clinic end generated code: output=6410d87141f6bb56 input=4a1a2326c59caeb3]*/ +{ + if (memoryview_pin(self) < 0) { + return NULL; + } + PyObject *result = memoryview_cast_pinned(self, format, shape, order); + memoryview_unpin(self); + return result; +} + /*[clinic input] memoryview.toreadonly @@ -1554,6 +1684,9 @@ static PyObject * memoryview_toreadonly_impl(PyMemoryViewObject *self) /*[clinic end generated code: output=2c7e056f04c99e62 input=dc06d20f19ba236f]*/ { + if (memoryview_pin(self) < 0) { + return NULL; + } CHECK_RELEASED(self); CHECK_RESTRICTED(self); /* Even if self is already readonly, we still need to create a new @@ -1563,6 +1696,7 @@ memoryview_toreadonly_impl(PyMemoryViewObject *self) if (self != NULL) { self->view.readonly = 1; }; + memoryview_unpin(self); return (PyObject *) self; } @@ -1576,7 +1710,7 @@ memory_getbuf(PyObject *_self, Py_buffer *view, int flags) { PyMemoryViewObject *self = (PyMemoryViewObject *)_self; Py_buffer *base = &self->view; - int baseflags = self->flags; + int baseflags = mv_get_flags(self); CHECK_RELEASED_INT(self); CHECK_RESTRICTED_INT(self); @@ -1644,10 +1778,16 @@ memory_getbuf(PyObject *_self, Py_buffer *view, int flags) view->shape = NULL; } +#ifdef Py_GIL_DISABLED + /* memoryview_add_export() keeps the buffer alive or fails. */ + if (memoryview_add_export(self) < 0) { + return -1; + } +#else + self->exports++; +#endif view->obj = Py_NewRef(self); - FT_ATOMIC_ADD_SSIZE(self->exports, 1); - return 0; } @@ -2322,15 +2462,8 @@ tolist_rec(PyMemoryViewObject *self, const char *ptr, Py_ssize_t ndim, const Py_ /* Return a list representation of the memoryview. Currently only buffers with native format strings are supported. */ -/*[clinic input] -memoryview.tolist - -Return the data in the buffer as a list of elements. -[clinic start generated code]*/ - static PyObject * -memoryview_tolist_impl(PyMemoryViewObject *self) -/*[clinic end generated code: output=a6cda89214fd5a1b input=21e7d0c1860b211a]*/ +memoryview_tolist_pinned(PyMemoryViewObject *self) { const Py_buffer *view = &self->view; const char *fmt; @@ -2356,23 +2489,26 @@ memoryview_tolist_impl(PyMemoryViewObject *self) } /*[clinic input] -memoryview.tobytes +memoryview.tolist - order: str(accept={str, NoneType}, c_default="NULL") = 'C' +Return the data in the buffer as a list of elements. +[clinic start generated code]*/ -Return the data in the buffer as a byte string. +static PyObject * +memoryview_tolist_impl(PyMemoryViewObject *self) +/*[clinic end generated code: output=a6cda89214fd5a1b input=21e7d0c1860b211a]*/ +{ + if (memoryview_pin(self) < 0) { + return NULL; + } + PyObject *result = memoryview_tolist_pinned(self); + memoryview_unpin(self); + return result; +} -Order can be {'C', 'F', 'A'}. When order is 'C' or 'F', the data of -the original array is converted to C or Fortran order. For -contiguous views, 'A' returns an exact copy of the physical memory. -In particular, in-memory Fortran order is preserved. For -non-contiguous views, the data is converted to C first. order=None -is the same as order='C'. -[clinic start generated code]*/ static PyObject * -memoryview_tobytes_impl(PyMemoryViewObject *self, const char *order) -/*[clinic end generated code: output=1288b62560a32a23 input=119c70aa91791dc8]*/ +memoryview_tobytes_pinned(PyMemoryViewObject *self, const char *order) { Py_buffer *src = VIEW_ADDR(self); char ord = 'C'; @@ -2408,38 +2544,42 @@ memoryview_tobytes_impl(PyMemoryViewObject *self, const char *order) } /*[clinic input] -memoryview.hex +memoryview.tobytes - sep: object = NULL - An optional single character or byte to separate hex bytes. - bytes_per_sep: Py_ssize_t = 1 - How many bytes between separators. Positive values count from - the right, negative values count from the left. + order: str(accept={str, NoneType}, c_default="NULL") = 'C' -Return the data in the buffer as a str of hexadecimal numbers. +Return the data in the buffer as a byte string. -Example: ->>> value = memoryview(b'\xb9\x01\xef') ->>> value.hex() -'b901ef' ->>> value.hex(':') -'b9:01:ef' ->>> value.hex(':', 2) -'b9:01ef' ->>> value.hex(':', -2) -'b901:ef' +Order can be {'C', 'F', 'A'}. When order is 'C' or 'F', the data of +the original array is converted to C or Fortran order. For +contiguous views, 'A' returns an exact copy of the physical memory. +In particular, in-memory Fortran order is preserved. For +non-contiguous views, the data is converted to C first. order=None +is the same as order='C'. [clinic start generated code]*/ static PyObject * -memoryview_hex_impl(PyMemoryViewObject *self, PyObject *sep, +memoryview_tobytes_impl(PyMemoryViewObject *self, const char *order) +/*[clinic end generated code: output=1288b62560a32a23 input=119c70aa91791dc8]*/ +{ + if (memoryview_pin(self) < 0) { + return NULL; + } + PyObject *result = memoryview_tobytes_pinned(self, order); + memoryview_unpin(self); + return result; +} + + +static PyObject * +memoryview_hex_pinned(PyMemoryViewObject *self, PyObject *sep, Py_ssize_t bytes_per_sep) -/*[clinic end generated code: output=c9bb00c7a8e86056 input=3f1c5d08906e3b70]*/ { Py_buffer *src = VIEW_ADDR(self); CHECK_RELEASED(self); - if (MV_C_CONTIGUOUS(self->flags)) { + if (MV_C_CONTIGUOUS(mv_get_flags(self))) { // Prevent 'self' from being freed if computing len(sep) mutates 'self' // in _Py_strhex_with_sep(). // See: https://github.com/python/cpython/issues/143195. @@ -2469,11 +2609,48 @@ memoryview_hex_impl(PyMemoryViewObject *self, PyObject *sep, return ret; } +/*[clinic input] +memoryview.hex + + sep: object = NULL + An optional single character or byte to separate hex bytes. + bytes_per_sep: Py_ssize_t = 1 + How many bytes between separators. Positive values count from + the right, negative values count from the left. + +Return the data in the buffer as a str of hexadecimal numbers. + +Example: +>>> value = memoryview(b'\xb9\x01\xef') +>>> value.hex() +'b901ef' +>>> value.hex(':') +'b9:01:ef' +>>> value.hex(':', 2) +'b9:01ef' +>>> value.hex(':', -2) +'b901:ef' +[clinic start generated code]*/ + +static PyObject * +memoryview_hex_impl(PyMemoryViewObject *self, PyObject *sep, + Py_ssize_t bytes_per_sep) +/*[clinic end generated code: output=c9bb00c7a8e86056 input=3f1c5d08906e3b70]*/ +{ + if (memoryview_pin(self) < 0) { + return NULL; + } + PyObject *result = memoryview_hex_pinned(self, sep, bytes_per_sep); + memoryview_unpin(self); + return result; +} + + static PyObject * memory_repr(PyObject *_self) { PyMemoryViewObject *self = (PyMemoryViewObject *)_self; - if (self->flags & _Py_MEMORYVIEW_RELEASED) + if (mv_has_flag(self, _Py_MEMORYVIEW_RELEASED)) return PyUnicode_FromFormat("", self); else return PyUnicode_FromFormat("", self); @@ -2546,9 +2723,9 @@ ptr_from_tuple(const Py_buffer *view, PyObject *tup) /* Return the item at index. In a one-dimensional view, this is an object with the type specified by view->format. Otherwise, the item is a sub-view. - The function is used in memory_subscript() and memory_as_sequence. */ + The function is used in memory_subscript_pinned() and by sq_item. */ static PyObject * -memory_item(PyObject *_self, Py_ssize_t index) +memory_item_pinned(PyObject *_self, Py_ssize_t index) { PyMemoryViewObject *self = (PyMemoryViewObject *)_self; Py_buffer *view = &(self->view); @@ -2674,7 +2851,7 @@ is_multiindex(PyObject *key) 0-d memoryview objects can be referenced using mv[...] or mv[()] but not with anything else. */ static PyObject * -memory_subscript(PyObject *_self, PyObject *key) +memory_subscript_pinned(PyObject *_self, PyObject *key) { PyMemoryViewObject *self = (PyMemoryViewObject *)_self; Py_buffer *view; @@ -2704,7 +2881,7 @@ memory_subscript(PyObject *_self, PyObject *key) index = PyNumber_AsSsize_t(key, PyExc_IndexError); if (index == -1 && PyErr_Occurred()) return NULL; - return memory_item((PyObject *)self, index); + return memory_item_pinned((PyObject *)self, index); } else if (PySlice_Check(key)) { CHECK_RESTRICTED(self); @@ -2737,7 +2914,7 @@ memory_subscript(PyObject *_self, PyObject *key) } static int -memory_ass_sub(PyObject *_self, PyObject *key, PyObject *value) +memory_ass_sub_pinned(PyObject *_self, PyObject *key, PyObject *value) { PyMemoryViewObject *self = (PyMemoryViewObject *)_self; Py_buffer *view = &(self->view); @@ -2827,7 +3004,7 @@ memory_ass_sub(PyObject *_self, PyObject *key, PyObject *value) return pack_single(self, ptr, value, fmt); } if (PySlice_Check(key) || is_multislice(key)) { - /* Call memory_subscript() to produce a sliced lvalue, then copy + /* Call memory_subscript_pinned() to produce a sliced lvalue, then copy rvalue into lvalue. This is already implemented in _testbuffer.c. */ PyErr_SetString(PyExc_NotImplementedError, "memoryview slice assignments are currently restricted " @@ -2851,6 +3028,42 @@ memory_length(PyObject *_self) return self->view.shape[0]; } +static PyObject * +memory_item(PyObject *_self, Py_ssize_t index) +{ + PyMemoryViewObject *self = (PyMemoryViewObject *)_self; + if (memoryview_pin(self) < 0) { + return NULL; + } + PyObject *result = memory_item_pinned(_self, index); + memoryview_unpin(self); + return result; +} + +static PyObject * +memory_subscript(PyObject *_self, PyObject *key) +{ + PyMemoryViewObject *self = (PyMemoryViewObject *)_self; + if (memoryview_pin(self) < 0) { + return NULL; + } + PyObject *result = memory_subscript_pinned(_self, key); + memoryview_unpin(self); + return result; +} + +static int +memory_ass_sub(PyObject *_self, PyObject *key, PyObject *value) +{ + PyMemoryViewObject *self = (PyMemoryViewObject *)_self; + if (memoryview_pin(self) < 0) { + return -1; + } + int result = memory_ass_sub_pinned(_self, key, value); + memoryview_unpin(self); + return result; +} + /* As mapping */ static PyMappingMethods memory_as_mapping = { memory_length, /* mp_length */ @@ -2871,18 +3084,8 @@ static PySequenceMethods memory_as_sequence = { /* Counting */ /****************************************************************************/ -/*[clinic input] -memoryview.count - - value: object - / - -Count the number of occurrences of a value. -[clinic start generated code]*/ - static PyObject * -memoryview_count_impl(PyMemoryViewObject *self, PyObject *value) -/*[clinic end generated code: output=a15cb19311985063 input=e3036ce1ed7d1823]*/ +memoryview_count_pinned(PyMemoryViewObject *self, PyObject *value) { PyObject *iter = PyObject_GetIter(_PyObject_CAST(self)); if (iter == NULL) { @@ -2915,28 +3118,36 @@ memoryview_count_impl(PyMemoryViewObject *self, PyObject *value) return PyLong_FromSsize_t(count); } - -/**************************************************************************/ -/* Lookup */ -/**************************************************************************/ - /*[clinic input] -memoryview.index +memoryview.count value: object - start: slice_index(accept={int}) = 0 - stop: slice_index(accept={int}, c_default="PY_SSIZE_T_MAX") = sys.maxsize / -Return the index of the first occurrence of a value. - -Raises ValueError if the value is not present. +Count the number of occurrences of a value. [clinic start generated code]*/ static PyObject * -memoryview_index_impl(PyMemoryViewObject *self, PyObject *value, - Py_ssize_t start, Py_ssize_t stop) -/*[clinic end generated code: output=e0185e3819e549df input=0697a0165bf90b5a]*/ +memoryview_count_impl(PyMemoryViewObject *self, PyObject *value) +/*[clinic end generated code: output=a15cb19311985063 input=e3036ce1ed7d1823]*/ +{ + if (memoryview_pin(self) < 0) { + return NULL; + } + PyObject *result = memoryview_count_pinned(self, value); + memoryview_unpin(self); + return result; +} + + + +/**************************************************************************/ +/* Lookup */ +/**************************************************************************/ + +static PyObject * +memoryview_index_pinned(PyMemoryViewObject *self, PyObject *value, + Py_ssize_t start, Py_ssize_t stop) { const Py_buffer *view = &self->view; CHECK_RELEASED(self); @@ -2973,7 +3184,7 @@ memoryview_index_impl(PyMemoryViewObject *self, PyObject *value, // entire loop. assert(index < n); - PyObject *item = memory_item(obj, index); + PyObject *item = memory_item_pinned(obj, index); if (item == NULL) { return NULL; } @@ -3001,6 +3212,32 @@ memoryview_index_impl(PyMemoryViewObject *self, PyObject *value, } +/*[clinic input] +memoryview.index + + value: object + start: slice_index(accept={int}) = 0 + stop: slice_index(accept={int}, c_default="PY_SSIZE_T_MAX") = sys.maxsize + / + +Return the index of the first occurrence of a value. + +Raises ValueError if the value is not present. +[clinic start generated code]*/ + +static PyObject * +memoryview_index_impl(PyMemoryViewObject *self, PyObject *value, + Py_ssize_t start, Py_ssize_t stop) +/*[clinic end generated code: output=e0185e3819e549df input=0697a0165bf90b5a]*/ +{ + if (memoryview_pin(self) < 0) { + return NULL; + } + PyObject *result = memoryview_index_pinned(self, value, start, stop); + memoryview_unpin(self); + return result; +} + /**************************************************************************/ /* Comparisons */ @@ -3216,7 +3453,7 @@ cmp_rec(const char *p, const char *q, } static PyObject * -memory_richcompare(PyObject *v, PyObject *w, int op) +memory_richcompare_pinned(PyObject *v, PyObject *w, int op) { PyObject *res; Py_buffer wbuf, *vv; @@ -3342,15 +3579,44 @@ memory_richcompare(PyObject *v, PyObject *w, int op) return Py_XNewRef(res); } +static PyObject * +memory_richcompare(PyObject *v, PyObject *w, int op) +{ + PyMemoryViewObject *self = (PyMemoryViewObject *)v; + if (memoryview_pin(self) < 0) { + /* Released memoryviews still compare by identity. */ + PyErr_Clear(); + return memory_richcompare_pinned(v, w, op); + } + + PyMemoryViewObject *other = NULL; + if (PyMemoryView_Check(w) && w != v) { + other = (PyMemoryViewObject *)w; + if (memoryview_pin(other) < 0) { + PyErr_Clear(); + memoryview_unpin(self); + return memory_richcompare_pinned(v, w, op); + } + } + + PyObject *result = memory_richcompare_pinned(v, w, op); + if (other != NULL) { + memoryview_unpin(other); + } + memoryview_unpin(self); + return result; +} + /**************************************************************************/ /* Hash */ /**************************************************************************/ static Py_hash_t -memory_hash(PyObject *_self) +memory_hash_pinned(PyObject *_self) { PyMemoryViewObject *self = (PyMemoryViewObject *)_self; - if (self->hash == -1) { + Py_hash_t hash = FT_ATOMIC_LOAD_SSIZE_RELAXED(self->hash); + if (hash == -1) { Py_buffer *view = &self->view; char *mem = view->buf; Py_ssize_t ret; @@ -3381,7 +3647,7 @@ memory_hash(PyObject *_self) } } - if (!MV_C_CONTIGUOUS(self->flags)) { + if (!MV_C_CONTIGUOUS(mv_get_flags(self))) { mem = PyMem_Malloc(view->len); if (mem == NULL) { PyErr_NoMemory(); @@ -3394,13 +3660,30 @@ memory_hash(PyObject *_self) } /* Can't fail */ - self->hash = Py_HashBuffer(mem, view->len); + hash = Py_HashBuffer(mem, view->len); + FT_ATOMIC_STORE_SSIZE_RELAXED(self->hash, hash); if (mem != view->buf) PyMem_Free(mem); } - return self->hash; + return hash; +} + +static Py_hash_t +memory_hash(PyObject *_self) +{ + PyMemoryViewObject *self = (PyMemoryViewObject *)_self; + Py_hash_t hash = FT_ATOMIC_LOAD_SSIZE_RELAXED(self->hash); + if (hash != -1) { + return hash; + } + if (memoryview_pin(self) < 0) { + return -1; + } + Py_hash_t result = memory_hash_pinned(_self); + memoryview_unpin(self); + return result; } @@ -3433,7 +3716,7 @@ _IntTupleFromSsizet(int len, Py_ssize_t *vals) } static PyObject * -memory_obj_get(PyObject *_self, void *Py_UNUSED(ignored)) +memory_obj_get_pinned(PyObject *_self, void *Py_UNUSED(ignored)) { PyMemoryViewObject *self = (PyMemoryViewObject *)_self; Py_buffer *view = &self->view; @@ -3454,7 +3737,7 @@ memory_nbytes_get(PyObject *_self, void *Py_UNUSED(ignored)) } static PyObject * -memory_format_get(PyObject *_self, void *Py_UNUSED(ignored)) +memory_format_get_pinned(PyObject *_self, void *Py_UNUSED(ignored)) { PyMemoryViewObject *self = (PyMemoryViewObject *)_self; CHECK_RELEASED(self); @@ -3514,7 +3797,7 @@ memory_c_contiguous(PyObject *_self, void *Py_UNUSED(ignored)) { PyMemoryViewObject *self = (PyMemoryViewObject *)_self; CHECK_RELEASED(self); - return PyBool_FromLong(MV_C_CONTIGUOUS(self->flags)); + return PyBool_FromLong(MV_C_CONTIGUOUS(mv_get_flags(self))); } static PyObject * @@ -3522,7 +3805,7 @@ memory_f_contiguous(PyObject *_self, void *Py_UNUSED(ignored)) { PyMemoryViewObject *self = (PyMemoryViewObject *)_self; CHECK_RELEASED(self); - return PyBool_FromLong(MV_F_CONTIGUOUS(self->flags)); + return PyBool_FromLong(MV_F_CONTIGUOUS(mv_get_flags(self))); } static PyObject * @@ -3530,9 +3813,27 @@ memory_contiguous(PyObject *_self, void *Py_UNUSED(ignored)) { PyMemoryViewObject *self = (PyMemoryViewObject *)_self; CHECK_RELEASED(self); - return PyBool_FromLong(MV_ANY_CONTIGUOUS(self->flags)); + return PyBool_FromLong(MV_ANY_CONTIGUOUS(mv_get_flags(self))); } +#define MEMORYVIEW_PINNED_GETTER(name) \ + static PyObject * \ + name(PyObject *_self, void *ignored) \ + { \ + PyMemoryViewObject *self = (PyMemoryViewObject *)_self; \ + if (memoryview_pin(self) < 0) { \ + return NULL; \ + } \ + PyObject *result = name##_pinned(_self, ignored); \ + memoryview_unpin(self); \ + return result; \ + } + +MEMORYVIEW_PINNED_GETTER(memory_obj_get) +MEMORYVIEW_PINNED_GETTER(memory_format_get) + +#undef MEMORYVIEW_PINNED_GETTER + PyDoc_STRVAR(memory_obj_doc, "The underlying object of the memoryview."); PyDoc_STRVAR(memory_nbytes_doc, @@ -3636,32 +3937,44 @@ static PyObject * memoryiter_next(PyObject *self) { memoryiterobject *it = (memoryiterobject *)self; - PyMemoryViewObject *seq; - seq = it->it_seq; + PyMemoryViewObject *seq = it->it_seq; if (seq == NULL) { return NULL; } - if (it->it_index < it->it_length) { - CHECK_RELEASED(seq); - Py_buffer *view = &(seq->view); - char *ptr = (char *)seq->view.buf; + if (it->it_index >= it->it_length) { + /* Preserve StopIteration for exhausted iterators over released views. */ + it->it_seq = NULL; + Py_DECREF(seq); + return NULL; + } + +#ifdef Py_GIL_DISABLED + /* Keep seq alive if another thread exhausts the iterator. */ + Py_INCREF(seq); +#endif + + PyObject *result = NULL; + if (memoryview_pin(seq) == 0) { + Py_buffer *view = &seq->view; + char *ptr = (char *)view->buf; ptr += view->strides[0] * it->it_index++; ptr = ADJUST_PTR(ptr, view->suboffsets, 0); - if (ptr == NULL) { - return NULL; + if (ptr != NULL) { + result = unpack_single(seq, ptr, it->it_fmt); } - return unpack_single(seq, ptr, it->it_fmt); + memoryview_unpin(seq); } - it->it_seq = NULL; +#ifdef Py_GIL_DISABLED Py_DECREF(seq); - return NULL; +#endif + return result; } static PyObject * -memory_iter(PyObject *seq) +memory_iter_pinned(PyObject *seq) { if (!PyMemoryView_Check(seq)) { PyErr_BadInternalCall(); @@ -3698,6 +4011,18 @@ memory_iter(PyObject *seq) return (PyObject *)it; } +static PyObject * +memory_iter(PyObject *seq) +{ + PyMemoryViewObject *self = (PyMemoryViewObject *)seq; + if (memoryview_pin(self) < 0) { + return NULL; + } + PyObject *result = memory_iter_pinned(seq); + memoryview_unpin(self); + return result; +} + PyTypeObject _PyMemoryIter_Type = { PyVarObject_HEAD_INIT(&PyType_Type, 0) .tp_name = "memory_iterator", From fb7e67229a12c0473caff346bebd2632a0b1312b Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Tue, 25 Aug 2026 18:55:40 -0700 Subject: [PATCH 02/12] Fix two bugs. * The CHECK_* macros can return. Restructure code so that unpin is always done for toreadonly(). * Use atomic load for asserts on `exports`. --- Objects/memoryobject.c | 47 +++++++++++++++++++++++++++++------------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index 2bd65d36f54f8ea..75c66853d849a1c 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -164,7 +164,7 @@ static void mbuf_dealloc(PyObject *_self) { _PyManagedBufferObject *self = (_PyManagedBufferObject *)_self; - assert(self->exports == 0); + assert(FT_ATOMIC_LOAD_SSIZE_RELAXED(self->exports) == 0); mbuf_release(self); if (mbuf_has_flag(self, _Py_MANAGED_BUFFER_FREE_FORMAT)) PyMem_Free(self->master.format); @@ -183,7 +183,7 @@ static int mbuf_clear(PyObject *_self) { _PyManagedBufferObject *self = (_PyManagedBufferObject *)_self; - assert(self->exports >= 0); + assert(FT_ATOMIC_LOAD_SSIZE_RELAXED(self->exports) >= 0); mbuf_release(self); return 0; } @@ -1674,6 +1674,21 @@ memoryview_cast_impl(PyMemoryViewObject *self, PyObject *format, return result; } +static PyObject * +memoryview_toreadonly_pinned(PyMemoryViewObject *self) +{ + CHECK_RELEASED(self); + CHECK_RESTRICTED(self); + /* Even if self is already readonly, we still need to create a new + * object for .release() to work correctly. + */ + self = (PyMemoryViewObject *) mbuf_add_view(self->mbuf, &self->view); + if (self != NULL) { + self->view.readonly = 1; + }; + return (PyObject *) self; +} + /*[clinic input] memoryview.toreadonly @@ -1687,17 +1702,9 @@ memoryview_toreadonly_impl(PyMemoryViewObject *self) if (memoryview_pin(self) < 0) { return NULL; } - CHECK_RELEASED(self); - CHECK_RESTRICTED(self); - /* Even if self is already readonly, we still need to create a new - * object for .release() to work correctly. - */ - self = (PyMemoryViewObject *) mbuf_add_view(self->mbuf, &self->view); - if (self != NULL) { - self->view.readonly = 1; - }; + PyObject *result = memoryview_toreadonly_pinned(self); memoryview_unpin(self); - return (PyObject *) self; + return result; } @@ -2583,7 +2590,13 @@ memoryview_hex_pinned(PyMemoryViewObject *self, PyObject *sep, // Prevent 'self' from being freed if computing len(sep) mutates 'self' // in _Py_strhex_with_sep(). // See: https://github.com/python/cpython/issues/143195. - FT_ATOMIC_ADD_SSIZE(self->exports, 1); +#ifdef Py_GIL_DISABLED + if (memoryview_add_export(self) < 0) { + return NULL; + } +#else + self->exports++; +#endif PyObject *ret = _Py_strhex_with_sep(src->buf, src->len, sep, bytes_per_sep); FT_ATOMIC_ADD_SSIZE(self->exports, -1); return ret; @@ -3638,7 +3651,13 @@ memory_hash_pinned(PyObject *_self) if (view->obj != NULL) { // Prevent 'self' from being freed when computing the item's hash. // See https://github.com/python/cpython/issues/142664. - FT_ATOMIC_ADD_SSIZE(self->exports, 1); +#ifdef Py_GIL_DISABLED + if (memoryview_add_export(self) < 0) { + return -1; + } +#else + self->exports++; +#endif Py_hash_t h = PyObject_Hash(view->obj); FT_ATOMIC_ADD_SSIZE(self->exports, -1); if (h == -1) { From 908a2a1fccf07546a5583dbfb4b05159e98aa97c Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Thu, 27 Aug 2026 11:58:03 -0700 Subject: [PATCH 03/12] Unit test improvements. Speed it up (less iterations, rounds and threads). Rename confusing assert_exporter_free() method to ensure_exporter_free(). --- .../test_free_threading/test_memoryview.py | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/Lib/test/test_free_threading/test_memoryview.py b/Lib/test/test_free_threading/test_memoryview.py index 8493f425d7b86b8..b8369e912d93bb6 100644 --- a/Lib/test/test_free_threading/test_memoryview.py +++ b/Lib/test/test_free_threading/test_memoryview.py @@ -4,11 +4,9 @@ from test.support import threading_helper -NTHREADS = 8 - -# Fresh objects expose one-time races under ThreadSanitizer. -ROUNDS = 20 -ITERS = 20 +NTHREADS = 4 +ROUNDS = 4 +ITERS = 10 HASH_DATA_TEMPLATE = bytes(range(256)) HASH_DATA_REPEAT = 256 @@ -44,7 +42,8 @@ def run_racy(func, *args): @threading_helper.requires_working_threading() class TestMemoryViewRaces(unittest.TestCase): - def assert_exporter_free(self, buf): + def ensure_exporter_free(self, buf): + # Will raise BufferError if any memoryview still exports it. buf.append(0) del buf[-1] @@ -77,7 +76,7 @@ def release(views=views): threading_helper.run_concurrently(release, nthreads=NTHREADS) - self.assert_exporter_free(buf) + self.ensure_exporter_free(buf) def test_release_races_with_reads(self): for _ in range(ROUNDS): @@ -104,7 +103,7 @@ def reader(): ) cell[0].release() - self.assert_exporter_free(buf) + self.ensure_exporter_free(buf) def test_read_keeps_exporter_alive_after_release(self): size = 1 << 20 @@ -157,7 +156,7 @@ def writer(slot): threading_helper.run_concurrently(workers, nthreads=NTHREADS) cell[0].release() - self.assert_exporter_free(buf) + self.ensure_exporter_free(buf) def test_release_races_with_buffer_exports(self): for _ in range(ROUNDS): @@ -177,7 +176,7 @@ def releaser(): ) mv.release() - self.assert_exporter_free(buf) + self.ensure_exporter_free(buf) def test_release_with_live_export(self): buf = bytearray(64) @@ -198,7 +197,7 @@ def release(mv=mv): held.release() mv.release() - self.assert_exporter_free(buf) + self.ensure_exporter_free(buf) def test_compare_two_views_races_with_release(self): for _ in range(ROUNDS): @@ -220,13 +219,14 @@ def comparer(): run_racy(lambda: left != right) threading_helper.run_concurrently( - [lambda: releaser(0), lambda: releaser(1)] + [comparer] * 6, + [lambda: releaser(0), lambda: releaser(1)] + + [comparer] * (NTHREADS - 2), nthreads=NTHREADS, ) for mv in cell: mv.release() - self.assert_exporter_free(buf) + self.ensure_exporter_free(buf) def test_release_parent_keeps_child_valid(self): for _ in range(ROUNDS): @@ -249,7 +249,7 @@ def release_parent(): self.assertEqual(child.tobytes(), bytes(range(32))) child.release() - self.assert_exporter_free(buf) + self.ensure_exporter_free(buf) def test_concurrent_iteration(self): for _ in range(ROUNDS): @@ -274,7 +274,7 @@ def iterator(): ) cell[0].release() - self.assert_exporter_free(buf) + self.ensure_exporter_free(buf) def test_iterator_exhaustion_drops_last_reference(self): def loop(): From 6692fa52c1f3b16d3d3197247549f82df5de388a Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Fri, 28 Aug 2026 08:43:06 -0700 Subject: [PATCH 04/12] Address review feedback. * Add comment to CAS while loop, yielding is unnecessary. * Use "relaxed" memory ordering, seq-cst is overkill. --- Objects/memoryobject.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index 75c66853d849a1c..1628a1b04323984 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -109,7 +109,9 @@ flags_test_and_set(int *flags, int bit) #ifdef Py_GIL_DISABLED int prev = _Py_atomic_load_int_relaxed(flags); while (!_Py_atomic_compare_exchange_int(flags, &prev, prev | bit)) { - // Compare-exchange updates prev. + // Compare-exchange updates prev. Note that retries are bounded by the + // number of flag bits being concurrently set. So, yielding here is + // unnecessary. } #else int prev = *flags; @@ -121,7 +123,7 @@ flags_test_and_set(int *flags, int bit) static inline int mv_get_flags(PyMemoryViewObject *mv) { - return FT_ATOMIC_LOAD_INT(mv->flags); + return FT_ATOMIC_LOAD_INT_RELAXED(mv->flags); } static inline int From e3c9012a878f5cc98279604229ba45104bf46833 Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Fri, 28 Aug 2026 09:37:31 -0700 Subject: [PATCH 05/12] Remove "while" loop for setting flags. We only need the "while" loop if we are setting multiple flag bits concurrently. That's not needed, we only set RELEASED. So, restructure code, no while loop needed and someone can't accidently call the function in the wrong way. --- Objects/memoryobject.c | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index 1628a1b04323984..2d8537ee5721c8c 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -104,20 +104,24 @@ _PyManagedBuffer_FromObject(PyObject *base, int flags) } static inline int -flags_test_and_set(int *flags, int bit) +flags_set_released(int *flags, int released) { #ifdef Py_GIL_DISABLED int prev = _Py_atomic_load_int_relaxed(flags); - while (!_Py_atomic_compare_exchange_int(flags, &prev, prev | bit)) { - // Compare-exchange updates prev. Note that retries are bounded by the - // number of flag bits being concurrently set. So, yielding here is - // unnecessary. + if (prev & released) { + return 1; } + if (!_Py_atomic_compare_exchange_int(flags, &prev, prev | released)) { + // RELEASED is the only flag that can change concurrently. + assert(prev & released); + return 1; + } + return 0; #else int prev = *flags; - *flags = prev | bit; + *flags = prev | released; + return (prev & released) != 0; #endif - return (prev & bit) != 0; } static inline int @@ -133,9 +137,9 @@ mv_has_flag(PyMemoryViewObject *mv, int bits) } static inline int -mv_set_flag(PyMemoryViewObject *mv, int bit) +mv_set_released(PyMemoryViewObject *mv) { - return flags_test_and_set(&mv->flags, bit); + return flags_set_released(&mv->flags, _Py_MEMORYVIEW_RELEASED); } static inline int @@ -145,15 +149,15 @@ mbuf_has_flag(_PyManagedBufferObject *mbuf, int bits) } static inline int -mbuf_set_flag(_PyManagedBufferObject *mbuf, int bit) +mbuf_set_released(_PyManagedBufferObject *mbuf) { - return flags_test_and_set(&mbuf->flags, bit); + return flags_set_released(&mbuf->flags, _Py_MANAGED_BUFFER_RELEASED); } static void mbuf_release(_PyManagedBufferObject *self) { - if (mbuf_set_flag(self, _Py_MANAGED_BUFFER_RELEASED)) { + if (mbuf_set_released(self)) { return; } @@ -1227,7 +1231,7 @@ static void _memory_release(PyMemoryViewObject *self) { assert(get_exports(self) == 0); - if (mv_set_flag(self, _Py_MEMORYVIEW_RELEASED)) { + if (mv_set_released(self)) { return; } From 2668057d42f0cea068118a60f5505511695d4c4f Mon Sep 17 00:00:00 2001 From: Neil Schemenauer Date: Tue, 20 Oct 2026 17:01:44 -0700 Subject: [PATCH 06/12] Handle GC-cleared memoryviews before pinning Incorporate review suggestions. Add a regression test using a tp_clear helper, recommend per-thread views for read-heavy workloads, and clarify the scope of the thread-safety NEWS entry. Co-authored-by: Nathan Goldbaum --- Doc/builtins/threadsafety.rst | 4 ++++ Lib/test/test_memoryview.py | 8 ++++++++ .../2026-08-21-14-37-50.gh-issue-127716.KrPth8.rst | 3 ++- Modules/_testcapi/buffer.c | 14 ++++++++++++++ Objects/memoryobject.c | 5 +++++ 5 files changed, 33 insertions(+), 1 deletion(-) diff --git a/Doc/builtins/threadsafety.rst b/Doc/builtins/threadsafety.rst index 8c463d1dbd0814d..e2e605a49db459c 100644 --- a/Doc/builtins/threadsafety.rst +++ b/Doc/builtins/threadsafety.rst @@ -610,3 +610,7 @@ thread-safe. If another thread has an in-flight operation on the memoryview when it is released, the underlying buffer may remain exported until that operation completes. Restrictions on the exporter, such as prohibiting resizing, remain in effect until then. + +For read-heavy workloads that share an underlying object across threads, +creating a separate memoryview of that object in each thread may improve +performance. diff --git a/Lib/test/test_memoryview.py b/Lib/test/test_memoryview.py index 878dff5c633811c..dbef7bc80c27ddc 100644 --- a/Lib/test/test_memoryview.py +++ b/Lib/test/test_memoryview.py @@ -696,6 +696,14 @@ class ArrayMemorySliceSliceTest(unittest.TestCase, class OtherTest(unittest.TestCase): + def test_gc_cleared_view(self): + _testcapi = import_helper.import_module("_testcapi") + + view = memoryview(bytearray(b"abc")) + _testcapi.clear_memoryview(view) + with self.assertRaisesRegex(ValueError, "released"): + view.tobytes() + def test_cast_order(self): # gh-78959: cast(order=...) selects the result memory layout. b = bytearray(range(6)) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-21-14-37-50.gh-issue-127716.KrPth8.rst b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-21-14-37-50.gh-issue-127716.KrPth8.rst index 731dd2bb83851c1..ca2e9bf7a892f21 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2026-08-21-14-37-50.gh-issue-127716.KrPth8.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2026-08-21-14-37-50.gh-issue-127716.KrPth8.rst @@ -1,4 +1,5 @@ -On the free-threaded build, :class:`memoryview` operations are now thread safe: +On the free-threaded build, :class:`memoryview` lifetime and bookkeeping +operations are now thread safe: an operation that reads the buffer holds the managed buffer for its duration, so a concurrent :meth:`~memoryview.release` can no longer free the buffer under it; slicing a memoryview from several threads no longer loses buffer diff --git a/Modules/_testcapi/buffer.c b/Modules/_testcapi/buffer.c index 48393a3dd530c65..c086d7250f67745 100644 --- a/Modules/_testcapi/buffer.c +++ b/Modules/_testcapi/buffer.c @@ -114,8 +114,22 @@ buffer_pointer_as_int(PyObject *Py_UNUSED(module), PyObject *obj) return out; } +static PyObject * +clear_memoryview(PyObject *Py_UNUSED(module), PyObject *obj) +{ + if (!PyMemoryView_Check(obj)) { + PyErr_SetString(PyExc_TypeError, "memoryview expected"); + return NULL; + } + if (Py_TYPE(obj)->tp_clear(obj) < 0) { + return NULL; + } + Py_RETURN_NONE; +} + static PyMethodDef test_methods[] = { {"buffer_pointer_as_int", buffer_pointer_as_int, METH_O}, + {"clear_memoryview", clear_memoryview, METH_O}, {NULL}, }; diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index b63fe90ab9afd64..6a4b4e9a2ad205d 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -823,6 +823,11 @@ static inline int memoryview_pin(PyMemoryViewObject *self) { #ifdef Py_GIL_DISABLED + if (mv_has_flag(self, _Py_MEMORYVIEW_RELEASED)) { + PyErr_SetString(PyExc_ValueError, + "operation forbidden on released memoryview object"); + return -1; + } FT_ATOMIC_ADD_SSIZE(self->mbuf->exports, 1); if (BASE_INACCESSIBLE(self)) { mbuf_drop_export(self->mbuf); From a5163e5d3ed6e928880682f3226f332a5818cc4f Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 21 Oct 2026 13:31:39 -0700 Subject: [PATCH 07/12] Prevent memoryview release during active operations --- Include/cpython/memoryobject.h | 2 +- Lib/test/test_memoryview.py | 152 +++++++++++++++++++++++---------- Objects/memoryobject.c | 112 ++++++++++++++---------- 3 files changed, 175 insertions(+), 91 deletions(-) diff --git a/Include/cpython/memoryobject.h b/Include/cpython/memoryobject.h index 419e57d1ad56a49..284c485049b8244 100644 --- a/Include/cpython/memoryobject.h +++ b/Include/cpython/memoryobject.h @@ -29,7 +29,7 @@ typedef struct { _PyManagedBufferObject *mbuf; /* managed buffer */ Py_hash_t hash; /* hash value for read-only views */ int flags; /* state flags */ - Py_ssize_t exports; /* number of buffer re-exports */ + Py_ssize_t exports; /* buffer re-exports and active operations */ Py_buffer view; /* private copy of the exporter's view */ PyObject *weakreflist; Py_ssize_t ob_array[1]; /* shape, strides, suboffsets */ diff --git a/Lib/test/test_memoryview.py b/Lib/test/test_memoryview.py index dbef7bc80c27ddc..58e753e8531e308 100644 --- a/Lib/test/test_memoryview.py +++ b/Lib/test/test_memoryview.py @@ -561,6 +561,88 @@ def _check_contents(self, tp, obj, contents): class BytesMemoryviewTest(unittest.TestCase, BaseMemoryviewTests, BaseBytesMemoryTests): + def test_comparison_blocks_release_during_buffer_callback(self): + data = bytearray(b'abc') + view = memoryview(data) + assert_raises = self.assertRaises + + class Other: + def __buffer__(self, flags): + with assert_raises(BufferError): + view.release() + with assert_raises(BufferError): + data.clear() + return memoryview(data) + + self.assertTrue(view == Other()) + self.assertEqual(view.tobytes(), b'abc') + view.release() + data.clear() + + def test_comparison_blocks_gc_clear_during_buffer_callback(self): + _testcapi = import_helper.import_module('_testcapi') + data = bytearray(b'abc') + view = memoryview(data) + + class Other: + def __buffer__(self, flags): + _testcapi.clear_memoryview(view) + return memoryview(data) + + self.assertTrue(view == Other()) + self.assertEqual(view.tobytes(), b'abc') + + def test_comparison_releases_buffer_after_access(self): + data = bytearray(b'abc') + view = memoryview(data) + + class Other: + def __buffer__(self, flags): + return view + + def __release_buffer__(self, buffer): + buffer.release() + + with support.catch_unraisable_exception() as cm: + self.assertTrue(view == Other()) + self.assertIsNone(cm.unraisable) + data.clear() + + def test_assignment_blocks_release_during_index_callback(self): + data = bytearray(b'abc') + view = memoryview(data).cast('c') + assert_raises = self.assertRaises + + class Index: + def __index__(self): + with assert_raises(BufferError): + view.release() + with assert_raises(BufferError): + data.clear() + return 0 + + view[Index()] = b'x' + self.assertEqual(data, b'xbc') + view.release() + data.clear() + + def test_assignment_releases_buffer_after_access(self): + data = bytearray(b'abc') + view = memoryview(data) + + class Other: + def __buffer__(self, flags): + return view + + def __release_buffer__(self, buffer): + buffer.release() + + with support.catch_unraisable_exception() as cm: + view[:] = Other() + self.assertIsNone(cm.unraisable) + self.assertEqual(data, b'abc') + data.clear() + def test_constructor(self): for tp in self._types: ob = tp(self._source) @@ -824,15 +906,11 @@ def test_pickle(self): with self.assertRaises(TypeError): pickle.dumps(m, proto) - def test_use_released_memory(self): - # gh-92888: Previously it was possible to use a memoryview even after - # backing buffer is freed in certain cases. This tests that those - # cases raise an exception. + def test_release_during_operation(self): + # gh-92888: A callback cannot release a view while an operation uses it. size = 128 def release(): m.release() - nonlocal ba - ba = bytearray(size) class MyIndex: def __index__(self): release() @@ -846,85 +924,73 @@ def __bool__(self): release() return True - ba = None m = memoryview(bytearray(b'\xff'*size)) - with self.assertRaises(ValueError): + with self.assertRaises(BufferError): m[MyIndex()] - ba = None m = memoryview(bytearray(b'\xff'*size)) - self.assertEqual(list(m[:MyIndex()]), [255] * 4) + with self.assertRaises(BufferError): + m[:MyIndex()] - ba = None m = memoryview(bytearray(b'\xff'*size)) - self.assertEqual(list(m[MyIndex():8]), [255] * 4) + with self.assertRaises(BufferError): + m[MyIndex():8] - ba = None m = memoryview(bytearray(b'\xff'*size)).cast('B', (64, 2)) - with self.assertRaisesRegex(ValueError, "operation forbidden"): + with self.assertRaises(BufferError): m[MyIndex(), 0] - ba = None m = memoryview(bytearray(b'\xff'*size)).cast('B', (2, 64)) - with self.assertRaisesRegex(ValueError, "operation forbidden"): + with self.assertRaises(BufferError): m[0, MyIndex()] - ba = None m = memoryview(bytearray(b'\xff'*size)) - with self.assertRaisesRegex(ValueError, "operation forbidden"): + with self.assertRaises(BufferError): m[MyIndex()] = 42 - self.assertEqual(ba[:8], b'\0'*8) + self.assertEqual(m.tobytes(), b'\xff'*size) - ba = None m = memoryview(bytearray(b'\xff'*size)) - with self.assertRaisesRegex(ValueError, "operation forbidden"): + with self.assertRaises(BufferError): m[:MyIndex()] = b'spam' - self.assertEqual(ba[:8], b'\0'*8) + self.assertEqual(m.tobytes(), b'\xff'*size) - ba = None m = memoryview(bytearray(b'\xff'*size)) - with self.assertRaisesRegex(ValueError, "operation forbidden"): + with self.assertRaises(BufferError): m[MyIndex():8] = b'spam' - self.assertEqual(ba[:8], b'\0'*8) + self.assertEqual(m.tobytes(), b'\xff'*size) - ba = None m = memoryview(bytearray(b'\xff'*size)).cast('B', (64, 2)) - with self.assertRaisesRegex(ValueError, "operation forbidden"): + with self.assertRaises(BufferError): m[MyIndex(), 0] = 42 - self.assertEqual(ba[8:16], b'\0'*8) - ba = None + self.assertEqual(m.tobytes(), b'\xff'*size) m = memoryview(bytearray(b'\xff'*size)).cast('B', (2, 64)) - with self.assertRaisesRegex(ValueError, "operation forbidden"): + with self.assertRaises(BufferError): m[0, MyIndex()] = 42 - self.assertEqual(ba[:8], b'\0'*8) + self.assertEqual(m.tobytes(), b'\xff'*size) - ba = None m = memoryview(bytearray(b'\xff'*size)) - with self.assertRaisesRegex(ValueError, "operation forbidden"): + with self.assertRaises(BufferError): m[0] = MyIndex() - self.assertEqual(ba[:8], b'\0'*8) + self.assertEqual(m.tobytes(), b'\xff'*size) for fmt in 'bhilqnBHILQN': with self.subTest(fmt=fmt): - ba = None m = memoryview(bytearray(b'\xff'*size)).cast(fmt) - with self.assertRaisesRegex(ValueError, "operation forbidden"): + with self.assertRaises(BufferError): m[0] = MyIndex() - self.assertEqual(ba[:8], b'\0'*8) + self.assertEqual(m.tobytes(), b'\xff'*size) for fmt in 'fd': with self.subTest(fmt=fmt): - ba = None m = memoryview(bytearray(b'\xff'*size)).cast(fmt) - with self.assertRaisesRegex(ValueError, "operation forbidden"): + with self.assertRaises(BufferError): m[0] = MyFloat() - self.assertEqual(ba[:8], b'\0'*8) + self.assertEqual(m.tobytes(), b'\xff'*size) - ba = None m = memoryview(bytearray(b'\xff'*size)).cast('?') - with self.assertRaisesRegex(ValueError, "operation forbidden"): + with self.assertRaises(BufferError): m[0] = MyBool() - self.assertEqual(ba[:8], b'\0'*8) + self.assertEqual(m.tobytes(), b'\xff'*size) def test_buffer_reference_loop(self): m = memoryview(b'abc').__buffer__(0) diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index 6a4b4e9a2ad205d..b0723b62b6735f1 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -822,26 +822,17 @@ mbuf_drop_export(_PyManagedBufferObject *mbuf) static inline int memoryview_pin(PyMemoryViewObject *self) { + /* Treat operations that access self->view across callbacks as exports. + This prevents a callback from releasing the view while it is in use. */ #ifdef Py_GIL_DISABLED - if (mv_has_flag(self, _Py_MEMORYVIEW_RELEASED)) { - PyErr_SetString(PyExc_ValueError, - "operation forbidden on released memoryview object"); - return -1; - } - FT_ATOMIC_ADD_SSIZE(self->mbuf->exports, 1); - if (BASE_INACCESSIBLE(self)) { - mbuf_drop_export(self->mbuf); - PyErr_SetString(PyExc_ValueError, - "operation forbidden on released memoryview object"); - return -1; - } - return 0; + return memoryview_add_export(self); #else if (BASE_INACCESSIBLE(self)) { PyErr_SetString(PyExc_ValueError, "operation forbidden on released memoryview object"); return -1; } + self->exports++; return 0; #endif } @@ -849,9 +840,7 @@ memoryview_pin(PyMemoryViewObject *self) static inline void memoryview_unpin(PyMemoryViewObject *self) { -#ifdef Py_GIL_DISABLED - mbuf_drop_export(self->mbuf); -#endif + FT_ATOMIC_ADD_SSIZE(self->exports, -1); } /* Expose a raw memory area as a view of contiguous bytes. flags can be @@ -2954,11 +2943,11 @@ memory_subscript_pinned(PyObject *_self, PyObject *key) } static int -memory_ass_sub_pinned(PyObject *_self, PyObject *key, PyObject *value) +memory_ass_sub_pinned(PyObject *_self, PyObject *key, PyObject *value, + Py_buffer *src, bool *src_acquired) { PyMemoryViewObject *self = (PyMemoryViewObject *)_self; Py_buffer *view = &(self->view); - Py_buffer src; const char *fmt; char *ptr; @@ -3011,8 +3000,9 @@ memory_ass_sub_pinned(PyObject *_self, PyObject *key, PyObject *value) int ret = -1; /* rvalue must be an exporter */ - if (PyObject_GetBuffer(value, &src, PyBUF_FULL_RO) < 0) + if (PyObject_GetBuffer(value, src, PyBUF_FULL_RO) < 0) return ret; + *src_acquired = true; dest = *view; dest.shape = &arrays[0]; dest.shape[0] = view->shape[0]; @@ -3025,10 +3015,9 @@ memory_ass_sub_pinned(PyObject *_self, PyObject *key, PyObject *value) goto end_block; dest.len = dest.shape[0] * dest.itemsize; - ret = copy_single(self, &dest, &src); + ret = copy_single(self, &dest, src); end_block: - PyBuffer_Release(&src); return ret; } if (is_multiindex(key)) { @@ -3096,11 +3085,16 @@ static int memory_ass_sub(PyObject *_self, PyObject *key, PyObject *value) { PyMemoryViewObject *self = (PyMemoryViewObject *)_self; + Py_buffer src; + bool src_acquired = false; if (memoryview_pin(self) < 0) { return -1; } - int result = memory_ass_sub_pinned(_self, key, value); + int result = memory_ass_sub_pinned(_self, key, value, &src, &src_acquired); memoryview_unpin(self); + if (src_acquired) { + PyBuffer_Release(&src); + } return result; } @@ -3492,14 +3486,30 @@ cmp_rec(const char *p, const char *q, return 1; } +struct memory_compare_state { + Py_buffer wbuf; + bool wbuf_acquired; + struct unpacker *unpack_v; + struct unpacker *unpack_w; +}; + +static void +memory_compare_clear(struct memory_compare_state *state) +{ + if (state->wbuf_acquired) { + PyBuffer_Release(&state->wbuf); + } + unpacker_free(state->unpack_v); + unpacker_free(state->unpack_w); +} + static PyObject * -memory_richcompare_pinned(PyObject *v, PyObject *w, int op) +memory_richcompare_pinned(PyObject *v, PyObject *w, int op, + struct memory_compare_state *state) { PyObject *res; - Py_buffer wbuf, *vv; + Py_buffer *vv; Py_buffer *ww = NULL; - struct unpacker *unpack_v = NULL; - struct unpacker *unpack_w = NULL; const char *vfmt, *wfmt; int equal = MV_COMPARE_NOT_IMPL; @@ -3545,11 +3555,12 @@ memory_richcompare_pinned(PyObject *v, PyObject *w, int op) ww = VIEW_ADDR(w); } else { - if (PyObject_GetBuffer(w, &wbuf, PyBUF_FULL_RO) < 0) { + if (PyObject_GetBuffer(w, &state->wbuf, PyBUF_FULL_RO) < 0) { PyErr_Clear(); goto result; /* Py_NotImplemented */ } - ww = &wbuf; + state->wbuf_acquired = true; + ww = &state->wbuf; } if (!equiv_shape(vv, ww)) { @@ -3569,13 +3580,13 @@ memory_richcompare_pinned(PyObject *v, PyObject *w, int op) incorrect results in the case of NaNs or uninitialized padding bytes. */ vfmt = "_"; - unpack_v = struct_get_unpacker(vv->format, vv->itemsize); - if (unpack_v == NULL) { + state->unpack_v = struct_get_unpacker(vv->format, vv->itemsize); + if (state->unpack_v == NULL) { equal = fix_struct_error_int(); goto result; } - unpack_w = struct_get_unpacker(ww->format, ww->itemsize); - if (unpack_w == NULL) { + state->unpack_w = struct_get_unpacker(ww->format, ww->itemsize); + if (state->unpack_w == NULL) { equal = fix_struct_error_int(); goto result; } @@ -3583,19 +3594,19 @@ memory_richcompare_pinned(PyObject *v, PyObject *w, int op) if (vv->ndim == 0) { equal = unpack_cmp(vv->buf, ww->buf, - vfmt, unpack_v, unpack_w); + vfmt, state->unpack_v, state->unpack_w); } else if (vv->ndim == 1) { equal = cmp_base(vv->buf, ww->buf, vv->shape, vv->strides, vv->suboffsets, ww->strides, ww->suboffsets, - vfmt, unpack_v, unpack_w); + vfmt, state->unpack_v, state->unpack_w); } else { equal = cmp_rec(vv->buf, ww->buf, vv->ndim, vv->shape, vv->strides, vv->suboffsets, ww->strides, ww->suboffsets, - vfmt, unpack_v, unpack_w); + vfmt, state->unpack_v, state->unpack_w); } result: @@ -3610,12 +3621,6 @@ memory_richcompare_pinned(PyObject *v, PyObject *w, int op) else res = Py_False; - if (ww == &wbuf) - PyBuffer_Release(ww); - - unpacker_free(unpack_v); - unpacker_free(unpack_w); - return Py_XNewRef(res); } @@ -3623,27 +3628,40 @@ static PyObject * memory_richcompare(PyObject *v, PyObject *w, int op) { PyMemoryViewObject *self = (PyMemoryViewObject *)v; + PyMemoryViewObject *other = NULL; + struct memory_compare_state state = {0}; + bool self_pinned = false; + bool other_pinned = false; + PyObject *result; + if (memoryview_pin(self) < 0) { /* Released memoryviews still compare by identity. */ PyErr_Clear(); - return memory_richcompare_pinned(v, w, op); + goto compare; } + self_pinned = true; - PyMemoryViewObject *other = NULL; if (PyMemoryView_Check(w) && w != v) { other = (PyMemoryViewObject *)w; if (memoryview_pin(other) < 0) { PyErr_Clear(); memoryview_unpin(self); - return memory_richcompare_pinned(v, w, op); + self_pinned = false; + goto compare; } + other_pinned = true; } - PyObject *result = memory_richcompare_pinned(v, w, op); - if (other != NULL) { +compare: + result = memory_richcompare_pinned(v, w, op, &state); + if (other_pinned) { memoryview_unpin(other); } - memoryview_unpin(self); + if (self_pinned) { + memoryview_unpin(self); + } + /* Buffer release may invoke the exporter's Python cleanup callback. */ + memory_compare_clear(&state); return result; } From e0b6fa592c5703cd75b6462c7874d2ec0114c3ec Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 21 Oct 2026 13:38:54 -0700 Subject: [PATCH 08/12] merge new tests with existing tests --- Lib/test/test_memoryview.py | 59 ++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/Lib/test/test_memoryview.py b/Lib/test/test_memoryview.py index 58e753e8531e308..0a3cfa6f7cded99 100644 --- a/Lib/test/test_memoryview.py +++ b/Lib/test/test_memoryview.py @@ -579,19 +579,6 @@ def __buffer__(self, flags): view.release() data.clear() - def test_comparison_blocks_gc_clear_during_buffer_callback(self): - _testcapi = import_helper.import_module('_testcapi') - data = bytearray(b'abc') - view = memoryview(data) - - class Other: - def __buffer__(self, flags): - _testcapi.clear_memoryview(view) - return memoryview(data) - - self.assertTrue(view == Other()) - self.assertEqual(view.tobytes(), b'abc') - def test_comparison_releases_buffer_after_access(self): data = bytearray(b'abc') view = memoryview(data) @@ -608,24 +595,6 @@ def __release_buffer__(self, buffer): self.assertIsNone(cm.unraisable) data.clear() - def test_assignment_blocks_release_during_index_callback(self): - data = bytearray(b'abc') - view = memoryview(data).cast('c') - assert_raises = self.assertRaises - - class Index: - def __index__(self): - with assert_raises(BufferError): - view.release() - with assert_raises(BufferError): - data.clear() - return 0 - - view[Index()] = b'x' - self.assertEqual(data, b'xbc') - view.release() - data.clear() - def test_assignment_releases_buffer_after_access(self): data = bytearray(b'abc') view = memoryview(data) @@ -786,6 +755,17 @@ def test_gc_cleared_view(self): with self.assertRaisesRegex(ValueError, "released"): view.tobytes() + data = bytearray(b"abc") + view = memoryview(data) + + class Other: + def __buffer__(self, flags): + _testcapi.clear_memoryview(view) + return memoryview(data) + + self.assertTrue(view == Other()) + self.assertEqual(view.tobytes(), b"abc") + def test_cast_order(self): # gh-78959: cast(order=...) selects the result memory layout. b = bytearray(range(6)) @@ -992,6 +972,23 @@ def __bool__(self): m[0] = MyBool() self.assertEqual(m.tobytes(), b'\xff'*size) + data = bytearray(b'abc') + m = memoryview(data).cast('c') + assert_raises = self.assertRaises + + class CatchingIndex: + def __index__(self): + with assert_raises(BufferError): + release() + with assert_raises(BufferError): + data.clear() + return 0 + + m[CatchingIndex()] = b'x' + self.assertEqual(data, b'xbc') + m.release() + data.clear() + def test_buffer_reference_loop(self): m = memoryview(b'abc').__buffer__(0) o = MyObject() From e8a9aa55754a1a8b08cc244a48248fd273074563 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 21 Oct 2026 15:06:01 -0700 Subject: [PATCH 09/12] Pin the managed buffer, not the view, during memoryview operations --- Include/cpython/memoryobject.h | 2 +- Lib/test/test_memoryview.py | 109 +++++++++-------- Objects/memoryobject.c | 213 +++++++++++++++++---------------- 3 files changed, 166 insertions(+), 158 deletions(-) diff --git a/Include/cpython/memoryobject.h b/Include/cpython/memoryobject.h index 284c485049b8244..419e57d1ad56a49 100644 --- a/Include/cpython/memoryobject.h +++ b/Include/cpython/memoryobject.h @@ -29,7 +29,7 @@ typedef struct { _PyManagedBufferObject *mbuf; /* managed buffer */ Py_hash_t hash; /* hash value for read-only views */ int flags; /* state flags */ - Py_ssize_t exports; /* buffer re-exports and active operations */ + Py_ssize_t exports; /* number of buffer re-exports */ Py_buffer view; /* private copy of the exporter's view */ PyObject *weakreflist; Py_ssize_t ob_array[1]; /* shape, strides, suboffsets */ diff --git a/Lib/test/test_memoryview.py b/Lib/test/test_memoryview.py index 0a3cfa6f7cded99..283f0077d7ae932 100644 --- a/Lib/test/test_memoryview.py +++ b/Lib/test/test_memoryview.py @@ -561,25 +561,28 @@ def _check_contents(self, tp, obj, contents): class BytesMemoryviewTest(unittest.TestCase, BaseMemoryviewTests, BaseBytesMemoryTests): - def test_comparison_blocks_release_during_buffer_callback(self): + def test_comparison_release_during_buffer_callback(self): + # Releasing the view from inside the other operand's __buffer__ + # succeeds, but the buffer stays pinned until the comparison ends: + # the exporter cannot be resized under the running comparison. data = bytearray(b'abc') view = memoryview(data) assert_raises = self.assertRaises class Other: def __buffer__(self, flags): - with assert_raises(BufferError): - view.release() + view.release() with assert_raises(BufferError): data.clear() return memoryview(data) - self.assertTrue(view == Other()) - self.assertEqual(view.tobytes(), b'abc') - view.release() + view == Other() + with self.assertRaisesRegex(ValueError, "released"): + view.tobytes() data.clear() def test_comparison_releases_buffer_after_access(self): + # __release_buffer__ releasing the very view being compared. data = bytearray(b'abc') view = memoryview(data) @@ -596,6 +599,7 @@ def __release_buffer__(self, buffer): data.clear() def test_assignment_releases_buffer_after_access(self): + # __release_buffer__ releasing the very view being assigned to. data = bytearray(b'abc') view = memoryview(data) @@ -755,6 +759,8 @@ def test_gc_cleared_view(self): with self.assertRaisesRegex(ValueError, "released"): view.tobytes() + # Clearing the view while an operation is in progress must not + # free the buffer under that operation, nor crash when it ends. data = bytearray(b"abc") view = memoryview(data) @@ -764,7 +770,9 @@ def __buffer__(self, flags): return memoryview(data) self.assertTrue(view == Other()) - self.assertEqual(view.tobytes(), b"abc") + with self.assertRaisesRegex(ValueError, "released"): + view.tobytes() + data.clear() def test_cast_order(self): # gh-78959: cast(order=...) selects the result memory layout. @@ -886,11 +894,15 @@ def test_pickle(self): with self.assertRaises(TypeError): pickle.dumps(m, proto) - def test_release_during_operation(self): - # gh-92888: A callback cannot release a view while an operation uses it. + def test_use_released_memory(self): + # gh-92888: Previously it was possible to use a memoryview even after + # backing buffer is freed in certain cases. This tests that those + # cases raise an exception. size = 128 def release(): m.release() + nonlocal ba + ba = bytearray(size) class MyIndex: def __index__(self): release() @@ -904,90 +916,85 @@ def __bool__(self): release() return True + ba = None m = memoryview(bytearray(b'\xff'*size)) - with self.assertRaises(BufferError): + with self.assertRaises(ValueError): m[MyIndex()] + ba = None m = memoryview(bytearray(b'\xff'*size)) - with self.assertRaises(BufferError): - m[:MyIndex()] + self.assertEqual(list(m[:MyIndex()]), [255] * 4) + ba = None m = memoryview(bytearray(b'\xff'*size)) - with self.assertRaises(BufferError): - m[MyIndex():8] + self.assertEqual(list(m[MyIndex():8]), [255] * 4) + ba = None m = memoryview(bytearray(b'\xff'*size)).cast('B', (64, 2)) - with self.assertRaises(BufferError): + with self.assertRaisesRegex(ValueError, "operation forbidden"): m[MyIndex(), 0] + ba = None m = memoryview(bytearray(b'\xff'*size)).cast('B', (2, 64)) - with self.assertRaises(BufferError): + with self.assertRaisesRegex(ValueError, "operation forbidden"): m[0, MyIndex()] + ba = None m = memoryview(bytearray(b'\xff'*size)) - with self.assertRaises(BufferError): + with self.assertRaisesRegex(ValueError, "operation forbidden"): m[MyIndex()] = 42 - self.assertEqual(m.tobytes(), b'\xff'*size) + self.assertEqual(ba[:8], b'\0'*8) + ba = None m = memoryview(bytearray(b'\xff'*size)) - with self.assertRaises(BufferError): + with self.assertRaisesRegex(ValueError, "operation forbidden"): m[:MyIndex()] = b'spam' - self.assertEqual(m.tobytes(), b'\xff'*size) + self.assertEqual(ba[:8], b'\0'*8) + ba = None m = memoryview(bytearray(b'\xff'*size)) - with self.assertRaises(BufferError): + with self.assertRaisesRegex(ValueError, "operation forbidden"): m[MyIndex():8] = b'spam' - self.assertEqual(m.tobytes(), b'\xff'*size) + self.assertEqual(ba[:8], b'\0'*8) + ba = None m = memoryview(bytearray(b'\xff'*size)).cast('B', (64, 2)) - with self.assertRaises(BufferError): + with self.assertRaisesRegex(ValueError, "operation forbidden"): m[MyIndex(), 0] = 42 - self.assertEqual(m.tobytes(), b'\xff'*size) + self.assertEqual(ba[8:16], b'\0'*8) + ba = None m = memoryview(bytearray(b'\xff'*size)).cast('B', (2, 64)) - with self.assertRaises(BufferError): + with self.assertRaisesRegex(ValueError, "operation forbidden"): m[0, MyIndex()] = 42 - self.assertEqual(m.tobytes(), b'\xff'*size) + self.assertEqual(ba[:8], b'\0'*8) + ba = None m = memoryview(bytearray(b'\xff'*size)) - with self.assertRaises(BufferError): + with self.assertRaisesRegex(ValueError, "operation forbidden"): m[0] = MyIndex() - self.assertEqual(m.tobytes(), b'\xff'*size) + self.assertEqual(ba[:8], b'\0'*8) for fmt in 'bhilqnBHILQN': with self.subTest(fmt=fmt): + ba = None m = memoryview(bytearray(b'\xff'*size)).cast(fmt) - with self.assertRaises(BufferError): + with self.assertRaisesRegex(ValueError, "operation forbidden"): m[0] = MyIndex() - self.assertEqual(m.tobytes(), b'\xff'*size) + self.assertEqual(ba[:8], b'\0'*8) for fmt in 'fd': with self.subTest(fmt=fmt): + ba = None m = memoryview(bytearray(b'\xff'*size)).cast(fmt) - with self.assertRaises(BufferError): + with self.assertRaisesRegex(ValueError, "operation forbidden"): m[0] = MyFloat() - self.assertEqual(m.tobytes(), b'\xff'*size) + self.assertEqual(ba[:8], b'\0'*8) + ba = None m = memoryview(bytearray(b'\xff'*size)).cast('?') - with self.assertRaises(BufferError): + with self.assertRaisesRegex(ValueError, "operation forbidden"): m[0] = MyBool() - self.assertEqual(m.tobytes(), b'\xff'*size) - - data = bytearray(b'abc') - m = memoryview(data).cast('c') - assert_raises = self.assertRaises - - class CatchingIndex: - def __index__(self): - with assert_raises(BufferError): - release() - with assert_raises(BufferError): - data.clear() - return 0 - - m[CatchingIndex()] = b'x' - self.assertEqual(data, b'xbc') - m.release() - data.clear() + self.assertEqual(ba[:8], b'\0'*8) def test_buffer_reference_loop(self): m = memoryview(b'abc').__buffer__(0) diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index b0723b62b6735f1..f6a0fdba25a5770 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -819,28 +819,38 @@ mbuf_drop_export(_PyManagedBufferObject *mbuf) } } -static inline int +/* Pin the managed buffer for the duration of an operation that reads + self->view. The pin is an export on the managed buffer, so a concurrent + or re-entrant release of this memoryview (or a tp_clear of it) cannot + free the underlying buffer while the operation is running. The pin does + not hold a reference to the managed buffer: the memoryview's own + reference is kept until memory_dealloc(), and the caller holds a + reference to the memoryview. On success, return the pinned managed + buffer; pass it to memoryview_unpin() afterwards. On failure, set an + exception and return NULL. */ +static inline _PyManagedBufferObject * memoryview_pin(PyMemoryViewObject *self) { - /* Treat operations that access self->view across callbacks as exports. - This prevents a callback from releasing the view while it is in use. */ -#ifdef Py_GIL_DISABLED - return memoryview_add_export(self); -#else + if (mv_has_flag(self, _Py_MEMORYVIEW_RELEASED)) { + PyErr_SetString(PyExc_ValueError, + "operation forbidden on released memoryview object"); + return NULL; + } + _PyManagedBufferObject *mbuf = self->mbuf; + FT_ATOMIC_ADD_SSIZE(mbuf->exports, 1); if (BASE_INACCESSIBLE(self)) { + mbuf_drop_export(mbuf); PyErr_SetString(PyExc_ValueError, "operation forbidden on released memoryview object"); - return -1; + return NULL; } - self->exports++; - return 0; -#endif + return mbuf; } static inline void -memoryview_unpin(PyMemoryViewObject *self) +memoryview_unpin(_PyManagedBufferObject *mbuf) { - FT_ATOMIC_ADD_SSIZE(self->exports, -1); + mbuf_drop_export(mbuf); } /* Expose a raw memory area as a view of contiguous bytes. flags can be @@ -913,7 +923,8 @@ PyMemoryView_FromObjectAndFlags(PyObject *v, int flags) if (PyMemoryView_Check(v)) { PyMemoryViewObject *mv = (PyMemoryViewObject *)v; - if (memoryview_pin(mv) < 0) { + _PyManagedBufferObject *mv_mbuf = memoryview_pin(mv); + if (mv_mbuf == NULL) { return NULL; } PyObject *result; @@ -923,9 +934,9 @@ PyMemoryView_FromObjectAndFlags(PyObject *v, int flags) result = NULL; } else { - result = mbuf_add_view(mv->mbuf, &mv->view); + result = mbuf_add_view(mv_mbuf, &mv->view); } - memoryview_unpin(mv); + memoryview_unpin(mv_mbuf); return result; } else if (PyObject_CheckBuffer(v)) { @@ -1291,8 +1302,11 @@ memory_clear(PyObject *_self) PyMemoryViewObject *self = (PyMemoryViewObject *)_self; if (get_exports(self) == 0) { _memory_release(self); - Py_CLEAR(self->mbuf); } + /* Keep the reference to self->mbuf until memory_dealloc(): an operation + that pinned the managed buffer may still be running (see + memoryview_pin()), and mbuf_clear() already breaks the managed + buffer's own reference cycle through the exporter. */ return 0; } @@ -1677,11 +1691,12 @@ memoryview_cast_impl(PyMemoryViewObject *self, PyObject *format, PyObject *shape, int order) /*[clinic end generated code: output=6410d87141f6bb56 input=4a1a2326c59caeb3]*/ { - if (memoryview_pin(self) < 0) { + _PyManagedBufferObject *self_mbuf = memoryview_pin(self); + if (self_mbuf == NULL) { return NULL; } PyObject *result = memoryview_cast_pinned(self, format, shape, order); - memoryview_unpin(self); + memoryview_unpin(self_mbuf); return result; } @@ -1710,11 +1725,12 @@ static PyObject * memoryview_toreadonly_impl(PyMemoryViewObject *self) /*[clinic end generated code: output=2c7e056f04c99e62 input=dc06d20f19ba236f]*/ { - if (memoryview_pin(self) < 0) { + _PyManagedBufferObject *self_mbuf = memoryview_pin(self); + if (self_mbuf == NULL) { return NULL; } PyObject *result = memoryview_toreadonly_pinned(self); - memoryview_unpin(self); + memoryview_unpin(self_mbuf); return result; } @@ -2524,11 +2540,12 @@ static PyObject * memoryview_tolist_impl(PyMemoryViewObject *self) /*[clinic end generated code: output=a6cda89214fd5a1b input=21e7d0c1860b211a]*/ { - if (memoryview_pin(self) < 0) { + _PyManagedBufferObject *self_mbuf = memoryview_pin(self); + if (self_mbuf == NULL) { return NULL; } PyObject *result = memoryview_tolist_pinned(self); - memoryview_unpin(self); + memoryview_unpin(self_mbuf); return result; } @@ -2588,11 +2605,12 @@ static PyObject * memoryview_tobytes_impl(PyMemoryViewObject *self, const char *order) /*[clinic end generated code: output=1288b62560a32a23 input=119c70aa91791dc8]*/ { - if (memoryview_pin(self) < 0) { + _PyManagedBufferObject *self_mbuf = memoryview_pin(self); + if (self_mbuf == NULL) { return NULL; } PyObject *result = memoryview_tobytes_pinned(self, order); - memoryview_unpin(self); + memoryview_unpin(self_mbuf); return result; } @@ -2666,11 +2684,12 @@ memoryview_hex_impl(PyMemoryViewObject *self, PyObject *sep, Py_ssize_t bytes_per_sep) /*[clinic end generated code: output=c9bb00c7a8e86056 input=3f1c5d08906e3b70]*/ { - if (memoryview_pin(self) < 0) { + _PyManagedBufferObject *self_mbuf = memoryview_pin(self); + if (self_mbuf == NULL) { return NULL; } PyObject *result = memoryview_hex_pinned(self, sep, bytes_per_sep); - memoryview_unpin(self); + memoryview_unpin(self_mbuf); return result; } @@ -2943,11 +2962,11 @@ memory_subscript_pinned(PyObject *_self, PyObject *key) } static int -memory_ass_sub_pinned(PyObject *_self, PyObject *key, PyObject *value, - Py_buffer *src, bool *src_acquired) +memory_ass_sub_pinned(PyObject *_self, PyObject *key, PyObject *value) { PyMemoryViewObject *self = (PyMemoryViewObject *)_self; Py_buffer *view = &(self->view); + Py_buffer src; const char *fmt; char *ptr; @@ -3000,9 +3019,8 @@ memory_ass_sub_pinned(PyObject *_self, PyObject *key, PyObject *value, int ret = -1; /* rvalue must be an exporter */ - if (PyObject_GetBuffer(value, src, PyBUF_FULL_RO) < 0) + if (PyObject_GetBuffer(value, &src, PyBUF_FULL_RO) < 0) return ret; - *src_acquired = true; dest = *view; dest.shape = &arrays[0]; dest.shape[0] = view->shape[0]; @@ -3015,9 +3033,10 @@ memory_ass_sub_pinned(PyObject *_self, PyObject *key, PyObject *value, goto end_block; dest.len = dest.shape[0] * dest.itemsize; - ret = copy_single(self, &dest, src); + ret = copy_single(self, &dest, &src); end_block: + PyBuffer_Release(&src); return ret; } if (is_multiindex(key)) { @@ -3061,11 +3080,12 @@ static PyObject * memory_item(PyObject *_self, Py_ssize_t index) { PyMemoryViewObject *self = (PyMemoryViewObject *)_self; - if (memoryview_pin(self) < 0) { + _PyManagedBufferObject *self_mbuf = memoryview_pin(self); + if (self_mbuf == NULL) { return NULL; } PyObject *result = memory_item_pinned(_self, index); - memoryview_unpin(self); + memoryview_unpin(self_mbuf); return result; } @@ -3073,11 +3093,12 @@ static PyObject * memory_subscript(PyObject *_self, PyObject *key) { PyMemoryViewObject *self = (PyMemoryViewObject *)_self; - if (memoryview_pin(self) < 0) { + _PyManagedBufferObject *self_mbuf = memoryview_pin(self); + if (self_mbuf == NULL) { return NULL; } PyObject *result = memory_subscript_pinned(_self, key); - memoryview_unpin(self); + memoryview_unpin(self_mbuf); return result; } @@ -3085,16 +3106,12 @@ static int memory_ass_sub(PyObject *_self, PyObject *key, PyObject *value) { PyMemoryViewObject *self = (PyMemoryViewObject *)_self; - Py_buffer src; - bool src_acquired = false; - if (memoryview_pin(self) < 0) { + _PyManagedBufferObject *self_mbuf = memoryview_pin(self); + if (self_mbuf == NULL) { return -1; } - int result = memory_ass_sub_pinned(_self, key, value, &src, &src_acquired); - memoryview_unpin(self); - if (src_acquired) { - PyBuffer_Release(&src); - } + int result = memory_ass_sub_pinned(_self, key, value); + memoryview_unpin(self_mbuf); return result; } @@ -3165,11 +3182,12 @@ static PyObject * memoryview_count_impl(PyMemoryViewObject *self, PyObject *value) /*[clinic end generated code: output=a15cb19311985063 input=e3036ce1ed7d1823]*/ { - if (memoryview_pin(self) < 0) { + _PyManagedBufferObject *self_mbuf = memoryview_pin(self); + if (self_mbuf == NULL) { return NULL; } PyObject *result = memoryview_count_pinned(self, value); - memoryview_unpin(self); + memoryview_unpin(self_mbuf); return result; } @@ -3264,11 +3282,12 @@ memoryview_index_impl(PyMemoryViewObject *self, PyObject *value, Py_ssize_t start, Py_ssize_t stop) /*[clinic end generated code: output=e0185e3819e549df input=0697a0165bf90b5a]*/ { - if (memoryview_pin(self) < 0) { + _PyManagedBufferObject *self_mbuf = memoryview_pin(self); + if (self_mbuf == NULL) { return NULL; } PyObject *result = memoryview_index_pinned(self, value, start, stop); - memoryview_unpin(self); + memoryview_unpin(self_mbuf); return result; } @@ -3486,30 +3505,14 @@ cmp_rec(const char *p, const char *q, return 1; } -struct memory_compare_state { - Py_buffer wbuf; - bool wbuf_acquired; - struct unpacker *unpack_v; - struct unpacker *unpack_w; -}; - -static void -memory_compare_clear(struct memory_compare_state *state) -{ - if (state->wbuf_acquired) { - PyBuffer_Release(&state->wbuf); - } - unpacker_free(state->unpack_v); - unpacker_free(state->unpack_w); -} - static PyObject * -memory_richcompare_pinned(PyObject *v, PyObject *w, int op, - struct memory_compare_state *state) +memory_richcompare_pinned(PyObject *v, PyObject *w, int op) { PyObject *res; - Py_buffer *vv; + Py_buffer wbuf, *vv; Py_buffer *ww = NULL; + struct unpacker *unpack_v = NULL; + struct unpacker *unpack_w = NULL; const char *vfmt, *wfmt; int equal = MV_COMPARE_NOT_IMPL; @@ -3555,12 +3558,11 @@ memory_richcompare_pinned(PyObject *v, PyObject *w, int op, ww = VIEW_ADDR(w); } else { - if (PyObject_GetBuffer(w, &state->wbuf, PyBUF_FULL_RO) < 0) { + if (PyObject_GetBuffer(w, &wbuf, PyBUF_FULL_RO) < 0) { PyErr_Clear(); goto result; /* Py_NotImplemented */ } - state->wbuf_acquired = true; - ww = &state->wbuf; + ww = &wbuf; } if (!equiv_shape(vv, ww)) { @@ -3580,13 +3582,13 @@ memory_richcompare_pinned(PyObject *v, PyObject *w, int op, incorrect results in the case of NaNs or uninitialized padding bytes. */ vfmt = "_"; - state->unpack_v = struct_get_unpacker(vv->format, vv->itemsize); - if (state->unpack_v == NULL) { + unpack_v = struct_get_unpacker(vv->format, vv->itemsize); + if (unpack_v == NULL) { equal = fix_struct_error_int(); goto result; } - state->unpack_w = struct_get_unpacker(ww->format, ww->itemsize); - if (state->unpack_w == NULL) { + unpack_w = struct_get_unpacker(ww->format, ww->itemsize); + if (unpack_w == NULL) { equal = fix_struct_error_int(); goto result; } @@ -3594,19 +3596,19 @@ memory_richcompare_pinned(PyObject *v, PyObject *w, int op, if (vv->ndim == 0) { equal = unpack_cmp(vv->buf, ww->buf, - vfmt, state->unpack_v, state->unpack_w); + vfmt, unpack_v, unpack_w); } else if (vv->ndim == 1) { equal = cmp_base(vv->buf, ww->buf, vv->shape, vv->strides, vv->suboffsets, ww->strides, ww->suboffsets, - vfmt, state->unpack_v, state->unpack_w); + vfmt, unpack_v, unpack_w); } else { equal = cmp_rec(vv->buf, ww->buf, vv->ndim, vv->shape, vv->strides, vv->suboffsets, ww->strides, ww->suboffsets, - vfmt, state->unpack_v, state->unpack_w); + vfmt, unpack_v, unpack_w); } result: @@ -3621,6 +3623,12 @@ memory_richcompare_pinned(PyObject *v, PyObject *w, int op, else res = Py_False; + if (ww == &wbuf) + PyBuffer_Release(ww); + + unpacker_free(unpack_v); + unpacker_free(unpack_w); + return Py_XNewRef(res); } @@ -3628,40 +3636,30 @@ static PyObject * memory_richcompare(PyObject *v, PyObject *w, int op) { PyMemoryViewObject *self = (PyMemoryViewObject *)v; - PyMemoryViewObject *other = NULL; - struct memory_compare_state state = {0}; - bool self_pinned = false; - bool other_pinned = false; - PyObject *result; - - if (memoryview_pin(self) < 0) { + _PyManagedBufferObject *self_mbuf = memoryview_pin(self); + if (self_mbuf == NULL) { /* Released memoryviews still compare by identity. */ PyErr_Clear(); - goto compare; + return memory_richcompare_pinned(v, w, op); } - self_pinned = true; + PyMemoryViewObject *other = NULL; + _PyManagedBufferObject *other_mbuf = NULL; if (PyMemoryView_Check(w) && w != v) { other = (PyMemoryViewObject *)w; - if (memoryview_pin(other) < 0) { + other_mbuf = memoryview_pin(other); + if (other_mbuf == NULL) { PyErr_Clear(); - memoryview_unpin(self); - self_pinned = false; - goto compare; + memoryview_unpin(self_mbuf); + return memory_richcompare_pinned(v, w, op); } - other_pinned = true; } -compare: - result = memory_richcompare_pinned(v, w, op, &state); - if (other_pinned) { - memoryview_unpin(other); - } - if (self_pinned) { - memoryview_unpin(self); + PyObject *result = memory_richcompare_pinned(v, w, op); + if (other != NULL) { + memoryview_unpin(other_mbuf); } - /* Buffer release may invoke the exporter's Python cleanup callback. */ - memory_compare_clear(&state); + memoryview_unpin(self_mbuf); return result; } @@ -3742,11 +3740,12 @@ memory_hash(PyObject *_self) if (hash != -1) { return hash; } - if (memoryview_pin(self) < 0) { + _PyManagedBufferObject *self_mbuf = memoryview_pin(self); + if (self_mbuf == NULL) { return -1; } Py_hash_t result = memory_hash_pinned(_self); - memoryview_unpin(self); + memoryview_unpin(self_mbuf); return result; } @@ -3885,11 +3884,12 @@ memory_contiguous(PyObject *_self, void *Py_UNUSED(ignored)) name(PyObject *_self, void *ignored) \ { \ PyMemoryViewObject *self = (PyMemoryViewObject *)_self; \ - if (memoryview_pin(self) < 0) { \ + _PyManagedBufferObject *mbuf = memoryview_pin(self); \ + if (mbuf == NULL) { \ return NULL; \ } \ PyObject *result = name##_pinned(_self, ignored); \ - memoryview_unpin(self); \ + memoryview_unpin(mbuf); \ return result; \ } @@ -4074,11 +4074,12 @@ static PyObject * memory_iter(PyObject *seq) { PyMemoryViewObject *self = (PyMemoryViewObject *)seq; - if (memoryview_pin(self) < 0) { + _PyManagedBufferObject *self_mbuf = memoryview_pin(self); + if (self_mbuf == NULL) { return NULL; } PyObject *result = memory_iter_pinned(seq); - memoryview_unpin(self); + memoryview_unpin(self_mbuf); return result; } From b68565cfd46aa712e22d7e97ad78f0556e3950c6 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 21 Oct 2026 15:06:40 -0700 Subject: [PATCH 10/12] Don't clear memoryview iterator's reference on exhaustion in free-threaded build --- Objects/memoryobject.c | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index f6a0fdba25a5770..1d91e0b1b4831c0 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -4003,32 +4003,30 @@ memoryiter_next(PyObject *self) if (it->it_index >= it->it_length) { /* Preserve StopIteration for exhausted iterators over released views. */ +#ifndef Py_GIL_DISABLED + /* Like list and tuple iterators, keep the reference in the + free-threaded build: clearing it here races with other threads + that are still using seq. */ it->it_seq = NULL; Py_DECREF(seq); +#endif return NULL; } -#ifdef Py_GIL_DISABLED - /* Keep seq alive if another thread exhausts the iterator. */ - Py_INCREF(seq); -#endif + _PyManagedBufferObject *mbuf = memoryview_pin(seq); + if (mbuf == NULL) { + return NULL; + } + Py_buffer *view = &seq->view; + char *ptr = (char *)view->buf; + ptr += view->strides[0] * it->it_index++; + ptr = ADJUST_PTR(ptr, view->suboffsets, 0); PyObject *result = NULL; - if (memoryview_pin(seq) == 0) { - Py_buffer *view = &seq->view; - char *ptr = (char *)view->buf; - - ptr += view->strides[0] * it->it_index++; - ptr = ADJUST_PTR(ptr, view->suboffsets, 0); - if (ptr != NULL) { - result = unpack_single(seq, ptr, it->it_fmt); - } - memoryview_unpin(seq); + if (ptr != NULL) { + result = unpack_single(seq, ptr, it->it_fmt); } - -#ifdef Py_GIL_DISABLED - Py_DECREF(seq); -#endif + memoryview_unpin(mbuf); return result; } From e84805c5fcd6d24f5ec3780328ddf253fd3b0ec6 Mon Sep 17 00:00:00 2001 From: Nathan Goldbaum Date: Wed, 21 Oct 2026 15:32:47 -0700 Subject: [PATCH 11/12] Restore memoryview pin helper signatures and release exhausted iterators --- .../test_free_threading/test_memoryview.py | 18 ++- Objects/memoryobject.c | 150 +++++++++--------- 2 files changed, 90 insertions(+), 78 deletions(-) diff --git a/Lib/test/test_free_threading/test_memoryview.py b/Lib/test/test_free_threading/test_memoryview.py index b8369e912d93bb6..3dba4b13ba40e2d 100644 --- a/Lib/test/test_free_threading/test_memoryview.py +++ b/Lib/test/test_free_threading/test_memoryview.py @@ -277,11 +277,21 @@ def iterator(): self.ensure_exporter_free(buf) def test_iterator_exhaustion_drops_last_reference(self): - def loop(): - for _ in range(ROUNDS * ITERS): - self.assertEqual(list(iter(memoryview(b"ab"))), [97, 98]) + for _ in range(ROUNDS): + data = bytearray(range(64)) + iterator = iter(memoryview(data)) + values = [] + lock = threading.Lock() + + def consume(): + local = list(iterator) + with lock: + values.extend(local) - threading_helper.run_concurrently(loop, nthreads=NTHREADS) + threading_helper.run_concurrently(consume, nthreads=NTHREADS) + self.assertEqual(sorted(values), list(data)) + data.extend(b"x") + self.assertRaises(StopIteration, next, iterator) def test_concurrent_hash(self): for _ in range(ROUNDS): diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index 1d91e0b1b4831c0..7bf3ee79c5acf16 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -825,16 +825,15 @@ mbuf_drop_export(_PyManagedBufferObject *mbuf) free the underlying buffer while the operation is running. The pin does not hold a reference to the managed buffer: the memoryview's own reference is kept until memory_dealloc(), and the caller holds a - reference to the memoryview. On success, return the pinned managed - buffer; pass it to memoryview_unpin() afterwards. On failure, set an - exception and return NULL. */ -static inline _PyManagedBufferObject * + reference to the memoryview through memoryview_unpin(self). Return 0 + on success and -1 with an exception on failure. */ +static inline int memoryview_pin(PyMemoryViewObject *self) { if (mv_has_flag(self, _Py_MEMORYVIEW_RELEASED)) { PyErr_SetString(PyExc_ValueError, "operation forbidden on released memoryview object"); - return NULL; + return -1; } _PyManagedBufferObject *mbuf = self->mbuf; FT_ATOMIC_ADD_SSIZE(mbuf->exports, 1); @@ -842,15 +841,15 @@ memoryview_pin(PyMemoryViewObject *self) mbuf_drop_export(mbuf); PyErr_SetString(PyExc_ValueError, "operation forbidden on released memoryview object"); - return NULL; + return -1; } - return mbuf; + return 0; } static inline void -memoryview_unpin(_PyManagedBufferObject *mbuf) +memoryview_unpin(PyMemoryViewObject *self) { - mbuf_drop_export(mbuf); + mbuf_drop_export(self->mbuf); } /* Expose a raw memory area as a view of contiguous bytes. flags can be @@ -923,8 +922,7 @@ PyMemoryView_FromObjectAndFlags(PyObject *v, int flags) if (PyMemoryView_Check(v)) { PyMemoryViewObject *mv = (PyMemoryViewObject *)v; - _PyManagedBufferObject *mv_mbuf = memoryview_pin(mv); - if (mv_mbuf == NULL) { + if (memoryview_pin(mv) < 0) { return NULL; } PyObject *result; @@ -934,9 +932,9 @@ PyMemoryView_FromObjectAndFlags(PyObject *v, int flags) result = NULL; } else { - result = mbuf_add_view(mv_mbuf, &mv->view); + result = mbuf_add_view(mv->mbuf, &mv->view); } - memoryview_unpin(mv_mbuf); + memoryview_unpin(mv); return result; } else if (PyObject_CheckBuffer(v)) { @@ -1691,12 +1689,11 @@ memoryview_cast_impl(PyMemoryViewObject *self, PyObject *format, PyObject *shape, int order) /*[clinic end generated code: output=6410d87141f6bb56 input=4a1a2326c59caeb3]*/ { - _PyManagedBufferObject *self_mbuf = memoryview_pin(self); - if (self_mbuf == NULL) { + if (memoryview_pin(self) < 0) { return NULL; } PyObject *result = memoryview_cast_pinned(self, format, shape, order); - memoryview_unpin(self_mbuf); + memoryview_unpin(self); return result; } @@ -1725,12 +1722,11 @@ static PyObject * memoryview_toreadonly_impl(PyMemoryViewObject *self) /*[clinic end generated code: output=2c7e056f04c99e62 input=dc06d20f19ba236f]*/ { - _PyManagedBufferObject *self_mbuf = memoryview_pin(self); - if (self_mbuf == NULL) { + if (memoryview_pin(self) < 0) { return NULL; } PyObject *result = memoryview_toreadonly_pinned(self); - memoryview_unpin(self_mbuf); + memoryview_unpin(self); return result; } @@ -2540,12 +2536,11 @@ static PyObject * memoryview_tolist_impl(PyMemoryViewObject *self) /*[clinic end generated code: output=a6cda89214fd5a1b input=21e7d0c1860b211a]*/ { - _PyManagedBufferObject *self_mbuf = memoryview_pin(self); - if (self_mbuf == NULL) { + if (memoryview_pin(self) < 0) { return NULL; } PyObject *result = memoryview_tolist_pinned(self); - memoryview_unpin(self_mbuf); + memoryview_unpin(self); return result; } @@ -2605,12 +2600,11 @@ static PyObject * memoryview_tobytes_impl(PyMemoryViewObject *self, const char *order) /*[clinic end generated code: output=1288b62560a32a23 input=119c70aa91791dc8]*/ { - _PyManagedBufferObject *self_mbuf = memoryview_pin(self); - if (self_mbuf == NULL) { + if (memoryview_pin(self) < 0) { return NULL; } PyObject *result = memoryview_tobytes_pinned(self, order); - memoryview_unpin(self_mbuf); + memoryview_unpin(self); return result; } @@ -2684,12 +2678,11 @@ memoryview_hex_impl(PyMemoryViewObject *self, PyObject *sep, Py_ssize_t bytes_per_sep) /*[clinic end generated code: output=c9bb00c7a8e86056 input=3f1c5d08906e3b70]*/ { - _PyManagedBufferObject *self_mbuf = memoryview_pin(self); - if (self_mbuf == NULL) { + if (memoryview_pin(self) < 0) { return NULL; } PyObject *result = memoryview_hex_pinned(self, sep, bytes_per_sep); - memoryview_unpin(self_mbuf); + memoryview_unpin(self); return result; } @@ -3080,12 +3073,11 @@ static PyObject * memory_item(PyObject *_self, Py_ssize_t index) { PyMemoryViewObject *self = (PyMemoryViewObject *)_self; - _PyManagedBufferObject *self_mbuf = memoryview_pin(self); - if (self_mbuf == NULL) { + if (memoryview_pin(self) < 0) { return NULL; } PyObject *result = memory_item_pinned(_self, index); - memoryview_unpin(self_mbuf); + memoryview_unpin(self); return result; } @@ -3093,12 +3085,11 @@ static PyObject * memory_subscript(PyObject *_self, PyObject *key) { PyMemoryViewObject *self = (PyMemoryViewObject *)_self; - _PyManagedBufferObject *self_mbuf = memoryview_pin(self); - if (self_mbuf == NULL) { + if (memoryview_pin(self) < 0) { return NULL; } PyObject *result = memory_subscript_pinned(_self, key); - memoryview_unpin(self_mbuf); + memoryview_unpin(self); return result; } @@ -3106,12 +3097,11 @@ static int memory_ass_sub(PyObject *_self, PyObject *key, PyObject *value) { PyMemoryViewObject *self = (PyMemoryViewObject *)_self; - _PyManagedBufferObject *self_mbuf = memoryview_pin(self); - if (self_mbuf == NULL) { + if (memoryview_pin(self) < 0) { return -1; } int result = memory_ass_sub_pinned(_self, key, value); - memoryview_unpin(self_mbuf); + memoryview_unpin(self); return result; } @@ -3182,12 +3172,11 @@ static PyObject * memoryview_count_impl(PyMemoryViewObject *self, PyObject *value) /*[clinic end generated code: output=a15cb19311985063 input=e3036ce1ed7d1823]*/ { - _PyManagedBufferObject *self_mbuf = memoryview_pin(self); - if (self_mbuf == NULL) { + if (memoryview_pin(self) < 0) { return NULL; } PyObject *result = memoryview_count_pinned(self, value); - memoryview_unpin(self_mbuf); + memoryview_unpin(self); return result; } @@ -3282,12 +3271,11 @@ memoryview_index_impl(PyMemoryViewObject *self, PyObject *value, Py_ssize_t start, Py_ssize_t stop) /*[clinic end generated code: output=e0185e3819e549df input=0697a0165bf90b5a]*/ { - _PyManagedBufferObject *self_mbuf = memoryview_pin(self); - if (self_mbuf == NULL) { + if (memoryview_pin(self) < 0) { return NULL; } PyObject *result = memoryview_index_pinned(self, value, start, stop); - memoryview_unpin(self_mbuf); + memoryview_unpin(self); return result; } @@ -3636,30 +3624,27 @@ static PyObject * memory_richcompare(PyObject *v, PyObject *w, int op) { PyMemoryViewObject *self = (PyMemoryViewObject *)v; - _PyManagedBufferObject *self_mbuf = memoryview_pin(self); - if (self_mbuf == NULL) { + if (memoryview_pin(self) < 0) { /* Released memoryviews still compare by identity. */ PyErr_Clear(); return memory_richcompare_pinned(v, w, op); } PyMemoryViewObject *other = NULL; - _PyManagedBufferObject *other_mbuf = NULL; if (PyMemoryView_Check(w) && w != v) { other = (PyMemoryViewObject *)w; - other_mbuf = memoryview_pin(other); - if (other_mbuf == NULL) { + if (memoryview_pin(other) < 0) { PyErr_Clear(); - memoryview_unpin(self_mbuf); + memoryview_unpin(self); return memory_richcompare_pinned(v, w, op); } } PyObject *result = memory_richcompare_pinned(v, w, op); if (other != NULL) { - memoryview_unpin(other_mbuf); + memoryview_unpin(other); } - memoryview_unpin(self_mbuf); + memoryview_unpin(self); return result; } @@ -3740,12 +3725,11 @@ memory_hash(PyObject *_self) if (hash != -1) { return hash; } - _PyManagedBufferObject *self_mbuf = memoryview_pin(self); - if (self_mbuf == NULL) { + if (memoryview_pin(self) < 0) { return -1; } Py_hash_t result = memory_hash_pinned(_self); - memoryview_unpin(self_mbuf); + memoryview_unpin(self); return result; } @@ -3884,12 +3868,11 @@ memory_contiguous(PyObject *_self, void *Py_UNUSED(ignored)) name(PyObject *_self, void *ignored) \ { \ PyMemoryViewObject *self = (PyMemoryViewObject *)_self; \ - _PyManagedBufferObject *mbuf = memoryview_pin(self); \ - if (mbuf == NULL) { \ + if (memoryview_pin(self) < 0) { \ return NULL; \ } \ PyObject *result = name##_pinned(_self, ignored); \ - memoryview_unpin(mbuf); \ + memoryview_unpin(self); \ return result; \ } @@ -3996,37 +3979,57 @@ static PyObject * memoryiter_next(PyObject *self) { memoryiterobject *it = (memoryiterobject *)self; - PyMemoryViewObject *seq = it->it_seq; - if (seq == NULL) { + PyMemoryViewObject *seq; + Py_ssize_t index; + + Py_BEGIN_CRITICAL_SECTION(it); + seq = it->it_seq; + index = it->it_index; + if (seq != NULL) { + if (index >= it->it_length) { + /* Transfer the iterator's reference to seq before clearing it. */ + it->it_seq = NULL; + } + else { + Py_INCREF(seq); + } + } + Py_END_CRITICAL_SECTION(); + + if (seq == NULL || index >= it->it_length) { + Py_XDECREF(seq); return NULL; } - if (it->it_index >= it->it_length) { - /* Preserve StopIteration for exhausted iterators over released views. */ -#ifndef Py_GIL_DISABLED - /* Like list and tuple iterators, keep the reference in the - free-threaded build: clearing it here races with other threads - that are still using seq. */ - it->it_seq = NULL; + /* Pin outside the iterator lock: a failed pin may release the buffer. */ + if (memoryview_pin(seq) < 0) { Py_DECREF(seq); -#endif return NULL; } - _PyManagedBufferObject *mbuf = memoryview_pin(seq); - if (mbuf == NULL) { + Py_BEGIN_CRITICAL_SECTION(it); + index = it->it_index; + if (index < it->it_length) { + it->it_index = index + 1; + } + Py_END_CRITICAL_SECTION(); + if (index >= it->it_length) { + memoryview_unpin(seq); + Py_DECREF(seq); return NULL; } + Py_buffer *view = &seq->view; char *ptr = (char *)view->buf; - ptr += view->strides[0] * it->it_index++; + ptr += view->strides[0] * index; ptr = ADJUST_PTR(ptr, view->suboffsets, 0); PyObject *result = NULL; if (ptr != NULL) { result = unpack_single(seq, ptr, it->it_fmt); } - memoryview_unpin(mbuf); + memoryview_unpin(seq); + Py_DECREF(seq); return result; } @@ -4072,12 +4075,11 @@ static PyObject * memory_iter(PyObject *seq) { PyMemoryViewObject *self = (PyMemoryViewObject *)seq; - _PyManagedBufferObject *self_mbuf = memoryview_pin(self); - if (self_mbuf == NULL) { + if (memoryview_pin(self) < 0) { return NULL; } PyObject *result = memory_iter_pinned(seq); - memoryview_unpin(self_mbuf); + memoryview_unpin(self); return result; } From 88334365a8bfaacf297512dd35313ca5ebdbf064 Mon Sep 17 00:00:00 2001 From: Kumar Aditya Date: Sat, 31 Oct 2026 18:48:39 +0530 Subject: [PATCH 12/12] Update Objects/memoryobject.c --- Objects/memoryobject.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Objects/memoryobject.c b/Objects/memoryobject.c index 7bf3ee79c5acf16..11a0a7ab04f08bc 100644 --- a/Objects/memoryobject.c +++ b/Objects/memoryobject.c @@ -1257,7 +1257,7 @@ memoryview_release_impl(PyMemoryViewObject *self) Py_ssize_t exports = get_exports(self); if (exports == 0) { _memory_release(self); - result = Py_NewRef(Py_None); + result = Py_None; } else if (exports > 0) { PyErr_Format(PyExc_BufferError,