Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-greenlet for openSUSE:Factory 
checked in at 2026-08-18 16:35:28
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-greenlet (Old)
 and      /work/SRC/openSUSE:Factory/.python-greenlet.new.1258 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-greenlet"

Tue Aug 18 16:35:28 2026 rev:63 rq:1371535 version:3.5.5

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-greenlet/python-greenlet.changes  
2026-07-07 20:59:57.567614824 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-greenlet.new.1258/python-greenlet.changes    
    2026-08-18 16:35:41.825241001 +0200
@@ -1,0 +2,23 @@
+Mon Aug 17 08:46:47 UTC 2026 - John Paul Adrian Glaubitz 
<[email protected]>
+
+- Update to 3.5.5
+  * Link the C++ runtime statically into the Windows wheels again, as the
+    Appveyor builds did through 3.3.0. Since 3.3.1 ``_greenlet.pyd``
+    imported ``MSVCP140.dll``, which no Windows CPython distribution ships,
+    so importing greenlet failed on machines without the Visual C++
+    redistributable. See issue 525. Issue and pull request by Daniel Sticker.
+- from version 3.5.4
+  * Fix a crash (segfault) on free-threaded builds of Python 3.14 and
+    later when the garbage collector runs while a greenlet that was
+    started from a non-empty C-stack-reference state is active.
+    See issue 515. Thanks to ddorian and Kumar Aditya.
+  * Fix a potential use-after-free on free-threaded builds of Python 3.14
+    and later when the garbage collector runs while a greenlet is
+    suspended holding a ``_PyCStackRef`` (for example, mid attribute
+    resolution). See issue 515. Thanks to ddorian and Kumar Aditya.
+  - Fix a deadlock on free-threaded builds when a greenlet switch happened
+    while a ``PyCriticalSection`` was held -- for example inside asyncio's
+    ``Task.__step``, which holds one on the running task for the duration of
+    the step. See PR 519. Thanks to ddorian and Kumar Aditya.
+
+-------------------------------------------------------------------

Old:
----
  greenlet-3.5.3.tar.gz

New:
----
  greenlet-3.5.5.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-greenlet.spec ++++++
--- /var/tmp/diff_new_pack.z5qtTX/_old  2026-08-18 16:35:42.586268227 +0200
+++ /var/tmp/diff_new_pack.z5qtTX/_new  2026-08-18 16:35:42.588268298 +0200
@@ -22,7 +22,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-greenlet
-Version:        3.5.3
+Version:        3.5.5
 Release:        0
 Summary:        Lightweight in-process concurrent programming
 License:        MIT

++++++ greenlet-3.5.3.tar.gz -> greenlet-3.5.5.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.5.3/.github/workflows/tests.yml 
new/greenlet-3.5.5/.github/workflows/tests.yml
--- old/greenlet-3.5.3/.github/workflows/tests.yml      2026-06-26 
20:18:23.000000000 +0200
+++ new/greenlet-3.5.5/.github/workflows/tests.yml      2026-08-10 
15:23:03.654935600 +0200
@@ -48,9 +48,9 @@
           - os: windows-11-arm
             python-version: "3.14t"
     steps:
-    - uses: actions/checkout@v6
+    - uses: actions/checkout@v7
     - name: Set up Python
-      uses: actions/setup-python@v6
+      uses: actions/setup-python@v7
       with:
         python-version: ${{ matrix.python-version }}
         cache: 'pip'
@@ -79,6 +79,12 @@
         # expect that some people will compile it themselves with that setting.
         CPPFLAGS: "-Ofast -UNDEBUG -Wall"
         CFLAGS:   "-Ofast -UNDEBUG -Wall"
+        # On Windows this step also produces the wheels we distribute, so
+        # link the C++ runtime statically, the way the Appveyor builds did
+        # through 3.3.0. Without it, _greenlet.pyd imports MSVCP140.dll,
+        # which no Windows CPython distribution ships. setup.py ignores
+        # this everywhere else.
+        GREENLET_STATIC_RUNTIME: "1"
     - name: Install greenlet (Mac)
       if: startsWith(runner.os, 'Mac')
       run: |
@@ -159,9 +165,9 @@
       # required for all workflows
       security-events: write
     steps:
-      - uses: actions/checkout@v6
+      - uses: actions/checkout@v7
       - name: Set up Python
-        uses: actions/setup-python@v6
+        uses: actions/setup-python@v7
         with:
           python-version: "3.10"
           cache: 'pip'
