gh-77894: Fix a crash when the GC breaks a loop containing a memoryview (GH-123898)

Now a memoryview object can only be cleared if there are no buffers
that refer it.
This commit is contained in:
Serhiy Storchaka 2024-09-11 12:05:46 +03:00 committed by GitHub
parent 00ffdf2736
commit a1dbf2ea69
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 57 additions and 33 deletions

View File

@ -2323,12 +2323,10 @@ class AbstractPicklingErrorTests:
'PickleBuffer can only be pickled with protocol >= 5') 'PickleBuffer can only be pickled with protocol >= 5')
def test_non_continuous_buffer(self): def test_non_continuous_buffer(self):
if self.pickler is pickle._Pickler:
self.skipTest('CRASHES (see gh-122306)')
for proto in protocols[5:]: for proto in protocols[5:]:
with self.subTest(proto=proto): with self.subTest(proto=proto):
pb = pickle.PickleBuffer(memoryview(b"foobar")[::2]) pb = pickle.PickleBuffer(memoryview(b"foobar")[::2])
with self.assertRaises(pickle.PicklingError): with self.assertRaises((pickle.PicklingError, BufferError)):
self.dumps(pb, proto) self.dumps(pb, proto)
def test_buffer_callback_error(self): def test_buffer_callback_error(self):

View File

@ -18,6 +18,10 @@ import struct
from test.support import import_helper from test.support import import_helper
class MyObject:
pass
class AbstractMemoryTests: class AbstractMemoryTests:
source_bytes = b"abcdef" source_bytes = b"abcdef"
@ -228,8 +232,6 @@ class AbstractMemoryTests:
self.m = memoryview(base) self.m = memoryview(base)
class MySource(tp): class MySource(tp):
pass pass
class MyObject:
pass
# Create a reference cycle through a memoryview object. # Create a reference cycle through a memoryview object.
# This exercises mbuf_clear(). # This exercises mbuf_clear().
@ -656,5 +658,26 @@ class OtherTest(unittest.TestCase):
m[0] = MyBool() m[0] = MyBool()
self.assertEqual(ba[:8], b'\0'*8) self.assertEqual(ba[:8], b'\0'*8)
def test_buffer_reference_loop(self):
m = memoryview(b'abc').__buffer__(0)
o = MyObject()
o.m = m
o.o = o
wr = weakref.ref(o)
del m, o
gc.collect()
self.assertIsNone(wr())
def test_picklebuffer_reference_loop(self):
pb = pickle.PickleBuffer(memoryview(b'abc'))
o = MyObject()
o.pb = pb
o.o = o
wr = weakref.ref(o)
del pb, o
gc.collect()
self.assertIsNone(wr())
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()

View File

@ -0,0 +1,4 @@
Fix possible crash in the garbage collector when it tries to break a
reference loop containing a :class:`memoryview` object. Now a
:class:`!memoryview` object can only be cleared if there are no buffers that
refer it.

View File

@ -109,8 +109,6 @@ mbuf_release(_PyManagedBufferObject *self)
if (self->flags&_Py_MANAGED_BUFFER_RELEASED) if (self->flags&_Py_MANAGED_BUFFER_RELEASED)
return; return;
/* NOTE: at this point self->exports can still be > 0 if this function
is called from mbuf_clear() to break up a reference cycle. */
self->flags |= _Py_MANAGED_BUFFER_RELEASED; self->flags |= _Py_MANAGED_BUFFER_RELEASED;
/* PyBuffer_Release() decrements master->obj and sets it to NULL. */ /* PyBuffer_Release() decrements master->obj and sets it to NULL. */
@ -1096,32 +1094,19 @@ PyBuffer_ToContiguous(void *buf, const Py_buffer *src, Py_ssize_t len, char orde
/* Inform the managed buffer that this particular memoryview will not access /* Inform the managed buffer that this particular memoryview will not access
the underlying buffer again. If no other memoryviews are registered with the underlying buffer again. If no other memoryviews are registered with
the managed buffer, the underlying buffer is released instantly and the managed buffer, the underlying buffer is released instantly and
marked as inaccessible for both the memoryview and the managed buffer. marked as inaccessible for both the memoryview and the managed buffer. */
static void
This function fails if the memoryview itself has exported buffers. */
static int
_memory_release(PyMemoryViewObject *self) _memory_release(PyMemoryViewObject *self)
{ {
assert(self->exports == 0);
if (self->flags & _Py_MEMORYVIEW_RELEASED) if (self->flags & _Py_MEMORYVIEW_RELEASED)
return 0; return;
if (self->exports == 0) { self->flags |= _Py_MEMORYVIEW_RELEASED;
self->flags |= _Py_MEMORYVIEW_RELEASED; assert(self->mbuf->exports > 0);
assert(self->mbuf->exports > 0); if (--self->mbuf->exports == 0) {
if (--self->mbuf->exports == 0) mbuf_release(self->mbuf);
mbuf_release(self->mbuf);
return 0;
} }
if (self->exports > 0) {
PyErr_Format(PyExc_BufferError,
"memoryview has %zd exported buffer%s", self->exports,
self->exports==1 ? "" : "s");
return -1;
}
PyErr_SetString(PyExc_SystemError,
"_memory_release(): negative export count");
return -1;
} }
/*[clinic input] /*[clinic input]
@ -1134,9 +1119,21 @@ static PyObject *
memoryview_release_impl(PyMemoryViewObject *self) memoryview_release_impl(PyMemoryViewObject *self)
/*[clinic end generated code: output=d0b7e3ba95b7fcb9 input=bc71d1d51f4a52f0]*/ /*[clinic end generated code: output=d0b7e3ba95b7fcb9 input=bc71d1d51f4a52f0]*/
{ {
if (_memory_release(self) < 0) if (self->exports == 0) {
_memory_release(self);
Py_RETURN_NONE;
}
if (self->exports > 0) {
PyErr_Format(PyExc_BufferError,
"memoryview has %zd exported buffer%s", self->exports,
self->exports==1 ? "" : "s");
return NULL; return NULL;
Py_RETURN_NONE; }
PyErr_SetString(PyExc_SystemError,
"memoryview: negative export count");
return NULL;
} }
static void static void
@ -1145,7 +1142,7 @@ memory_dealloc(PyObject *_self)
PyMemoryViewObject *self = (PyMemoryViewObject *)_self; PyMemoryViewObject *self = (PyMemoryViewObject *)_self;
assert(self->exports == 0); assert(self->exports == 0);
_PyObject_GC_UNTRACK(self); _PyObject_GC_UNTRACK(self);
(void)_memory_release(self); _memory_release(self);
Py_CLEAR(self->mbuf); Py_CLEAR(self->mbuf);
if (self->weakreflist != NULL) if (self->weakreflist != NULL)
PyObject_ClearWeakRefs((PyObject *) self); PyObject_ClearWeakRefs((PyObject *) self);
@ -1164,8 +1161,10 @@ static int
memory_clear(PyObject *_self) memory_clear(PyObject *_self)
{ {
PyMemoryViewObject *self = (PyMemoryViewObject *)_self; PyMemoryViewObject *self = (PyMemoryViewObject *)_self;
(void)_memory_release(self); if (self->exports == 0) {
Py_CLEAR(self->mbuf); _memory_release(self);
Py_CLEAR(self->mbuf);
}
return 0; return 0;
} }