Skip to content

Commit 0f86075

Browse files
authored
Merge pull request #504 from python-greenlet/freethreading-fixes
Various free-threading fixes
2 parents 6b281d3 + 4596574 commit 0f86075

14 files changed

Lines changed: 269 additions & 50 deletions

CHANGES.rst

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,20 @@
1616
<https://github.com/python-greenlet/greenlet/pull/499>`_ by Nicolas
1717
Bouvrette.
1818
- Address the results of an automated code audit performed by
19-
devdanzin. This includes several minor correctness changes that
19+
Daniel Diniz. This includes several minor correctness changes that
2020
theoretically could have been crashing bugs, but typically only in
2121
very rare circumstances.
2222

2323
See `PR 502 <https://github.com/python-greenlet/greenlet/pull/502>`_.
2424

25+
- Fix several race conditions that could arise in free-threaded
26+
builds when using greenlet objects from multiple threads, some of
27+
which could lead to assertion failures or interpreter crashes.
28+
29+
See `issue 503
30+
<https://github.com/python-greenlet/greenlet/issues/503>`_, with
31+
thanks to Nitay Dariel and Daniel Diniz.
32+
2533
3.3.2 (2026-02-20)
2634
==================
2735

docs/caveats.rst

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ Free-threading Is Experimental
3737
==============================
3838

3939
Beginning with greenlet 3.3.0, support for Python 3.14's free-threaded
40-
mode is enabled. Use caution, as it has only limited testing.
40+
mode is enabled; greenlet 3.4.0 expands this support and fixes several
41+
race conditions. Use caution, as it has only limited testing.
4142

4243
There are known issues running greenlets in a free-threaded CPython.
4344
These include:

src/greenlet/PyGreenlet.cpp

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ using greenlet::MainGreenlet;
4747
using greenlet::BrokenGreenlet;
4848
using greenlet::ThreadState;
4949
using greenlet::PythonState;
50-
50+
using greenlet::refs::PyCriticalObjectSection;
5151

5252

