Skip to content

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

Open
ddorian wants to merge 5 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 5 commits into
python-greenlet:masterfrom
ddorian:issue515-c-stack-refs

Conversation

@ddorian

@ddorian ddorian commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Commited the change in 39bf70c

ddorian and others added 2 commits July 6, 2026 14:00
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.
…detail

Interested readers can follow the link to the issue or PR.

@jamadden jamadden left a comment

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.

Thank you! The actual code changes seem reasonable and correct, but one of the tests isn't working: it passes when it shouldn't, on unmodified greenlet versions. Is there a way to improve it so it fails when it's supposed to?

Comment on lines +19 to +21
Prints "C STACK REFS GC OK" on a fixed build (and on with-GIL builds, where
ordinary refcounting keeps the class alive); a regressed free-threaded build
reports the class was collected and exits non-zero.

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.

Hmm. I get the output that's supposed to indicate a fixed version on stock greenlet (i.e., without this fix applied).

$ python /tmp/sus-crash.py
py=3.15.0b3 gil=False greenlet=3.5.4.dev0 alive=True
C STACK REFS GC OK

This is true both on my Apple Silicon mac with 3.15.0b3, and the manylinux_x86_64 image with 3.15.0b4. So it doesn't seem this is a reliable test.

Comment on lines +21 to +23
A fixed build prints "ISSUE 515 OK"; a regressed one segfaults in about a
second. With-GIL builds are unaffected -- ordinary refcounting keeps the nodes
alive -- so there it simply prints the OK line.

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 one does fail as expected.

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

4 participants