Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions Lib/test/pickletester.py
Original file line number Diff line number Diff line change
Expand Up @@ -3497,6 +3497,30 @@ class MyClass:
ValueError, 'The reducer just failed'):
p.dump(h)

@support.cpython_only
def test_reducer_override_no_reference_cycle(self):
# bpo-39492: reducer_override used to induce a spurious reference cycle
# inside the Pickler object, that could prevent all serialized objects
# from being garbage-collected without explicity invoking gc.collect.

for proto in range(0, pickle.HIGHEST_PROTOCOL + 1):
with self.subTest(proto=proto):
def f():
pass

wr = weakref.ref(f)

bio = io.BytesIO()
p = self.pickler_class(bio, proto)
p.dump(f)
new_f = pickle.loads(bio.getvalue())
assert new_f == 5

del p
del f

self.assertIsNone(wr())


class AbstractDispatchTableTests(unittest.TestCase):

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fix a reference cycle in the C Pickler that was preventing the garbage collection of deleted, pickled objects.
22 changes: 18 additions & 4 deletions Modules/_pickle.c
Original file line number Diff line number Diff line change
Expand Up @@ -4457,12 +4457,13 @@ static int
dump(PicklerObject *self, PyObject *obj)
{
const char stop_op = STOP;
int status = -1;
PyObject *tmp;
_Py_IDENTIFIER(reducer_override);

if (_PyObject_LookupAttrId((PyObject *)self, &PyId_reducer_override,
&tmp) < 0) {
return -1;
goto error;
}
/* Cache the reducer_override method, if it exists. */
if (tmp != NULL) {
Expand All @@ -4479,17 +4480,30 @@ dump(PicklerObject *self, PyObject *obj)
assert(self->proto >= 0 && self->proto < 256);
header[1] = (unsigned char)self->proto;
if (_Pickler_Write(self, header, 2) < 0)
return -1;
goto error;
if (self->proto >= 4)
self->framing = 1;
}

if (save(self, obj, 0) < 0 ||
_Pickler_Write(self, &stop_op, 1) < 0 ||
_Pickler_CommitFrame(self) < 0)
return -1;
goto error;

// Success
status = 0;

error:
self->framing = 0;
return 0;

/* Break the reference cycle we generated at the beginning this function
* call when setting the reducer_override attribute of the Pickler instance
* to a bound method of the same instance. This is important as the Pickler
* instance holds a reference to each object it has pickled (through its
* memo): thus, these objects wont be garbage-collected as long as the
* Pickler itself is not collected. */
Py_CLEAR(self->reducer_override);
return status;
}

/*[clinic input]
Expand Down