with threading_helper.start_threads(gcs + mutators):
pass
+ def test_freeze_object_in_brc_queue(self):
+ # GH-142975: Freezing objects in the BRC queue could result in some
+ # objects having a zero refcount without being deallocated.
+
+ class Weird:
+ # We need a destructor to trigger the check for object resurrection
+ def __del__(self):
+ pass
+
+ # This is owned by the main thread, so the subthread will have to increment
+ # this object's reference count.
+ weird = Weird()
+
+ def evil():
+ gc.freeze()
+
+ # Decrement the reference count from this thread, which will trigger the
+ # slow path during resurrection and add our weird object to the BRC queue.
+ nonlocal weird
+ del weird
+
+ # Collection will merge the object's reference count and make it zero.
+ gc.collect()
+
+ # Unfreeze the object, making it visible to the GC.
+ gc.unfreeze()
+ gc.collect()
+
+ thread = Thread(target=evil)
+ thread.start()
+ thread.join()
+
if __name__ == "__main__":
unittest.main()
static void
queue_untracked_obj_decref(PyObject *op, struct collection_state *state)
{
- if (!_PyObject_GC_IS_TRACKED(op)) {
+ assert(Py_REFCNT(op) == 0);
+ // gh-142975: We have to treat frozen objects as untracked in this function
+ // or else they might be picked up in a future collection, which breaks the
+ // assumption that all incoming objects have a non-zero reference count.
+ if (!_PyObject_GC_IS_TRACKED(op) || gc_is_frozen(op)) {
// GC objects with zero refcount are handled subsequently by the
// GC as if they were cyclic trash, but we have to handle dead
// non-GC objects here. Add one to the refcount so that we can