Skip to content

Commit 2acc2c2

Browse files
author
Anselm Kruis
committed
Stackless issue python#220: fix hard switching error handling
Add a test function to provoke switching errors, add test cases and fix the error handling. (cherry picked from commit 8ae5fe5)
1 parent 8ab70e8 commit 2acc2c2

File tree

8 files changed

+157
-18
lines changed

8 files changed

+157
-18
lines changed

Stackless/changelog.txt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ What's New in Stackless 3.X.X?
99

1010
*Release date: 20XX-XX-XX*
1111

12-
- https://github.com/stackless-dev/stackless/issues/219
12+
- https://github.com/stackless-dev/stackless/issues/220
13+
Improve the error handling in case of failed stack transfers / hard tasklet
14+
switches. Call Py_FatalError, if a clean recovery is impossible.
15+
16+
- https://github.com/stackless-dev/stackless/issues/217
1317
C-API documentation update: update the names of watchdog flags to match the
1418
implementation.
1519

Stackless/core/stacklesseval.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -907,7 +907,7 @@ eval_frame_callback(PyFrameObject *f, int exc, PyObject *retval)
907907
SLP_FRAME_EXECFUNC_DECREF(cf);
908908
slp_transfer_return(cst);
909909
/* should never come here */
910-
assert(0);
910+
Py_FatalError("Return from stack spilling failed.");
911911
fatal:
912912
SLP_STORE_NEXT_FRAME(ts, cf->f_back);
913913
return NULL;

Stackless/module/scheduling.c

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,7 @@ kill_wrap_bad_guy(PyTaskletObject *prev, PyTaskletObject *bad_guy)
531531
if (prev->next == NULL)
532532
slp_current_insert(prev);
533533
SLP_STORE_NEXT_FRAME(ts, prev->f.frame);
534+
Py_CLEAR(prev->f.frame);
534535
ts->st.current = prev;
535536
if (newval != NULL) {
536537
/* merge bad guy into exception */
@@ -612,8 +613,11 @@ jump_soft_to_hard(PyFrameObject *f, int exc, PyObject *retval)
612613
Py_DECREF(retval); /* consume ref according to protocol */
613614
SLP_FRAME_EXECFUNC_DECREF(f);
614615
slp_transfer_return(ts->st.current->cstate);
615-
/* we either have an error or don't come back, so: */
616-
assert(0);
616+
/* We either have an error or don't come back, so bail out.
617+
* There is no way to recover, because we don't know the previous
618+
* tasklet.
619+
*/
620+
Py_FatalError("Soft-to-hard tasklet switch failed.");
617621
return NULL;
618622
}
619623

@@ -1013,7 +1017,9 @@ slp_schedule_task(PyObject **result, PyTaskletObject *prev, PyTaskletObject *nex
10131017

10141018
fail = slp_schedule_task_prepared(ts, result, prev, next, stackless, did_switch);
10151019
if (fail && inserted) {
1016-
slp_current_uninsert(next);
1020+
/* in case of an error, it is unknown, if the tasklet is still scheduled */
1021+
if (next->next)
1022+
slp_current_uninsert(next);
10171023
if (u_chan)
10181024
slp_channel_insert(u_chan, next, u_dir, u_next);
10191025
else
@@ -1221,7 +1227,7 @@ slp_schedule_task_prepared(PyThreadState *ts, PyObject **result, PyTaskletObject
12211227
SLP_EXCHANGE_EXCINFO(ts, next);
12221228
SLP_EXCHANGE_EXCINFO(ts, prev);
12231229
PyFrameObject *f = SLP_CLAIM_NEXT_FRAME(ts);
1224-
Py_XDECREF(f);
1230+
Py_XSETREF(next->f.frame, f); /* revert the Py_CLEAR(next->f.frame) */
12251231
kill_wrap_bad_guy(prev, next);
12261232
return -1;
12271233
}
@@ -1445,8 +1451,7 @@ slp_tasklet_end(PyObject *retval)
14451451
Py_DECREF(retval);
14461452
SLP_STORE_NEXT_FRAME(ts, NULL);
14471453
slp_transfer_return(ts->st.initial_stub); /* does not return */
1448-
assert(0);
1449-
return NULL;
1454+
Py_FatalError("Failed to restore the initial C-stack.");
14501455
}
14511456
}
14521457

