From 5b3e2a40e2f0b98e4c56d6e637495f8415c86808 Mon Sep 17 00:00:00 2001 From: Bas Schoenmaeckers <7943856+bschoenmaeckers@users.noreply.github.com> Date: Sun, 4 Oct 2026 06:52:36 +0200 Subject: [PATCH] Add recursion guard to the c-api (#8796) * Add recursion guard to the c-api * Do not change with_recursion as this has side effects --- crates/capi/src/ceval.rs | 34 ++++++++++++++++++++++++++++++++-- crates/vm/src/vm/mod.rs | 27 +++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/crates/capi/src/ceval.rs b/crates/capi/src/ceval.rs index 4064b289c..ace789616 100644 --- a/crates/capi/src/ceval.rs +++ b/crates/capi/src/ceval.rs @@ -1,7 +1,7 @@ use crate::pyframe::PyFrameObject; use crate::pystate::with_vm; use crate::unicodeobject::decode_fsdefault_and_size; -use crate::util::FfiPtrExt; +use crate::util::{CStrExt, FfiPtrExt}; use core::ffi::{CStr, c_char, c_int}; use rustpython_vm::builtins::{PyCode, PyDict}; use rustpython_vm::function::ArgMapping; @@ -157,10 +157,21 @@ pub unsafe extern "C" fn PyEval_GetFuncDesc(func: *mut PyObject) -> *const c_cha }) } +#[unsafe(no_mangle)] +pub unsafe extern "C" fn Py_EnterRecursiveCall(where_: *const c_char) -> c_int { + with_vm(|vm| vm.enter_recursive_call(unsafe { where_.try_as_str(vm)? })) +} + +#[unsafe(no_mangle)] +pub unsafe extern "C" fn Py_LeaveRecursiveCall() { + with_vm(|vm| vm.leave_recursive_call()) +} + #[cfg(test)] mod tests { + use super::{Py_EnterRecursiveCall, Py_LeaveRecursiveCall}; use alloc::ffi::CString; - use pyo3::exceptions::PyException; + use pyo3::exceptions::{PyException, PyRecursionError}; use pyo3::prelude::*; #[pyfunction] @@ -268,4 +279,23 @@ assert not hidden_leaked .unwrap(); }) } + + #[test] + fn recursion_error() { + fn recurse(py: Python<'_>) -> PyResult<()> { + unsafe { + if Py_EnterRecursiveCall(c"".as_ptr().cast()) != 0 { + return Err(PyErr::fetch(py)); + }; + let result = recurse(py); + Py_LeaveRecursiveCall(); + result + } + } + + Python::attach(|py| { + let err = recurse(py).unwrap_err(); + assert!(err.is_instance_of::(py)); + }) + } } diff --git a/crates/vm/src/vm/mod.rs b/crates/vm/src/vm/mod.rs index 445ff7474..7cc636b37 100644 --- a/crates/vm/src/vm/mod.rs +++ b/crates/vm/src/vm/mod.rs @@ -2585,6 +2585,33 @@ impl VirtualMachine { false } + /// Enter a native-recursion section equivalent to `Py_EnterRecursiveCall`. + pub fn enter_recursive_call(&self, _where: &str) -> PyResult<()> { + #[cfg(any(miri, target_env = "musl"))] + let counted_too_deep = + self.native_recursion_depth.get() >= Self::NATIVE_RECURSION_LIMIT_UNMEASURED; + #[cfg(not(any(miri, target_env = "musl")))] + let counted_too_deep = false; + + if counted_too_deep || self.check_c_stack_overflow() { + return Err( + self.new_recursion_error(format!("maximum recursion depth exceeded {_where}")) + ); + } + + #[cfg(any(miri, target_env = "musl"))] + self.native_recursion_depth.update(|d| d + 1); + + Ok(()) + } + + /// Leave a native-recursion section equivalent to + /// `Py_LeaveRecursiveCall`. + pub fn leave_recursive_call(&self) { + #[cfg(any(miri, target_env = "musl"))] + self.native_recursion_depth.update(|d| d.saturating_sub(1)); + } + /// Used to run the body of a (possibly) recursive function. It will raise a /// RecursionError if recursive functions are nested far too many times, /// preventing a stack overflow.