diff --git a/src/greenlet/tests/_test_extension_cpp.cpp b/src/greenlet/tests/_test_extension_cpp.cpp index 72e3d812..628e2255 100644 --- a/src/greenlet/tests/_test_extension_cpp.cpp +++ b/src/greenlet/tests/_test_extension_cpp.cpp @@ -66,12 +66,76 @@ test_exception_switch(PyObject* self, PyObject* args) return p_test_exception_switch_recurse(depth, depth); } + +static PyObject* +py_test_exception_throw(PyObject* self, PyObject* args) +{ + if (!PyArg_ParseTuple(args, "")) + return NULL; + p_test_exception_throw(0); + PyErr_SetString(PyExc_AssertionError, "unreachable code running after throw"); + return NULL; +} + + +/* test_exception_switch_and_do_in_g2(g2func) + * - creates new greenlet g2 to run g2func + * - switches to g2 inside try/catch block + * - verifies that no exception has been caught + * + * it is used together with test_exception_throw to verify that unhandled + * exceptions thrown in one greenlet do not propagate to other greenlet nor + * segfault the process. + */ +static PyObject* +test_exception_switch_and_do_in_g2(PyObject* self, PyObject* args) +{ + PyObject* g2func = NULL; + PyObject* result = NULL; + + if (!PyArg_ParseTuple(args, "O", &g2func)) + return NULL; + PyGreenlet* g2 = PyGreenlet_New(g2func, NULL); + if (!g2) { + return NULL; + } + + try { + result = PyGreenlet_Switch(g2, NULL, NULL); + if (!result) { + return NULL; + } + } + catch (...) { + /* if we are here the memory can be already corrupted and the program + * might crash before below py-level exception might become printed. + * -> print something to stderr to make it clear that we had entered + * this catch block. + */ + fprintf(stderr, "C++ exception unexpectedly caught in g1\n"); + PyErr_SetString(PyExc_AssertionError, "C++ exception unexpectedly caught in g1"); + } + + Py_XDECREF(result); + Py_RETURN_NONE; +} + static PyMethodDef test_methods[] = { {"test_exception_switch", (PyCFunction)&test_exception_switch, METH_VARARGS, "Switches to parent twice, to test exception handling and greenlet " "switching."}, + {"test_exception_switch_and_do_in_g2", + (PyCFunction)&test_exception_switch_and_do_in_g2, + METH_VARARGS, + "Creates new greenlet g2 to run g2func and switches to it inside try/catch " + "block. Used together with test_exception_throw to verify that unhandled " + "C++ exceptions thrown in a greenlet doe not corrupt memory."}, + {"test_exception_throw", + (PyCFunction)&py_test_exception_throw, + METH_VARARGS, + "Throws C++ exception. Calling this function directly should abort the process."}, {NULL, NULL, 0, NULL}}; #if PY_MAJOR_VERSION >= 3 diff --git a/src/greenlet/tests/test_cpp.py b/src/greenlet/tests/test_cpp.py index 741ea105..dc6fcf4b 100644 --- a/src/greenlet/tests/test_cpp.py +++ b/src/greenlet/tests/test_cpp.py @@ -2,6 +2,8 @@ from __future__ import absolute_import import unittest +import os +import signal import greenlet from . import _test_extension_cpp @@ -16,3 +18,45 @@ def test_exception_switch(self): greenlets.append(g) for i, g in enumerate(greenlets): self.assertEqual(g.switch(), i) + + @unittest.skipIf(not hasattr(os, 'fork'), + "test should abort when run -> need to run it in separate process") + def test_exception_switch_and_throw(self): + # procrun runs f in separate process + def procrun(f): # -> (ret, sig, coredumped) + pid = os.fork() + + # child + if pid == 0: + f() + os.exit(0) + + # parent + _, st = os.waitpid(pid, 0) + ret = st >> 8 + sig = st & 0x7f + coredumped = ((st & 0x80) != 0) + return (ret, sig, coredumped) + + + # verify that plain unhandled throw aborts + # (unhandled throw -> std::terminate -> abort) + ret, sig, _ = procrun(_test_extension_cpp.test_exception_throw) + if not (ret == 0 and sig == signal.SIGABRT): + self.fail("unhandled throw -> ret=%d sig=%d ; expected 0/SIGABRT" % (ret, sig)) + + + # verify that unhandled throw called in greenlet aborts too + # (does not segfaults nor is handled by try/catch on preceeding greenlet C stack) + def _(): + def _(): + _test_extension_cpp.test_exception_switch_and_do_in_g2( + _test_extension_cpp.test_exception_throw + ) + g1 = greenlet.greenlet(_) + g1.switch() + + ret, sig, core = procrun(_) + if not (ret == 0 and sig == signal.SIGABRT): + self.fail("failed with ret=%d sig=%d%s" % + (ret, sig, " (core dumped)" if core else ""))