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