Skip to content

Commit 3823a20

Browse files
gh-77894: Fix a crash when the GC breaks a loop containing a memoryview
Now a memoryview object can only be cleared if there are no buffers that refer it.
1 parent c0c2aa7 commit 3823a20

File tree

5 files changed

+75
-37
lines changed

5 files changed

+75
-37
lines changed

Lib/test/pickletester.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2323,8 +2323,6 @@ def test_picklebuffer_error(self):
23232323
'PickleBuffer can only be pickled with protocol >= 5')
23242324

23252325
def test_non_continuous_buffer(self):
2326-
if self.pickler is pickle._Pickler:
2327-
self.skipTest('CRASHES (see gh-122306)')
23282326
for proto in protocols[5:]:
23292327
with self.subTest(proto=proto):
23302328
pb = pickle.PickleBuffer(memoryview(b"foobar")[::2])
@@ -4399,10 +4397,6 @@ def test_load_from_and_dump_to_file(self):
43994397
unpickled = self.load(stream)
44004398
self.assertEqual(unpickled, data)
44014399

4402-
def test_highest_protocol(self):
4403-
# Of course this needs to be changed when HIGHEST_PROTOCOL changes.
4404-
self.assertEqual(pickle.HIGHEST_PROTOCOL, 5)
4405-
44064400
def test_callapi(self):
44074401
f = io.BytesIO()
44084402
# With and without keyword arguments

Lib/test/test_memoryview.py

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@
1818
from test.support import import_helper
1919

2020

21+
class MyObject:
22+
pass
23+
24+
2125
class AbstractMemoryTests:
2226
source_bytes = b"abcdef"
2327

@@ -228,8 +232,6 @@ def __init__(self, base):
228232
self.m = memoryview(base)
229233
class MySource(tp):
230234
pass
231-
class MyObject:
232-
pass
233235

234236
# Create a reference cycle through a memoryview object.
235237
# This exercises mbuf_clear().
@@ -656,5 +658,26 @@ def __bool__(self):
656658
m[0] = MyBool()
657659
self.assertEqual(ba[:8], b'\0'*8)
658660

661+
def test_buffer_reference_loop(self):
662+
m = memoryview(b'abc').__buffer__(0)
663+
o = MyObject()
664+
o.m = m
665+
o.o = o
666+
wr = weakref.ref(o)
667+
del m, o
668+
gc.collect()
669+
self.assertIsNone(wr())
670+
671+
def test_picklebuffer_reference_loop(self):
672+
pb = pickle.PickleBuffer(memoryview(b'abc'))
673+
o = MyObject()
674+
o.pb = pb
675+
o.o = o
676+
wr = weakref.ref(o)
677+
del pb, o
678+
gc.collect()
679+
self.assertIsNone(wr())
680+
681+
659682
if __name__ == "__main__":
660683
unittest.main()

Lib/test/test_pickle.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
NAME_MAPPING, REVERSE_NAME_MAPPING)
33
import builtins
44
import pickle
5+
import gc
56
import io
67
import collections
78
import struct
@@ -351,7 +352,6 @@ def setUp(self):
351352
)
352353

353354
def test_have_gc(self):
354-
import gc
355355
for tp in self._types:
356356
with self.subTest(tp=tp):
357357
self.assertTrue(gc.is_tracked(tp))
@@ -615,6 +615,24 @@ def test_multiprocessing_exceptions(self):
615615
('multiprocessing.context', name))
616616

617617

618+
class MiscTests(unittest.TestCase):
619+
def test_highest_protocol(self):
620+
# Of course this needs to be changed when HIGHEST_PROTOCOL changes.
621+
self.assertEqual(pickle.HIGHEST_PROTOCOL, 5)
622+
623+
def test_reference_loop(self):
624+
try:
625+
pb = pickle.PickleBuffer(memoryview(b"foobar"))
626+
a = []
627+
a.append(pb)
628+
a.append(a)
629+
finally:
630+
# Should not crash.
631+
del a, pb
632+
gc.collect()
633+
gc.collect()
634+
635+
618636
def load_tests(loader, tests, pattern):
619637
tests.addTest(doctest.DocTestSuite())
620638
return tests
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
Fix possible crash in the garbage collector when it tries to break a
2+
reference loop containing a :class:`memoryview` object. Now a
3+
:class:`!memoryview` object can only be cleared if there are no buffers that
4+
refer it.

