Skip to content

Commit f93ed3f

Browse files
committed
Issue python#16602: When a weakref's target was part of a long deallocation chain, the object could remain reachable through its weakref even though its refcount had dropped to zero.
Thanks to Eugene Toder for diagnosing and reporting the issue.
2 parents a7129d3 + 62a0d6e commit f93ed3f

4 files changed

Lines changed: 38 additions & 4 deletions

File tree

Include/weakrefobject.h

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,17 @@ PyAPI_FUNC(Py_ssize_t) _PyWeakref_GetWeakrefCount(PyWeakReference *head);
7070
PyAPI_FUNC(void) _PyWeakref_ClearRef(PyWeakReference *self);
7171
#endif
7272

73-
#define PyWeakref_GET_OBJECT(ref) (((PyWeakReference *)(ref))->wr_object)
73+
/* Explanation for the Py_REFCNT() check: when a weakref's target is part
74+
of a long chain of deallocations which triggers the trashcan mechanism,
75+
clearing the weakrefs can be delayed long after the target's refcount
76+
has dropped to zero. In the meantime, code accessing the weakref will
77+
be able to "see" the target object even though it is supposed to be
78+
unreachable. See issue #16602. */
79+
80+
#define PyWeakref_GET_OBJECT(ref) \
81+
(Py_REFCNT(((PyWeakReference *)(ref))->wr_object) > 0 \
82+
? ((PyWeakReference *)(ref))->wr_object \
83+
: Py_None)
7484

7585

7686
#ifdef __cplusplus

Lib/test/test_weakref.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,27 @@ def test_hashing(self):
776776
self.assertEqual(hash(a), hash(42))
777777
self.assertRaises(TypeError, hash, b)
778778

779+
def test_trashcan_16602(self):
780+
# Issue #16602: when a weakref's target was part of a long
781+
# deallocation chain, the trashcan mechanism could delay clearing
782+
# of the weakref and make the target object visible from outside
783+
# code even though its refcount had dropped to 0. A crash ensued.
784+
class C:
785+
def __init__(self, parent):
786+
if not parent:
787+
return
788+
wself = weakref.ref(self)
789+
def cb(wparent):
790+
o = wself()
791+
self.wparent = weakref.ref(parent, cb)
792+
793+
d = weakref.WeakKeyDictionary()
794+
root = c = C(None)
795+
for n in range(100):
796+
d[c] = c = C(c)
797+
del root
798+
gc.collect()
799+
779800

780801
class SubclassableWeakrefTestCase(TestBase):
781802

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@ What's New in Python 3.3.1?
1212
Core and Builtins
1313
-----------------
1414

15+
- Issue #16602: When a weakref's target was part of a long deallocation
16+
chain, the object could remain reachable through its weakref even though
17+
its refcount had dropped to zero.
18+
1519
- Issue #16416: On Mac OS X, operating system data are now always
1620
encoded/decoded to/from UTF-8/surrogateescape, instead of the locale encoding
1721
(which may be ASCII if no locale environment variable is set), to avoid

Objects/weakrefobject.c

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,8 @@ clear_weakref(PyWeakReference *self)
5252
{
5353
PyObject *callback = self->wr_callback;
5454

55-
if (PyWeakref_GET_OBJECT(self) != Py_None) {
56-
PyWeakReference **list = GET_WEAKREFS_LISTPTR(
57-
PyWeakref_GET_OBJECT(self));
55+
if (self->wr_object != Py_None) {
56+
PyWeakReference **list = GET_WEAKREFS_LISTPTR(self->wr_object);
5857

5958
if (*list == self)
6059
/* If 'self' is the end of the list (and thus self->wr_next == NULL)

0 commit comments

Comments
 (0)