Skip to content

Commit b54c4bd

Browse files
committed
Fix some crashes with CPython assertions enabled, pointing to things we were doing wrong it free-threading.
Specifically, we need to preserve the c_stack_refs attribute, and be careful about what we say can be GC'd.
1 parent 47e9925 commit b54c4bd

File tree

6 files changed

+79
-3
lines changed

6 files changed

+79
-3
lines changed

.github/workflows/tests.yml

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ jobs:
5151
- name: Install greenlet (non-Mac)
5252
if: ${{ ! startsWith(runner.os, 'Mac') }}
5353
run: |
54-
# Stupid setuptools doesn't want you running 'python setup.py' anymore,
55-
# but stupid pip hides all the intersting compiler output by default, and the
54+
# setuptools doesn't want you running 'python setup.py' anymore,
55+
# but pip hides all the intersting compiler output by default, and the
5656
# only way to get anything useful out is to ask *everything* to be verbose,
5757
# which is much more junk than we need to wade through, making it hard to
5858
# see what we want. What's the point of having warnings at all if we can't
@@ -104,6 +104,20 @@ jobs:
104104
python -c 'import greenlet._greenlet as G; assert G.GREENLET_USE_STANDARD_THREADING'
105105
python -m unittest discover -v greenlet.tests
106106
- name: Doctest
107+
env:
108+
# XXX: On 3.14t, when the thread-local bytecode cache is
109+
# enabled, sphinx crashes on module cleanup: there is a
110+
# reference to pdb.set_trace in ``glob``, and when the
111+
# shutdown sequence tries to clear that value, it segfaults
112+
# dec-reffing it after taking it out of the module dict. The
113+
# object appears to be corrupt, but it is utterly unclear how
114+
# we could have done this. It is 100% reliable on bath Mac and
115+
# Linux. It can be traced down to a very simple doctest
116+
# snippet in greenlet_gc.rst, but running that same snippet
117+
# standalone in a unit test doesn't produce the error.
118+
#
119+
# So this is a temporary workaround.
120+
PYTHON_TLBC: "0"
107121
run: |
108122
sphinx-build -b doctest -d docs/_build/doctrees2 docs docs/_build/doctest2
109123
- name: Lint

src/greenlet/PyGreenlet.cpp

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -741,7 +741,30 @@ PyTypeObject PyGreenlet_Type = {
741741
.tp_alloc=PyType_GenericAlloc, /* tp_alloc */
742742
.tp_new=(newfunc)green_new, /* tp_new */
743743
.tp_free=PyObject_GC_Del, /* tp_free */
744+
#ifndef Py_GIL_DISABLED
745+
/*
746+
We may have been handling this wrong all along.
747+
748+
It shows as a problem with the GIL disabled. In builds of 3.14 with
749+
assertions enabled, we break the garbage collector if we *ever*
750+
return false from this function. The docs say this is to distinguish
751+
some objects that are collectable vs some that are not, specifically
752+
giving the example of PyTypeObject as the only place this is done,
753+
where it distinguishes between static types like this one (allocated
754+
by the C runtime at load time) and dynamic heap types (created at
755+
runtime as objects). With the GIL disabled, all allocations that are
756+
potentially collectable go in the mimalloc heap, and the collector
757+
asserts that tp_is_gc() is true for them as it walks through the
758+
heap object by object. Since we set the Py_TPFLAGS_HAS_GC bit, we
759+
are always allocated in that mimalloc heap, so we must always be
760+
collectable.
761+
762+
XXX: TODO: Could this be responsible for some apparent leaks, even
763+
on GIL builds, at least in 3.14? See if we can catch an assertion
764+
failure in the GC on regular 3.14 as well.
765+
*/
744766
.tp_is_gc=(inquiry)green_is_gc, /* tp_is_gc */
767+
#endif
745768
};
746769

747770
#endif

src/greenlet/TGreenlet.hpp

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ using greenlet::refs::BorrowedGreenlet;
3434
#else
3535
# include "internal/pycore_interpframe.h"
3636
#endif
37+
#ifdef Py_GIL_DISABLED
38+
# include "internal/pycore_tstate.h"
39+
#endif
3740
#endif
3841

