Skip to content

Commit b5f9d23

Browse files
committed
Restore stack introspection ability on 3.12
1 parent edbdda2 commit b5f9d23

File tree

10 files changed

+379
-26
lines changed

10 files changed

+379
-26
lines changed

CHANGES.rst

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,20 @@
22
Changes
33
=========
44

5-
3.0.3 (unreleased)
5+
3.1.0 (unreleased)
66
==================
77

8-
- Nothing changed yet.
9-
8+
- Python 3.12: Restore the full ability to walk the stack of a suspended
9+
greenlet; previously only the innermost frame was exposed.
10+
For performance reasons, there are still some restrictions relative to
11+
older Python versions; in particular, by default it is only valid to walk
12+
a suspended greenlet's stack in between a ``gr_frame`` access and the next
13+
resumption of the greenlet. A more flexible mode can be enabled by setting
14+
the greenlet's new ``gr_frames_always_exposed`` attribute to True. If you
15+
get a Python interpreter crash on 3.12+ when accessing the ``f_back`` of a
16+
suspended greenlet frame, you're probably accessing it in a way that
17+
requires you to set this attribute. See `issue 388
18+
<https://github.com/python-greenlet/greenlet/issues/388>`_.
1019

1120
3.0.2 (2023-12-08)
1221
==================

docs/api.rst

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ Greenlets
3232

3333
.. autoattribute:: gr_context
3434

35-
3635
The :class:`contextvars.Context` in which ``g`` will run.
3736
Writable; defaults to ``None``, reflecting that a greenlet
3837
starts execution in an empty context unless told otherwise.
@@ -56,6 +55,30 @@ Greenlets
5655
for suspended greenlets; it is None if the greenlet is dead, not
5756
yet started, or currently executing.
5857

58+
.. warning:: Greenlet stack introspection is fragile on CPython 3.12
59+
and later. The frame objects of a suspended greenlet are not safe
60+
to access as-is, but must be adjusted by the greenlet package in
61+
order to make traversing ``f_back`` links not crash the interpreter,
62+
and restored to their original state when resuming the greenlet.
63+
This is all handled transparently as long as you obtain references
64+
to greenlet frames only via the ``gr_frame`` attribute and you finish
65+
accessing them before the greenlet next resumes. If you obtain
66+
frames in other ways, or hold onto them across their greenlet's
67+
resumption, you must set the ``gr_frames_always_exposed`` attribute
68+
in order to make that safe.
69+
70+
.. autoattribute:: gr_frames_always_exposed
71+
72+
Writable boolean indicating whether this greenlet will take extra action,
73+
each time it is suspended, to ensure that its frame objects are always
74+
safe to access. Normally such action is only taken when an access
75+
to the ``gr_frame`` attribute occurs, which means you can only safely
76+
walk a greenlet's stack in between accessing ``gr_frame`` and resuming
77+
the greenlet. This is relevant only on CPython 3.12 and later; earlier
78+
versions still permit writing the attribute, but because their frame
79+
objects are safe to access regardless, such writes have no effect and
80+
the attribute always reads as true.
81+
5982
.. autoattribute:: parent
6083

6184
The parent greenlet. This is writable, but it is not allowed to create

src/greenlet/TGreenlet.cpp

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,11 @@ Greenlet::g_switchstack(void)
168168
current->exception_state << tstate;
169169
this->python_state.will_switch_from(tstate);
170170
switching_thread_state = this;
171+
#if GREENLET_PY312
172+
if (current->python_state.expose_frames_on_every_suspension) {
173+
current->expose_frames();
174+
}
175+
#endif
171176
}
172177
assert(this->args() || PyErr_Occurred());
173178
// If this is the first switch into a greenlet, this will
@@ -606,5 +611,103 @@ bool Greenlet::is_currently_running_in_some_thread() const
606611
return this->stack_state.active() && !this->python_state.top_frame();
607612
}
608613

