Skip to content

GH-91636: Clear weakrefs created by finalizers. #136401

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Clear weakrefs created by finalizers.
Weakrefs to unreachable garbage that are created during running of
finalizers need to be cleared.  This avoids exposing objects that
have `tp_clear` called on them to Python-level code.
  • Loading branch information
nascheme committed Jul 8, 2025
commit 6a1b24b0742d7c7931a45a76a5741401522a011d
20 changes: 13 additions & 7 deletions Lib/test/test_gc.py
Original file line number Diff line number Diff line change
Expand Up @@ -262,9 +262,11 @@ class Cyclic(tuple):
# finalizer.
def __del__(self):

# 5. Create a weakref to `func` now. If we had created
# it earlier, it would have been cleared by the
# garbage collector before calling the finalizers.
# 5. Create a weakref to `func` now. In previous
# versions of Python, this would avoid having it
# cleared by the garbage collector before calling
# the finalizers. Now, weakrefs get cleared after
# calling finalizers.
self[1].ref = weakref.ref(self[0])

# 6. Drop the global reference to `latefin`. The only
Expand Down Expand Up @@ -293,14 +295,18 @@ def func():
# which will find `cyc` and `func` as garbage.
gc.collect()

# 9. Previously, this would crash because `func_qualname`
# had been NULL-ed out by func_clear().
# 9. Previously, this would crash because the weakref
# created in the finalizer revealed the function after
# `tp_clear` was called and `func_qualname`
# had been NULL-ed out by func_clear(). Now, we clear
# weakrefs to unreachable objects before calling `tp_clear`
# but after calling finalizers.
print(f"{func=}")
"""
# We're mostly just checking that this doesn't crash.
rc, stdout, stderr = assert_python_ok("-c", code)
self.assertEqual(rc, 0)
self.assertRegex(stdout, rb"""\A\s*func=<function at \S+>\s*\z""")
# The `func` global is None because the weakref was cleared.
self.assertRegex(stdout, rb"""\A\s*func=None""")
self.assertFalse(stderr)

@refcount_test
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
While performing garbage collection, clear weakrefs to unreachable objects
that are created during running of finalizers. If those weakrefs were are
not cleared, they could reveal unreachable objects.
64 changes: 64 additions & 0 deletions Python/gc.c
Original file line number Diff line number Diff line change
Expand Up @@ -1037,6 +1037,62 @@ handle_weakrefs(PyGC_Head *unreachable, PyGC_Head *old)
return num_freed;
}

/* Clear all weakrefs to unreachable objects. When this returns, no object in
* `unreachable` is weakly referenced anymore.
*/
static void
clear_weakrefs(PyGC_Head *unreachable)
{
PyGC_Head *gc;
PyGC_Head *next;

for (gc = GC_NEXT(unreachable); gc != unreachable; gc = next) {
PyWeakReference **wrlist;

PyObject *op = FROM_GC(gc);
next = GC_NEXT(gc);

if (PyWeakref_Check(op)) {
/* A weakref inside the unreachable set must be cleared. If we
* allow its callback to execute inside delete_garbage(), it
* could expose objects that have tp_clear already called on
* them. Or, it could resurrect unreachable objects. One way
* this can happen is if some container objects do not implement
* tp_traverse. Then, wr_object can be outside the unreachable
* set but can be deallocated as a result of breaking the
* reference cycle. If we don't clear the weakref, the callback
* will run and potentially cause a crash. See bpo-38006 for
* one example.
*/
_PyWeakref_ClearRef((PyWeakReference *)op);
}

if (! _PyType_SUPPORTS_WEAKREFS(Py_TYPE(op))) {
continue;
}

/* It supports weakrefs. Does it have any?
*
* This is never triggered for static types so we can avoid the
* (slightly) more costly _PyObject_GET_WEAKREFS_LISTPTR().
*/
wrlist = _PyObject_GET_WEAKREFS_LISTPTR_FROM_OFFSET(op);

/* `op` may have some weakrefs. March over the list, clear
* all the weakrefs.
*/
for (PyWeakReference *wr = *wrlist; wr != NULL; wr = *wrlist) {
/* _PyWeakref_ClearRef clears the weakref but leaves
* the callback pointer intact. Obscure: it also
* changes *wrlist.
*/
_PyObject_ASSERT((PyObject *)wr, wr->wr_object == op);
_PyWeakref_ClearRef(wr);
_PyObject_ASSERT((PyObject *)wr, wr->wr_object == Py_None);
}
}
}

static void
debug_cycle(const char *msg, PyObject *op)
{
Expand Down Expand Up @@ -1751,6 +1807,14 @@ gc_collect_region(PyThreadState *tstate,
gc_list_init(&final_unreachable);
handle_resurrected_objects(&unreachable, &final_unreachable, to);

/* Clear weakrefs to objects in the unreachable set. No Python-level
* code must be allowed to access those unreachable objects. During
* delete_garbage(), finalizers outside the unreachable set might run
* and create new weakrefs. If those weakrefs were not cleared, they
* could reveal unreachable objects.
*/
clear_weakrefs(&final_unreachable);

/* Call tp_clear on objects in the final_unreachable set. This will cause
* the reference cycles to be broken. It may also cause some objects
* in finalizers to be freed.
Expand Down
50 changes: 47 additions & 3 deletions Python/gc_free_threading.c
Original file line number Diff line number Diff line change
Expand Up @@ -1494,7 +1494,7 @@ move_legacy_finalizer_reachable(struct collection_state *state)
// Clear all weakrefs to unreachable objects. Weakrefs with callbacks are
// enqueued in `wrcb_to_call`, but not invoked yet.
static void
clear_weakrefs(struct collection_state *state)
handle_weakrefs(struct collection_state *state)
{
PyObject *op;
WORKSTACK_FOR_EACH(&state->unreachable, op) {
Expand Down Expand Up @@ -1545,6 +1545,42 @@ clear_weakrefs(struct collection_state *state)
}
}

// Clear all weakrefs to unreachable objects.
static void
clear_weakrefs(struct collection_state *state)
{
PyObject *op;
WORKSTACK_FOR_EACH(&state->unreachable, op) {
if (PyWeakref_Check(op)) {
// Clear weakrefs that are themselves unreachable to ensure their
// callbacks will not be executed later from a `tp_clear()`
// inside delete_garbage(). That would be unsafe: it could
// resurrect a dead object or access a an already cleared object.
// See bpo-38006 for one example.
_PyWeakref_ClearRef((PyWeakReference *)op);
}

if (!_PyType_SUPPORTS_WEAKREFS(Py_TYPE(op))) {
continue;
}

// NOTE: This is never triggered for static types so we can avoid the
// (slightly) more costly _PyObject_GET_WEAKREFS_LISTPTR().
PyWeakReference **wrlist = _PyObject_GET_WEAKREFS_LISTPTR_FROM_OFFSET(op);

// `op` may have some weakrefs. March over the list, clear
// all the weakrefs.
for (PyWeakReference *wr = *wrlist; wr != NULL; wr = *wrlist) {
// _PyWeakref_ClearRef clears the weakref but leaves
// the callback pointer intact. Obscure: it also
// changes *wrlist.
_PyObject_ASSERT((PyObject *)wr, wr->wr_object == op);
_PyWeakref_ClearRef(wr);
_PyObject_ASSERT((PyObject *)wr, wr->wr_object == Py_None);
}
}
}

static void
call_weakref_callbacks(struct collection_state *state)
{
Expand Down Expand Up @@ -2211,7 +2247,7 @@ gc_collect_internal(PyInterpreterState *interp, struct collection_state *state,
interp->gc.long_lived_total = state->long_lived_total;

// Clear weakrefs and enqueue callbacks (but do not call them).
clear_weakrefs(state);
handle_weakrefs(state);
_PyEval_StartTheWorld(interp);

// Deallocate any object from the refcount merge step
Expand All @@ -2222,11 +2258,19 @@ gc_collect_internal(PyInterpreterState *interp, struct collection_state *state,
call_weakref_callbacks(state);
finalize_garbage(state);

// Handle any objects that may have resurrected after the finalization.
_PyEval_StopTheWorld(interp);
// Handle any objects that may have resurrected after the finalization.
err = handle_resurrected_objects(state);
// Clear free lists in all threads
_PyGC_ClearAllFreeLists(interp);
if (err == 0) {
// Clear weakrefs to objects in the unreachable set. No Python-level
// code must be allowed to access those unreachable objects. During
// delete_garbage(), finalizers outside the unreachable set might
// run and create new weakrefs. If those weakrefs were not cleared,
// they could reveal unreachable objects.
clear_weakrefs(state);
}
_PyEval_StartTheWorld(interp);

if (err < 0) {
Expand Down
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy