#6392·pyo3

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:

rust
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 reached

Backtrace

bash

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