Fix free-threaded GC crash from inherited C-stack refs (issue #515)#517
Fix free-threaded GC crash from inherited C-stack refs (issue #515)#517ddorian wants to merge 5 commits into
Conversation
…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.
8e12181 to
14e24b2
Compare
|
Ping @kumaraditya303 |
|
|
…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.
| // 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; |
There was a problem hiding this comment.
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.
…detail Interested readers can follow the link to the issue or PR.
jamadden
left a comment
There was a problem hiding this comment.
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?
| 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. |
There was a problem hiding this comment.
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 OKThis 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.
| 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. |
There was a problem hiding this comment.
This one does fail as expected.
fixes #515