@@ -172,7 +178,7 @@
           python -m pip install -U setuptools wheel
       # Initializes the CodeQL tools for scanning.
       - name: Initialize CodeQL
-        uses: github/codeql-action/init@v4
+        uses: github/codeql-action/[email protected]
         with:
           languages: python, cpp
       - name: Install greenlet
@@ -181,7 +187,7 @@
       # - name: Autobuild
       #   uses: github/codeql-action/autobuild@v1
       - name: Perform CodeQL Analysis
-        uses: github/codeql-action/analyze@v4
+        uses: github/codeql-action/[email protected]
 
 
   manylinux:
@@ -211,9 +217,9 @@
 
     steps:
       - name: checkout
-        uses: actions/checkout@v6
+        uses: actions/checkout@v7
       - name: Set up Python ${{ matrix.python-version }}
-        uses: actions/setup-python@v6
+        uses: actions/setup-python@v7
         with:
           python-version: ${{ matrix.python-version }}
       - name: Set up QEMU
@@ -230,7 +236,7 @@
           path: wheelhouse/*whl
           name: ${{ matrix.image }}_wheels.zip
       - name: Publish package to PyPI
-        uses: pypa/[email protected]
+        uses: pypa/[email protected]
         if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
         with:
           user: __token__
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.5.3/CHANGES.rst 
new/greenlet-3.5.5/CHANGES.rst
--- old/greenlet-3.5.3/CHANGES.rst      2026-06-26 20:18:23.000000000 +0200
+++ new/greenlet-3.5.5/CHANGES.rst      2026-08-10 15:23:03.655521400 +0200
@@ -2,6 +2,53 @@
  Changes
 =========
 
+3.5.5 (2026-08-10)
+==================
+
+- Link the C++ runtime statically into the Windows wheels again, as the
+  Appveyor builds did through 3.3.0. Since 3.3.1 ``_greenlet.pyd``
+  imported ``MSVCP140.dll``, which no Windows CPython distribution ships,
+  so importing greenlet failed on machines without the Visual C++
+  redistributable. See `issue 525
+  <https://github.com/python-greenlet/greenlet/issues/525>`_. Issue
+  and pull request by Daniel Sticker.
+
+
+.. note::
+   Binary 3.15 wheels are now built with 3.15.0rc1. This should be
+   compatible with future 3.15 releases and is believed compatible
+   with 3.15b4 as well (but not earlier versions).
+
+
+3.5.4 (2026-07-22)
+==================
+
+- Fix a crash (segfault) on free-threaded builds of Python 3.14 and
+  later when the garbage collector runs while a greenlet that was
+  started from a non-empty C-stack-reference state is active.
+  See `issue 515
+  <https://github.com/python-greenlet/greenlet/issues/515>`_.
+  Thanks to ddorian and Kumar Aditya.
+
+- Fix a potential use-after-free on free-threaded builds of Python 3.14
+  and later when the garbage collector runs while a greenlet is
+  suspended holding a ``_PyCStackRef`` (for example, mid attribute
+  resolution). See
+  `issue 515 <https://github.com/python-greenlet/greenlet/issues/515>`_.
+  Thanks to ddorian and Kumar Aditya.
+
+- Fix a deadlock on free-threaded builds when a greenlet switch happened
+  while a ``PyCriticalSection`` was held -- for example inside asyncio's
+  ``Task.__step``, which holds one on the running task for the duration of
+  the step. See `PR 519 
<https://github.com/python-greenlet/greenlet/pull/519/>`.
+  Thanks to ddorian and Kumar Aditya.
+
+.. note::
+    Binary 3.15 wheels are now built with Python 3.15b4. These may not
+    be compatible with earlier or later versions of 3.15. Binary
+    3.15 wheels of greenlet from previous releases (e.g., 3.5.3)
+    may not be compatible with Python 3.15b4.
+
 3.5.3 (2026-06-26)
 ==================
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.5.3/PKG-INFO new/greenlet-3.5.5/PKG-INFO
--- old/greenlet-3.5.3/PKG-INFO 2026-06-26 20:18:27.252837400 +0200
+++ new/greenlet-3.5.5/PKG-INFO 2026-08-10 15:23:07.284722800 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: greenlet
-Version: 3.5.3
+Version: 3.5.5
 Summary: Lightweight in-process concurrent programming
 Author-email: Alexey Borzenkov <[email protected]>
 Maintainer-email: Jason Madden <[email protected]>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.5.3/src/greenlet/TGreenlet.hpp 
new/greenlet-3.5.5/src/greenlet/TGreenlet.hpp
--- old/greenlet-3.5.3/src/greenlet/TGreenlet.hpp       2026-06-26 
20:18:23.000000000 +0200
+++ new/greenlet-3.5.5/src/greenlet/TGreenlet.hpp       2026-08-10 
15:23:03.660435200 +0200
@@ -8,6 +8,7 @@
 #include <Python.h>
 
 #include <atomic>
+#include <vector>
 
 #include "greenlet_compiler_compat.hpp"
 #include "greenlet_refs.hpp"
@@ -39,6 +40,7 @@
 #endif
 #ifdef Py_GIL_DISABLED
 #   include "internal/pycore_tstate.h"
+#   include "internal/pycore_critical_section.h"
 #endif
 #endif
 
@@ -139,6 +141,12 @@
         _PyStackRef* stackpointer;
     #ifdef Py_GIL_DISABLED
         _PyCStackRef* c_stack_refs;
+        // Strong references to the objects held by the _PyCStackRef nodes on
+        // our C stack, snapshotted by operator<< when we suspend so 
tp_traverse
+        // 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.
+        std::vector<OwnedObject> c_stack_ref_snapshot;
     #endif
 #elif GREENLET_PY312
         int py_recursion_depth;
@@ -170,6 +178,14 @@
         // need to be present for the eval loop to work.
         void unexpose_frames();
 
+#if GREENLET_PY314 && defined(Py_GIL_DISABLED)
+        // Take a strong reference to every object held by tstate's 
_PyCStackRef
+        // list into c_stack_ref_snapshot so tp_traverse can keep them alive
+        // while we're suspended. Must run while our C stack is still live
+        // (operator<<). The snapshot is dropped by clear()ing the vector.
+        void capture_c_stack_refs(const PyThreadState* tstate) noexcept;
+#endif
+
     public:
 
         PythonState();
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.5.3/src/greenlet/TPythonState.cpp 
new/greenlet-3.5.5/src/greenlet/TPythonState.cpp
--- old/greenlet-3.5.3/src/greenlet/TPythonState.cpp    2026-06-26 
20:18:23.000000000 +0200
+++ new/greenlet-3.5.5/src/greenlet/TPythonState.cpp    2026-08-10 
15:23:03.661210000 +0200
@@ -92,6 +92,30 @@
 #endif
 }
 
+#if GREENLET_PY314 && defined(Py_GIL_DISABLED)
+void PythonState::capture_c_stack_refs(const PyThreadState* tstate) noexcept
+{
+    // Runs from operator<< while our C stack is still live and coherent, so we
+    // can walk tstate's _PyCStackRef list and take a strong reference to every
+    // object it holds. tp_traverse visits these once we're suspended, because
+    // by then the nodes themselves have been relocated into the heap stack 
copy
+    // and the saved list head no longer points at them. Strong references (not
+    // _Py_VISIT_STACKREF, whose _PyGC_VisitStackRef isn't exported before 
3.15);
+    // a std::vector rather than a Python list/tuple because operator<< must 
not
+    // allocate a GC-tracked object mid-switch. Rebuilt from scratch each time;
+    // the list is empty at a typical switch, so this is usually just an empty
+    // loop.
+    this->c_stack_ref_snapshot.clear();
+    for (const _PyCStackRef* node = 
((_PyThreadStateImpl*)tstate)->c_stack_refs;
+         node != nullptr; node = node->next) {
+        if (!PyStackRef_IsNullOrInt(node->ref)) {
+            this->c_stack_ref_snapshot.push_back(
+                OwnedObject::owning(PyStackRef_AsPyObjectBorrow(node->ref)));
+        }
+    }
+}
+#endif
+
 
 inline void PythonState::may_switch_away() noexcept
 {
@@ -145,6 +169,9 @@
     this->current_executor = tstate->current_executor;
     #ifdef Py_GIL_DISABLED
     this->c_stack_refs = ((_PyThreadStateImpl*)tstate)->c_stack_refs;
+    // Capture the deferred references now, while our C stack is still live, so
+    // tp_traverse can keep them from being collected while we're suspended.
+    this->capture_c_stack_refs(tstate);
     #endif
   #elif GREENLET_PY312
     this->py_recursion_depth = tstate->py_recursion_limit - 
tstate->py_recursion_remaining;
@@ -192,6 +219,19 @@
     // ``greenlet.tests.test_greenlet_trash`` tries, but under 3.14,
     // at least, fails to do so.
     this->delete_later = Py_XNewRef(tstate->delete_later);
+#ifdef Py_GIL_DISABLED
+    // Switching greenlets swaps C stacks, which to the free-threaded runtime 
is
+    // the same predicament as detaching the thread: the PyCriticalSection 
nodes
+    // chained off tstate->critical_section live on the stack we're leaving, 
and
+    // their PyMutexes would stay locked behind our back. The greenlet we 
switch
+    // to could then block forever taking one of those same locks -- e.g. an
+    // asyncio event dispatched onto another fiber re-enters a Task/Future that
+    // the suspended fiber is mid-step on. So drop the locks here the way
+    // _PyThreadState_Detach() does and let operator>> re-take them on resume.
+    if (tstate->critical_section != 0) {
+        _PyCriticalSection_SuspendAll(const_cast<PyThreadState*>(tstate));
+    }
+#endif
     this->critical_section = tstate->critical_section;
   #elif GREENLET_PY312
     this->trash_delete_nesting = tstate->trash.delete_nesting;
@@ -252,6 +292,10 @@
     tstate->current_executor = this->current_executor;
     #ifdef Py_GIL_DISABLED
     ((_PyThreadStateImpl*)tstate)->c_stack_refs = this->c_stack_refs;
+    // We're the running greenlet again: our C-stack refs live in the thread
+    // state now and gc_visit_thread_stacks() covers them, so drop the strong
+    // references tp_traverse held on our behalf while we were suspended.
+    this->c_stack_ref_snapshot.clear();
     #endif
     this->unexpose_frames();
   #elif GREENLET_PY312
@@ -301,6 +345,16 @@
         Py_CLEAR(this->delete_later);
     }
     tstate->critical_section = this->critical_section;
+#ifdef Py_GIL_DISABLED
+    // Re-acquire whatever operator<< suspended when this greenlet last 
yielded.
+    // A no-op for a greenlet that held no locks, and for a brand-new one whose
+    // chain starts empty. Mirrors the resume in _PyThreadState_Attach(); note
+    // _PyCriticalSection_Resume() dereferences the head, so the != 0 guard is
+    // load-bearing, not just a fast path.
+    if (tstate->critical_section != 0) {
+        _PyCriticalSection_Resume(tstate);
+    }
+#endif
 
   #elif GREENLET_PY312
     tstate->trash.delete_nesting = this->trash_delete_nesting;
@@ -332,7 +386,14 @@
     this->py_recursion_depth = tstate->py_recursion_limit - 
tstate->py_recursion_remaining;
     this->current_executor = tstate->current_executor;
     #ifdef Py_GIL_DISABLED
-    this->c_stack_refs = ((_PyThreadStateImpl*)tstate)->c_stack_refs;
+    // Start with an empty C-stack-ref list, the way a brand-new thread does;
+    // do NOT copy the parent thread state's head. Those _PyCStackRef nodes sit
+    // on the parent greenlet's C stack, so once we start running on our own
+    // stack and overwrite that region, following them reads garbage. The
+    // free-threaded collector walks c_stack_refs for every thread in
+    // gc_visit_thread_stacks(), so leaving the stale head here crashed it.
+    // See https://github.com/python-greenlet/greenlet/issues/515.
+    this->c_stack_refs = nullptr;
     #endif
     // this->stackpointer is left null because this->_top_frame is
     // null so there is no value to copy.
@@ -383,6 +444,18 @@
         }
     }
 #endif
+#if GREENLET_PY314 && defined(Py_GIL_DISABLED)
+    // Visit the objects this greenlet's C-stack refs were holding when it
+    // suspended (captured by capture_c_stack_refs). The free-threaded 
collector
+    // only walks the running thread's _PyCStackRef list in
+    // gc_visit_thread_stacks(), so without this a collection could free an
+    // object reachable only through a suspended greenlet's C-stack ref and 
we'd
+    // use it after free once the greenlet resumed. The snapshot is empty while
+    // we're the running greenlet, so this is a no-op there.
+    for (const OwnedObject& ref : this->c_stack_ref_snapshot) {
+        Py_VISIT(ref.borrow());
+    }
+#endif
     // Note that we DO NOT visit ``delete_later``. Even if it's
     // non-null and we technically own a reference to it, its
     // reference count already went to 0 once and it was in the
@@ -395,6 +468,9 @@
 void PythonState::tp_clear(bool own_top_frame) noexcept
 {
     PythonStateContext::tp_clear();
+#if GREENLET_PY314 && defined(Py_GIL_DISABLED)
+    this->c_stack_ref_snapshot.clear();
+#endif
     // If we get here owning a frame,
     // we got dealloc'd without being finished. We may or may not be
     // in the same thread.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.5.3/src/greenlet/__init__.py 
new/greenlet-3.5.5/src/greenlet/__init__.py
--- old/greenlet-3.5.3/src/greenlet/__init__.py 2026-06-26 20:18:23.000000000 
+0200
+++ new/greenlet-3.5.5/src/greenlet/__init__.py 2026-08-10 15:23:03.662009000 
+0200
@@ -22,7 +22,7 @@
 ###
 # Metadata
 ###
-__version__ = '3.5.3'
+__version__ = '3.5.5'
 from ._greenlet import _C_API # pylint:disable=no-name-in-module
 
 ###
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.5.3/src/greenlet/tests/_test_extension.c 
new/greenlet-3.5.5/src/greenlet/tests/_test_extension.c
--- old/greenlet-3.5.3/src/greenlet/tests/_test_extension.c     2026-06-26 
20:18:23.000000000 +0200
+++ new/greenlet-3.5.5/src/greenlet/tests/_test_extension.c     2026-08-10 
15:23:03.666226600 +0200
@@ -162,6 +162,12 @@
 {
     const char msg[] = "take that sucka!";
     PyObject* msg_obj = Py_BuildValue("s", msg);
+
+    if (msg_obj == NULL) {
+        return NULL;
+    }
+
+
     PyGreenlet_Throw(g, PyExc_ValueError, msg_obj, NULL);
     Py_DECREF(msg_obj);
     if (PyErr_Occurred()) {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/greenlet-3.5.3/src/greenlet/tests/fail_c_stack_refs_suspended_gc.py 
new/greenlet-3.5.5/src/greenlet/tests/fail_c_stack_refs_suspended_gc.py
--- old/greenlet-3.5.3/src/greenlet/tests/fail_c_stack_refs_suspended_gc.py     
1970-01-01 01:00:00.000000000 +0100
+++ new/greenlet-3.5.5/src/greenlet/tests/fail_c_stack_refs_suspended_gc.py     
2026-08-10 15:23:03.666535100 +0200
@@ -0,0 +1,85 @@
+"""Regression test for issue #515: GC of a suspended greenlet's C-stack refs.
+
+A greenlet that suspends mid attribute-resolution holds a ``_PyCStackRef`` to
+the looked-up object on its C stack. The free-threaded collector only walks the
+running thread's refs, so greenlet must visit a suspended greenlet's from
+``tp_traverse``, or an object reachable only through them is freed early.
+
+The test pins a deferred-refcounted class (a metaclass ``__get__`` makes the
+class act as a descriptor), switches away from inside ``__get__``, drops every
+other reference, and collects. It passes only if the class is still alive and
+the suspended greenlet is what keeps it alive. Prints "C STACK REFS GC OK" and
+exits 0 on a fixed build, non-zero on a regressed one.
+"""
+import gc
+import sys
+import sysconfig
+import weakref
+
+import greenlet
+
+# Only free-threaded builds have the C-stack-ref / deferred-refcount machinery.
+FREETHREAD = bool(sysconfig.get_config_var("Py_GIL_DISABLED"))
+
+parent = greenlet.getcurrent()
+observed = {}
+
+
+class Meta(type):
+    def __get__(cls, obj, objtype=None):
+        # ``cls`` is the class being resolved, pinned in a _PyCStackRef across
+        # this call. Drop the descriptor-protocol locals so a suspended frame
+        # can't keep the class alive on its own and mask the bug.
+        del cls, obj, objtype
+        child.switch()
+        return 42
+
+
+pinned = Meta('pinned', (), {})                    # a class => 
deferred-refcounted
+holder = type('holder', (), {'attr': pinned})      # holder.attr invokes 
Meta.__get__
+box = [pinned]
+del pinned
+
+
+def child_work():
+    # parent is suspended inside Meta.__get__ holding a C-stack ref to the 
class.
+    # Drop every other reference, then collect.
+    ref = weakref.ref(box[0])
+    box[0] = None
+    del holder.attr
+    for _ in range(5):
+        gc.collect()
+    cls = ref()
+    if cls is None:
+        observed['status'] = 'collected'
+    elif not FREETHREAD or any(r is parent for r in gc.get_referrers(cls)):
+        # Kept alive by the suspended greenlet (or, with the GIL, by 
refcounting).
+        observed['status'] = 'ok'
+    else:
+        # Alive, but not because of the greenlet: a masking reference hid the
+        # C-stack-ref path (the class's own mro/bases are a self-cycle, not 
one).
+        observed['status'] = 'masked'
+        own = (cls.__mro__, cls.__bases__)
+        observed['maskers'] = sorted(
+            {type(r).__name__ for r in gc.get_referrers(cls)
+             if r is not parent and r is not own[0] and r is not own[1]}
+        ) or ['<invisible to gc; strong C-stack ref / non-deferred class>']
+    del cls
+    parent.switch()
+
+
+child = greenlet.greenlet(child_work)
+result = holder().attr
+assert result == 42, result
+
+status = observed.get('status')
+print(f"py={sys.version.split()[0]} gil={getattr(sys, '_is_gil_enabled', 
lambda: True)()} "
+      f"greenlet={greenlet.__version__} status={status}", flush=True)
+if status == 'collected':
+    raise SystemExit("REGRESSED: class reachable only through a suspended "
+                     "greenlet's C-stack ref was collected early")
+if status == 'masked':
+    raise SystemExit("REGRESSED: class stayed alive but not through the 
suspended "
+                     "greenlet; a masking reference (%s) hid the C-stack-ref 
path"
+                     % ', '.join(observed.get('maskers', ())))
+print("C STACK REFS GC OK", flush=True)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/greenlet-3.5.3/src/greenlet/tests/fail_issue_515_freethread_gc.py 
new/greenlet-3.5.5/src/greenlet/tests/fail_issue_515_freethread_gc.py
--- old/greenlet-3.5.3/src/greenlet/tests/fail_issue_515_freethread_gc.py       
1970-01-01 01:00:00.000000000 +0100
+++ new/greenlet-3.5.5/src/greenlet/tests/fail_issue_515_freethread_gc.py       
2026-08-10 15:23:03.666981500 +0200
@@ -0,0 +1,112 @@
+"""Reproducer for the free-threaded GC crash in issue #515.
+
+https://github.com/python-greenlet/greenlet/issues/515
+
+Before the fix, ``set_initial_state`` copied the parent thread state's
+``_PyCStackRef`` list head into each new greenlet. Those nodes sit on the
+parent greenlet's C stack, so once the child starts running on its own stack
+and overwrites that region their ``next`` pointers dangle. The free-threaded
+collector walks ``c_stack_refs`` for every thread, so a GC on any thread ends
+up chasing those dangling nodes (inside ``gc_collect_main`` ->
+``gc_visit_thread_stacks``) while such a child is active, and segfaults.
+
+The fiddly part is getting a *non-empty* list to inherit at all. The
+interpreter only holds a ``_PyCStackRef`` transiently, while it is resolving an
+attribute -- ``_Py_LoadAttr_StackRefSteal`` pins ``self`` across ``obj.attr()``
+-- so the greenlet has to be primed from inside that window. Priming it from a
+property getter does exactly that; it is also the same deep-attribute context
+Playwright's sync bridge happens to start its fiber from. A churn thread then
+keeps the collector busy while the dispatcher runs on its stale list.
+
+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.
+"""
+import gc
+import os
+import sys
+import threading
+
+import greenlet
+
+# The crash lands within a second, so a few seconds leaves plenty of margin on
+# a slow machine without dragging out a fixed build.
+DURATION = float(os.environ.get("REPRO_SECONDS", "4"))
+NWORKERS = int(os.environ.get("REPRO_THREADS", "6"))
+
+
+def _busy():
+    # Keep the dispatcher doing work (resolving str methods) so a collection on
+    # the churn thread catches it while it is the active greenlet.
+    return "abc".upper().lower().strip().title()
+
+
+class Bridge:
+    def __init__(self, stop):
+        self.stop = stop
+        self.main = greenlet.getcurrent()
+        self.disp = greenlet.greenlet(self._loop)
+
+    def _loop(self):
+        while not self.stop[0]:
+            for _ in range(50):
+                _busy()
+            self.main.switch()
+
+    @property
+    def _prime(self):
+        # Reaching this getter means we are mid attribute-resolution, so a
+        # _PyCStackRef for ``self`` is live right now. Priming the dispatcher
+        # from here is what makes it inherit a non-empty c_stack_refs head.
+        self.disp.switch()
+        return None
+
+    def start(self):
+        # Reading the property (rather than calling a method) is deliberate: 
the
+        # getter runs *during* attribute resolution, so it primes the 
dispatcher
+        # while the _PyCStackRef for ``self`` is still held. We return the 
value
+        # only so this isn't a bare, pointless-looking expression statement.
+        return self._prime
+
+    def resume(self):
+        self.disp.switch()
+
+
+def worker(stop):
+    bridge = Bridge(stop)
+    bridge.start()
+    # start() has returned, so the stack the inherited nodes point at is being
+    # reused -- the dispatcher's list is stale now. Keep it active to trip the 
GC.
+    while not stop[0]:
+        bridge.resume()
+
+
+def churn(stop):
+    while not stop[0]:
+        # A cycle forces a real (stop-the-world) collection rather than just a
+        # refcount drop.
+        nodes = [{"i": i, "next": None} for i in range(500)]
+        for a, b in zip(nodes, nodes[1:]):
+            a["next"] = b
+        nodes[-1]["next"] = nodes[0]
+        del nodes
+        gc.collect()
+
+
+def main():
+    stop = [False]
+    threads = [threading.Thread(target=churn, args=(stop,), daemon=True)]
+    threads += [threading.Thread(target=worker, args=(stop,), daemon=True)
+                for _ in range(NWORKERS)]
+    for t in threads:
+        t.start()
+    threading.Event().wait(DURATION)
+    stop[0] = True
+    for t in threads:
+        t.join(timeout=5)
+    print("ISSUE 515 OK")
+
+
+if __name__ == "__main__":
+    main()
+    sys.exit(0)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/greenlet-3.5.3/src/greenlet/tests/fail_switch_critical_section.py 
new/greenlet-3.5.5/src/greenlet/tests/fail_switch_critical_section.py
--- old/greenlet-3.5.3/src/greenlet/tests/fail_switch_critical_section.py       
1970-01-01 01:00:00.000000000 +0100
+++ new/greenlet-3.5.5/src/greenlet/tests/fail_switch_critical_section.py       
2026-08-10 15:23:03.667235400 +0200
@@ -0,0 +1,38 @@
+"""Out-of-process payload for the free-threaded switch/critical-section 
deadlock.
+
+See ``freethread_switch_deadlock.py`` in the repo root for the full write-up.
+Before the fix, a greenlet switch swapped C stacks but left any held
+``PyCriticalSection`` locks (tracked in ``tstate->critical_section``) locked.
+asyncio's ``Task.__step`` holds such a lock on the running task across the 
step,
+so switching into a child greenlet and touching that task from there blocked
+forever. A fixed build (and any GIL-enabled build) prints the sentinel; a
+regressed free-threaded build deadlocks, and the watchdog turns that hang into 
a
+non-zero exit so the test fails loudly instead of stalling the whole suite.
+"""
+import asyncio
+import faulthandler
+import sys
+
+import greenlet
+
+# The deadlock is immediate when present; the generous timeout is only so a
+# genuinely regressed build still exits on the slowest CI, never on a good one.
+faulthandler.dump_traceback_later(15, exit=True)
+
+
+async def main():
+    task = asyncio.current_task()  # its running __step holds a lock on `task`
+
+    def in_child_fiber():
+        # Fresh C stack; the task's lock is still held by the fiber we left.
+        # A regressed build never returns from this first call.
+        task.add_done_callback(lambda _: None)
+        task.remove_done_callback(lambda _: None)
+
+    greenlet.greenlet(in_child_fiber).switch()
+
+
+asyncio.run(main())
+faulthandler.cancel_dump_traceback_later()
+print("SWITCH CS OK")
+sys.exit(0)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.5.3/src/greenlet/tests/test_gc.py 
new/greenlet-3.5.5/src/greenlet/tests/test_gc.py
--- old/greenlet-3.5.3/src/greenlet/tests/test_gc.py    2026-06-26 
20:18:23.000000000 +0200
+++ new/greenlet-3.5.5/src/greenlet/tests/test_gc.py    2026-08-10 
15:23:03.668282300 +0200
@@ -6,6 +6,7 @@
 
 
 from . import TestCase
+from . import RUNNING_ON_FREETHREAD_BUILD
 from .leakcheck import fails_leakcheck_on_py314_or_less
 # These only work with greenlet gc support
 # which is no longer optional.
@@ -84,6 +85,27 @@
         greenlet.getcurrent()
         gc.collect()
 
+    def test_issue515_freethread_c_stack_refs(self):
+        # Guards issue #515: a new greenlet inherited the parent's C-stack refs
+        # and the free-threaded collector segfaulted following the dangling
+        # nodes. This has to run out of process because the regression is a 
hard
+        # crash, not something we can catch.
+        # https://github.com/python-greenlet/greenlet/issues/515
+        if not RUNNING_ON_FREETHREAD_BUILD:
+            self.skipTest("Only free-threaded builds are affected")
+        output = self.run_script('fail_issue_515_freethread_gc.py')
+        self.assertIn('ISSUE 515 OK', output)
+
+    def test_c_stack_refs_suspended_gc(self):
+        # Issue #515: a greenlet suspended while holding a _PyCStackRef must 
have
+        # those refs visited by tp_traverse, or the free-threaded collector 
frees
+        # an object reachable only through the suspended C stack. Runs the 
repro
+        # out of process. 
https://github.com/python-greenlet/greenlet/issues/515
+        if not RUNNING_ON_FREETHREAD_BUILD:
+            self.skipTest("Only free-threaded builds are affected")
+        output = self.run_script('fail_c_stack_refs_suspended_gc.py')
+        self.assertIn('C STACK REFS GC OK', output)
+
     def test_crashing_deferred_object(self):
         if sys.version_info < (3, 15):
             self.skipTest("Test is 3.15+ only")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.5.3/src/greenlet/tests/test_greenlet.py 
new/greenlet-3.5.5/src/greenlet/tests/test_greenlet.py
--- old/greenlet-3.5.3/src/greenlet/tests/test_greenlet.py      2026-06-26 
20:18:23.000000000 +0200
+++ new/greenlet-3.5.5/src/greenlet/tests/test_greenlet.py      2026-08-10 
15:23:03.668829000 +0200
@@ -1468,6 +1468,20 @@
             output
         )
 
+    def test_switch_leaves_no_critical_section_held(self):
+        # Free-threaded builds take per-object locks via
+        # Py_BEGIN_CRITICAL_SECTION, tracked in tstate->critical_section. A
+        # switch swaps C stacks, so leaving those locks held strands them on 
the
+        # fiber we left: asyncio's Task.__step holds one on the running task
+        # across the step, and touching that task from a child fiber then
+        # deadlocked re-taking it. Out of process because a regression is a
+        # hang, not a catchable error.
+        # (repro: freethread_switch_deadlock.py in the repo root)
+        if not RUNNING_ON_FREETHREAD_BUILD:
+            self.skipTest("Only free-threaded builds take critical sections")
+        output = self.run_script('fail_switch_critical_section.py')
+        self.assertIn('SWITCH CS OK', output)
+
 class TestModule(TestCase):
 
     @unittest.skipUnless(hasattr(sys, '_is_gil_enabled'),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.5.3/src/greenlet.egg-info/PKG-INFO 
new/greenlet-3.5.5/src/greenlet.egg-info/PKG-INFO
--- old/greenlet-3.5.3/src/greenlet.egg-info/PKG-INFO   2026-06-26 
20:18:27.000000000 +0200
+++ new/greenlet-3.5.5/src/greenlet.egg-info/PKG-INFO   2026-08-10 
15:23:07.249332400 +0200
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: greenlet
-Version: 3.5.3
+Version: 3.5.5
 Summary: Lightweight in-process concurrent programming
 Author-email: Alexey Borzenkov <[email protected]>
 Maintainer-email: Jason Madden <[email protected]>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/greenlet-3.5.3/src/greenlet.egg-info/SOURCES.txt 
new/greenlet-3.5.5/src/greenlet.egg-info/SOURCES.txt
--- old/greenlet-3.5.3/src/greenlet.egg-info/SOURCES.txt        2026-06-26 
20:18:27.000000000 +0200
+++ new/greenlet-3.5.5/src/greenlet.egg-info/SOURCES.txt        2026-08-10 
15:23:07.266925600 +0200
@@ -102,10 +102,13 @@
 src/greenlet/tests/__init__.py
 src/greenlet/tests/_test_extension.c
 src/greenlet/tests/_test_extension_cpp.cpp
+src/greenlet/tests/fail_c_stack_refs_suspended_gc.py
 src/greenlet/tests/fail_clearing_run_switches.py
 src/greenlet/tests/fail_cpp_exception.py
 src/greenlet/tests/fail_initialstub_already_started.py
+src/greenlet/tests/fail_issue_515_freethread_gc.py
 src/greenlet/tests/fail_slp_switch.py
+src/greenlet/tests/fail_switch_critical_section.py
 src/greenlet/tests/fail_switch_three_greenlets.py
 src/greenlet/tests/fail_switch_three_greenlets2.py
 src/greenlet/tests/fail_switch_two_greenlets.py

Reply via email to