Skip to content

Commit 983c106

Browse files
author
Tim Peters
committed
Merge from 3.4.
Issue python#21435: Segfault in gc with cyclic trash Changed the iteration logic in finalize_garbage() to tolerate objects vanishing from the list as a side effect of executing a finalizer.
2 parents 38ca5a7 + 5fbc7b1 commit 983c106

3 files changed

Lines changed: 59 additions & 11 deletions

File tree

Lib/test/test_gc.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -580,6 +580,38 @@ def callback(ignored):
580580
# would be damaged, with an empty __dict__.
581581
self.assertEqual(x, None)
582582

583+
def test_bug21435(self):
584+
# This is a poor test - its only virtue is that it happened to
585+
# segfault on Tim's Windows box before the patch for 21435 was
586+
# applied. That's a nasty bug relying on specific pieces of cyclic
587+
# trash appearing in exactly the right order in finalize_garbage()'s
588+
# input list.
589+
# But there's no reliable way to force that order from Python code,
590+
# so over time chances are good this test won't really be testing much
591+
# of anything anymore. Still, if it blows up, there's _some_
592+
# problem ;-)
593+
gc.collect()
594+
595+
class A:
596+
pass
597+
598+
class B:
599+
def __init__(self, x):
600+
self.x = x
601+
602+
def __del__(self):
603+
self.attr = None
604+
605+
def do_work():
606+
a = A()
607+
b = B(A())
608+
609+
a.attr = b
610+
b.attr = a
611+
612+
do_work()
613+
gc.collect() # this blows up (bad C pointer) when it fails
614+
583615
@cpython_only
584616
def test_garbage_at_shutdown(self):
585617
import subprocess

Misc/NEWS

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ Release date: TBA
1010
Core and Builtins
1111
-----------------
1212

13+
- Issue #21435: In rare cases, when running finalizers on objects in cyclic
14+
trash a bad pointer dereference could occur due to a subtle flaw in
15+
internal iteration logic.
16+
1317
- Issue #21233: Add new C functions: PyMem_RawCalloc(), PyMem_Calloc(),
1418
PyObject_Calloc(), _PyObject_GC_Calloc(). bytes(int) and bytearray(int)
1519
are now using ``calloc()`` instead of ``malloc()`` for large objects which

Modules/gcmodule.c

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -776,28 +776,40 @@ handle_legacy_finalizers(PyGC_Head *finalizers, PyGC_Head *old)
776776
return 0;
777777
}
778778

779+
/* Run first-time finalizers (if any) on all the objects in collectable.
780+
* Note that this may remove some (or even all) of the objects from the
781+
* list, due to refcounts falling to 0.
782+
*/
779783
static void
780-
finalize_garbage(PyGC_Head *collectable, PyGC_Head *old)
784+
finalize_garbage(PyGC_Head *collectable)
781785
{
782786
destructor finalize;
783-
PyGC_Head *gc = collectable->gc.gc_next;
787+
PyGC_Head seen;
788+
789+
/* While we're going through the loop, `finalize(op)` may cause op, or
790+
* other objects, to be reclaimed via refcounts falling to zero. So
791+
* there's little we can rely on about the structure of the input
792+
* `collectable` list across iterations. For safety, we always take the
793+
* first object in that list and move it to a temporary `seen` list.
794+
* If objects vanish from the `collectable` and `seen` lists we don't
795+
* care.
796+
*/
797+
gc_list_init(&seen);
784798

785-
for (; gc != collectable; gc = gc->gc.gc_next) {
799+
while (!gc_list_is_empty(collectable)) {
800+
PyGC_Head *gc = collectable->gc.gc_next;
786801
PyObject *op = FROM_GC(gc);
787-
802+
gc_list_move(gc, &seen);
788803
if (!_PyGCHead_FINALIZED(gc) &&
789-
PyType_HasFeature(Py_TYPE(op), Py_TPFLAGS_HAVE_FINALIZE) &&
790-
(finalize = Py_TYPE(op)->tp_finalize) != NULL) {
804+
PyType_HasFeature(Py_TYPE(op), Py_TPFLAGS_HAVE_FINALIZE) &&
805+
(finalize = Py_TYPE(op)->tp_finalize) != NULL) {
791806
_PyGCHead_SET_FINALIZED(gc, 1);
792807
Py_INCREF(op);
793808
finalize(op);
794-
if (Py_REFCNT(op) == 1) {
795-
/* op will be destroyed */
796-
gc = gc->gc.gc_prev;
797-
}
798809
Py_DECREF(op);
799810
}
800811
}
812+
gc_list_merge(&seen, collectable);
801813
}
802814

803815
/* Walk the collectable list and check that they are really unreachable
@@ -1006,7 +1018,7 @@ collect(int generation, Py_ssize_t *n_collected, Py_ssize_t *n_uncollectable,
10061018
m += handle_weakrefs(&unreachable, old);
10071019

10081020
/* Call tp_finalize on objects which have one. */
1009-
finalize_garbage(&unreachable, old);
1021+
finalize_garbage(&unreachable);
10101022

10111023
if (check_garbage(&unreachable)) {
10121024
revive_garbage(&unreachable);

0 commit comments

Comments
 (0)