3942
// XXX: TODO: Work to remove all virtual functions
@@ -122,6 +125,9 @@ namespace greenlet
122125
// Assertion `tstate->current_executor == NULL' failed.
123126
// see https://github.com/python-greenlet/greenlet/issues/460
124127
PyObject* current_executor;
128+
#ifdef Py_GIL_DISABLED
129+
_PyCStackRef* c_stack_refs;
130+
#endif
125131
#elif GREENLET_PY312
126132
int py_recursion_depth;
127133
int c_recursion_depth;

src/greenlet/TPythonState.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ PythonState::PythonState()
1515
#if GREENLET_PY314
1616
,py_recursion_depth(0)
1717
,current_executor(nullptr)
18+
#ifdef Py_GIL_DISABLED
19+
,c_stack_refs(nullptr)
20+
#endif
1821
#elif GREENLET_PY312
1922
,py_recursion_depth(0)
2023
,c_recursion_depth(0)
@@ -138,6 +141,9 @@ void PythonState::operator<<(const PyThreadState *const tstate) noexcept
138141
#if GREENLET_PY314
139142
this->py_recursion_depth = tstate->py_recursion_limit - tstate->py_recursion_remaining;
140143
this->current_executor = tstate->current_executor;
144+
#ifdef Py_GIL_DISABLED
145+
this->c_stack_refs = ((_PyThreadStateImpl*)tstate)->c_stack_refs;
146+
#endif
141147
#elif GREENLET_PY312
142148
this->py_recursion_depth = tstate->py_recursion_limit - tstate->py_recursion_remaining;
143149
this->c_recursion_depth = Py_C_RECURSION_LIMIT - tstate->c_recursion_remaining;
@@ -216,6 +222,9 @@ void PythonState::operator>>(PyThreadState *const tstate) noexcept
216222
#if GREENLET_PY314
217223
tstate->py_recursion_remaining = tstate->py_recursion_limit - this->py_recursion_depth;
218224
tstate->current_executor = this->current_executor;
225+
#ifdef Py_GIL_DISABLED
226+
((_PyThreadStateImpl*)tstate)->c_stack_refs = this->c_stack_refs;
227+
#endif
219228
this->unexpose_frames();
220229
#elif GREENLET_PY312
221230
tstate->py_recursion_remaining = tstate->py_recursion_limit - this->py_recursion_depth;

src/greenlet/greenlet_allocator.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ namespace greenlet
7070
};
7171

7272
};
73+
7374
}
7475

7576
#endif

src/greenlet/tests/test_gc.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# which is no longer optional.
1212
assert greenlet.GREENLET_USE_GC
1313

14-
class GCTests(TestCase):
14+
class TestGC(TestCase):
1515
def test_dead_circular_ref(self):
1616
o = weakref.ref(greenlet.greenlet(greenlet.getcurrent).switch())
1717
gc.collect()
@@ -84,3 +84,26 @@ def greenlet_body():
8484
del g
8585
greenlet.getcurrent()
8686
gc.collect()
87+
88+
# def test_crashing_cycle(self):
89+
# # CPython 3.14 free threaded crashes on this
90+
# # (from docs/greenlet_gc.rst)
91+
# import doctest
92+
93+
# def with_doctest():
94+
# """
95+
# >>> import gc
96+
# >>> from greenlet import getcurrent, greenlet, GreenletExit
97+
# >>> def collect_it():
98+
# ... print("Collecting garbage")
99+
# ... gc.collect()
100+
# >>> def outer():
101+
# ... gc.collect()
102+
# >>> outer_glet = greenlet(outer)
103+
# >>> outer_glet.switch()
104+
# Collecting garbage
105+
# >>> print(list(globals()))
106+
# >>> print(list(locals()))
107+
# """
108+
109+
# doctest.run_docstring_examples(with_doctest, dict())

0 commit comments

Comments
 (0)