PyO3 / PyO3/pyo3

`PyRefMut::as_super` and `PyClassGuardMut::as_super` enable type confusion

Open
#6,083 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
Rust
Stars
16.2k
Forks
1k
Avg merge
2d 6h
Merged PRs (30d)
66

Description

Both PyRefMut::as_super and PyClassGuardMut::as_super return &mut references to the base-type guard.

e.g. PyRefMut<T>::as_super() -> &mut PyRefMut<T::BaseType>.

Unfortunately because of the &mut reference, they can be swapped with other guards to T::BaseType which are from base type objects, i.e. not the same subclass. After such a swap and the .as_super() borrow ends, the original PyRefMut<T> is not sound because it allows access to fields of T on a value which isn't actually T, i.e. creates an out of bounds read.

Here's a quick repro which shows an out of bounds read:

#[crate::pyclass]
#[pyo3(crate = "crate", subclass)]
struct BaseClass {
    val1: usize,
}

#[crate::pyclass]
#[pyo3(crate = "crate", extends=BaseClass, subclass)]
struct SubClass {
    val2: usize,
}

#[test]
fn test_pyrefmut_as_super() {
    Python::attach(|py| {
        let obj = Bound::new(py, PyClassInitializer::from(BaseClass { val1: 10 }).add_subclass(SubClass { val2: 20 })).unwrap();

        let base_obj = BaseClass { val1: 10 }.into_pyobject(py).unwrap();
        let mut obj_guard_mut = obj.borrow_mut();

        // after the swap, `obj_guard_mut` actually points to an instance of the base type
        std::mem::swap(
            obj_guard_mut.as_super(),
            &mut base_obj.borrow_mut(),
        );

        // XXX: Read of uninitialized memory, this panic message is basically arbitrary
        panic!("{}", obj_guard_mut.val2);
    });
}

cc @Icxolu (not your API but you most recently worked on these types with me 😄)

I think the only reasonable thing we can do is replace these APIs with types such as PyRefMutSuper<'_, T::BaseType> and PyClassGuardMutSuper<'_, T::BaseType> which don't allow for this invalid swap.

I think for PyClassGuardMut it makes sense to just fix the API now, similar to #6073. For PyRefMut I am less sure, it will be a bigger change. One option is that we deprecate PyRefMut::as_super with a note why the API should be used with caution, and aim to land PyClassGuardMut as the more complete replacement for PyRefMut in 0.30 or 0.31 (as we'd like to do that anyway).

(Credit to Codex security scanning for this discovery.)

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the supplied reproduction and inspect the PyRefMut::as_super and PyClassGuardMut::as_super entry points. Confirm that swapping the returned mutable references permits access to the subclass field through the wrong object. Done means preventing that invalid swap while resolving the separate PyRefMut API compatibility decision described in the issue.

Written by the indexing model from the issue text.

Assessment

Tech stack
python, rust
Domain
api, security
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.