Skip to content

Commit

Permalink
[3.12] pythongh-77894: Fix a crash when the GC breaks a loop containi…
Browse files Browse the repository at this point in the history
…ng a memoryview (pythonGH-123898)

Now a memoryview object can only be cleared if there are no buffers
that refer it.
(cherry picked from commit a1dbf2e)

Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
  • Loading branch information
serhiy-storchaka committed Sep 11, 2024
1 parent c6ae90a commit 0de080f
Show file tree
Hide file tree
Showing 4 changed files with 57 additions and 33 deletions.
4 changes: 1 addition & 3 deletions Lib/test/pickletester.py
Original file line number Diff line number Diff line change
Expand Up @@ -2019,12 +2019,10 @@ def test_picklebuffer_error(self):
'PickleBuffer can only be pickled with protocol >= 5')

def test_non_continuous_buffer(self):
if self.pickler is pickle._Pickler:
self.skipTest('CRASHES (see gh-122306)')
for proto in protocols[5:]:
with self.subTest(proto=proto):
pb = pickle.PickleBuffer(memoryview(b"foobar")[::2])
with self.assertRaises(pickle.PicklingError):
with self.assertRaises((pickle.PicklingError, BufferError)):
self.dumps(pb, proto)

def test_buffer_callback_error(self):
Expand Down
27 changes: 25 additions & 2 deletions Lib/test/test_memoryview.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@
from test.support import import_helper


class MyObject:
pass


class AbstractMemoryTests:
source_bytes = b"abcdef"

Expand Down Expand Up @@ -228,8 +232,6 @@ def __init__(self, base):
self.m = memoryview(base)
class MySource(tp):
pass
class MyObject:
pass

# Create a reference cycle through a memoryview object.
# This exercises mbuf_clear().
Expand Down Expand Up @@ -656,5 +658,26 @@ def __bool__(self):
m[0] = MyBool()
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__":
unittest.main()
Original file line number Diff line number Diff line change
@@ -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.
55 changes: 27 additions & 28 deletions Objects/memoryobject.c
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,6 @@ mbuf_release(_PyManagedBufferObject *self)
if (self->flags&_Py_MANAGED_BUFFER_RELEASED)
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;

/* PyBuffer_Release() decrements master->obj and sets it to NULL. */
Expand Down Expand Up @@ -1092,32 +1090,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
the underlying buffer again. If no other memoryviews are registered with
the managed buffer, the underlying buffer is released instantly and
marked as inaccessible for both the memoryview and the managed buffer.
This function fails if the memoryview itself has exported buffers. */
static int
marked as inaccessible for both the memoryview and the managed buffer. */
static void
_memory_release(PyMemoryViewObject *self)
{
assert(self->exports == 0);
if (self->flags & _Py_MEMORYVIEW_RELEASED)
return 0;
return;

if (self->exports == 0) {
self->flags |= _Py_MEMORYVIEW_RELEASED;
assert(self->mbuf->exports > 0);
if (--self->mbuf->exports == 0)
mbuf_release(self->mbuf);
return 0;
self->flags |= _Py_MEMORYVIEW_RELEASED;
assert(self->mbuf->exports > 0);
if (--self->mbuf->exports == 0) {
mbuf_release(self->mbuf);
}
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]
Expand All @@ -1130,17 +1115,29 @@ static PyObject *
memoryview_release_impl(PyMemoryViewObject *self)
/*[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;
Py_RETURN_NONE;
}

PyErr_SetString(PyExc_SystemError,
"memoryview: negative export count");
return NULL;
}

static void
memory_dealloc(PyMemoryViewObject *self)
{
assert(self->exports == 0);
_PyObject_GC_UNTRACK(self);
(void)_memory_release(self);
_memory_release(self);
Py_CLEAR(self->mbuf);
if (self->weakreflist != NULL)
PyObject_ClearWeakRefs((PyObject *) self);
Expand All @@ -1157,8 +1154,10 @@ memory_traverse(PyMemoryViewObject *self, visitproc visit, void *arg)
static int
memory_clear(PyMemoryViewObject *self)
{
(void)_memory_release(self);
Py_CLEAR(self->mbuf);
if (self->exports == 0) {
_memory_release(self);
Py_CLEAR(self->mbuf);
}
return 0;
}

Expand Down

0 comments on commit 0de080f

Please sign in to comment.