Skip to content

Commit 75abd86

Browse files
committed
fix memory corruption when subclassing variable-size types (e.g. abi3 + 3.12 subclassing of native types) (#5823)
* add test for subclassing variable-sized type * fix type confusion inside `get_contents_of_obj` * newsfragment
1 parent b62c7a2 commit 75abd86

File tree

3 files changed

+51
-14
lines changed

3 files changed

+51
-14
lines changed

newsfragments/5823.fixed.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix memory corruption when subclassing native types with `abi3` feature on Python 3.12+ (newly enabled in PyO3 0.28.0).

src/pycell/impl_.rs

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ use crate::impl_::pyclass::{
1111
PyClassBaseType, PyClassDict, PyClassImpl, PyClassThreadChecker, PyClassWeakRef, PyObjectOffset,
1212
};
1313
use crate::internal::get_slot::{TP_DEALLOC, TP_FREE};
14-
use crate::type_object::{PyLayout, PySizedLayout};
14+
use crate::type_object::{PyLayout, PySizedLayout, PyTypeInfo};
1515
use crate::types::PyType;
16-
use crate::{ffi, PyClass, PyTypeInfo, Python};
16+
use crate::{ffi, PyClass, Python};
1717

1818
use crate::types::PyTypeMethods;
1919

@@ -477,21 +477,31 @@ pub struct PyVariableClassObject<T: PyClassImpl> {
477477
}
478478

479479
#[cfg(Py_3_12)]
480-
impl<T: PyClassImpl<Layout = Self>> PyVariableClassObject<T> {
481-
fn get_contents_of_obj(obj: *mut ffi::PyObject) -> *mut PyClassObjectContents<T> {
482-
// https://peps.python.org/pep-0697/
483-
let type_obj = unsafe { ffi::Py_TYPE(obj) };
480+
impl<T: PyClass<Layout = Self>> PyVariableClassObject<T> {
481+
/// # Safety
482+
/// - `obj` must have the layout that the implementation is expecting
483+
/// - thread must be attached to the interpreter
484+
unsafe fn get_contents_of_obj(
485+
obj: *mut ffi::PyObject,
486+
) -> *mut MaybeUninit<PyClassObjectContents<T>> {
487+
// TODO: it would be nice to eventually avoid coupling to the PyO3 statics here, maybe using
488+
// 3.14's PyType_GetBaseByToken, to support PEP 587 / multiple interpreters better
489+
// SAFETY: caller guarantees attached to the interpreter
490+
let type_obj = T::type_object_raw(unsafe { Python::assume_attached() });
484491
let pointer = unsafe { ffi::PyObject_GetTypeData(obj, type_obj) };
485492
pointer.cast()
486493
}
487494

488495
fn get_contents_ptr(&self) -> *mut PyClassObjectContents<T> {
489-
Self::get_contents_of_obj(self as *const PyVariableClassObject<T> as *mut ffi::PyObject)
496+
unsafe {
497+
Self::get_contents_of_obj(self as *const PyVariableClassObject<T> as *mut ffi::PyObject)
498+
}
499+
.cast()
490500
}
491501
}
492502

493503
#[cfg(Py_3_12)]
494-
impl<T: PyClassImpl<Layout = Self>> PyClassObjectLayout<T> for PyVariableClassObject<T> {
504+
impl<T: PyClass<Layout = Self>> PyClassObjectLayout<T> for PyVariableClassObject<T> {
495505
/// Gets the offset of the contents from the start of the struct in bytes.
496506
const CONTENTS_OFFSET: PyObjectOffset = PyObjectOffset::Relative(0);
497507
const BASIC_SIZE: ffi::Py_ssize_t = {
@@ -514,7 +524,7 @@ impl<T: PyClassImpl<Layout = Self>> PyClassObjectLayout<T> for PyVariableClassOb
514524
unsafe fn contents_uninit(
515525
obj: *mut ffi::PyObject,
516526
) -> *mut MaybeUninit<PyClassObjectContents<T>> {
517-
Self::get_contents_of_obj(obj).cast()
527+
unsafe { Self::get_contents_of_obj(obj) }
518528
}
519529

520530
fn get_ptr(&self) -> *mut T {
@@ -543,7 +553,7 @@ impl<T: PyClassImpl<Layout = Self>> PyClassObjectLayout<T> for PyVariableClassOb
543553
unsafe impl<T: PyClassImpl> PyLayout<T> for PyVariableClassObject<T> {}
544554

545555
#[cfg(Py_3_12)]
546-
impl<T: PyClassImpl<Layout = Self>> PyClassObjectBaseLayout<T> for PyVariableClassObject<T>
556+
impl<T: PyClass<Layout = Self>> PyClassObjectBaseLayout<T> for PyVariableClassObject<T>
547557
where
548558
<T::BaseType as PyClassBaseType>::LayoutAsBase: PyClassObjectBaseLayout<T::BaseType>,
549559
{

tests/test_inheritance.rs

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -304,7 +304,7 @@ mod inheriting_native_type {
304304
#[test]
305305
#[cfg(Py_3_12)]
306306
fn inherit_list() {
307-
#[pyclass(extends=pyo3::types::PyList)]
307+
#[pyclass(extends=pyo3::types::PyList, subclass)]
308308
struct ListWithName {
309309
#[pyo3(get)]
310310
name: &'static str,
@@ -318,12 +318,38 @@ mod inheriting_native_type {
318318
}
319319
}
320320

321+
#[pyclass(extends=ListWithName)]
322+
struct SubListWithName {
323+
#[pyo3(get)]
324+
sub_name: &'static str,
325+
}
326+
327+
#[pymethods]
328+
impl SubListWithName {
329+
#[new]
330+
fn new() -> PyClassInitializer<Self> {
331+
PyClassInitializer::from(ListWithName::new()).add_subclass(Self {
332+
sub_name: "Sublist",
333+
})
334+
}
335+
}
336+
321337
Python::attach(|py| {
322-
let list_sub = pyo3::Bound::new(py, ListWithName::new()).unwrap();
338+
let list_with_name = pyo3::Bound::new(py, ListWithName::new()).unwrap();
339+
let sub_list_with_name = pyo3::Bound::new(py, SubListWithName::new()).unwrap();
323340
py_run!(
324341
py,
325-
list_sub,
326-
r#"list_sub.append(1); assert list_sub[0] == 1; assert list_sub.name == "Hello :)""#
342+
list_with_name sub_list_with_name,
343+
r#"
344+
list_with_name.append(1)
345+
assert list_with_name[0] == 1
346+
assert list_with_name.name == "Hello :)", list_with_name.name
347+
348+
sub_list_with_name.append(1)
349+
assert sub_list_with_name[0] == 1
350+
assert sub_list_with_name.name == "Hello :)", sub_list_with_name.name
351+
assert sub_list_with_name.sub_name == "Sublist", sub_list_with_name.sub_name
352+
"#
327353
);
328354
});
329355
}

0 commit comments

Comments
 (0)