Skip to content

Fix free-threaded GC crash from inherited C-stack refs (issue #515)#517

Open
ddorian wants to merge 3 commits into
python-greenlet:masterfrom
ddorian:issue515-c-stack-refs
Open

Fix free-threaded GC crash from inherited C-stack refs (issue #515)#517
ddorian wants to merge 3 commits into
python-greenlet:masterfrom
ddorian:issue515-c-stack-refs

Conversation

@ddorian

@ddorian ddorian commented Jul 3, 2026

Copy link
Copy Markdown

fixes #515

…greenlet#515)

set_initial_state copied the parent thread state's _PyCStackRef list head
into every newly-started greenlet. Those nodes live on the parent greenlet's
C stack, so once the child ran on its own stack and overwrote that region the
next pointers dangled. The free-threaded collector walks c_stack_refs for
every thread in gc_visit_thread_stacks(), so a collection on any thread would
follow the dangling nodes and segfault while a child greenlet was active
(fault inside gc_collect_main). This reproduced on 3.14t and 3.15t alike.

Start new greenlets with an empty C-stack-ref list, the way a fresh thread
does. Adds a pure-greenlet regression test that crashes a regressed build and
runs clean once fixed.
@ddorian ddorian force-pushed the issue515-c-stack-refs branch from 8e12181 to 14e24b2 Compare July 3, 2026 16:02
@ngoldbaum

Copy link
Copy Markdown

Ping @kumaraditya303

@kumaraditya303

Copy link
Copy Markdown
Contributor

c_stack_refs contains deferred refs to objects, it needs to be saved and traversed and setting it to null is not enough because otherwise it can lose those references and cause a use-after-free by GC collecting objects early.

…enlet#515)

Follow-up to the python-greenlet#515 fix. A greenlet that suspends while the interpreter
is holding a _PyCStackRef (for example, mid attribute resolution) parks
those deferred references on its C stack. The free-threaded collector only
walks the running thread's list in gc_visit_thread_stacks(), so an object
reachable only through a suspended greenlet's C-stack ref could be collected
early and used after free once the greenlet resumes.

greenlet can't just walk the saved list head from tp_traverse: those nodes
live on the greenlet's C stack, which is relocated into a heap copy while
suspended, so the head points into memory that now belongs to whichever
greenlet is running. Instead, snapshot strong references to the held objects
in operator<< (while the stack is still coherent), visit them from
tp_traverse, and release them in operator>>. update_refs() derives gc_refs
from Py_REFCNT, so the held incref is balanced by the traverse subtract.
Strong references rather than _Py_VISIT_STACKREF because _PyGC_VisitStackRef
is not exported before 3.15, and a raw array rather than a Python container
because operator<< must not allocate a GC-tracked object mid-switch.

The accompanying test pins a deferred-refcounted class through a metaclass
__get__, switches away from inside it, drops every other reference and
collects; the class survives only with the fix.
Comment thread src/greenlet/TGreenlet.hpp Outdated
// can keep them alive for the free-threaded GC (capture_c_stack_refs
// explains why we snapshot rather than walk the list). Empty while we
// run.
PyObject** c_stack_ref_snapshot;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be a std::vector of OwnedReference objects instead of a array.

Lets the RAII wrapper own and release the references, replacing the
hand-managed PyObject* array (manual incref/decref, a destructor, a length
field). No behavior change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Segmentation fault in free-threading 3.14.6

3 participants