Borrowed constructor argument can outlive its owner
Author: Eclips4Created Sep 6, 2026Updated Sep 6, 2026
Bug Description
PyO3 keeps borrowed references to keyword arguments while extracting constructor arguments. However, converting one argument can execute Python code through __index__, __str__, or another conversion hook. That code can delete another value from the same kwargs dictionary.
Steps to Reproduce
Minimal Rust reproducer and observed output:
use std::cell::RefCell;
use pyo3::prelude::*;
use pyo3::ffi;
use pyo3::types::{PyDict, PyTuple};
struct Ignored;
impl<'a, 'py> FromPyObject<'a, 'py> for Ignored {
type Error = PyErr;
fn extract(obj: Borrowed<'a, 'py, PyAny>) -> Result<Self, Self::Error> {
let _ = obj.get_type();
println!("second conversion reached");
Ok(Self)
}
}
#[pyclass]
struct Victim;
impl Drop for Victim {
fn drop(&mut self) { println!("Victim::drop: second finalized"); }
}
#[pyclass(unsendable)]
struct Index { kwargs: RefCell<Option<Py<PyDict>>> }
#[pymethods]
impl Index {
fn __index__(&self, py: Python<'_>) -> PyResult<i64> {
println!("Index::__index__: deleting kwargs['second']");
let kwargs = self.kwargs.borrow_mut().take().unwrap();
let victim_ptr = {
let victim = kwargs.bind(py).get_item("second")?.unwrap();
victim.as_ptr()
};
println!("Victim refcount before deletion: {}", unsafe {
ffi::Py_REFCNT(victim_ptr)
});
kwargs.bind(py).del_item("second")?;
println!("Victim refcount after deletion: {}", unsafe {
ffi::Py_REFCNT(victim_ptr)
});
Ok(7)
}
}
#[pyclass]
struct C;
#[pymethods]
impl C {
#[new]
fn new(first: i64, second: Ignored) -> Self {
let _ = (first, second);
println!("constructor reached");
C
}
}
fn main() -> PyResult<()> {
Python::attach(|py| {
let kwargs = PyDict::new(py);
let index = Py::new(py, Index {
kwargs: RefCell::new(Some(kwargs.clone().unbind())),
})?;
kwargs.set_item("first", index)?;
kwargs.set_item("second", Py::new(py, Victim)?)?;
let _ = py.get_type::<C>().call(PyTuple::empty(py), Some(&kwargs))?;
Ok(())
})
}Output:
Index::__index__: deleting kwargs['second']
Victim refcount before deletion: 1
Victim::drop: second finalized
Victim refcount after deletion: 0
second conversion reached
constructor reachedBacktrace
Your operating system and version
macOS 26.5.2
Your Python version (python --version)
3.14.2
Your Rust version (rustc --version)
1.91.1
Your PyO3 version
0.29.2
How did you install python? Did you use a virtualenv?
brew
Additional Info
I suppose the safest fix is to make argument extraction own each value before running any conversion code
Source: PyO3/pyo3