Skip to content

Commit 6ec0bbb

Browse files
authored
Merge pull request #514 from python-greenlet/issue513-preserve-crit-section
Py3.13+: Preserve thread state critical_section to prevent crash on free-threaded builds
2 parents bc10829 + c03a7e6 commit 6ec0bbb

4 files changed

Lines changed: 50 additions & 3 deletions

File tree

‎CHANGES.rst‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
3.5.3 (unreleased)
66
==================
77

8-
- Nothing changed yet.
8+
- Fix a crash on free-threaded builds when multiple greenlets were
9+
holding a critical section on an object and the GIL for the thread
10+
was dropped. See `issue 513
11+
<https://github.com/python-greenlet/greenlet/issues/513>`_. Thanks
12+
to ddorian.
913

1014

1115
3.5.2 (2026-06-17)

‎src/greenlet/TGreenlet.hpp‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,8 @@ namespace greenlet
147147
int recursion_depth;
148148
#endif
149149
#if GREENLET_PY313
150-
PyObject *delete_later;
150+
PyObject* delete_later;
151+
uintptr_t critical_section;
151152
#else
152153
int trash_delete_nesting;
153154
#endif

‎src/greenlet/TPythonState.cpp‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ PythonState::PythonState()
2727
#endif
2828
#if GREENLET_PY313
2929
,delete_later(nullptr)
30+
,critical_section(0)
3031
#else
3132
,trash_delete_nesting(0)
3233
#endif
@@ -191,6 +192,7 @@ void PythonState::operator<<(const PyThreadState *const tstate) noexcept
191192
// ``greenlet.tests.test_greenlet_trash`` tries, but under 3.14,
192193
// at least, fails to do so.
193194
this->delete_later = Py_XNewRef(tstate->delete_later);
195+
this->critical_section = tstate->critical_section;
194196
#elif GREENLET_PY312
195197
this->trash_delete_nesting = tstate->trash.delete_nesting;
196198
#else // not 312 or 3.13+
@@ -298,7 +300,7 @@ void PythonState::operator>>(PyThreadState *const tstate) noexcept
298300
tstate->delete_later = this->delete_later;
299301
Py_CLEAR(this->delete_later);
300302
}
301-
303+
tstate->critical_section = this->critical_section;
302304

303305
#elif GREENLET_PY312
304306
tstate->trash.delete_nesting = this->trash_delete_nesting;

‎src/greenlet/tests/test_greenlet.py‎

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1027,6 +1027,46 @@ def inner(frame):
10271027
# The next line crashes on 3.12 if we haven't exposed the frames.
10281028
self.assertIsNone(frame.f_back)
10291029

1030+
def test_switching_holding_critical_section_no_crash(self):
1031+
# https://github.com/python-greenlet/greenlet/issues/513
1032+
# In no-GIL builds, we were failing to restore the
1033+
# critical_section pointer, leading to
1034+
# ``Fatal Python error: PyMutex_Unlock: unlocking mutex that is not locked``
1035+
# when two greenlets both held a lock on an object, and then
1036+
# the GIL was released, which, according to
1037+
# ``Include/cpython/critical_section.h`` causes the critical
1038+
# section to get released.
1039+
# The field is present on Python 3.13+, and only used
1040+
# in no-GIL builds.
1041+
# Initial version of this test case provided in the
1042+
# github issue by ddorian
1043+
def k1(x):
1044+
g2.switch() # into G2 while holding G1's sort critical section
1045+
return x
1046+
1047+
1048+
def k2(x):
1049+
g1.switch() # back into G1 while holding G2's sort critical section
1050+
return x
1051+
1052+
1053+
def g1_body():
1054+
sorted([0], key=k1)
1055+
g2.switch()
1056+
1057+
1058+
def g2_body():
1059+
sorted([0], key=k2)
1060+
# Do a blocking I/O operation which would cause the
1061+
# critical sections to get suspended
1062+
with open(__file__, "rt", encoding='utf-8') as f:
1063+
f.read()
1064+
1065+
1066+
g1 = greenlet.greenlet(g1_body)
1067+
g2 = greenlet.greenlet(g2_body)
1068+
g1.switch()
1069+
10301070

10311071
class TestGreenletSetParentErrors(TestCase):
10321072
def test_threaded_reparent(self):

0 commit comments

Comments
 (0)