614+
#if GREENLET_PY312
615+
void GREENLET_NOINLINE(Greenlet::expose_frames)()
616+
{
617+
if (!this->python_state.frame_exposure_needs_stack_rewrite()) {
618+
return; // nothing to do
619+
}
620+
621+
_PyInterpreterFrame* last_complete_iframe = nullptr;
622+
_PyInterpreterFrame* iframe = this->python_state.top_frame()->f_frame;
623+
while (iframe) {
624+
// We must make a copy before looking at the iframe contents,
625+
// since iframe might point to a portion of the greenlet's C stack
626+
// that was spilled when switching greenlets.
627+
_PyInterpreterFrame iframe_copy;
628+
this->stack_state.copy_from_stack(&iframe_copy, iframe, sizeof(*iframe));
629+
if (!_PyFrame_IsIncomplete(&iframe_copy)) {
630+
// If the iframe were OWNED_BY_CSTACK then it would always be
631+
// incomplete. Since it's not incomplete, it's not on the C stack
632+
// and we can access it through the original `iframe` pointer
633+
// directly. This is important since GetFrameObject might
634+
// lazily _create_ the frame object and we don't want the
635+
// interpreter to lose track of it.
636+
assert(iframe_copy.owner != FRAME_OWNED_BY_CSTACK);
637+
638+
// We really want to just write:
639+
// PyFrameObject* frame = _PyFrame_GetFrameObject(iframe);
640+
// but _PyFrame_GetFrameObject calls _PyFrame_MakeAndSetFrameObject
641+
// which is not a visible symbol in libpython. The easiest
642+
// way to get a public function to call it is using
643+
// PyFrame_GetBack, which is defined as follows:
644+
// assert(frame != NULL);
645+
// assert(!_PyFrame_IsIncomplete(frame->f_frame));
646+
// PyFrameObject *back = frame->f_back;
647+
// if (back == NULL) {
648+
// _PyInterpreterFrame *prev = frame->f_frame->previous;
649+
// prev = _PyFrame_GetFirstComplete(prev);
650+
// if (prev) {
651+
// back = _PyFrame_GetFrameObject(prev);
652+
// }
653+
// }
654+
// return (PyFrameObject*)Py_XNewRef(back);
655+
if (!iframe->frame_obj) {
656+
PyFrameObject dummy_frame;
657+
_PyInterpreterFrame dummy_iframe;
658+
dummy_frame.f_back = nullptr;
659+
dummy_frame.f_frame = &dummy_iframe;
660+
// force the iframe to be considered complete without
661+
// needing to check its code object:
662+
dummy_iframe.owner = FRAME_OWNED_BY_GENERATOR;
663+
dummy_iframe.previous = iframe;
664+
assert(!_PyFrame_IsIncomplete(&dummy_iframe));
665+
// Drop the returned reference immediately; the iframe
666+
// continues to hold a strong reference
667+
Py_XDECREF(PyFrame_GetBack(&dummy_frame));
668+
assert(iframe->frame_obj);
669+
}
670+
671+
// This is a complete frame, so make the last one of those we saw
672+
// point at it, bypassing any incomplete frames (which may have
673+
// been on the C stack) in between the two. We're overwriting
674+
// last_complete_iframe->previous and need that to be reversible,
675+
// so we store the original previous ptr in the frame object
676+
// (which we must have created on a previous iteration through
677+
// this loop). The frame object has a bunch of storage that is
678+
// only used when its iframe is OWNED_BY_FRAME_OBJECT, which only
679+
// occurs when the frame object outlives the frame's execution,
680+
// which can't have happened yet because the frame is currently
681+
// executing as far as the interpreter is concerned. So, we can
682+
// reuse it for our own purposes.
683+
assert(iframe->owner == FRAME_OWNED_BY_THREAD ||
684+
iframe->owner == FRAME_OWNED_BY_GENERATOR);
685+
if (last_complete_iframe) {
686+
assert(last_complete_iframe->frame_obj);
687+
memcpy(&last_complete_iframe->frame_obj->_f_frame_data[0],
688+
&last_complete_iframe->previous, sizeof(void *));
689+
last_complete_iframe->previous = iframe;
690+
}
691+
last_complete_iframe = iframe;
692+
}
693+
// Frames that are OWNED_BY_FRAME_OBJECT are linked via the
694+
// frame's f_back while all others are linked via the iframe's
695+
// previous ptr. Since all the frames we traverse are running
696+
// as far as the interpreter is concerned, we don't have to
697+
// worry about the OWNED_BY_FRAME_OBJECT case.
698+
iframe = iframe_copy.previous;
699+
}
700+
701+
// Give the outermost complete iframe a null previous pointer to
702+
// account for any potential incomplete/C-stack iframes between it
703+
// and the actual top-of-stack
704+
if (last_complete_iframe) {
705+
assert(last_complete_iframe->frame_obj);
706+
memcpy(&last_complete_iframe->frame_obj->_f_frame_data[0],
707+
&last_complete_iframe->previous, sizeof(void *));
708+
last_complete_iframe->previous = nullptr;
709+
}
710+
}
711+
#endif
609712