5353
static PyGreenlet*
@@ -372,6 +372,29 @@ PyDoc_STRVAR(
372372
static PyObject*
373373
green_switch(PyGreenlet* self, PyObject* args, PyObject* kwargs)
374374
{
375+
// Our use of Greenlet::args() makes this method non-reentrant.
376+
// Therefore, check to be sure the switch will be allowed ---
377+
// we're calling from the same thread that ``self`` belongs to ---
378+
// BEFORE doing anything with args(). If we don't do this, we can
379+
// find args() getting clobbered by switches that will never
380+
// succeed.
381+
//
382+
// TODO: We're only doing this for free-threaded builds because
383+
// those are the only ones that have demonstrated an issue,
384+
// trusting our later checks in g_switch to perform the same
385+
// function and the GIL to keep us from being reentered in regular
386+
// builds. BUT should we always do this as an extra measure of
387+
// safety in case we run code at unexpected times (e.g., a GC?)
388+
#ifdef Py_GIL_DISABLED
389+
try {
390+
self->pimpl->check_switch_allowed();
391+
}
392+
catch (const PyErrOccurred&) {
393+
return nullptr;
394+
}
395+
#endif
396+
397+
375398
using greenlet::SwitchingArgs;
376399
SwitchingArgs switch_args(OwnedObject::owning(args), OwnedObject::owning(kwargs));
377400
self->pimpl->may_switch_away();
@@ -454,6 +477,16 @@ PyDoc_STRVAR(
454477
static PyObject*
455478
green_throw(PyGreenlet* self, PyObject* args)
456479
{
480+
// See green_switch for why we call this early.
481+
#ifdef Py_GIL_DISABLED
482+
try {
483+
self->pimpl->check_switch_allowed();
484+
}
485+
catch (const PyErrOccurred&) {
486+
return nullptr;
487+
}
488+
#endif
489+
457490
PyArgParseParam typ(mod_globs->PyExc_GreenletExit);
458491
PyArgParseParam val;
459492
PyArgParseParam tb;
@@ -489,6 +522,7 @@ green_bool(PyGreenlet* self)
489522
static PyObject*
490523
green_getdict(PyGreenlet* self, void* UNUSED(context))
491524
{
525+
PyCriticalObjectSection cs(self);
492526
if (self->dict == NULL) {
493527
self->dict = PyDict_New();
494528
if (self->dict == NULL) {
@@ -502,8 +536,6 @@ green_getdict(PyGreenlet* self, void* UNUSED(context))
502536
static int
503537
green_setdict(PyGreenlet* self, PyObject* val, void* UNUSED(context))
504538
{
505-
PyObject* tmp;
506-
507539
if (val == NULL) {
508540
PyErr_SetString(PyExc_TypeError, "__dict__ may not be deleted");
509541
return -1;
@@ -512,7 +544,8 @@ green_setdict(PyGreenlet* self, PyObject* val, void* UNUSED(context))
512544
PyErr_SetString(PyExc_TypeError, "__dict__ must be a dictionary");
513545
return -1;
514546
}
515-
tmp = self->dict;
547+
PyCriticalObjectSection cs(self);
548+
PyObject* tmp = self->dict;
516549
Py_INCREF(val);
517550
self->dict = val;
518551
Py_XDECREF(tmp);
@@ -535,6 +568,7 @@ _green_not_dead(BorrowedGreenlet self)
535568
static PyObject*
536569
green_getdead(PyGreenlet* self, void* UNUSED(context))
537570
{
571+
PyCriticalObjectSection cs(self);
538572
if (_green_not_dead(self)) {
539573
Py_RETURN_FALSE;
540574
}
@@ -553,6 +587,7 @@ green_get_stack_saved(PyGreenlet* self, void* UNUSED(context))
553587
static PyObject*
554588
green_getrun(PyGreenlet* self, void* UNUSED(context))
555589
{
590+
PyCriticalObjectSection cs(self);
556591
try {
557592
OwnedObject result(BorrowedGreenlet(self)->run());
558593
return result.relinquish_ownership();
@@ -566,6 +601,7 @@ green_getrun(PyGreenlet* self, void* UNUSED(context))
566601
static int
567602
green_setrun(PyGreenlet* self, PyObject* nrun, void* UNUSED(context))
568603
{
604+
PyCriticalObjectSection cs(self);
569605
try {
570606
BorrowedGreenlet(self)->run(nrun);
571607
return 0;
@@ -578,13 +614,15 @@ green_setrun(PyGreenlet* self, PyObject* nrun, void* UNUSED(context))
578614
static PyObject*
579615
green_getparent(PyGreenlet* self, void* UNUSED(context))
580616
{
617+
PyCriticalObjectSection cs(self);
581618
return BorrowedGreenlet(self)->parent().acquire_or_None();
582619
}
583620

584621

585622
static int
586623
green_setparent(PyGreenlet* self, PyObject* nparent, void* UNUSED(context))
587624
{
625+
PyCriticalObjectSection cs(self);
588626
try {
589627
BorrowedGreenlet(self)->parent(nparent);
590628
}
@@ -598,6 +636,7 @@ green_setparent(PyGreenlet* self, PyObject* nparent, void* UNUSED(context))
598636
static PyObject*
599637
green_getcontext(const PyGreenlet* self, void* UNUSED(context))
600638
{
639+
PyCriticalObjectSection cs(self);
601640
const Greenlet *const g = self->pimpl;
602641
try {
603642
OwnedObject result(g->context());
@@ -611,6 +650,7 @@ green_getcontext(const PyGreenlet* self, void* UNUSED(context))
611650
static int
612651
green_setcontext(PyGreenlet* self, PyObject* nctx, void* UNUSED(context))
613652
{
653+
PyCriticalObjectSection cs(self);
614654
try {
615655
BorrowedGreenlet(self)->context(nctx);
616656
return 0;
@@ -624,6 +664,7 @@ green_setcontext(PyGreenlet* self, PyObject* nctx, void* UNUSED(context))
624664
static PyObject*
625665
green_getframe(PyGreenlet* self, void* UNUSED(context))
626666
{
667+
PyCriticalObjectSection cs(self);
627668
const PythonState::OwnedFrame& top_frame = BorrowedGreenlet(self)->top_frame();
628669
return top_frame.acquire_or_None();
629670
}

src/greenlet/TGreenlet.cpp

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ Greenlet::g_switchstack(void)
198198

199199
// No stack-based variables are valid anymore.
200200

201-
// But the global is volatile so we can reload it without the
201+
// But the global is thread_local volatile so we can reload it without the
202202
// compiler caching it from earlier.
203203
Greenlet* greenlet_that_switched_in = switching_thread_state; // aka this
204204
switching_thread_state = nullptr;
@@ -230,18 +230,18 @@ Greenlet::check_switch_allowed() const
230230

231231
// If the thread this greenlet was running in is dead,
232232
// we'll still have a reference to a main greenlet, but the
233-
// thread state pointer we have is bogus.
233+
// thread state pointer we have is bogus (should be nullptr)
234234
// TODO: Give the objects an API to determine if they belong
235235
// to a dead thread.
236236

237-
const BorrowedMainGreenlet main_greenlet = this->find_main_greenlet_in_lineage();
237+
const BorrowedMainGreenlet my_main_greenlet = this->find_main_greenlet_in_lineage();
238238

239-
if (!main_greenlet) {
239+
if (!my_main_greenlet) {
240240
throw PyErrOccurred(mod_globs->PyExc_GreenletError,
241241
"cannot switch to a garbage collected greenlet");
242242
}
243243

244-
if (!main_greenlet->thread_state()) {
244+
if (!my_main_greenlet->thread_state()) {
245245
throw PyErrOccurred(mod_globs->PyExc_GreenletError,
246246
"cannot switch to a different thread (which happens to have exited)");
247247
}
@@ -255,26 +255,26 @@ Greenlet::check_switch_allowed() const
255255
// may not be visible yet. So we need to check against the
256256
// current thread state (once the cheaper checks are out of
257257
// the way)
258-
const BorrowedMainGreenlet current_main_greenlet = GET_THREAD_STATE().state().borrow_main_greenlet();
258+
const BorrowedMainGreenlet main_greenlet_cur_thread = GET_THREAD_STATE().state().borrow_main_greenlet();
259259
if (
260260
// lineage main greenlet is not this thread's greenlet
261-
current_main_greenlet != main_greenlet
261+
main_greenlet_cur_thread != my_main_greenlet
262262
|| (
263263
// atteched to some thread
264264
this->main_greenlet()
265265
// XXX: Same condition as above. Was this supposed to be
266266
// this->main_greenlet()?
267-
&& current_main_greenlet != main_greenlet)
267+
&& main_greenlet_cur_thread != my_main_greenlet)
268268
// switching into a known dead thread (XXX: which, if we get here,
269269
// is bad, because we just accessed the thread state, which is
270270
// gone!)
271-
|| (!current_main_greenlet->thread_state())) {
271+
|| (!main_greenlet_cur_thread->thread_state())) {
272272
// CAUTION: This may trigger memory allocations, gc, and
273273
// arbitrary Python code.
274274
throw PyErrOccurred(
275275
mod_globs->PyExc_GreenletError,
276276
"Cannot switch to a different thread\n\tCurrent: %R\n\tExpected: %R",
277-
current_main_greenlet, main_greenlet);
277+
main_greenlet_cur_thread, my_main_greenlet);
278278
}
279279
}
280280

src/greenlet/TGreenlet.hpp

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
#define PY_SSIZE_T_CLEAN
88
#include <Python.h>
99

10+
#include <atomic>
11+
1012
#include "greenlet_compiler_compat.hpp"
1113
#include "greenlet_refs.hpp"
1214
#include "greenlet_cpython_compat.hpp"
@@ -401,6 +403,18 @@ namespace greenlet
401403
}
402404

403405
virtual OwnedObject throw_GreenletExit_during_dealloc(const ThreadState& current_thread_state);
406+
407+
/**
408+
* Depends on the state of this->args() or the current Python
409+
* error indicator. Thus, it is not threadsafe or reentrant.
410+
* You (you being ``green_switch``, the Python-level
411+
* ``greenlet.switch`` method) should call
412+
* ``check_switch_allowed`` in free-threaded builds before
413+
* calling this method and catch ``PyErrOccurred`` if it isn't
414+
* a valid switch. This method should also call that method
415+
* because there are places where we can switch internally
416+
* without going through the Python method.
417+
*/
404418
virtual OwnedObject g_switch() = 0;
405419
/**
406420
* Force the greenlet to appear dead. Used when it's not
@@ -498,6 +512,11 @@ namespace greenlet
498512
// slp_switch() failed.
499513
virtual bool force_slp_switch_error() const noexcept;
500514

515+
// Check the preconditions for switching to this greenlet; if they
516+
// aren't met, throws PyErrOccurred. Most callers will want to
517+
// catch this and clear the arguments if they've been set.
518+
inline void check_switch_allowed() const;
519+
501520
protected:
502521
inline void release_args();
503522

@@ -564,11 +583,6 @@ namespace greenlet
564583
// Returns the previous greenlet we just switched away from.
565584
virtual OwnedGreenlet g_switchstack_success() noexcept;
566585

567-
568-
// Check the preconditions for switching to this greenlet; if they
569-
// aren't met, throws PyErrOccurred. Most callers will want to
570-
// catch this and clear the arguments
571-
inline void check_switch_allowed() const;
572586
class GreenletStartedWhileInPython : public std::runtime_error
573587
{
574588
public:
@@ -754,7 +768,7 @@ class TracingGuard
754768
private:
755769
static greenlet::PythonAllocator<MainGreenlet> allocator;
756770
refs::BorrowedMainGreenlet _self;
757-
ThreadState* _thread_state;
771+
std::atomic<ThreadState*> _thread_state;
758772
G_NO_COPIES_OF_CLS(MainGreenlet);
759773
public:
760774
static void* operator new(size_t UNUSED(count));

src/greenlet/TGreenletGlobals.cpp

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
#ifndef T_GREENLET_GLOBALS
1313
#define T_GREENLET_GLOBALS
1414

15+
#include <algorithm>
16+
1517
#include "greenlet_refs.hpp"
1618
#include "greenlet_exceptions.hpp"
1719
#include "greenlet_thread_support.hpp"
@@ -66,6 +68,9 @@ class GreenletGlobals
6668
// do any deallocation.)
6769
}
6870

71+
/**
72+
* Must be holding the ``thread_states_to_destroy`` lock.
73+
*/
6974
void queue_to_destroy(ThreadState* ts) const
7075
{
7176
// we're currently accessed through a static const object,
@@ -74,13 +79,27 @@ class GreenletGlobals
7479
// const.
7580
//
7681
// Do that for callers.
77-
greenlet::cleanup_queue_t& q = const_cast<greenlet::cleanup_queue_t&>(this->thread_states_to_destroy);
82+
greenlet::cleanup_queue_t& q = const_cast<greenlet::cleanup_queue_t&>(
83+
this->thread_states_to_destroy);
84+
// make sure we don't ever try to clean up a state more than
85+
// once. Because they're thread-local, and we ultimately call this
86+
// method from the destructor of the thread local variable,
87+
// we should never find the item already present. This check
88+
// is nominally O(n) in the size of the vector.
89+
assert(std::find(q.begin(), q.end(), ts) == q.end());
7890
q.push_back(ts);
7991
}
8092

93+
/**
94+
* Must be holding the ``thread_states_to_destroy`` lock.
95+
*/
8196
ThreadState* take_next_to_destroy() const
8297
{
83-
greenlet::cleanup_queue_t& q = const_cast<greenlet::cleanup_queue_t&>(this->thread_states_to_destroy);
98+
greenlet::cleanup_queue_t& q = const_cast<greenlet::cleanup_queue_t&>(
99+
this->thread_states_to_destroy);
100+
if (q.empty()) {
101+
return nullptr;
102+
}
84103
ThreadState* result = q.back();
85104
q.pop_back();
86105
return result;

src/greenlet/TMainGreenlet.cpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,8 @@ MainGreenlet::thread_state() const noexcept
6666
void
6767
MainGreenlet::thread_state(ThreadState* t) noexcept
6868
{
69+
// this method is only used during thread tear down, when it is
70+
// called with nullptr, signalling the thread is dead.
6971
assert(!t);
7072
this->_thread_state = t;
7173
}
@@ -118,9 +120,10 @@ MainGreenlet::g_switch()
118120
int
119121
MainGreenlet::tp_traverse(visitproc visit, void* arg)
120122
{
121-
if (this->_thread_state) {
123+
ThreadState* thread_state = this->_thread_state.load();
124+
if (thread_state) {
122125
// we've already traversed main, (self), don't do it again.
123-
int result = this->_thread_state->tp_traverse(visit, arg, false);
126+
int result = thread_state->tp_traverse(visit, arg, false);
124127
if (result) {
125128
return result;
126129
}

0 commit comments

Comments
 (0)