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.)
Both
PyRefMut::as_superandPyClassGuardMut::as_superreturn&mutreferences to the base-type guard.e.g.
PyRefMut<T>::as_super() -> &mut PyRefMut<T::BaseType>.Unfortunately because of the
&mutreference, they can be swapped with other guards toT::BaseTypewhich are from base type objects, i.e. not the same subclass. After such a swap and the.as_super()borrow ends, the originalPyRefMut<T>is not sound because it allows access to fields ofTon a value which isn't actuallyT, i.e. creates an out of bounds read.Here's a quick repro which shows an out of bounds read:
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>andPyClassGuardMutSuper<'_, T::BaseType>which don't allow for this invalid swap.I think for
PyClassGuardMutit makes sense to just fix the API now, similar to #6073. ForPyRefMutI am less sure, it will be a bigger change. One option is that we deprecatePyRefMut::as_superwith a note why the API should be used with caution, and aim to landPyClassGuardMutas the more complete replacement forPyRefMutin 0.30 or 0.31 (as we'd like to do that anyway).(Credit to Codex security scanning for this discovery.)