From 72986b8ad46aec589fe1879bddb7d289b95645ff Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Thu, 4 Dec 2025 15:20:08 -0500 Subject: [PATCH 1/2] gh-123241: Make gc_free_threading.c more robust to increfs/decrefs in `tp_traverse` The GC for the free threading build reuses `ob_ref_local` when determining if any objects in the unreachable set were resurrected. This is generally okay because the refcount fields are already merged. However, if the tp_traverse calls `Py_INCREF`/`Py_DECREF`, it may see an `ob_ref_local` of `UINT32_MAX` that looks like an immortal object. Refactor `handle_resurrected_objects` so that `ob_ref_local` doesn't reach `UINT32_MAX`. --- ...-12-04-16-08-42.gh-issue-123241.A1dE-t.rst | 3 ++ Python/gc_free_threading.c | 42 ++++++++++++++----- 2 files changed, 34 insertions(+), 11 deletions(-) create mode 100644 Misc/NEWS.d/next/Core_and_Builtins/2025-12-04-16-08-42.gh-issue-123241.A1dE-t.rst diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-12-04-16-08-42.gh-issue-123241.A1dE-t.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-12-04-16-08-42.gh-issue-123241.A1dE-t.rst new file mode 100644 index 00000000000000..3f8153cfe47710 --- /dev/null +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-12-04-16-08-42.gh-issue-123241.A1dE-t.rst @@ -0,0 +1,3 @@ +Make the GC for the :term:`free threading` build more robust to +:c:member:`~PyTypeObject.tp_traverse` implementations that call +:c:func:`Py_INCCREF` and :c:func:`Py_DECREF`. diff --git a/Python/gc_free_threading.c b/Python/gc_free_threading.c index e672e870db2f27..757746bf3ebbac 100644 --- a/Python/gc_free_threading.c +++ b/Python/gc_free_threading.c @@ -1788,12 +1788,36 @@ show_stats_each_generations(GCState *gcstate) // TODO } + +// Initialize the `ob_ref_local` field to the refcount if it's not already +// initialized. This is for detecting resurrected objects in the unreachable +// set. Returns true if initialization was performed and false if the refcount +// doesn't fit in `ob_ref_local`. +static bool +gc_maybe_init_unreachable_refs(PyObject *op) +{ + Py_ssize_t refcount = (op->ob_ref_shared >> _Py_REF_SHARED_SHIFT); + assert(refcount >= 1); + if (refcount > INT32_MAX) { + // The refcount is too big to fit in `ob_ref_local` + return false; + } + + if (op->ob_ref_local == 0) { + op->ob_ref_local = (uint32_t)refcount; + } + return true; +} + // Traversal callback for handle_resurrected_objects. static int visit_decref_unreachable(PyObject *op, void *data) { if (gc_is_unreachable(op) && _PyObject_GC_IS_TRACKED(op)) { - op->ob_ref_local -= 1; + if (gc_maybe_init_unreachable_refs(op)) { + assert(op->ob_ref_local > 1); + op->ob_ref_local -= 1; + } } return 0; } @@ -1846,8 +1870,7 @@ handle_resurrected_objects(struct collection_state *state) continue; } - Py_ssize_t refcount = (op->ob_ref_shared >> _Py_REF_SHARED_SHIFT); - if (refcount > INT32_MAX) { + if (!gc_maybe_init_unreachable_refs(op)) { // The refcount is too big to fit in `ob_ref_local`. Mark the // object as immortal and bail out. gc_clear_unreachable(op); @@ -1856,11 +1879,6 @@ handle_resurrected_objects(struct collection_state *state) continue; } - op->ob_ref_local += (uint32_t)refcount; - - // Subtract one to account for the reference from the worklist. - op->ob_ref_local -= 1; - traverseproc traverse = Py_TYPE(op)->tp_traverse; (void)traverse(op, visit_decref_unreachable, NULL); } @@ -1868,12 +1886,14 @@ handle_resurrected_objects(struct collection_state *state) // Find resurrected objects bool any_resurrected = false; WORKSTACK_FOR_EACH(&state->unreachable, op) { - int32_t gc_refs = (int32_t)op->ob_ref_local; + uint32_t gc_refs = op->ob_ref_local; op->ob_ref_local = 0; // restore ob_ref_local - _PyObject_ASSERT(op, gc_refs >= 0); + // Subtract one for the worklist reference + _PyObject_ASSERT(op, gc_refs > 0); + gc_refs -= 1; - if (gc_is_unreachable(op) && gc_refs > 0) { + if (gc_is_unreachable(op) && gc_refs != 0) { // Clear the unreachable flag on any transitively reachable objects // from this one. any_resurrected = true; From 005d80032756ed99d758848f89211d9c6fd60109 Mon Sep 17 00:00:00 2001 From: Sam Gross Date: Thu, 4 Dec 2025 16:24:55 -0500 Subject: [PATCH 2/2] Fix NEWS --- .../2025-12-04-16-08-42.gh-issue-123241.A1dE-t.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Misc/NEWS.d/next/Core_and_Builtins/2025-12-04-16-08-42.gh-issue-123241.A1dE-t.rst b/Misc/NEWS.d/next/Core_and_Builtins/2025-12-04-16-08-42.gh-issue-123241.A1dE-t.rst index 3f8153cfe47710..c78f7bf4df8d1d 100644 --- a/Misc/NEWS.d/next/Core_and_Builtins/2025-12-04-16-08-42.gh-issue-123241.A1dE-t.rst +++ b/Misc/NEWS.d/next/Core_and_Builtins/2025-12-04-16-08-42.gh-issue-123241.A1dE-t.rst @@ -1,3 +1,3 @@ Make the GC for the :term:`free threading` build more robust to :c:member:`~PyTypeObject.tp_traverse` implementations that call -:c:func:`Py_INCCREF` and :c:func:`Py_DECREF`. +:c:func:`Py_INCREF` and :c:func:`Py_DECREF`.