Objects/memoryobject.c

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,6 @@ mbuf_release(_PyManagedBufferObject *self)
109109
if (self->flags&_Py_MANAGED_BUFFER_RELEASED)
110110
return;
111111

112-
/* NOTE: at this point self->exports can still be > 0 if this function
113-
is called from mbuf_clear() to break up a reference cycle. */
114112
self->flags |= _Py_MANAGED_BUFFER_RELEASED;
115113

116114
/* 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
10961094
/* Inform the managed buffer that this particular memoryview will not access
10971095
the underlying buffer again. If no other memoryviews are registered with
10981096
the managed buffer, the underlying buffer is released instantly and
1099-
marked as inaccessible for both the memoryview and the managed buffer.
1100-
1101-
This function fails if the memoryview itself has exported buffers. */
1102-
static int
1097+
marked as inaccessible for both the memoryview and the managed buffer. */
1098+
static void
11031099
_memory_release(PyMemoryViewObject *self)
11041100
{
1101+
assert(self->exports == 0);
11051102
if (self->flags & _Py_MEMORYVIEW_RELEASED)
1106-
return 0;
1103+
return;
11071104

1108-
if (self->exports == 0) {
1109-
self->flags |= _Py_MEMORYVIEW_RELEASED;
1110-
assert(self->mbuf->exports > 0);
1111-
if (--self->mbuf->exports == 0)
1112-
mbuf_release(self->mbuf);
1113-
return 0;
1105+
self->flags |= _Py_MEMORYVIEW_RELEASED;
1106+
assert(self->mbuf->exports > 0);
1107+
if (--self->mbuf->exports == 0) {
1108+
mbuf_release(self->mbuf);
11141109
}
1115-
if (self->exports > 0) {
1116-
PyErr_Format(PyExc_BufferError,
1117-
"memoryview has %zd exported buffer%s", self->exports,
1118-
self->exports==1 ? "" : "s");
1119-
return -1;
1120-
}
1121-
1122-
PyErr_SetString(PyExc_SystemError,
1123-
"_memory_release(): negative export count");
1124-
return -1;
11251110
}
11261111

11271112
/*[clinic input]
@@ -1134,9 +1119,21 @@ static PyObject *
11341119
memoryview_release_impl(PyMemoryViewObject *self)
11351120
/*[clinic end generated code: output=d0b7e3ba95b7fcb9 input=bc71d1d51f4a52f0]*/
11361121
{
1137-
if (_memory_release(self) < 0)
1122+
if (self->exports == 0) {
1123+
_memory_release(self);
1124+
Py_RETURN_NONE;
1125+
}
1126+
1127+
if (self->exports > 0) {
1128+
PyErr_Format(PyExc_BufferError,
1129+
"memoryview has %zd exported buffer%s", self->exports,
1130+
self->exports==1 ? "" : "s");
11381131
return NULL;
1139-
Py_RETURN_NONE;
1132+
}
1133+
1134+
PyErr_SetString(PyExc_SystemError,
1135+
"memoryview: negative export count");
1136+
return NULL;
11401137
}
11411138

11421139
static void
@@ -1145,7 +1142,7 @@ memory_dealloc(PyObject *_self)
11451142
PyMemoryViewObject *self = (PyMemoryViewObject *)_self;
11461143
assert(self->exports == 0);
11471144
_PyObject_GC_UNTRACK(self);
1148-
(void)_memory_release(self);
1145+
_memory_release(self);
11491146
Py_CLEAR(self->mbuf);
11501147
if (self->weakreflist != NULL)
11511148
PyObject_ClearWeakRefs((PyObject *) self);
@@ -1164,8 +1161,10 @@ static int
11641161
memory_clear(PyObject *_self)
11651162
{
11661163
PyMemoryViewObject *self = (PyMemoryViewObject *)_self;
1167-
(void)_memory_release(self);
1168-
Py_CLEAR(self->mbuf);
1164+
if (self->exports == 0) {
1165+
_memory_release(self);
1166+
Py_CLEAR(self->mbuf);
1167+
}
11691168
return 0;
11701169
}
11711170

0 commit comments

Comments
 (0)