610713
}; // namespace greenlet

src/greenlet/TPythonState.cpp

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,8 @@ PythonState::PythonState()
2626
,datastack_limit(nullptr)
2727
#endif
2828
#if GREENLET_PY312
29-
,_prev_frame(nullptr)
29+
,frames_were_exposed(false)
30+
,expose_frames_on_every_suspension(false)
3031
#endif
3132
{
3233
#if GREENLET_USE_CFRAME
@@ -146,12 +147,6 @@ void PythonState::operator<<(const PyThreadState *const tstate) noexcept
146147
Py_XDECREF(frame); // PyThreadState_GetFrame gives us a new
147148
// reference.
148149
this->_top_frame.steal(frame);
149-
#if GREENLET_PY312
150-
if (frame) {
151-
this->_prev_frame = frame->f_frame->previous;
152-
frame->f_frame->previous = nullptr;
153-
}
154-
#endif
155150
#if GREENLET_PY312
156151
this->trash_delete_nesting = tstate->trash.delete_nesting;
157152
#else // not 312
@@ -164,6 +159,25 @@ void PythonState::operator<<(const PyThreadState *const tstate) noexcept
164159
#endif // GREENLET_PY311
165160
}
166161

162+
#if GREENLET_PY312
163+
void GREENLET_NOINLINE(PythonState::unexpose_frames)()
164+
{
165+
if (!this->frames_were_exposed) {
166+
return;
167+
}
168+
// See GreenletState::expose_frames() and the comment on frames_were_exposed
169+
// for more information about this logic.
170+
for (_PyInterpreterFrame *iframe = this->_top_frame->f_frame;
171+
iframe != nullptr; ) {
172+
_PyInterpreterFrame *prev_exposed = iframe->previous;
173+
assert(iframe->frame_obj);
174+
memcpy(&iframe->previous, &iframe->frame_obj->_f_frame_data[0],
175+
sizeof(void *));
176+
iframe = prev_exposed;
177+
}
178+
this->frames_were_exposed = false;
179+
}
180+
#endif
167181

168182
void PythonState::operator>>(PyThreadState *const tstate) noexcept
169183
{
@@ -187,13 +201,9 @@ void PythonState::operator>>(PyThreadState *const tstate) noexcept
187201
#if GREENLET_PY312
188202
tstate->py_recursion_remaining = tstate->py_recursion_limit - this->py_recursion_depth;
189203
tstate->c_recursion_remaining = C_RECURSION_LIMIT - this->c_recursion_depth;
190-
// We're just going to throw this object away anyway, go ahead and
191-
// do it now.
192-
PyFrameObject* frame = this->_top_frame.relinquish_ownership();
193-
if (frame && frame->f_frame) {
194-
frame->f_frame->previous = this->_prev_frame;
204+
if (this->frames_were_exposed) {
205+
this->unexpose_frames();
195206
}
196-
this->_prev_frame = nullptr;
197207
#else // \/ 3.11
198208
tstate->recursion_remaining = tstate->recursion_limit - this->recursion_depth;
199209
#endif // GREENLET_PY312

src/greenlet/TStackState.cpp

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,37 @@ StackState::~StackState()
226226
}
227227
}
228228

