Repository navigation
test_weakref.test_threaded_weak_key_dict_deepcopy crash: merged objects should have ob_tid == 0 #135106
Description
Activity
- addedtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or errortestsTests in the Lib/test dirTests in the Lib/test dir
on Jun 3, 2025 Seen in main (3.15) on Windows debug build with free threading
The bug was also seen on the 3.14 branch on Windows with Free Threading: https://buildbot.python.org/#/builders/1717/builds/206
Reacted by Sam Gross- changed the title
[-]`test_weakref.test_threaded_weak_key_dict_deepcopy` crash[/-][+]`test_weakref.test_threaded_weak_key_dict_deepcopy` crash: merged objects should have ob_tid == 0[/+]on Jun 5, 2025 The trashcan mechanism is broken in the 3.14 free threaded build.
_PyTrash_thread_deposit_objectoverwrites theob_tidin the free threading build, but that's not safe if the object is still tracked by the GC.(from #132280)Lines 3166 to 3176 in 21bac3a
void _Py_Dealloc(PyObject *op) { PyTypeObject *type = Py_TYPE(op); destructor dealloc = type->tp_dealloc; PyThreadState *tstate = _PyThreadState_GET(); intptr_t margin = _Py_RecursionLimit_GetMargin(tstate); if (margin < 2) { _PyTrash_thread_deposit_object(tstate, (PyObject *)op); return; } We're only seeing this currently on Windows probably because of the smaller stack size.
I think this is also broken in the default build. If you change
_Py_RecursionLimit_GetMarginto occasionally return1as in #135589 you get lots of crashes.How does the FT GC use
ob_tid?
ob_tidshould only be overwritten when the refcount reaches zero, so the GC shouldn't be looking at the object.3 remaining items
Another issue is that we now can trigger arbitrary code from
Py_DECREF()on types likePyLongObjectif there's trash intstate->delete_laterwhereas previously we assumed that these destructors had constrained behavior.I think we should revert it sooner rather than later unless there are easy fixes to the problems.
I'm not sure there are easy fixes. The objects that previously used Py_TRASHCAN_BEGIN would expect that the object is untracked when executing the rest of their dealloc function. However, now the trashcan applies to all "container" objects, we don't know if the dealloc function expects that or not. Should
_PyTrash_thread_destroy_chainshould re-track or not?My old PR (GH-124716) untracked in the deposit and didn't re-track. That made the change a fair bit more complex than GH-132280. I think it is more logical that
Py_Deallocuntracks automatically but I suspect that could could break some extension types.The objects are still visible to the GC and may be treated as cyclic trash, which then corrupts the reference count field.
If we untrack in deposit, this is solved, right?
[a deposited] object can still be accessed via weakrefs
Ugh, I don't think we want to clear the weakrefs when we deposit. If we untrack the object, we could make the trashcan chain use
_PyGCHead_SET_PREV(), at least in the default build. All of this doesn't sound very easy, at least in terms of putting it in a beta.The objects that previously used Py_TRASHCAN_BEGIN would expect that the object is untracked when executing the rest of their dealloc function. However, now the trashcan applies to all "container" objects, we don't know if the dealloc function expects that or not.
If we allow
_PyObject_GC_UNTRACK()to be called on an already untracked object, things should mostly work if deposit untracks. Types that previously usedPy_TRASHCAN_BEGINmust call untrack first and will expect that the object is untracked in the body. So those are okay.Container objects that don't explicitly use the trashcan should untrack before tearing down the object. There could be an issue if a type resurrects an object and therefore doesn't expect that dealloc is called with the object already untracked.
Weakrefs are a pain, and why we can't thread the destroy chain through objects 😞
I'd like to keep the general approach of using the stack limits to determine when to use the trashcan as that is key for performance.
Once we've hit the trashcan, performance is less important, so adding a
_PyObject_IS_GCcheck should be fine.
With a_PyObject_IS_GCcheck we can go back to only putting GC-able objects in the trashcan and using the GC fields, not the object header for the linked list.
We'll need to untrack the objects to put them in the list and track them again before cleaning them up.I'll make a PR later in the week if no one beats me to it.
Closing, this has been fixed and backported, thanks all. Please re-open if necessary.
- moved this from Todo to Done in Release and Deferred blockers 🚫
on Jul 7, 2025
Metadata
Metadata
Assignees
Labels
Projects
- StatusShow more project fieldsDone
Bug report
Looks like it happens during a
gc.collect()call:cpython/Lib/test/test_weakref.py
Lines 1992 to 1999 in 1ffe913
https://buildbot.python.org/#/builders/1295/builds/591
The test passed on retry, but this seems like a bug.
Seen in main (3.15) on Windows debug build with free threading
Linked PRs
_Py_Dealloc#135589