Skip to content
This repository was archived by the owner on Feb 13, 2025. It is now read-only.

Commit a05b5e5

Browse files
Anselm KruisAnselm Kruis
Anselm Kruis
authored and
Anselm Kruis
committed
Stackless issue #168: make Stackless compatible with old Cython modules (#170)
Many extension modules were created by Cython versions before commit 037bcf0 and were compiled with regular C-Python. These modules call PyEval_EvalFrameEx() with a broken frame object. This commit add code to recover a broken frame in PyEval_EvalFrameEx(). (cherry picked from commit f9094d2)
1 parent 070e7f0 commit a05b5e5

File tree

5 files changed

+127
-10
lines changed

5 files changed

+127
-10
lines changed

Objects/frameobject.c

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,13 @@ frame_dealloc(PyFrameObject *f)
442442

443443
PyObject_GC_UnTrack(f);
444444
Py_TRASHCAN_SAFE_BEGIN(f)
445+
446+
#if defined(STACKLESS) && PY_VERSION_HEX < 0x03080000
447+
/* Clear the magic for the old Cython frame hack.
448+
* See below in PyFrame_New() for a detailed explanation.
449+
*/
450+
f->f_blockstack[0].b_type = 0;
451+
#endif
445452
/* Kill all local variables */
446453
valuestack = f->f_valuestack;
447454
for (p = f->f_localsplus; p < valuestack; p++)
@@ -754,6 +761,25 @@ PyFrame_New(PyThreadState *tstate, PyCodeObject *code, PyObject *globals,
754761

755762
#ifdef STACKLESS
756763
f->f_execute = NULL;
764+
#if PY_VERSION_HEX < 0x03080000
765+
if (code->co_argcount > 0) {
766+
/*
767+
* A hack for binary compatibility with Cython extension modules, which
768+
* were created with an older Cythoncompiled with regular C-Python. These
769+
* modules create frames using PyFrame_New then write to frame->f_localsplus
770+
* to set the arguments. But C-Python f_localsplus is Stackless f_code.
771+
* Therefore we add a copy of f_code and a magic number in the
772+
* uninitialized f_blockstack array.
773+
* If the blockstack is used, the magic is overwritten.
774+
* To make sure, the pointer is aligned correctly, we address it relative to
775+
* f_code.
776+
* See Stackless issue #168
777+
*/
778+
(&(f->f_code))[-1] = code;
779+
/* an arbitrary negative number which is not an opcode */
780+
f->f_blockstack[0].b_type = -31683;
781+
}
782+
#endif
757783
#endif
758784
_PyObject_GC_TRACK(f);
759785
return f;

Python/ceval.c

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4160,16 +4160,51 @@ _PyEval_EvalFrameDefault(PyFrameObject *f, int throwflag)
41604160
if (f == NULL)
41614161
return NULL;
41624162

4163+
/* The layout of PyFrameObject differs between Stackless and C-Python.
4164+
* Stackless f->f_execute is C-Python f->f_code. Stackless f->f_code is at
4165+
* the end, just before f_localsplus.
4166+
*/
41634167
if (PyFrame_Check(f) && f->f_execute == NULL) {
41644168
/* A new frame returned from PyFrame_New() has f->f_execute == NULL.
4169+
* Set the usual execution function.
41654170
*/
41664171
f->f_execute = PyEval_EvalFrameEx_slp;
4172+
4173+
#if PY_VERSION_HEX < 0x03080000
4174+
/* Older versions of Cython used to create frames using C-Python layout
4175+
* of PyFrameObject. As a consequence f_code is overwritten by the first
4176+
* item of f_localsplus[]. To be able to fix it, we have a copy of
4177+
* f_code and a signature at the end of the block-stack.
4178+
* The Py_BUILD_ASSERT_EXPR checks,that our assumptions about the layout
4179+
* of PyFrameObject are true.
4180+
* See Stackless issue #168
4181+
*/
4182+
(void) Py_BUILD_ASSERT_EXPR(offsetof(PyFrameObject, f_code) ==
4183+
offsetof(PyFrameObject, f_localsplus) - Py_MEMBER_SIZE(PyFrameObject, f_localsplus[0]));
4184+
4185+
/* Check for an old Cython frame */
4186+
if (f->f_iblock == 0 && f->f_lasti == -1 && /* blockstack is empty */
4187+
f->f_blockstack[0].b_type == -31683 && /* magic is present */
4188+
/* and f_code has been overwritten */
4189+
f->f_code != (&(f->f_code))[-1] &&
4190+
/* and (&(f->f_code))[-1] looks like a valid code object */
4191+
(&(f->f_code))[-1] && PyCode_Check((&(f->f_code))[-1]) &&
4192+
/* and there are arguments */
4193+
(&(f->f_code))[-1]->co_argcount > 0 &&
4194+
/* the last argument is NULL */
4195+
f->f_localsplus[(&(f->f_code))[-1]->co_argcount - 1] == NULL)
4196+
{
4197+
PyCodeObject * code = (&(f->f_code))[-1];
4198+
memmove(f->f_localsplus, f->f_localsplus-1, code->co_argcount * sizeof(f->f_localsplus[0]));
4199+
f->f_code = code;
4200+
} else
4201+
#endif
4202+
if (!(f->f_code != NULL && PyCode_Check(f->f_code))) {
4203+
PyErr_BadInternalCall();
4204+
return NULL;
4205+
}
41674206
} else {
4168-
/* The layout of PyFrameObject differs between Stackless and C-Python.
4169-
* Stackless f->f_execute is C-Python f->f_code. Stackless f->f_code is at
4170-
* the end, just before f_localsplus.
4171-
*
4172-
* In order to detect a C-Python frame, we must compare f->f_execute
4207+
/* In order to detect a broken C-Python frame, we must compare f->f_execute
41734208
* with every valid frame function. Hard to implement completely.
41744209
* Therefore I'll check only for relevant functions.
41754210
* Amend the list as needed.

Stackless/changelog.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,10 @@ What's New in Stackless 3.X.X?
1313
Fix C-API functions PyEval_EvalFrameEx() and PyEval_EvalFrame().
1414
They are now compatible with C-Python.
1515

16+
- https://github.com/stackless-dev/stackless/issues/168
17+
Make Stackless compatible with old Cython extension modules compiled
18+
for regular C-Python.
19+
1620
- https://github.com/stackless-dev/stackless/issues/167
1721
Replace 'printf(...)' calls by PySys_WriteStderr(...). They are used to emit
1822
an error message, if there is a pending error while entering Stackless

Stackless/module/stacklessmodule.c

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1134,20 +1134,22 @@ by Stackless Python.\n\
11341134
The function creates a frame from code, globals and args and executes the frame.");
11351135

11361136
static PyObject* test_PyEval_EvalFrameEx(PyObject *self, PyObject *args, PyObject *kwds) {
1137-
static char *kwlist[] = {"code", "globals", "args", "alloca", "throw", NULL};
1137+
static char *kwlist[] = {"code", "globals", "args", "alloca", "throw", "oldcython",
1138+
"code2", NULL};
11381139
PyThreadState *tstate = PyThreadState_GET();
1139-
PyCodeObject *co;
1140+
PyCodeObject *co, *code2 = NULL;
11401141
PyObject *globals, *co_args = NULL;
11411142
Py_ssize_t alloca_size = 0;
11421143
PyObject *exc = NULL;
1144+
PyObject *oldcython = NULL;
11431145
PyFrameObject *f;
11441146
PyObject *result = NULL;
11451147
void *p;
11461148
Py_ssize_t na;
11471149

1148-
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!O!|O!nO:test_PyEval_EvalFrameEx", kwlist,
1150+
if (!PyArg_ParseTupleAndKeywords(args, kwds, "O!O!|O!nOO!O!:test_PyEval_EvalFrameEx", kwlist,
11491151
&PyCode_Type, &co, &PyDict_Type, &globals, &PyTuple_Type, &co_args, &alloca_size,
1150-
&exc))
1152+
&exc, &PyBool_Type, &oldcython, &PyCode_Type, &code2))
11511153
return NULL;
11521154
if (exc && !PyExceptionInstance_Check(exc)) {
11531155
PyErr_SetString(PyExc_TypeError, "exc must be an exception instance");
@@ -1178,6 +1180,13 @@ static PyObject* test_PyEval_EvalFrameEx(PyObject *self, PyObject *args, PyObjec
11781180
goto exit;
11791181
}
11801182
fastlocals = f->f_localsplus;
1183+
if (oldcython == Py_True) {
1184+
/* Use the f_localsplus offset from regular C-Python. Old versions of cython used to
1185+
* access f_localplus directly. Current versions compute the field offset for
1186+
* f_localsplus at run-time.
1187+
*/
1188+
fastlocals--;
1189+
}
11811190
for (i = 0; i < na; i++) {
11821191
PyObject *arg = PyTuple_GetItem(co_args, i);
11831192
if (arg == NULL) {
@@ -1193,6 +1202,10 @@ static PyObject* test_PyEval_EvalFrameEx(PyObject *self, PyObject *args, PyObjec
11931202
if (exc) {
11941203
PyErr_SetObject(PyExceptionInstance_Class(exc), exc);
11951204
}
1205+
if (code2) {
1206+
Py_INCREF(code2);
1207+
Py_SETREF(f->f_code, code2);
1208+
}
11961209
result = PyEval_EvalFrameEx(f, exc != NULL);
11971210
/* result = Py_None; Py_INCREF(Py_None); */
11981211
exit:

Stackless/unittests/test_capi.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
import unittest
3232

3333
from support import test_main # @UnusedImport
34-
from support import StacklessTestCase, withThreads, require_one_thread
34+
from support import (StacklessTestCase, withThreads, require_one_thread,
35+
testcase_leaks_references)
3536

3637
if withThreads:
3738
try:
@@ -161,6 +162,44 @@ def f():
161162
self.assertIn(str(exc), msg)
162163
#print(msg)
163164

165+
def test_oldcython_frame(self):
166+
# A test for Stackless issue #168
167+
self.assertEqual(self.call_PyEval_EvalFrameEx(47110816, oldcython=True), 47110816)
168+
169+
def test_oldcython_frame_code_is_1st_arg_good(self):
170+
# A pathological test for Stackless issue #168
171+
def f(code):
172+
return code
173+
174+
def f2(code):
175+
return code
176+
177+
self.assertIs(stackless.test_PyEval_EvalFrameEx(f.__code__, f.__globals__, (f.__code__,), oldcython=False), f.__code__)
178+
self.assertIs(stackless.test_PyEval_EvalFrameEx(f.__code__, f.__globals__, (f2.__code__,), oldcython=True), f2.__code__)
179+
180+
@testcase_leaks_references("f->f_code get overwritten without Py_DECREF")
181+
def test_oldcython_frame_code_is_1st_arg_bad(self):
182+
# A pathological test for Stackless issue #168
183+
def f(code):
184+
return code
185+
186+
# we can't fix this particular case:
187+
# - running code object is its 1st arg and
188+
# - oldcython=True,
189+
# because a fix would result in a segmentation fault, if the number of
190+
# arguments is to low (test case test_0_args)
191+
self.assertRaises(UnboundLocalError, stackless.test_PyEval_EvalFrameEx, f.__code__, f.__globals__, (f.__code__,), oldcython=True)
192+
193+
def test_other_code_object(self):
194+
# A pathological test for Stackless issue #168
195+
def f(arg):
196+
return arg
197+
198+
def f2(arg):
199+
return arg
200+
201+
self.assertIs(stackless.test_PyEval_EvalFrameEx(f.__code__, f.__globals__, (f2,), code2=f2.__code__), f2)
202+
164203

165204
if __name__ == "__main__":
166205
if not sys.argv[1:]:

0 commit comments

Comments
 (0)