229+
void StackState::copy_from_stack(void* vdest, const void* vsrc, size_t n) const
230+
{
231+
char* dest = static_cast<char*>(vdest);
232+
const char* src = static_cast<const char*>(vsrc);
233+
if (src + n <= _stack_start || src >= _stack_start + _stack_saved ||
234+
_stack_saved == 0) {
235+
// Nothing we're copying was spilled from the stack
236+
memcpy(dest, src, n);
237+
return;
238+
}
239+
if (src < _stack_start) {
240+
// Copy the part before the saved stack.
241+
// We know src + n > _stack_start due to the test above.
242+
size_t nbefore = _stack_start - src;
243+
memcpy(dest, src, nbefore);
244+
dest += nbefore;
245+
src += nbefore;
246+
n -= nbefore;
247+
}
248+
// We know src >= _stack_start after the before-copy, and
249+
// src < _stack_start + _stack_saved due to the first if condition
250+
size_t nspilled = std::min<size_t>(n, _stack_start + _stack_saved - src);
251+
memcpy(dest, stack_copy + (src - _stack_start), nspilled);
252+
dest += nspilled;
253+
src += nspilled;
254+
n -= nspilled;
255+
if (n > 0) {
256+
// Copy the part after the saved stack
257+
memcpy(dest, src, n);
258+
}
259+
}
229260

230261
}; // namespace greenlet
231262

src/greenlet/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
###
2626
# Metadata
2727
###
28-
__version__ = '3.0.3.dev0'
28+
__version__ = '3.1.0.dev0'
2929
from ._greenlet import _C_API # pylint:disable=no-name-in-module
3030

3131
###

src/greenlet/greenlet.cpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,10 +758,39 @@ green_setcontext(BorrowedGreenlet self, PyObject* nctx, void* UNUSED(context))
758758
static PyObject*
759759
green_getframe(BorrowedGreenlet self, void* UNUSED(context))
760760
{
761+
#if GREENLET_PY312
762+
self->expose_frames();
763+
#endif
761764
const PythonState::OwnedFrame& top_frame = self->top_frame();
762765
return top_frame.acquire_or_None();
763766
}
764767

768+
static PyObject*
769+
green_getframeexposed(BorrowedGreenlet self, void* UNUSED(context))
770+
{
771+
#if GREENLET_PY312
772+
if (!self->expose_frames_on_every_suspension()) {
773+
Py_RETURN_FALSE;
774+
}
775+
#endif
776+
Py_RETURN_TRUE;
777+
}
778+
779+
static int
780+
green_setframeexposed(BorrowedGreenlet self, PyObject* val, void* UNUSED(context))
781+
{
782+
if (val != Py_True && val != Py_False) {
783+
PyErr_Format(PyExc_TypeError,
784+
"expected a bool, not '%s'",
785+
Py_TYPE(val)->tp_name);
786+
return -1;
787+
}
788+
#if GREENLET_PY312
789+
self->set_expose_frames_on_every_suspension(val == Py_True);
790+
#endif
791+
return 0;
792+
}
793+
765794
static PyObject*
766795
green_getstate(PyGreenlet* self)
767796
{
@@ -979,6 +1008,10 @@ static PyGetSetDef green_getsets[] = {
9791008
{"run", (getter)green_getrun, (setter)green_setrun, /*XXX*/ NULL},
9801009
{"parent", (getter)green_getparent, (setter)green_setparent, /*XXX*/ NULL},
9811010
{"gr_frame", (getter)green_getframe, NULL, /*XXX*/ NULL},
1011+
{"gr_frames_always_exposed",
1012+
(getter)green_getframeexposed,
1013+
(setter)green_setframeexposed,
1014+
/*XXX*/ NULL},
9821015
{"gr_context",
9831016
(getter)green_getcontext,
9841017
(setter)green_setcontext,

0 commit comments

Comments
 (0)