Skip to content

Commit 1723687

Browse files
pitroupierreglaser
andauthored
[3.8] bpo-39492: Fix a reference cycle between reducer_override and a Pickler instance (GH-18266) (#18316)
https://bugs.python.org/issue39492 Automerge-Triggered-By: @pitrou (cherry picked from commit 0f2f35e) Co-authored-by: Pierre Glaser <pierreglaser@msn.com>
1 parent 83d3202 commit 1723687

3 files changed

Lines changed: 43 additions & 4 deletions

File tree

Lib/test/pickletester.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3497,6 +3497,30 @@ class MyClass:
34973497
ValueError, 'The reducer just failed'):
34983498
p.dump(h)
34993499

3500+
@support.cpython_only
3501+
def test_reducer_override_no_reference_cycle(self):
3502+
# bpo-39492: reducer_override used to induce a spurious reference cycle
3503+
# inside the Pickler object, that could prevent all serialized objects
3504+
# from being garbage-collected without explicity invoking gc.collect.
3505+
3506+
for proto in range(0, pickle.HIGHEST_PROTOCOL + 1):
3507+
with self.subTest(proto=proto):
3508+
def f():
3509+
pass
3510+
3511+
wr = weakref.ref(f)
3512+
3513+
bio = io.BytesIO()
3514+
p = self.pickler_class(bio, proto)
3515+
p.dump(f)
3516+
new_f = pickle.loads(bio.getvalue())
3517+
assert new_f == 5
3518+
3519+
del p
3520+
del f
3521+
3522+
self.assertIsNone(wr())
3523+
35003524

35013525
class AbstractDispatchTableTests(unittest.TestCase):
35023526

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix a reference cycle in the C Pickler that was preventing the garbage collection of deleted, pickled objects.

Modules/_pickle.c

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4457,12 +4457,13 @@ static int
44574457
dump(PicklerObject *self, PyObject *obj)
44584458
{
44594459
const char stop_op = STOP;
4460+
int status = -1;
44604461
PyObject *tmp;
44614462
_Py_IDENTIFIER(reducer_override);
44624463

44634464
if (_PyObject_LookupAttrId((PyObject *)self, &PyId_reducer_override,
44644465
&tmp) < 0) {
4465-
return -1;
4466+
goto error;
44664467
}
44674468
/* Cache the reducer_override method, if it exists. */
44684469
if (tmp != NULL) {
@@ -4479,17 +4480,30 @@ dump(PicklerObject *self, PyObject *obj)
44794480
assert(self->proto >= 0 && self->proto < 256);
44804481
header[1] = (unsigned char)self->proto;
44814482
if (_Pickler_Write(self, header, 2) < 0)
4482-
return -1;
4483+
goto error;
44834484
if (self->proto >= 4)
44844485
self->framing = 1;
44854486
}
44864487

44874488
if (save(self, obj, 0) < 0 ||
44884489
_Pickler_Write(self, &stop_op, 1) < 0 ||
44894490
_Pickler_CommitFrame(self) < 0)
4490-
return -1;
4491+
goto error;
4492+
4493+
// Success
4494+
status = 0;
4495+
4496+
error:
44914497
self->framing = 0;
4492-
return 0;
4498+
4499+
/* Break the reference cycle we generated at the beginning this function
4500+
* call when setting the reducer_override attribute of the Pickler instance
4501+
* to a bound method of the same instance. This is important as the Pickler
4502+
* instance holds a reference to each object it has pickled (through its
4503+
* memo): thus, these objects wont be garbage-collected as long as the
4504+
* Pickler itself is not collected. */
4505+
Py_CLEAR(self->reducer_override);
4506+
return status;
44934507
}
44944508

44954509
/*[clinic input]

0 commit comments

Comments
 (0)