-
-
Notifications
You must be signed in to change notification settings - Fork 32.3k
gh-135552: Make the GC clear weakrefs later. #136189
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
Open
nascheme
wants to merge
8
commits into
python:main
Choose a base branch
from
nascheme:gh-135552-wr-clear-later
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+254
−87
Open
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
42abb05
Make the GC clear weakrefs later.
nascheme 17a4f9e
Remove inaccurate comment.
nascheme 12f0b5c
Run clear_weakrefs() with world stopped.
nascheme 2f3daba
Defer weakref clears only for refs to classes.
nascheme 123bc25
Ensure weakrefs with callbacks are cleared early.
nascheme 496c0b1
Add NEWS.
nascheme 8a553d1
Add comment about wrlist iteration.
nascheme 01c882f
Merge 'origin/main' into gh-135552-wr-clear-later
nascheme File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
16 changes: 16 additions & 0 deletions
16
Misc/NEWS.d/next/Core_and_Builtins/2025-07-03-06-04-42.gh-issue-135552.CbBQof.rst
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
Fix two bugs related to the interaction of weakrefs and the garbage | ||
collector. The weakrefs in the ``tp_subclasses`` dictionary are needed in | ||
order to correctly invalidate type caches (for example, by calling | ||
``PyType_Modified()``). Clearing weakrefs before calling finalizers causes | ||
the caches to not be correctly invalidated. That can cause crashes since the | ||
caches can refer to invalid objects. This is fixed by deferring the | ||
clearing of weakrefs to classes and without callbacks until after finalizers | ||
are executed. | ||
|
||
The second bug is caused by weakrefs created while running finalizers. Those | ||
weakrefs can be outside the set of unreachable garbage and therefore survive | ||
the ``delete_garbage()`` phase (where ``tp_clear()`` is called on objects). | ||
Those weakrefs can expose to Python-level code objects that have had | ||
``tp_clear()`` called on them. See GH-91636 as an example of this kind of | ||
bug. This is fixes be clearing all weakrefs to unreachable objects after | ||
finalizers have been executed. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -858,58 +858,31 @@ move_legacy_finalizer_reachable(PyGC_Head *finalizers) | |
} | ||
} | ||
|
||
/* Clear all weakrefs to unreachable objects, and if such a weakref has a | ||
* callback, invoke it if necessary. Note that it's possible for such | ||
* weakrefs to be outside the unreachable set -- indeed, those are precisely | ||
* the weakrefs whose callbacks must be invoked. See gc_weakref.txt for | ||
* overview & some details. Some weakrefs with callbacks may be reclaimed | ||
* directly by this routine; the number reclaimed is the return value. Other | ||
* weakrefs with callbacks may be moved into the `old` generation. Objects | ||
* moved into `old` have gc_refs set to GC_REACHABLE; the objects remaining in | ||
* unreachable are left at GC_TENTATIVELY_UNREACHABLE. When this returns, | ||
* no object in `unreachable` is weakly referenced anymore. | ||
/* Handle weakref callbacks. Note that it's possible for such weakrefs to be | ||
* outside the unreachable set -- indeed, those are precisely the weakrefs | ||
* whose callbacks must be invoked. See gc_weakref.txt for overview & some | ||
* details. | ||
*/ | ||
static int | ||
handle_weakrefs(PyGC_Head *unreachable, PyGC_Head *old) | ||
handle_weakref_callbacks(PyGC_Head *unreachable, PyGC_Head *old) | ||
{ | ||
PyGC_Head *gc; | ||
PyObject *op; /* generally FROM_GC(gc) */ | ||
PyWeakReference *wr; /* generally a cast of op */ | ||
PyGC_Head wrcb_to_call; /* weakrefs with callbacks to call */ | ||
PyGC_Head *next; | ||
int num_freed = 0; | ||
|
||
gc_list_init(&wrcb_to_call); | ||
|
||
/* Clear all weakrefs to the objects in unreachable. If such a weakref | ||
* also has a callback, move it into `wrcb_to_call` if the callback | ||
* needs to be invoked. Note that we cannot invoke any callbacks until | ||
* all weakrefs to unreachable objects are cleared, lest the callback | ||
* resurrect an unreachable object via a still-active weakref. We | ||
* make another pass over wrcb_to_call, invoking callbacks, after this | ||
* pass completes. | ||
/* Find all weakrefs with callbacks and move into `wrcb_to_call` if the | ||
* callback needs to be invoked. We make another pass over wrcb_to_call, | ||
* invoking callbacks, after this pass completes. | ||
*/ | ||
for (gc = GC_NEXT(unreachable); gc != unreachable; gc = next) { | ||
PyWeakReference **wrlist; | ||
|
||
op = FROM_GC(gc); | ||
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; | ||
} | ||
|
@@ -921,20 +894,47 @@ handle_weakrefs(PyGC_Head *unreachable, PyGC_Head *old) | |
*/ | ||
wrlist = _PyObject_GET_WEAKREFS_LISTPTR_FROM_OFFSET(op); | ||
|
||
/* `op` may have some weakrefs. March over the list, clear | ||
* all the weakrefs, and move the weakrefs with callbacks | ||
* that must be called into wrcb_to_call. | ||
/* `op` may have some weakrefs. March over the list and move the | ||
* weakrefs with callbacks that must be called into wrcb_to_call. | ||
*/ | ||
for (wr = *wrlist; wr != NULL; wr = *wrlist) { | ||
PyGC_Head *wrasgc; /* AS_GC(wr) */ | ||
PyWeakReference *next_wr; | ||
for (PyWeakReference *wr = *wrlist; wr != NULL; wr = next_wr) { | ||
// Get the next list element to get iterator progress if we omit | ||
// clearing of the weakref (because _PyWeakref_ClearRef changes | ||
// next pointer in the wrlist). | ||
next_wr = wr->wr_next; | ||
|
||
// Weakrefs with callbacks always need to be cleared before | ||
// executing the callback. Sometimes the callback will call | ||
// the ref object, to check if it's actually a dead reference | ||
// (KeyedRef does this, for example). We want to indicate that it | ||
// is dead, even though it is possible a finalizer might resurrect | ||
// it. Clearing also prevents the callback from being executing | ||
// more than once. | ||
// | ||
// Since Python 2.3, all weakrefs to cyclic garbage have | ||
// been cleared *before* calling finalizers. However, since | ||
// tp_subclasses started being necessary to invalidate caches | ||
// (e.g. by PyType_Modified()), that clearing has created a bug. | ||
// If the weakref to the subclass is cleared before a finalizer | ||
// is called, the cache may not be correctly invalidated. That | ||
// can lead to segfaults since the caches can refer to deallocated | ||
// objects. Delaying the clear of weakrefs until *after* | ||
// finalizers have been called fixes that bug. However, that | ||
// deferral could introduce other problems if some finalizer | ||
// code expects that the weakrefs will be cleared first. So, we | ||
// have the PyType_Check() test below to only defer the clear of | ||
// weakrefs to types. That solves the issue for tp_subclasses. | ||
// In a future version of Python, we should likely defer the | ||
// weakref clear for all objects, not just types. | ||
if (wr->wr_callback != NULL || !PyType_Check(wr->wr_object)) { | ||
// _PyWeakref_ClearRef clears the weakref but leaves the | ||
// callback pointer intact. Obscure: it also changes *wrlist. | ||
nascheme marked this conversation as resolved.
Show resolved
Hide resolved
|
||
_PyObject_ASSERT((PyObject *)wr, wr->wr_object == op); | ||
_PyWeakref_ClearRef(wr); | ||
_PyObject_ASSERT((PyObject *)wr, wr->wr_object == Py_None); | ||
} | ||
|
||
/* _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); | ||
if (wr->wr_callback == NULL) { | ||
/* no callback */ | ||
continue; | ||
|
@@ -955,10 +955,10 @@ handle_weakrefs(PyGC_Head *unreachable, PyGC_Head *old) | |
* outside the current generation, CT may be reachable from the | ||
* callback. Then the callback could resurrect insane objects. | ||
* | ||
* Since the callback is never needed and may be unsafe in this case, | ||
* wr is simply left in the unreachable set. Note that because we | ||
* already called _PyWeakref_ClearRef(wr), its callback will never | ||
* trigger. | ||
* Since the callback is never needed and may be unsafe in this | ||
* case, wr is simply left in the unreachable set. Note that | ||
* clear_weakrefs() will ensure its callback will not trigger | ||
* inside delete_garbage(). | ||
* | ||
* OTOH, if wr isn't part of CT, we should invoke the callback: the | ||
* weakref outlived the trash. Note that since wr isn't CT in this | ||
|
@@ -969,8 +969,6 @@ handle_weakrefs(PyGC_Head *unreachable, PyGC_Head *old) | |
* is moved to wrcb_to_call in this case. | ||
*/ | ||
if (gc_is_collecting(AS_GC((PyObject *)wr))) { | ||
/* it should already have been cleared above */ | ||
_PyObject_ASSERT((PyObject*)wr, wr->wr_object == Py_None); | ||
continue; | ||
} | ||
|
||
|
@@ -980,7 +978,7 @@ handle_weakrefs(PyGC_Head *unreachable, PyGC_Head *old) | |
Py_INCREF(wr); | ||
|
||
/* Move wr to wrcb_to_call, for the next pass. */ | ||
wrasgc = AS_GC((PyObject *)wr); | ||
PyGC_Head *wrasgc = AS_GC((PyObject *)wr); | ||
// wrasgc is reachable, but next isn't, so they can't be the same | ||
_PyObject_ASSERT((PyObject *)wr, wrasgc != next); | ||
gc_list_move(wrasgc, &wrcb_to_call); | ||
|
@@ -996,9 +994,9 @@ handle_weakrefs(PyGC_Head *unreachable, PyGC_Head *old) | |
PyObject *callback; | ||
|
||
gc = (PyGC_Head*)wrcb_to_call._gc_next; | ||
op = FROM_GC(gc); | ||
PyObject *op = FROM_GC(gc); | ||
_PyObject_ASSERT(op, PyWeakref_Check(op)); | ||
wr = (PyWeakReference *)op; | ||
PyWeakReference *wr = (PyWeakReference *)op; | ||
callback = wr->wr_callback; | ||
_PyObject_ASSERT(op, callback != NULL); | ||
|
||
|
@@ -1037,6 +1035,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) | ||
{ | ||
|
@@ -1736,8 +1790,8 @@ gc_collect_region(PyThreadState *tstate, | |
} | ||
} | ||
|
||
/* Clear weakrefs and invoke callbacks as necessary. */ | ||
stats->collected += handle_weakrefs(&unreachable, to); | ||
/* Invoke weakref callbacks as necessary. */ | ||
stats->collected += handle_weakref_callbacks(&unreachable, to); | ||
gc_list_validate_space(to, gcstate->visited_space); | ||
validate_list(to, collecting_clear_unreachable_clear); | ||
validate_list(&unreachable, collecting_set_unreachable_clear); | ||
|
@@ -1751,6 +1805,22 @@ 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 if those weakrefs were not cleared, that could reveal unreachable | ||
* objects. | ||
* | ||
* We used to clear weakrefs earlier, before calling finalizers. That | ||
* causes at least two problems. First, the finalizers could create | ||
* new weakrefs, that refer to unreachable objects. Those would not be | ||
* cleared and could cause the problem described above (see GH-91636 as | ||
* an example). Second, we need the weakrefs in the tp_subclasses to | ||
* *not* be cleared so that caches based on the type version are correctly | ||
* invalidated (see GH-135552 as a bug caused by this). | ||
*/ | ||
clear_weakrefs(&final_unreachable); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ough! Nice trick with |
||
|
||
/* 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. | ||
|
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.