Skip to content

Commit 392a13b

Browse files
authored
bpo-38006: Add unit test for weakref clear bug (GH-16788)
1 parent fab4ef2 commit 392a13b

File tree

2 files changed

+121
-0
lines changed

2 files changed

+121
-0
lines changed

Lib/test/test_gc.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import unittest
2+
import unittest.mock
23
from test.support import (verbose, refcount_test, run_unittest,
34
strip_python_stderr, cpython_only, start_threads,
45
temp_dir, requires_type_collecting, TESTFN, unlink,
@@ -22,6 +23,11 @@ def __new__(cls, *args, **kwargs):
2223
raise TypeError('requires _testcapi.with_tp_del')
2324
return C
2425

26+
try:
27+
from _testcapi import ContainerNoGC
28+
except ImportError:
29+
ContainerNoGC = None
30+
2531
### Support code
2632
###############################################################################
2733

@@ -959,6 +965,66 @@ def getstats():
959965

960966
gc.enable()
961967

968+
@unittest.skipIf(ContainerNoGC is None,
969+
'requires ContainerNoGC extension type')
970+
def test_trash_weakref_clear(self):
971+
# Test that trash weakrefs are properly cleared (bpo-38006).
972+
#
973+
# Structure we are creating:
974+
#
975+
# Z <- Y <- A--+--> WZ -> C
976+
# ^ |
977+
# +--+
978+
# where:
979+
# WZ is a weakref to Z with callback C
980+
# Y doesn't implement tp_traverse
981+
# A contains a reference to itself, Y and WZ
982+
#
983+
# A, Y, Z, WZ are all trash. The GC doesn't know that Z is trash
984+
# because Y does not implement tp_traverse. To show the bug, WZ needs
985+
# to live long enough so that Z is deallocated before it. Then, if
986+
# gcmodule is buggy, when Z is being deallocated, C will run.
987+
#
988+
# To ensure WZ lives long enough, we put it in a second reference
989+
# cycle. That trick only works due to the ordering of the GC prev/next
990+
# linked lists. So, this test is a bit fragile.
991+
#
992+
# The bug reported in bpo-38006 is caused because the GC did not
993+
# clear WZ before starting the process of calling tp_clear on the
994+
# trash. Normally, handle_weakrefs() would find the weakref via Z and
995+
# clear it. However, since the GC cannot find Z, WR is not cleared and
996+
# it can execute during delete_garbage(). That can lead to disaster
997+
# since the callback might tinker with objects that have already had
998+
# tp_clear called on them (leaving them in possibly invalid states).
999+
1000+
callback = unittest.mock.Mock()
1001+
1002+
class A:
1003+
__slots__ = ['a', 'y', 'wz']
1004+
1005+
class Z:
1006+
pass
1007+
1008+
# setup required object graph, as described above
1009+
a = A()
1010+
a.a = a
1011+
a.y = ContainerNoGC(Z())
1012+
a.wz = weakref.ref(a.y.value, callback)
1013+
# create second cycle to keep WZ alive longer
1014+
wr_cycle = [a.wz]
1015+
wr_cycle.append(wr_cycle)
1016+
# ensure trash unrelated to this test is gone
1017+
gc.collect()
1018+
gc.disable()
1019+
# release references and create trash
1020+
del a, wr_cycle
1021+
gc.collect()
1022+
# if called, it means there is a bug in the GC. The weakref should be
1023+
# cleared before Z dies.
1024+
callback.assert_not_called()
1025+
gc.enable()
1026+
1027+
9621028
class GCCallbackTests(unittest.TestCase):
9631029
def setUp(self):
9641030
# Save gc state and disable it.

Modules/_testcapimodule.c

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6538,6 +6538,53 @@ static PyTypeObject MethStatic_Type = {
65386538
"Class with static methods to test calling conventions"),
65396539
};
65406540

6541+
/* ContainerNoGC -- a simple container without GC methods */
6542+
6543+
typedef struct {
6544+
PyObject_HEAD
6545+
PyObject *value;
6546+
} ContainerNoGCobject;
6547+
6548+
static PyObject *
6549+
ContainerNoGC_new(PyTypeObject *type, PyObject *args, PyObject *kwargs)
6550+
{
6551+
PyObject *value;
6552+
char *names[] = {"value", NULL};
6553+
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O", names, &value)) {
6554+
return NULL;
6555+
}
6556+
PyObject *self = type->tp_alloc(type, 0);
6557+
if (self == NULL) {
6558+
return NULL;
6559+
}
6560+
Py_INCREF(value);
6561+
((ContainerNoGCobject *)self)->value = value;
6562+
return self;
6563+
}
6564+
6565+
static void
6566+
ContainerNoGC_dealloc(ContainerNoGCobject *self)
6567+
{
6568+
Py_DECREF(self->value);
6569+
Py_TYPE(self)->tp_free((PyObject *)self);
6570+
}
6571+
6572+
static PyMemberDef ContainerNoGC_members[] = {
6573+
{"value", T_OBJECT, offsetof(ContainerNoGCobject, value), READONLY,
6574+
PyDoc_STR("a container value for test purposes")},
6575+
{0}
6576+
};
6577+
6578+
static PyTypeObject ContainerNoGC_type = {
6579+
PyVarObject_HEAD_INIT(NULL, 0)
6580+
"_testcapi.ContainerNoGC",
6581+
sizeof(ContainerNoGCobject),
6582+
.tp_dealloc = (destructor)ContainerNoGC_dealloc,
6583+
.tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
6584+
.tp_members = ContainerNoGC_members,
6585+
.tp_new = ContainerNoGC_new,
6586+
};
6587+
65416588

65426589
static struct PyModuleDef _testcapimodule = {
65436590
PyModuleDef_HEAD_INIT,
@@ -6735,6 +6782,14 @@ PyInit__testcapi(void)
67356782
Py_DECREF(subclass_with_finalizer_bases);
67366783
PyModule_AddObject(m, "HeapCTypeSubclassWithFinalizer", HeapCTypeSubclassWithFinalizer);
67376784

6785+
if (PyType_Ready(&ContainerNoGC_type) < 0) {
6786+
return NULL;
6787+
}
6788+
Py_INCREF(&ContainerNoGC_type);
6789+
if (PyModule_AddObject(m, "ContainerNoGC",
6790+
(PyObject *) &ContainerNoGC_type) < 0)
6791+
return NULL;
6792+
67386793
PyState_AddModule(m, &_testcapimodule);
67396794
return m;
67406795
}

0 commit comments

Comments
 (0)