Repository: cython/cython

Issue:
Python exceptions can't propagate from an "except +" function
I couldn't find an existing issue for this; hopefully I didn't miss one.

Given this code in `test.pyx`

```python
# distutils: language = c++

cdef func() except +:
    raise NotImplementedError()

def call_func():
    func()
```

the interpreter will segfault if you call `test.call_func()`.  This appears to be caused by Cython [disabling all other exception handling](https://github.com/cython/cython/blob/cd594eb1d70148906d4ec9facc69d47cf5241ed5/Cython/Compiler/ExprNodes.py#L5860-L5863) if `except +` is specified, including the check for whether `NULL` was returned after a Python exception was set.

My expectation was that I could use the `except +` to handle truly unexpected things like `std::bad_alloc`, while explicitly raising Python exceptions for less exceptional exceptions.


Discussion:
Thanks for the bug report, I'll try to fix this by the next release.

On Wed, Sep 12, 2018 at 6:55 PM Matt Wozniski <notifications@github.com>
wrote:

> I couldn't find an existing issue for this; hopefully I didn't miss one.
>
> Given this code in test.pyx
>
> # distutils: language = c++
>
> cdef func() except +:
>     raise NotImplementedError()
> def call_func():
>     func()
>
> the interpreter will segfault if you call test.call_func(). This appears
> to be caused by Cython disabling all other exception handling
> <https://github.com/cython/cython/blob/cd594eb1d70148906d4ec9facc69d47cf5241ed5/Cython/Compiler/ExprNodes.py#L5860-L5863>
> if except + is specified, including the check for whether NULL was
> returned after a Python exception was set.
>
> My expectation was that I could use the except + to handle truly
> unexpected things like std::bad_alloc, while explicitly raising Python
> exceptions for less exceptional exceptions.
>
> —
> You are receiving this because you are subscribed to this thread.
> Reply to this email directly, view it on GitHub
> <https://github.com/cython/cython/issues/2603>, or mute the thread
> <https://github.com/notifications/unsubscribe-auth/AAdqgdO2LlNPVzKuvQ9m9XzomJTN2lRHks5uaTyQgaJpZM4Wlylb>
> .
>


Resolving patch:
diff --git a/Cython/Compiler/ExprNodes.py b/Cython/Compiler/ExprNodes.py
--- a/Cython/Compiler/ExprNodes.py
+++ b/Cython/Compiler/ExprNodes.py
@@ -5838,7 +5838,7 @@ def generate_result_code(self, code):
             elif self.type.is_memoryviewslice:
                 assert self.is_temp
                 exc_checks.append(self.type.error_condition(self.result()))
-            else:
+            elif func_type.exception_check != '+':
                 exc_val = func_type.exception_value
                 exc_check = func_type.exception_check
                 if exc_val is not None:
@@ -5859,20 +5859,22 @@ def generate_result_code(self, code):
                         rhs = typecast(py_object_type, self.type, rhs)
                 else:
                     lhs = ""
+                if (self.overflowcheck
+                    and self.type.is_int
+                    and self.type.signed
+                    and self.function.result() in ('abs', 'labs', '__Pyx_abs_longlong')):
+                    goto_error = 'if (unlikely(%s < 0)) { PyErr_SetString(PyExc_OverflowError, "value too large"); %s; }' % (
+                        self.result(), code.error_goto(self.pos))
+                elif exc_checks:
+                    goto_error = code.error_goto_if(" && ".join(exc_checks), self.pos)
+                else:
+                    goto_error = ""
                 if func_type.exception_check == '+':
                     translate_cpp_exception(code, self.pos, '%s%s;' % (lhs, rhs),
                                             func_type.exception_value, self.nogil)
+                    if goto_error:
+                        code.putln(goto_error)
                 else:
-                    if (self.overflowcheck
-                        and self.type.is_int
-                        and self.type.signed
-                        and self.function.result() in ('abs', 'labs', '__Pyx_abs_longlong')):
-                        goto_error = 'if (unlikely(%s < 0)) { PyErr_SetString(PyExc_OverflowError, "value too large"); %s; }' % (
-                            self.result(), code.error_goto(self.pos))
-                    elif exc_checks:
-                        goto_error = code.error_goto_if(" && ".join(exc_checks), self.pos)
-                    else:
-                        goto_error = ""
                     code.putln("%s%s; %s" % (lhs, rhs, goto_error))
                 if self.type.is_pyobject and self.result():
                     code.put_gotref(self.py_result())


Test patch:
diff --git a/tests/run/cpp_exceptions.pyx b/tests/run/cpp_exceptions.pyx
--- a/tests/run/cpp_exceptions.pyx
+++ b/tests/run/cpp_exceptions.pyx
@@ -21,6 +21,8 @@ cdef extern from "cpp_exceptions_helper.h":
     cdef void raise_typeerror() except +
     cdef void raise_underflow() except +
 
+    cdef raise_or_throw(bint py) except +
+
     cdef cppclass Foo:
         int bar_raw "bar"(bint fire) except +
         int bar_value "bar"(bint fire) except +ValueError
@@ -98,6 +100,19 @@ def test_underflow():
     """
     raise_underflow()
 
+def test_func_that_can_raise_or_throw(bint py):
+    """
+    >>> test_func_that_can_raise_or_throw(0)
+    Traceback (most recent call last):
+    ...
+    RuntimeError: oopsie
+    >>> test_func_that_can_raise_or_throw(1)
+    Traceback (most recent call last):
+    ...
+    ValueError: oopsie
+    """
+    raise_or_throw(py)
+
 def test_int_raw(bint fire):
     """
     >>> test_int_raw(False)
diff --git a/tests/run/cpp_exceptions_helper.h b/tests/run/cpp_exceptions_helper.h
--- a/tests/run/cpp_exceptions_helper.h
+++ b/tests/run/cpp_exceptions_helper.h
@@ -1,3 +1,4 @@
+#include <Python.h>
 #include <ios>
 #include <new>
 #include <stdexcept>
@@ -61,3 +62,11 @@ void raise_typeerror() {
 void raise_underflow() {
     throw std::underflow_error("underflow_error");
 }
+
+PyObject *raise_or_throw(int py) {
+    if (!py) {
+        throw std::runtime_error("oopsie");
+    }
+    PyErr_SetString(PyExc_ValueError, "oopsie");
+    return NULL;
+}