Stackless/module/stacklessmodule.c

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -589,7 +589,9 @@ PyStackless_RunWatchdogEx(long timeout, int flags)
589589
/* we failed to switch */
590590
PyTaskletObject *undo = pop_watchdog(ts);
591591
assert(undo == old_current);
592-
slp_current_unremove(undo);
592+
/* In case of an error, we don't know the state */
593+
if (undo->next == NULL)
594+
slp_current_unremove(undo);
593595
return NULL;
594596
}
595597

@@ -1004,6 +1006,7 @@ PyDoc_STRVAR(test_cframe_nr__doc__,
10041006
"test_cframe_nr(switches) -- a builtin testing function that does nothing\n\
10051007
but soft tasklet switching. The function will call PyStackless_Schedule_nr() for switches\n\
10061008
times and then finish.\n\
1009+
All remaining arguments are intentionally undocumented. Don't use them!\n\
10071010
Usage: Cf. test_cframe().");
10081011

10091012
static
@@ -1036,14 +1039,23 @@ static
10361039
PyObject *
10371040
test_cframe_nr(PyObject *self, PyObject *args, PyObject *kwds)
10381041
{
1039-
static char *argnames[] = {"switches", NULL};
1042+
static char *argnames[] = {"switches", "cstate_add_addr", NULL};
10401043
PyThreadState *ts = PyThreadState_GET();
10411044
PyCFrameObject *cf;
10421045
long switches;
1046+
PyObject *cstack = NULL;
10431047

1044-
if (!PyArg_ParseTupleAndKeywords(args, kwds, "l:test_cframe_nr",
1045-
argnames, &switches))
1048+
if (!PyArg_ParseTupleAndKeywords(args, kwds, "l|O!:test_cframe_nr",
1049+
argnames, &switches, &PyCStack_Type, &cstack))
10461050
return NULL;
1051+
if (cstack) {
1052+
/* Manipulate the startaddr of a cstack to make it (in)valid.
1053+
* This can be used to provoke switching errors. Without such a functionality,
1054+
* it is not possible to test the error handling code for switching errors.
1055+
*/
1056+
((PyCStackObject*)cstack)->startaddr += switches;
1057+
Py_RETURN_NONE;
1058+
}
10471059
cf = slp_cframe_new(test_cframe_nr_loop, 1);
10481060
if (cf == NULL)
10491061
return NULL;

Stackless/module/taskletobject.c

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,10 @@ slp_current_remove_tasklet(PyTaskletObject *task)
110110
PyThreadState *ts = task->cstate->tstate;
111111
PyTaskletObject **chain = &ts->st.current, *ret, *hold;
112112

113+
/* Make sure the tasklet is scheduled.
114+
*/
115+
assert(task->next != NULL);
116+
assert(task->prev != NULL);
113117
assert(ts != NULL);
114118
--ts->st.runcount;
115119
assert(ts->st.runcount >= 0);
@@ -1039,11 +1043,13 @@ impl_tasklet_run_remove(PyTaskletObject *task, int remove)
10391043
if (removed) {
10401044
assert(ts->st.del_post_switch == (PyObject *)prev);
10411045
ts->st.del_post_switch = NULL;
1042-
slp_current_unremove(prev);
1046+
if (prev->next == NULL) /* in case of an error, the state is unknown */
1047+
slp_current_unremove(prev);
10431048
}
10441049
if (inserted) {
1045-
/* we must undo the insertion that we did */
1046-
slp_current_uninsert(task);
1050+
/* we must undo the insertion that we did, but we don't know the state */
1051+
if (task->next != NULL)
1052+
slp_current_uninsert(task);
10471053
Py_DECREF(task);
10481054
}
10491055
} else if (!switched) {

Stackless/pickling/safe_pickle.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ pickle_callback(PyFrameObject *f, int exc, PyObject *retval)
4747
SLP_FRAME_EXECFUNC_DECREF(f);
4848
slp_transfer_return(cst);
4949
/* never come here */
50-
assert(0);
50+
Py_FatalError("Return from pickle stack spilling failed.");
5151
return NULL;
5252
}
5353

Stackless/unittests/support.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
try:
4747
import threading
4848
withThreads = True
49-
except:
49+
except Exception:
5050
withThreads = False
5151

5252

@@ -735,6 +735,7 @@ def _addSkip(self, result, reason):
735735
self._ran_AsTaskletTestCase_setUp = True
736736
super(StacklessTestCase, self)._addSkip(result, reason)
737737

738+
738739
_tc = StacklessTestCase(methodName='run')
739740
try:
740741
_tc.setUp()
@@ -780,7 +781,7 @@ def run(self, result=None):
780781
def helper():
781782
try:
782783
c.send(super(AsTaskletTestCase, self).run(result))
783-
except:
784+
except: # @IgnorePep8
784785
c.send_throw(*sys.exc_info())
785786
stackless.tasklet(helper)()
786787
result = c.receive()
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
from __future__ import absolute_import, division, print_function
2+
3+
import unittest
4+
import sys
5+
import stackless
6+
import contextlib
7+
8+
from support import test_main # @UnusedImport
9+
from support import StacklessTestCase, testcase_leaks_references
10+
11+
12+
class TestStackTransferFailures(StacklessTestCase):
13+
"""Test the error handling of failures in slp_transfer()
14+
15+
In theory, slp_transfer can't fail, but in reality there bugs. Unfortunately
16+
it is usually not possible to recover from slp_transfer failures. But we try to avoid
17+
undefined behavior.
18+
19+
The test coverage is incomplete, patches are welcome. Especially failed soft-to-hard switches
20+
result in Py_FatalError("Soft-to-hard tasklet switch failed.").
21+
"""
22+
def setUp(self):
23+
StacklessTestCase.setUp(self)
24+
25+
@contextlib.contextmanager
26+
def corrupted_cstack_startaddr(self, tasklet):
27+
"""Increment the cstack startaddr of this tasklet.
28+
29+
Hard-switching to this C-stack will fail. This context is used
30+
by error handling tests
31+
"""
32+
self.assertTrue(tasklet.alive)
33+
self.assertEqual(tasklet.thread_id, stackless.current.thread_id)
34+
self.assertFalse(tasklet.restorable)
35+
self.assertFalse(tasklet.is_current)
36+
cstate = tasklet.cstate
37+
self.assertNotEqual(cstate.startaddr, 0) # not invalidated
38+
self.assertIs(cstate.task, tasklet)
39+
test_cframe_nr = stackless.test_cframe_nr
40+
# Otherwise undocumented and only for this test.
41+
# Add 1 to the startaddr of cstate
42+
test_cframe_nr(1, cstate_add_addr=cstate)
43+
try:
44+
yield
45+
finally:
46+
# Restore the pprevious value
47+
test_cframe_nr(-1, cstate_add_addr=cstate)
48+
49+
def __task(self):
50+
stackless.schedule_remove(None)
51+
self.tasklet_done = True
52+
53+
def prepare_tasklet(self, task=None):
54+
if task is None:
55+
task = self.__task
56+
57+
self.tasklet_done = False
58+
t = stackless.tasklet(self.__task)()
59+
sw = stackless.enable_softswitch(False)
60+
try:
61+
t.run()
62+
finally:
63+
stackless.enable_softswitch(sw)
64+
return t
65+
66+
def end(self, t):
67+
# if t.alive:
68+
t.run()
69+
self.assertTrue(self.tasklet_done)
70+
71+
@testcase_leaks_references("unknown")
72+
def test_scheduler(self):
73+
t = self.prepare_tasklet()
74+
with self.corrupted_cstack_startaddr(t):
75+
t.insert()
76+
self.assertRaisesRegex(SystemError, 'bad stack reference in transfer', stackless.run)
77+
self.end(t)
78+
test_scheduler.enable_softswitch = False # soft switching causes soft-to-hard
79+
80+
@testcase_leaks_references("unknown")
81+
def test_run(self):
82+
t = self.prepare_tasklet()
83+
with self.corrupted_cstack_startaddr(t):
84+
self.assertRaisesRegex(SystemError, 'bad stack reference in transfer', t.run)
85+
self.end(t)
86+
test_run.enable_softswitch = False # soft switching causes soft-to-hard
87+
88+
@testcase_leaks_references("unknown")
89+
def test_switch(self):
90+
t = self.prepare_tasklet()
91+
with self.corrupted_cstack_startaddr(t):
92+
self.assertRaisesRegex(SystemError, 'bad stack reference in transfer', t.switch)
93+
self.end(t)
94+
test_switch.enable_softswitch = False # soft switching causes soft-to-hard
95+
96+
@testcase_leaks_references("unknown")
97+
def test_kill(self):
98+
t = self.prepare_tasklet()
99+
with self.corrupted_cstack_startaddr(t):
100+
self.assertRaisesRegex(SystemError, 'bad stack reference in transfer', t.kill)
101+
self.end(t)
102+
test_kill.enable_softswitch = False # soft switching causes soft-to-hard
103+
104+
105+
#///////////////////////////////////////////////////////////////////////////////
106+
107+
if __name__ == '__main__':
108+
if not sys.argv[1:]:
109+
sys.argv.append('-v')
110+
111+
unittest.main()

0 commit comments

Comments
 (0)