Skip to content

Commit ead8b7a

Browse files
committed
SF 1055820: weakref callback vs gc vs threads
In cyclic gc, clear weakrefs to unreachable objects before allowing any Python code (weakref callbacks or __del__ methods) to run. This is a critical bugfix, affecting all versions of Python since weakrefs were introduced. I'll backport to 2.3.
1 parent d7bcf4d commit ead8b7a

6 files changed

Lines changed: 529 additions & 108 deletions

File tree

Include/weakrefobject.h

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,31 @@ extern "C" {
99

1010
typedef struct _PyWeakReference PyWeakReference;
1111

12+
/* PyWeakReference is the base struct for the Python ReferenceType, ProxyType,
13+
* and CallableProxyType.
14+
*/
1215
struct _PyWeakReference {
1316
PyObject_HEAD
17+
18+
/* The object to which this is a weak reference, or Py_None if none.
19+
* Note that this is a stealth reference: wr_object's refcount is
20+
* not incremented to reflect this pointer.
21+
*/
1422
PyObject *wr_object;
23+
24+
/* A callable to invoke when wr_object dies, or NULL if none. */
1525
PyObject *wr_callback;
26+
27+
/* A cache for wr_object's hash code. As usual for hashes, this is -1
28+
* if the hash code isn't known yet.
29+
*/
1630
long hash;
31+
32+
/* If wr_object is weakly referenced, wr_object has a doubly-linked NULL-
33+
* terminated list of weak references to it. These are the list pointers.
34+
* If wr_object goes away, wr_object is set to Py_None, and these pointers
35+
* have no meaning then.
36+
*/
1737
PyWeakReference *wr_prev;
1838
PyWeakReference *wr_next;
1939
};

Lib/test/test_gc.py

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from test.test_support import verify, verbose, TestFailed, vereq
22
import sys
33
import gc
4+
import weakref
45

56
def expect(actual, expected, name):
67
if actual != expected:
@@ -369,6 +370,191 @@ def test_get_referents():
369370

370371
expect(gc.get_referents(1, 'a', 4j), [], "get_referents")
371372

373+
# Bug 1055820 has several tests of longstanding bugs involving weakrefs and
374+
# cyclic gc.
375+
376+
# An instance of C1055820 has a self-loop, so becomes cyclic trash when
377+
# unreachable.
378+
class C1055820(object):
379+
def __init__(self, i):
380+
self.i = i
381+
self.loop = self
382+
383+
class GC_Detector(object):
384+
# Create an instance I. Then gc hasn't happened again so long as
385+
# I.gc_happened is false.
386+
387+
def __init__(self):
388+
self.gc_happened = False
389+
390+
def it_happened(ignored):
391+
self.gc_happened = True
392+
393+
# Create a piece of cyclic trash that triggers it_happened when
394+
# gc collects it.
395+
self.wr = weakref.ref(C1055820(666), it_happened)
396+
397+
def test_bug1055820b():
398+
# Corresponds to temp2b.py in the bug report.
399+
400+
ouch = []
401+
def callback(ignored):
402+
ouch[:] = [wr() for wr in WRs]
403+
404+
Cs = [C1055820(i) for i in range(2)]
405+
WRs = [weakref.ref(c, callback) for c in Cs]
406+
c = None
407+
408+
gc.collect()
409+
expect(len(ouch), 0, "bug1055820b")
410+
# Make the two instances trash, and collect again. The bug was that
411+
# the callback materialized a strong reference to an instance, but gc
412+
# cleared the instance's dict anyway.
413+
Cs = None
414+
gc.collect()
415+
expect(len(ouch), 2, "bug1055820b") # else the callbacks didn't run
416+
for x in ouch:
417+
# If the callback resurrected one of these guys, the instance
418+
# would be damaged, with an empty __dict__.
419+
expect(x, None, "bug1055820b")
420+
421+
def test_bug1055820c():
422+
# Corresponds to temp2c.py in the bug report. This is pretty elaborate.
423+
424+
c0 = C1055820(0)
425+
# Move c0 into generation 2.
426+
gc.collect()
427+
428+
c1 = C1055820(1)
429+
c1.keep_c0_alive = c0
430+
del c0.loop # now only c1 keeps c0 alive
431+
432+
c2 = C1055820(2)
433+
c2wr = weakref.ref(c2) # no callback!
434+
435+
ouch = []
436+
def callback(ignored):
437+
ouch[:] = [c2wr()]
438+
439+
# The callback gets associated with a wr on an object in generation 2.
440+
c0wr = weakref.ref(c0, callback)
441+
442+
c0 = c1 = c2 = None
443+
444+
# What we've set up: c0, c1, and c2 are all trash now. c0 is in
445+
# generation 2. The only thing keeping it alive is that c1 points to it.
446+
# c1 and c2 are in generation 0, and are in self-loops. There's a global
447+
# weakref to c2 (c2wr), but that weakref has no callback. There's also
448+
# a global weakref to c0 (c0wr), and that does have a callback, and that
449+
# callback references c2 via c2wr().
450+
#
451+
# c0 has a wr with callback, which references c2wr
452+
# ^
453+
# |
454+
# | Generation 2 above dots
455+
#. . . . . . . .|. . . . . . . . . . . . . . . . . . . . . . . .
456+
# | Generation 0 below dots
457+
# |
458+
# |
459+
# ^->c1 ^->c2 has a wr but no callback
460+
# | | | |
461+
# <--v <--v
462+
#
463+
# So this is the nightmare: when generation 0 gets collected, we see that
464+
# c2 has a callback-free weakref, and c1 doesn't even have a weakref.
465+
# Collecting generation 0 doesn't see c0 at all, and c0 is the only object
466+
# that has a weakref with a callback. gc clears c1 and c2. Clearing c1
467+
# has the side effect of dropping the refcount on c0 to 0, so c0 goes
468+
# away (despite that it's in an older generation) and c0's wr callback
469+
# triggers. That in turn materializes a reference to c2 via c2wr(), but
470+
# c2 gets cleared anyway by gc.
471+
472+
# We want to let gc happen "naturally", to preserve the distinction
473+
# between generations.
474+
junk = []
475+
i = 0
476+
detector = GC_Detector()
477+
while not detector.gc_happened:
478+
i += 1
479+
if i > 10000:
480+
raise TestFailed("gc didn't happen after 10000 iterations")
481+
expect(len(ouch), 0, "bug1055820c")
482+
junk.append([]) # this will eventually trigger gc
483+
484+
expect(len(ouch), 1, "bug1055820c") # else the callback wasn't invoked
485+
for x in ouch:
486+
# If the callback resurrected c2, the instance would be damaged,
487+
# with an empty __dict__.
488+
expect(x, None, "bug1055820c")
489+
490+
def test_bug1055820d():
491+
# Corresponds to temp2d.py in the bug report. This is very much like
492+
# test_bug1055820c, but uses a __del__ method instead of a weakref
493+
# callback to sneak in a resurrection of cyclic trash.
494+
495+
ouch = []
496+
class D(C1055820):
497+
def __del__(self):
498+
ouch[:] = [c2wr()]
499+
500+
d0 = D(0)
501+
# Move all the above into generation 2.
502+
gc.collect()
503+
504+
c1 = C1055820(1)
505+
c1.keep_d0_alive = d0
506+
del d0.loop # now only c1 keeps d0 alive
507+
508+
c2 = C1055820(2)
509+
c2wr = weakref.ref(c2) # no callback!
510+
511+
d0 = c1 = c2 = None
512+
513+
# What we've set up: d0, c1, and c2 are all trash now. d0 is in
514+
# generation 2. The only thing keeping it alive is that c1 points to it.
515+
# c1 and c2 are in generation 0, and are in self-loops. There's a global
516+
# weakref to c2 (c2wr), but that weakref has no callback. There are no
517+
# other weakrefs.
518+
#
519+
# d0 has a __del__ method that references c2wr
520+
# ^
521+
# |
522+
# | Generation 2 above dots
523+
#. . . . . . . .|. . . . . . . . . . . . . . . . . . . . . . . .
524+
# | Generation 0 below dots
525+
# |
526+
# |
527+
# ^->c1 ^->c2 has a wr but no callback
528+
# | | | |
529+
# <--v <--v
530+
#
531+
# So this is the nightmare: when generation 0 gets collected, we see that
532+
# c2 has a callback-free weakref, and c1 doesn't even have a weakref.
533+
# Collecting generation 0 doesn't see d0 at all. gc clears c1 and c2.
534+
# Clearing c1 has the side effect of dropping the refcount on d0 to 0, so
535+
# d0 goes away (despite that it's in an older generation) and d0's __del__
536+
# triggers. That in turn materializes a reference to c2 via c2wr(), but
537+
# c2 gets cleared anyway by gc.
538+
539+
# We want to let gc happen "naturally", to preserve the distinction
540+
# between generations.
541+
detector = GC_Detector()
542+
junk = []
543+
i = 0
544+
while not detector.gc_happened:
545+
i += 1
546+
if i > 10000:
547+
raise TestFailed("gc didn't happen after 10000 iterations")
548+
expect(len(ouch), 0, "bug1055820d")
549+
junk.append([]) # this will eventually trigger gc
550+
551+
expect(len(ouch), 1, "bug1055820d") # else __del__ wasn't invoked
552+
for x in ouch:
553+
# If __del__ resurrected c2, the instance would be damaged, with an
554+
# empty __dict__.
555+
expect(x, None, "bug1055820d")
556+
557+
372558
def test_all():
373559
gc.collect() # Delete 2nd generation garbage
374560
run_test("lists", test_list)
@@ -392,6 +578,19 @@ def test_all():
392578
run_test("boom_new", test_boom_new)
393579
run_test("boom2_new", test_boom2_new)
394580
run_test("get_referents", test_get_referents)
581+
run_test("bug1055820b", test_bug1055820b)
582+
583+
gc.enable()
584+
try:
585+
run_test("bug1055820c", test_bug1055820c)
586+
finally:
587+
gc.disable()
588+
589+
gc.enable()
590+
try:
591+
run_test("bug1055820d", test_bug1055820d)
592+
finally:
593+
gc.disable()
395594

396595
def test():
397596
if verbose:

Misc/NEWS

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,17 @@ License Version 2.
3232
Core and builtins
3333
-----------------
3434

35+
- Bug #1055820 Cyclic garbage collection was not protecting against that
36+
calling a live weakref to a piece of cyclic trash could resurrect an
37+
insane mutation of the trash if any Python code ran during gc (via
38+
running a dead object's __del__ method, running another callback on a
39+
weakref to a dead object, or via any Python code run in any other thread
40+
that managed to obtain the GIL while a __del__ or callback was running
41+
in the thread doing gc). The most likely symptom was "impossible"
42+
``AttributeEror`` exceptions, appearing seemingly at random, on weakly
43+
referenced objects. The cure was to clear all weakrefs to unreachable
44+
objects before allowing any callbacks to run.
45+
3546
- Bug #1054139 _PyString_Resize() now invalidates its cached hash value.
3647

3748
Extension Modules

0 commit comments

Comments
 (0)