Skip to content

Soundness issue: users can create out-of-bounds reads and writes using PyClassInitializer #4452

@ChayimFriedman2

Description

@ChayimFriedman2

Bug Description

PyClassInitializer can be given a (chain of) struct to initialize the class, or a Py<Struct>. But in the second case, it propagates the instance down the inheritance line, when subclasses gets a pointer to it and treat it like it were of their type - while it was originally an other type, with less bytes! As such, a user can exploit this to cause a OOB read/write.

Steps to Reproduce

The following code (you may need to increase the bytes count):

use pyo3::prelude::*;

#[pyclass(subclass)]
pub struct BaseClass {}

#[pymethods]
impl BaseClass {
    #[new]
    pub fn new() -> Self {
        Self {}
    }
}

#[pyclass(extends=BaseClass)]
pub struct SubClass {
    data: [u8; 100_000],
}

#[pymethods]
impl SubClass {
    #[new]
    pub fn new(py: Python<'_>) -> PyResult<PyClassInitializer<Self>> {
        Ok(
            PyClassInitializer::from(Py::new(py, BaseClass::new())?).add_subclass(SubClass {
                data: [1; 100_000],
            }),
        )
    }
}

/// A Python module implemented in Rust.
#[pymodule]
fn oob(m: &Bound<'_, PyModule>) -> PyResult<()> {
    m.add_class::<BaseClass>()?;
    m.add_class::<SubClass>()?;
    Ok(())
}

Crashes the Python interpreter on my system when SubClass is instantiated, probably due to a page fault. Really, anything can happen here.

Backtrace

No response

Your operating system and version

Windows 11

Your Python version (python --version)

Python 3.12.0

Your Rust version (rustc --version)

rustc 1.80.1 (3f5fd8dd4 2024-08-06)

Your PyO3 version

0.22.2

How did you install python? Did you use a virtualenv?

The official website.

Additional Info

Unfortunately, I don't believe this bug can be fixed, since I don't think there exists a Python API that allows us to construct a new object, with a custom target class, that is a copy of an existing (base) class.

A partial fix may be to check we are using the same class (this can be enforced at compile time even). However, I believe it's unlikely that users that provide a value to PyClassInitializer actually want to overwrite it, rather they probably want to duplicate it (which is impossible). Furthermore, this is still unsound with frozen classes (we can forbid them too, though).

I think this safest way will be for this option to be removed.

Discovered while working on #4443.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions