Skip to content

Commit c8fb8fc

Browse files
authored
Merge pull request #797 from davidhewitt/catch-unwind
Add catch_unwind! macro to prevent panics crossing ffi boundaries
2 parents c4f3653 + 9380bfd commit c8fb8fc

File tree

5 files changed

+74
-8
lines changed

5 files changed

+74
-8
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
99

1010
### Changed
1111

12+
* Panics from Rust will now be caught and raised as Python errors. [#797](https://github.com/PyO3/pyo3/pull/797)
1213
* `PyObject` and `Py<T>` reference counts are now decremented sooner after `drop()`. [#851](https://github.com/PyO3/pyo3/pull/851)
1314
* When the GIL is held, the refcount is now decreased immediately on drop. (Previously would wait until just before releasing the GIL.)
1415
* When the GIL is not held, the refcount is now decreased when the GIL is next acquired. (Previously would wait until next time the GIL was released.)

src/callback.rs

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ where
157157
/// It sets up the GILPool and converts the output into a Python object. It also restores
158158
/// any python error returned as an Err variant from the body.
159159
///
160+
/// Finally, any panics inside the callback body will be caught and translated into PanicExceptions.
161+
///
160162
/// # Safety
161163
/// This macro assumes the GIL is held. (It makes use of unsafe code, so usage of it is only
162164
/// possible inside unsafe blocks.)
@@ -204,11 +206,27 @@ macro_rules! callback_body {
204206
macro_rules! callback_body_without_convert {
205207
($py:ident, $body:expr) => {{
206208
let pool = $crate::GILPool::new();
209+
let unwind_safe_py = std::panic::AssertUnwindSafe(pool.python());
210+
let result = match std::panic::catch_unwind(move || -> $crate::PyResult<_> {
211+
let $py = *unwind_safe_py;
212+
$body
213+
}) {
214+
Ok(result) => result,
215+
Err(e) => {
216+
// Try to format the error in the same way panic does
217+
if let Some(string) = e.downcast_ref::<String>() {
218+
Err($crate::panic::PanicException::py_err((string.clone(),)))
219+
} else if let Some(s) = e.downcast_ref::<&str>() {
220+
Err($crate::panic::PanicException::py_err((s.to_string(),)))
221+
} else {
222+
Err($crate::panic::PanicException::py_err((
223+
"panic from Rust code",
224+
)))
225+
}
226+
}
227+
};
207228

208-
let $py = pool.python();
209-
let callback = move || -> $crate::PyResult<_> { $body };
210-
211-
callback().unwrap_or_else(|e| {
229+
result.unwrap_or_else(|e| {
212230
e.restore(pool.python());
213231
$crate::callback::callback_error()
214232
})

src/err.rs

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
// Copyright (c) 2017-present PyO3 Project and Contributors
22

3+
use crate::panic::PanicException;
34
use crate::type_object::PyTypeObject;
45
use crate::types::PyType;
56
use crate::{exceptions, ffi};
67
use crate::{
7-
AsPyPointer, FromPy, IntoPy, IntoPyPointer, Py, PyAny, PyObject, Python, ToBorrowedObject,
8-
ToPyObject,
8+
AsPyPointer, FromPy, FromPyPointer, IntoPy, IntoPyPointer, ObjectProtocol, Py, PyAny, PyObject,
9+
Python, ToBorrowedObject, ToPyObject,
910
};
1011
use libc::c_int;
1112
use std::ffi::CString;
@@ -168,13 +169,33 @@ impl PyErr {
168169
///
169170
/// The error is cleared from the Python interpreter.
170171
/// If no error is set, returns a `SystemError`.
171-
pub fn fetch(_: Python) -> PyErr {
172+
///
173+
/// If the error fetched is a `PanicException` (which would have originated from a panic in a
174+
/// pyo3 callback) then this function will resume the panic.
175+
pub fn fetch(py: Python) -> PyErr {
172176
unsafe {
173177
let mut ptype: *mut ffi::PyObject = std::ptr::null_mut();
174178
let mut pvalue: *mut ffi::PyObject = std::ptr::null_mut();
175179
let mut ptraceback: *mut ffi::PyObject = std::ptr::null_mut();
176180
ffi::PyErr_Fetch(&mut ptype, &mut pvalue, &mut ptraceback);
177-
PyErr::new_from_ffi_tuple(ptype, pvalue, ptraceback)
181+
182+
let err = PyErr::new_from_ffi_tuple(ptype, pvalue, ptraceback);
183+
184+
if ptype == PanicException::type_object().as_ptr() {
185+
let msg: String = PyAny::from_borrowed_ptr_or_opt(py, pvalue)
186+
.and_then(|obj| obj.extract().ok())
187+
.unwrap_or_else(|| String::from("Unwrapped panic from Python code"));
188+
189+
eprintln!(
190+
"--- PyO3 is resuming a panic after fetching a PanicException from Python. ---"
191+
);
192+
eprintln!("Python stack trace below:");
193+
err.print(py);
194+
195+
std::panic::resume_unwind(Box::new(msg))
196+
}
197+
198+
err
178199
}
179200
}
180201

@@ -564,6 +585,7 @@ pub fn error_on_minusone(py: Python, result: c_int) -> PyResult<()> {
564585
#[cfg(test)]
565586
mod tests {
566587
use crate::exceptions;
588+
use crate::panic::PanicException;
567589
use crate::{PyErr, Python};
568590

569591
#[test]
@@ -575,4 +597,16 @@ mod tests {
575597
assert!(PyErr::occurred(py));
576598
drop(PyErr::fetch(py));
577599
}
600+
601+
#[test]
602+
fn fetching_panic_exception_panics() {
603+
let gil = Python::acquire_gil();
604+
let py = gil.python();
605+
let err: PyErr = PanicException::py_err("new panic");
606+
err.restore(py);
607+
assert!(PyErr::occurred(py));
608+
let started_unwind =
609+
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| PyErr::fetch(py))).is_err();
610+
assert!(started_unwind);
611+
}
578612
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ mod internal_tricks;
188188
pub mod marshal;
189189
mod object;
190190
mod objectprotocol;
191+
pub mod panic;
191192
pub mod prelude;
192193
pub mod pycell;
193194
pub mod pyclass;

src/panic.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
use crate::exceptions::BaseException;
2+
3+
/// The exception raised when Rust code called from Python panics.
4+
///
5+
/// Like SystemExit, this exception is derived from BaseException so that
6+
/// it will typically propagate all the way through the stack and cause the
7+
/// Python interpreter to exit.
8+
pub struct PanicException {
9+
_private: (),
10+
}
11+
12+
pyo3_exception!(PanicException, BaseException);

0 commit comments

Comments
 (0)