diff --git a/newsfragments/5930.changed.md b/newsfragments/5930.changed.md new file mode 100644 index 00000000000..8b801aa06a6 --- /dev/null +++ b/newsfragments/5930.changed.md @@ -0,0 +1 @@ +Remove redundant type checks for methods where CPython guarantees the type of `self` diff --git a/pyo3-macros-backend/src/method.rs b/pyo3-macros-backend/src/method.rs index 3ec89dc08ca..dab64618ad4 100644 --- a/pyo3-macros-backend/src/method.rs +++ b/pyo3-macros-backend/src/method.rs @@ -263,6 +263,7 @@ impl FnType { &self, cls: Option<&syn::Type>, error_mode: ExtractErrorMode, + self_conversion: SelfConversionPolicy, holders: &mut Holders, ctx: &Ctx, ) -> Option { @@ -272,6 +273,7 @@ impl FnType { Some(st.receiver( cls.expect("no class given for Fn with a \"self\" receiver"), error_mode, + self_conversion, holders, ctx, )) @@ -320,6 +322,56 @@ pub enum SelfType { }, } +#[derive(Clone, Copy, Debug)] +enum SelfConversionPolicyInner { + /// The receiver's type is guaranteed by CPython's slot/method dispatch contract. + /// Used for all extension-type method and slot entrypoints. + Trusted, + /// The receiver's type is verified at runtime. Used for number-protocol + /// binary operator fragments where the CPython dispatch contract does not + /// guarantee the receiver type. + Checked, +} + +/// Receiver conversion policy for extension-type method wrappers. +/// +/// Controls whether the `self` receiver is validated with a runtime type check +/// (`Checked`) or treated as trusted and cast directly without checking +/// (`Trusted`). +/// +/// # Invariant +/// +/// The `Trusted` path is valid due to CPython's slot/method receiver contract: +/// when CPython dispatches a method call on an extension type — whether through +/// a type slot or through `tp_methods` — the receiver is guaranteed to be an +/// instance of the owning type (or a compatible subtype). For `tp_methods` +/// entries, CPython's method-wrapper descriptor enforces this before the C +/// function is reached. +/// +/// `Checked` should be used in cases where that guarantee does not hold: +/// - Number-protocol binary operator fragments (`__add__`, `__radd__`, …, +/// `__pow__`, `__rpow__`): CPython combines the forward and reflected +/// fragments into a single `nb_add`/`nb_power` slot, and the runtime helper +/// may call the reflected fragment with the operands swapped, meaning `_slf` +/// can arrive with a non-class type. The existing +/// `ExtractErrorMode::NotImplemented` behavior on type mismatch is preserved +/// by using `Checked` for those fragments. +#[derive(Clone, Copy, Debug)] +pub struct SelfConversionPolicy(SelfConversionPolicyInner); + +impl SelfConversionPolicy { + pub const fn checked() -> Self { + Self(SelfConversionPolicyInner::Checked) + } + + // Using the trusted conversion incorrectly can lead to incorrect runtime + // behavior and memory safety issues, so this is marked `unsafe` and usage + // should be justified by the caller. + pub const unsafe fn trusted() -> Self { + Self(SelfConversionPolicyInner::Trusted) + } +} + #[derive(Clone, Copy)] pub enum ExtractErrorMode { NotImplemented, @@ -346,6 +398,7 @@ impl SelfType { &self, cls: &syn::Type, error_mode: ExtractErrorMode, + self_conversion: SelfConversionPolicy, holders: &mut Holders, ctx: &Ctx, ) -> TokenStream { @@ -367,22 +420,47 @@ impl SelfType { }; let arg = quote! { unsafe { #pyo3_path::impl_::extract_argument::#cast_fn(#py, #slf) } }; - let method = if *mutable { - syn::Ident::new("extract_pyclass_ref_mut", *span) - } else { - syn::Ident::new("extract_pyclass_ref", *span) - }; let holder = holders.push_holder(*span); let pyo3_path = pyo3_path.to_tokens_spanned(*span); - error_mode.handle_error( - quote_spanned! { *span => - #pyo3_path::impl_::extract_argument::#method::<#cls>( - #arg, - &mut #holder, + match self_conversion.0 { + SelfConversionPolicyInner::Trusted => { + let method = if *mutable { + syn::Ident::new("extract_pyclass_ref_mut_trusted", *span) + } else { + syn::Ident::new("extract_pyclass_ref_trusted", *span) + }; + // Use `quote!` (not `quote_spanned!`) for the `unsafe` block so that + // the `unsafe` keyword has `Span::call_site()` and does not inherit the + // user's code span. This prevents triggering `#![forbid(unsafe_code)]` + // in user crates (see the analogous comment in `impl_py_getter_def`). + // Safety: slot wrappers are only installed on the extension type itself. + // CPython's slot dispatch contract ensures the receiver is an instance + // of the correct type before invoking the slot. + let trusted_call = quote! { + unsafe { #pyo3_path::impl_::extract_argument::#method::<#cls>( + #arg, + &mut #holder, + ) } + }; + error_mode.handle_error(trusted_call, ctx) + } + SelfConversionPolicyInner::Checked => { + let method = if *mutable { + syn::Ident::new("extract_pyclass_ref_mut", *span) + } else { + syn::Ident::new("extract_pyclass_ref", *span) + }; + error_mode.handle_error( + quote_spanned! { *span => + #pyo3_path::impl_::extract_argument::#method::<#cls>( + #arg, + &mut #holder, + ) + }, + ctx, ) - }, - ctx, - ) + } + } } SelfType::TryFromBoundRef { span, non_null } => { let bound_ref = if *non_null { @@ -391,10 +469,32 @@ impl SelfType { quote! { unsafe { #pyo3_path::Bound::ref_from_ptr(#py, &#slf) } } }; let pyo3_path = pyo3_path.to_tokens_spanned(*span); + let receiver = match self_conversion.0 { + SelfConversionPolicyInner::Trusted => { + // Use `quote!` (not `quote_spanned!`) for the inner `unsafe` block so + // that it has `Span::call_site()` and does not trigger + // `#![forbid(unsafe_code)]` in user crates. + // Safety: slot wrappers are only installed on the extension type + // itself. CPython's slot dispatch contract ensures the receiver is + // an instance of the correct type (or a compatible subtype) before + // invoking the slot. + let cast = quote! { + unsafe { #bound_ref.cast_unchecked::<#cls>() } + }; + quote_spanned! { *span => + ::std::result::Result::<_, #pyo3_path::PyErr>::Ok(#cast) + } + } + SelfConversionPolicyInner::Checked => { + quote_spanned! { *span => + #bound_ref.cast::<#cls>() + .map_err(::std::convert::Into::<#pyo3_path::PyErr>::into) + } + } + }; error_mode.handle_error( quote_spanned! { *span => - #bound_ref.cast::<#cls>() - .map_err(::std::convert::Into::<#pyo3_path::PyErr>::into) + #receiver .and_then( #[allow( clippy::unnecessary_fallible_conversions, @@ -677,6 +777,7 @@ impl<'a> FnSpec<'a> { ident: &proc_macro2::Ident, cls: Option<&syn::Type>, convention: CallingConvention, + self_conversion: SelfConversionPolicy, ctx: &Ctx, ) -> Result { let Ctx { @@ -699,9 +800,13 @@ impl<'a> FnSpec<'a> { } let rust_call = |args: Vec, mut holders: Holders| { - let self_arg = self - .tp - .self_arg(cls, ExtractErrorMode::Raise, &mut holders, ctx); + let self_arg = self.tp.self_arg( + cls, + ExtractErrorMode::Raise, + self_conversion, + &mut holders, + ctx, + ); let init_holders = holders.init_holders(ctx); // We must assign the output_span to the return value of the call, diff --git a/pyo3-macros-backend/src/pyfunction.rs b/pyo3-macros-backend/src/pyfunction.rs index 3fa4b9b5317..3605005323f 100644 --- a/pyo3-macros-backend/src/pyfunction.rs +++ b/pyo3-macros-backend/src/pyfunction.rs @@ -10,7 +10,7 @@ use crate::{ self, get_pyo3_options, take_attributes, take_pyo3_options, CrateAttribute, FromPyWithAttribute, NameAttribute, TextSignatureAttribute, }, - method::{self, CallingConvention, FnArg}, + method::{self, CallingConvention, FnArg, SelfConversionPolicy}, pymethod::check_generic, }; use proc_macro2::{Span, TokenStream}; @@ -430,7 +430,13 @@ pub fn impl_wrap_pyfunction( ); } let calling_convention = CallingConvention::from_signature(&spec.signature); - let wrapper = spec.get_wrapper_function(&wrapper_ident, None, calling_convention, ctx)?; + let wrapper = spec.get_wrapper_function( + &wrapper_ident, + None, + calling_convention, + SelfConversionPolicy::checked(), + ctx, + )?; let methoddef = spec.get_methoddef( wrapper_ident, spec.get_doc(&func.attrs).as_ref(), diff --git a/pyo3-macros-backend/src/pymethod.rs b/pyo3-macros-backend/src/pymethod.rs index d82bc2241bc..18b0ac87abd 100644 --- a/pyo3-macros-backend/src/pymethod.rs +++ b/pyo3-macros-backend/src/pymethod.rs @@ -4,7 +4,7 @@ use std::ffi::CString; use crate::attributes::{FromPyWithAttribute, NameAttribute, RenamingRule}; #[cfg(feature = "experimental-inspect")] use crate::introspection::unique_element_id; -use crate::method::{CallingConvention, ExtractErrorMode, PyArg}; +use crate::method::{CallingConvention, ExtractErrorMode, PyArg, SelfConversionPolicy}; use crate::params::{impl_arg_params, impl_regular_arg_param, Holders}; use crate::pyfunction::WarningFactory; use crate::utils::PythonDoc; @@ -374,8 +374,16 @@ pub fn impl_py_method_def( let Ctx { pyo3_path, .. } = ctx; let wrapper_ident = format_ident!("__pymethod_{}__", spec.python_name); let calling_convention = CallingConvention::from_signature(&spec.signature); - let associated_method = - spec.get_wrapper_function(&wrapper_ident, Some(cls), calling_convention, ctx)?; + let associated_method = spec.get_wrapper_function( + &wrapper_ident, + Some(cls), + calling_convention, + // Methods in `tp_methods` are dispatched through CPython's method-wrapper + // descriptor, which enforces that the receiver is an instance of the owning + // type before reaching the C function. The trusted path is therefore valid. + unsafe { SelfConversionPolicy::trusted() }, + ctx, + )?; let methoddef = spec.get_methoddef( quote! { #cls::#wrapper_ident }, doc, @@ -394,8 +402,15 @@ pub fn impl_py_method_def( fn impl_call_slot(cls: &syn::Type, spec: &FnSpec<'_>, ctx: &Ctx) -> Result { let Ctx { pyo3_path, .. } = ctx; let wrapper_ident = syn::Ident::new("__pymethod___call____", Span::call_site()); - let associated_method = - spec.get_wrapper_function(&wrapper_ident, Some(cls), CallingConvention::Varargs, ctx)?; + let associated_method = spec.get_wrapper_function( + &wrapper_ident, + Some(cls), + CallingConvention::Varargs, + // The `tp_call` slot is dispatched by CPython, which guarantees the receiver + // is of the correct type. + unsafe { SelfConversionPolicy::trusted() }, + ctx, + )?; let slot_def = quote! { #pyo3_path::ffi::PyType_Slot { slot: #pyo3_path::ffi::Py_tp_call, @@ -473,7 +488,15 @@ fn impl_clear_slot(cls: &syn::Type, spec: &FnSpec<'_>, ctx: &Ctx) -> syn::Result _ => bail_spanned!(spec.name.span() => "expected instance method for `__clear__` function"), }; let mut holders = Holders::new(); - let slf = self_type.receiver(cls, ExtractErrorMode::Raise, &mut holders, ctx); + let slf = self_type.receiver( + cls, + ExtractErrorMode::Raise, + // The `tp_clear` slot is dispatched by CPython, which guarantees the + // receiver is of the correct type. + unsafe { SelfConversionPolicy::trusted() }, + &mut holders, + ctx, + ); if let [arg, ..] = args { bail_spanned!(arg.ty().span() => "`__clear__` function expected to have no arguments"); @@ -571,7 +594,15 @@ fn impl_call_setter( ctx: &Ctx, ) -> syn::Result { let (py_arg, args) = split_off_python_arg(&spec.signature.arguments); - let slf = self_type.receiver(cls, ExtractErrorMode::Raise, holders, ctx); + let slf = self_type.receiver( + cls, + ExtractErrorMode::Raise, + // The setter function is dispatched by CPython's method-wrapper + // descriptor, which enforces the receiver is of the correct type. + unsafe { SelfConversionPolicy::trusted() }, + holders, + ctx, + ); if args.is_empty() { bail_spanned!(spec.name.span() => "setter function expected to have one argument"); @@ -611,7 +642,15 @@ pub fn impl_py_setter_def( span: Span::call_site(), non_null: true, } - .receiver(cls, ExtractErrorMode::Raise, &mut holders, ctx); + .receiver( + cls, + ExtractErrorMode::Raise, + // The setter function is dispatched by CPython, which + // guarantees the receiver is of the correct type. + unsafe { SelfConversionPolicy::trusted() }, + &mut holders, + ctx, + ); if let Some(ident) = &field.ident { // named struct field quote!({ #slf.#ident = _val; }) @@ -757,7 +796,15 @@ fn impl_call_getter( ctx: &Ctx, ) -> syn::Result { let (py_arg, args) = split_off_python_arg(&spec.signature.arguments); - let slf = self_type.receiver(cls, ExtractErrorMode::Raise, holders, ctx); + let slf = self_type.receiver( + cls, + ExtractErrorMode::Raise, + // The getter function is dispatched by CPython's method-wrapper + //descriptor, which enforces the receiver is of the correct type. + unsafe { SelfConversionPolicy::trusted() }, + holders, + ctx, + ); ensure_spanned!( args.is_empty(), args[0].ty().span() => "getter function can only have one argument (of type pyo3::Python)" @@ -932,7 +979,15 @@ fn impl_call_deleter( ctx: &Ctx, ) -> Result { let (py_arg, args) = split_off_python_arg(&spec.signature.arguments); - let slf = self_type.receiver(cls, ExtractErrorMode::Raise, holders, ctx); + let slf = self_type.receiver( + cls, + ExtractErrorMode::Raise, + // The deleter function is dispatched by CPython's method-wrapper + // descriptor, which enforces the receiver is of the correct type. + unsafe { SelfConversionPolicy::trusted() }, + holders, + ctx, + ); if !args.is_empty() { bail_spanned!(spec.name.span() => @@ -1395,6 +1450,9 @@ impl SlotDef { spec, calling_convention, *extract_error_mode, + // All extension-type slots use trusted self: CPython's slot dispatch + // contract guarantees the receiver is of the correct type. + unsafe { SelfConversionPolicy::trusted() }, &mut holders, return_mode.as_ref(), ctx, @@ -1425,11 +1483,16 @@ impl SlotDef { } } +#[allow( + clippy::too_many_arguments, + reason = "slot wrapper generation needs the self-conversion policy flag" +)] fn generate_method_body( cls: &syn::Type, spec: &FnSpec<'_>, calling_convention: &SlotCallingConvention, extract_error_mode: ExtractErrorMode, + self_conversion: SelfConversionPolicy, holders: &mut Holders, // NB ignored if calling_convention is SlotCallingConvention::TpNew, possibly should merge into that enum return_mode: Option<&ReturnMode>, @@ -1441,7 +1504,7 @@ fn generate_method_body( } = ctx; let self_arg = spec .tp - .self_arg(Some(cls), extract_error_mode, holders, ctx); + .self_arg(Some(cls), extract_error_mode, self_conversion, holders, ctx); let rust_name = spec.name; let warnings = spec.warnings.build_py_warning(ctx); @@ -1564,6 +1627,16 @@ struct SlotFragmentDef { arguments: &'static [Ty], extract_error_mode: ExtractErrorMode, ret_ty: Ty, + /// Self-conversion policy for this slot fragment. + /// + /// Most slot fragments are called by CPython with a receiver that is + /// guaranteed to be of the correct type (`Trusted`). However, binary + /// operator fragments are combined into a single slot (e.g. `nb_add`) + /// where the runtime helper may swap operands and call the reflected + /// fragment with a receiver of an unknown (potentially wrong) type. + /// Those fragments must use `Checked` so that a type mismatch returns + /// `NotImplemented` instead of causing undefined behaviour. + self_conversion: SelfConversionPolicy, } impl SlotFragmentDef { @@ -1573,16 +1646,26 @@ impl SlotFragmentDef { arguments, extract_error_mode: ExtractErrorMode::Raise, ret_ty: Ty::Void, + self_conversion: SelfConversionPolicy::checked(), } } - /// Specialized constructor for binary operators (which are a common pattern) + /// Specialized constructor for binary operators. + /// + /// Binary operator fragments (`__add__`, `__radd__`, etc.) are combined + /// into a shared slot (e.g. `nb_add`) that may call the forward fragment + /// with a non-class receiver (e.g. `1 + MyClass()` → `nb_add(1, c)`). + /// The runtime helper then tries the reflected fragment with the operands + /// swapped, which can also produce a non-class `_slf`. Both cases require + /// a checked type conversion so that a mismatch gracefully returns + /// `NotImplemented` rather than causing undefined behaviour. const fn binary_operator(fragment: &'static str) -> Self { SlotFragmentDef { fragment, arguments: &[Ty::Object], extract_error_mode: ExtractErrorMode::NotImplemented, ret_ty: Ty::Object, + self_conversion: SelfConversionPolicy::checked(), } } @@ -1596,6 +1679,11 @@ impl SlotFragmentDef { self } + const unsafe fn trusted_self(mut self) -> Self { + self.self_conversion = unsafe { SelfConversionPolicy::trusted() }; + self + } + fn generate_pyproto_fragment( &self, cls: &syn::Type, @@ -1608,6 +1696,7 @@ impl SlotFragmentDef { arguments, extract_error_mode, ret_ty, + self_conversion, } = self; let fragment_trait = format_ident!("PyClass{}SlotFragment", fragment); let method = syn::Ident::new(fragment, Span::call_site()); @@ -1623,6 +1712,7 @@ impl SlotFragmentDef { spec, &SlotCallingConvention::FixedArguments(arguments), *extract_error_mode, + *self_conversion, &mut holders, None, ctx, @@ -1663,18 +1753,28 @@ pub struct MethodBody { pub body: TokenStream, } -const __GETATTRIBUTE__: SlotFragmentDef = - SlotFragmentDef::new("__getattribute__", &[Ty::Object]).ret_ty(Ty::Object); -const __GETATTR__: SlotFragmentDef = - SlotFragmentDef::new("__getattr__", &[Ty::Object]).ret_ty(Ty::Object); +const __GETATTRIBUTE__: SlotFragmentDef = unsafe { + SlotFragmentDef::new("__getattribute__", &[Ty::Object]) + .ret_ty(Ty::Object) + .trusted_self() +}; +const __GETATTR__: SlotFragmentDef = unsafe { + SlotFragmentDef::new("__getattr__", &[Ty::Object]) + .ret_ty(Ty::Object) + .trusted_self() +}; const __SETATTR__: SlotFragmentDef = - SlotFragmentDef::new("__setattr__", &[Ty::Object, Ty::NonNullObject]); -const __DELATTR__: SlotFragmentDef = SlotFragmentDef::new("__delattr__", &[Ty::Object]); -const __SET__: SlotFragmentDef = SlotFragmentDef::new("__set__", &[Ty::Object, Ty::NonNullObject]); -const __DELETE__: SlotFragmentDef = SlotFragmentDef::new("__delete__", &[Ty::Object]); + unsafe { SlotFragmentDef::new("__setattr__", &[Ty::Object, Ty::NonNullObject]).trusted_self() }; +const __DELATTR__: SlotFragmentDef = + unsafe { SlotFragmentDef::new("__delattr__", &[Ty::Object]).trusted_self() }; +const __SET__: SlotFragmentDef = + unsafe { SlotFragmentDef::new("__set__", &[Ty::Object, Ty::NonNullObject]).trusted_self() }; +const __DELETE__: SlotFragmentDef = + unsafe { SlotFragmentDef::new("__delete__", &[Ty::Object]).trusted_self() }; const __SETITEM__: SlotFragmentDef = - SlotFragmentDef::new("__setitem__", &[Ty::Object, Ty::NonNullObject]); -const __DELITEM__: SlotFragmentDef = SlotFragmentDef::new("__delitem__", &[Ty::Object]); + unsafe { SlotFragmentDef::new("__setitem__", &[Ty::Object, Ty::NonNullObject]).trusted_self() }; +const __DELITEM__: SlotFragmentDef = + unsafe { SlotFragmentDef::new("__delitem__", &[Ty::Object]).trusted_self() }; const __ADD__: SlotFragmentDef = SlotFragmentDef::binary_operator("__add__"); const __RADD__: SlotFragmentDef = SlotFragmentDef::binary_operator("__radd__"); @@ -1710,24 +1810,42 @@ const __RPOW__: SlotFragmentDef = SlotFragmentDef::new("__rpow__", &[Ty::Object, .extract_error_mode(ExtractErrorMode::NotImplemented) .ret_ty(Ty::Object); -const __LT__: SlotFragmentDef = SlotFragmentDef::new("__lt__", &[Ty::Object]) - .extract_error_mode(ExtractErrorMode::NotImplemented) - .ret_ty(Ty::Object); -const __LE__: SlotFragmentDef = SlotFragmentDef::new("__le__", &[Ty::Object]) - .extract_error_mode(ExtractErrorMode::NotImplemented) - .ret_ty(Ty::Object); -const __EQ__: SlotFragmentDef = SlotFragmentDef::new("__eq__", &[Ty::Object]) - .extract_error_mode(ExtractErrorMode::NotImplemented) - .ret_ty(Ty::Object); -const __NE__: SlotFragmentDef = SlotFragmentDef::new("__ne__", &[Ty::Object]) - .extract_error_mode(ExtractErrorMode::NotImplemented) - .ret_ty(Ty::Object); -const __GT__: SlotFragmentDef = SlotFragmentDef::new("__gt__", &[Ty::Object]) - .extract_error_mode(ExtractErrorMode::NotImplemented) - .ret_ty(Ty::Object); -const __GE__: SlotFragmentDef = SlotFragmentDef::new("__ge__", &[Ty::Object]) - .extract_error_mode(ExtractErrorMode::NotImplemented) - .ret_ty(Ty::Object); +const __LT__: SlotFragmentDef = unsafe { + SlotFragmentDef::new("__lt__", &[Ty::Object]) + .extract_error_mode(ExtractErrorMode::NotImplemented) + .ret_ty(Ty::Object) + .trusted_self() +}; +const __LE__: SlotFragmentDef = unsafe { + SlotFragmentDef::new("__le__", &[Ty::Object]) + .extract_error_mode(ExtractErrorMode::NotImplemented) + .ret_ty(Ty::Object) + .trusted_self() +}; +const __EQ__: SlotFragmentDef = unsafe { + SlotFragmentDef::new("__eq__", &[Ty::Object]) + .extract_error_mode(ExtractErrorMode::NotImplemented) + .ret_ty(Ty::Object) + .trusted_self() +}; +const __NE__: SlotFragmentDef = unsafe { + SlotFragmentDef::new("__ne__", &[Ty::Object]) + .extract_error_mode(ExtractErrorMode::NotImplemented) + .ret_ty(Ty::Object) + .trusted_self() +}; +const __GT__: SlotFragmentDef = unsafe { + SlotFragmentDef::new("__gt__", &[Ty::Object]) + .extract_error_mode(ExtractErrorMode::NotImplemented) + .ret_ty(Ty::Object) + .trusted_self() +}; +const __GE__: SlotFragmentDef = unsafe { + SlotFragmentDef::new("__ge__", &[Ty::Object]) + .extract_error_mode(ExtractErrorMode::NotImplemented) + .ret_ty(Ty::Object) + .trusted_self() +}; fn extract_proto_arguments( spec: &FnSpec<'_>, diff --git a/pytests/tests/test_comparisons.py b/pytests/tests/test_comparisons.py index 9e8c05ad5fd..dec16ae9b49 100644 --- a/pytests/tests/test_comparisons.py +++ b/pytests/tests/test_comparisons.py @@ -64,6 +64,12 @@ def test_eq(ty: Type[EqType]): assert not c == 1 assert c != 1 + # Ensure that passing a wrong self type from Python does not cause UB + with pytest.raises(TypeError): + ty.__eq__(object(), 1) # type: ignore[operator] + with pytest.raises(TypeError): + ty.__ne__(object(), 1) # type: ignore[operator] + with pytest.raises(TypeError): assert a <= b # type: ignore[operator] @@ -176,6 +182,16 @@ def test_ordered(ty: Type[OrderedType]): assert c > b assert c >= b + # Ensure that passing a wrong self type from Python does not cause UB + with pytest.raises(TypeError): + ty.__lt__(object(), 1) # type: ignore[operator] + with pytest.raises(TypeError): + ty.__le__(object(), 1) # type: ignore[operator] + with pytest.raises(TypeError): + ty.__gt__(object(), 1) # type: ignore[operator] + with pytest.raises(TypeError): + ty.__ge__(object(), 1) # type: ignore[operator] + class PyOrderedDefaultNe: def __init__(self, x: int) -> None: diff --git a/src/impl_/extract_argument.rs b/src/impl_/extract_argument.rs index 6fb9c84057d..d0189a89c69 100644 --- a/src/impl_/extract_argument.rs +++ b/src/impl_/extract_argument.rs @@ -216,6 +216,47 @@ pub fn extract_pyclass_ref_mut<'a, 'holder, T: PyClass>( Ok(&mut *holder.insert(PyClassGuardMut::try_borrow_mut_from_borrowed(obj.cast()?)?)) } +/// Trusted variant of [`extract_pyclass_ref`]: performs an unchecked cast for +/// extension-type slot receivers where CPython guarantees the receiver type. +/// +/// This is valid when called from generated slot wrappers installed on a specific +/// extension type, because CPython's slot dispatch contract ensures the receiver +/// is an instance of that type (or a compatible subtype) before invoking the slot. +/// +/// # Safety +/// The caller must ensure that `obj` is an instance of `T`. This invariant is +/// upheld by CPython when dispatching through type slots. +#[inline] +pub unsafe fn extract_pyclass_ref_trusted<'a, 'holder, T: PyClass>( + obj: Borrowed<'a, '_, PyAny>, + holder: &'holder mut Option>, +) -> PyResult<&'holder T> { + // Safety: caller guarantees obj is of type T via CPython slot receiver contract + Ok( + &*holder.insert(PyClassGuard::try_borrow_from_borrowed(unsafe { + obj.cast_unchecked::() + })?), + ) +} + +/// Trusted variant of [`extract_pyclass_ref_mut`]: performs an unchecked cast for +/// extension-type slot receivers where CPython guarantees the receiver type. +/// +/// # Safety +/// Same as [`extract_pyclass_ref_trusted`]. +#[inline] +pub unsafe fn extract_pyclass_ref_mut_trusted<'a, 'holder, T: PyClass>( + obj: Borrowed<'a, '_, PyAny>, + holder: &'holder mut Option>, +) -> PyResult<&'holder mut T> { + // Safety: caller guarantees obj is of type T via CPython slot receiver contract + Ok( + &mut *holder.insert(PyClassGuardMut::try_borrow_mut_from_borrowed(unsafe { + obj.cast_unchecked::() + })?), + ) +} + /// The standard implementation of how PyO3 extracts a `#[pyfunction]` or `#[pymethod]` function argument. pub fn extract_argument<'a, 'holder, 'py, T, const IMPLEMENTS_FROMPYOBJECT: bool>( obj: Borrowed<'a, 'py, PyAny>, diff --git a/tests/test_arithmetics.rs b/tests/test_arithmetics.rs index bf3f99c3462..94c332e3713 100644 --- a/tests/test_arithmetics.rs +++ b/tests/test_arithmetics.rs @@ -2,6 +2,7 @@ use pyo3::class::basic::CompareOp; use pyo3::py_run; +use pyo3::types::IntoPyDict; use pyo3::{prelude::*, BoundObject}; mod test_utils; @@ -63,6 +64,13 @@ fn unary_arithmetic() { c.bitnot().unwrap().repr().unwrap().as_any(), "UA(0.37037037037037035)" ); + + // Ensure that passing a wrong self type from Python does not cause UB + py_expect_exception!(py, c, "type(c).__neg__(object())", PyTypeError); + py_expect_exception!(py, c, "type(c).__pos__(object())", PyTypeError); + py_expect_exception!(py, c, "type(c).__abs__(object())", PyTypeError); + py_expect_exception!(py, c, "type(c).__invert__(object())", PyTypeError); + py_expect_exception!(py, c, "type(c).__round__(object())", PyTypeError); }); } @@ -96,6 +104,12 @@ fn indexable() { py_run!(py, i, "assert [0, 1, 2, 3, 4, 5][i] == 5"); py_run!(py, i, "assert float(i) == 5.0"); py_run!(py, i, "assert int(~i) == -6"); + + // Ensure that passing a wrong self type from Python does not cause UB + py_expect_exception!(py, i, "type(i).__index__(object())", PyTypeError); + py_expect_exception!(py, i, "type(i).__int__(object())", PyTypeError); + py_expect_exception!(py, i, "type(i).__float__(object())", PyTypeError); + py_expect_exception!(py, i, "type(i).__invert__(object())", PyTypeError); }) } @@ -168,6 +182,18 @@ fn inplace_operations() { 3, "d = c; c.__ipow__(4); assert repr(c) == repr(d) == 'IPO(81)'", ); + + let c = Py::new(py, InPlaceOperations { value: 0 }).unwrap(); + // Ensure that passing a wrong self type from Python does not cause UB + py_expect_exception!(py, c, "type(c).__iadd__(object(), 1)", PyTypeError); + py_expect_exception!(py, c, "type(c).__isub__(object(), 1)", PyTypeError); + py_expect_exception!(py, c, "type(c).__imul__(object(), 1)", PyTypeError); + py_expect_exception!(py, c, "type(c).__ilshift__(object(), 1)", PyTypeError); + py_expect_exception!(py, c, "type(c).__irshift__(object(), 1)", PyTypeError); + py_expect_exception!(py, c, "type(c).__iand__(object(), 1)", PyTypeError); + py_expect_exception!(py, c, "type(c).__ixor__(object(), 1)", PyTypeError); + py_expect_exception!(py, c, "type(c).__ior__(object(), 1)", PyTypeError); + py_expect_exception!(py, c, "type(c).__ipow__(object(), 1)", PyTypeError); }); } @@ -297,51 +323,51 @@ fn binary_arithmetic() { } #[pyclass] -struct RhsArithmetic {} +struct RhsArithmetic(String); #[pymethods] impl RhsArithmetic { fn __radd__(&self, other: &Bound<'_, PyAny>) -> String { - format!("{other:?} + RA") + format!("{other:?} + {}", self.0) } fn __rsub__(&self, other: &Bound<'_, PyAny>) -> String { - format!("{other:?} - RA") + format!("{other:?} - {}", self.0) } fn __rmul__(&self, other: &Bound<'_, PyAny>) -> String { - format!("{other:?} * RA") + format!("{other:?} * {}", self.0) } fn __rlshift__(&self, other: &Bound<'_, PyAny>) -> String { - format!("{other:?} << RA") + format!("{other:?} << {}", self.0) } fn __rrshift__(&self, other: &Bound<'_, PyAny>) -> String { - format!("{other:?} >> RA") + format!("{other:?} >> {}", self.0) } fn __rand__(&self, other: &Bound<'_, PyAny>) -> String { - format!("{other:?} & RA") + format!("{other:?} & {}", self.0) } fn __rxor__(&self, other: &Bound<'_, PyAny>) -> String { - format!("{other:?} ^ RA") + format!("{other:?} ^ {}", self.0) } fn __ror__(&self, other: &Bound<'_, PyAny>) -> String { - format!("{other:?} | RA") + format!("{other:?} | {}", self.0) } fn __rpow__(&self, other: &Bound<'_, PyAny>, _mod: Option<&Bound<'_, PyAny>>) -> String { - format!("{other:?} ** RA") + format!("{other:?} ** {}", self.0) } } #[test] fn rhs_arithmetic() { Python::attach(|py| { - let c = Py::new(py, RhsArithmetic {}).unwrap(); + let c = Py::new(py, RhsArithmetic("RA".to_string())).unwrap(); py_run!(py, c, "assert c.__radd__(1) == '1 + RA'"); py_run!(py, c, "assert 1 + c == '1 + RA'"); py_run!(py, c, "assert c.__rsub__(1) == '1 - RA'"); @@ -363,6 +389,24 @@ fn rhs_arithmetic() { }); } +#[test] +fn rhs_fallback() { + Python::attach(|py| { + let cl = Py::new(py, RhsArithmetic("AR".to_string())).unwrap(); + let cr = Py::new(py, RhsArithmetic("RA".to_string())).unwrap(); + let locals = [("cl", cl), ("cr", cr)].into_py_dict(py).unwrap(); + py_run!(py, locals, "assert cl + cr == 'AR + RA'"); + py_run!(py, locals, "assert cl - cr == 'AR - RA'"); + py_run!(py, locals, "assert cl * cr == 'AR * RA'"); + py_run!(py, locals, "assert cl << cr == 'AR << RA'"); + py_run!(py, locals, "assert cl >> cr == 'AR >> RA'"); + py_run!(py, locals, "assert cl & cr == 'AR & RA'"); + py_run!(py, locals, "assert cl ^ cr == 'AR ^ RA'"); + py_run!(py, locals, "assert cl | cr == 'AR | RA'"); + py_run!(py, locals, "assert cl ** cr == 'AR ** RA (mod: None)'"); + }); +} + #[pyclass] struct LhsAndRhs {} @@ -471,7 +515,7 @@ impl LhsAndRhs { fn lhs_fellback_to_rhs() { Python::attach(|py| { let c = Py::new(py, LhsAndRhs {}).unwrap(); - // If the light hand value is `LhsAndRhs`, LHS is used. + // If the left hand value is `LhsAndRhs`, LHS is used. py_run!(py, c, "assert c + 1 == 'LR + 1'"); py_run!(py, c, "assert c - 1 == 'LR - 1'"); py_run!(py, c, "assert c * 1 == 'LR * 1'"); @@ -565,6 +609,9 @@ fn rich_comparisons() { py_run!(py, c, "assert (c >= c) == 'RC >= RC'"); py_run!(py, c, "assert (c >= 1) == 'RC >= 1'"); py_run!(py, c, "assert (1 >= c) == 'RC <= 1'"); + + // Ensure that passing a wrong self type from Python does not cause UB + py_expect_exception!(py, c, "type(c).__richcmp__(object(), 1)", PyTypeError); }); } diff --git a/tests/test_methods.rs b/tests/test_methods.rs index 9153845a1ea..c0fec231f36 100644 --- a/tests/test_methods.rs +++ b/tests/test_methods.rs @@ -44,6 +44,22 @@ fn instance_method() { }); } +/// Test that CPython's method-wrapper descriptor rejects wrong receiver types +/// when `tp_methods` entries are called with a bad `self` from Python. +/// This validates that the trusted self conversion in generated wrappers is safe: +/// even though the Rust code skips a runtime type check, CPython enforces the +/// receiver type before the C function is reached. +#[test] +fn tp_methods_receiver_type_checked_by_cpython() { + Python::attach(|py| { + let cls = py.get_type::(); + // Calling an unbound method with a wrong-type `self` raises TypeError. + // CPython's method-wrapper descriptor enforces the type before our Rust + // wrapper is invoked. + py_expect_exception!(py, cls, "cls.method(object())", PyTypeError); + }); +} + #[pyclass] struct InstanceMethodWithArgs { member: i32, diff --git a/tests/test_proto_methods.rs b/tests/test_proto_methods.rs index a44025cb45e..9311c5af5b8 100644 --- a/tests/test_proto_methods.rs +++ b/tests/test_proto_methods.rs @@ -99,6 +99,14 @@ fn test_getattr() { .getattr("other_attr") .unwrap_err() .is_instance_of::(py)); + + // Ensure that passing a wrong self type from Python does not cause UB + py_expect_exception!( + py, + example_py, + "type(example_py).__getattr__(object(), 'test')", + PyTypeError + ); }) } @@ -115,6 +123,14 @@ fn test_setattr() { .unwrap(), 15, ); + + // Ensure that passing a wrong self type from Python does not cause UB + py_expect_exception!( + py, + example_py, + "type(example_py).__setattr__(object(), 'test', None)", + PyTypeError + ); }) } @@ -124,6 +140,14 @@ fn test_delattr() { let example_py = make_example(py); example_py.delattr("special_custom_attr").unwrap(); assert!(example_py.getattr("special_custom_attr").unwrap().is_none()); + + // Ensure that passing a wrong self type from Python does not cause UB + py_expect_exception!( + py, + example_py, + "type(example_py).__delattr__(object(), 'test')", + PyTypeError + ); }) } @@ -132,6 +156,14 @@ fn test_str() { Python::attach(|py| { let example_py = make_example(py); assert_eq!(example_py.str().unwrap(), "5"); + + // Ensure that passing a wrong self type from Python does not cause UB + py_expect_exception!( + py, + example_py, + "type(example_py).__str__(object())", + PyTypeError + ); }) } @@ -140,6 +172,14 @@ fn test_repr() { Python::attach(|py| { let example_py = make_example(py); assert_eq!(example_py.repr().unwrap(), "ExampleClass(value=5)"); + + // Ensure that passing a wrong self type from Python does not cause UB + py_expect_exception!( + py, + example_py, + "type(example_py).__repr__(object())", + PyTypeError + ); }) } @@ -148,6 +188,14 @@ fn test_hash() { Python::attach(|py| { let example_py = make_example(py); assert_eq!(example_py.hash().unwrap(), 5); + + // Ensure that passing a wrong self type from Python does not cause UB + py_expect_exception!( + py, + example_py, + "type(example_py).__hash__(object())", + PyTypeError + ); }) } @@ -158,6 +206,14 @@ fn test_bool() { assert!(example_py.is_truthy().unwrap()); example_py.borrow_mut().value = 0; assert!(!example_py.is_truthy().unwrap()); + + // Ensure that passing a wrong self type from Python does not cause UB + py_expect_exception!( + py, + example_py, + "type(example_py).__bool__(object())", + PyTypeError + ); }) } @@ -387,6 +443,10 @@ fn iterator() { .unwrap(); py_assert!(py, inst, "iter(inst) is inst"); py_assert!(py, inst, "list(inst) == [5, 6, 7]"); + + // Ensure that passing a wrong self type from Python does not cause UB + py_expect_exception!(py, inst, "type(inst).__iter__(object())", PyTypeError); + py_expect_exception!(py, inst, "type(inst).__next__(object())", PyTypeError); }); } @@ -412,6 +472,9 @@ fn callable() { let nc = Py::new(py, NotCallable).unwrap(); py_assert!(py, nc, "not callable(nc)"); + + // Ensure that passing a wrong self type from Python does not cause UB + py_expect_exception!(py, c, "type(c).__call__(object(), 7)", PyTypeError); }); } @@ -441,6 +504,9 @@ fn setitem() { assert_eq!(c.val, 2); } py_expect_exception!(py, c, "del c[1]", PyNotImplementedError); + + // Ensure that passing a wrong self type from Python does not cause UB + py_expect_exception!(py, c, "type(c).__setitem__(object(), 1, 2)", PyTypeError); }); } @@ -466,6 +532,9 @@ fn delitem() { assert_eq!(c.key, 1); } py_expect_exception!(py, c, "c[1] = 2", PyNotImplementedError); + + // Ensure that passing a wrong self type from Python does not cause UB + py_expect_exception!(py, c, "type(c).__delitem__(object(), 1)", PyTypeError); }); } @@ -517,6 +586,9 @@ fn contains() { py_run!(py, c, "assert 1 in c"); py_run!(py, c, "assert -1 not in c"); py_expect_exception!(py, c, "assert 'wrong type' not in c", PyTypeError); + + // Ensure that passing a wrong self type from Python does not cause UB + py_expect_exception!(py, c, "type(c).__contains__(object(), 1)", PyTypeError); }); } @@ -547,6 +619,9 @@ fn test_getitem() { py_assert!(py, ob, "ob[1] == 'int'"); py_assert!(py, ob, "ob[100:200:1] == 'slice'"); + + // Ensure that passing a wrong self type from Python does not cause UB + py_expect_exception!(py, ob, "type(ob).__getitem__(object(), 1)", PyTypeError); }); } @@ -569,6 +644,14 @@ fn getattr_doesnt_override_member() { let inst = Py::new(py, ClassWithGetAttr { data: 4 }).unwrap(); py_assert!(py, inst, "inst.data == 4"); py_assert!(py, inst, "inst.a == 8"); + + // Ensure that passing a wrong self type from Python does not cause UB + py_expect_exception!( + py, + inst, + "type(inst).__getattr__(object(), 'a')", + PyTypeError + ); }); } @@ -597,6 +680,14 @@ fn getattribute_overrides_member() { let inst = Py::new(py, ClassWithGetAttribute { data: 4 }).unwrap(); py_assert!(py, inst, "inst.data == 8"); py_expect_exception!(py, inst, "inst.y == 8", PyAttributeError, "y"); + + // Ensure that passing a wrong self type from Python does not cause UB + py_expect_exception!( + py, + inst, + "type(inst).__getattribute__(object(), 'data')", + PyTypeError + ); }); } @@ -690,10 +781,13 @@ if sys.platform == "win32" and sys.version_info >= (3, 8, 0): asyncio.run(main()) "#; let globals = PyModule::import(py, "__main__").unwrap().dict(); - globals.set_item("Once", once).unwrap(); + globals.set_item("Once", once.clone()).unwrap(); py.run(source, Some(&globals), None) .map_err(|e| e.display(py)) .unwrap(); + + // Ensure that passing a wrong self type from Python does not cause UB + py_expect_exception!(py, once, "Once.__await__(object())", PyTypeError); }); } @@ -751,6 +845,11 @@ asyncio.run(main()) py.run(source, Some(&globals), None) .map_err(|e| e.display(py)) .unwrap(); + + // Ensure that passing a wrong self type from Python does not cause UB + let atype = py.get_type::(); + py_expect_exception!(py, atype, "AsyncIterator.__aiter__(object())", PyTypeError); + py_expect_exception!(py, atype, "AsyncIterator.__anext__(object())", PyTypeError); }); } @@ -813,6 +912,18 @@ assert c.counter.count == 4 # __delete__ del c.counter assert c.counter.count == 1 + +# wrong receiver type should be rejected by CPython slot wrapper +for call in ( + lambda: Counter.__get__(object(), Class()), + lambda: Counter.__set__(object(), Class(), Counter()), + lambda: Counter.__delete__(object(), Class()), +): + try: + call() + assert False, "expected TypeError" + except TypeError: + pass "# ); let globals = PyModule::import(py, "__main__").unwrap().dict(); diff --git a/tests/ui/invalid_frozen_pyclass_borrow.stderr b/tests/ui/invalid_frozen_pyclass_borrow.stderr index d049ab98f8d..187a4732845 100644 --- a/tests/ui/invalid_frozen_pyclass_borrow.stderr +++ b/tests/ui/invalid_frozen_pyclass_borrow.stderr @@ -4,24 +4,6 @@ error: cannot use `#[pyo3(set)]` on a `frozen` class 38 | #[pyo3(set)] | ^^^ -error[E0271]: type mismatch resolving `::Frozen == False` - --> tests/ui/invalid_frozen_pyclass_borrow.rs:11:19 - | -11 | fn mut_method(&mut self) {} - | ^ type mismatch resolving `::Frozen == False` - | -note: expected this to be `pyo3::pyclass::boolean_struct::False` - --> tests/ui/invalid_frozen_pyclass_borrow.rs:3:1 - | - 3 | #[pyclass(frozen)] - | ^^^^^^^^^^^^^^^^^^ -note: required by a bound in `pyo3::impl_::extract_argument::extract_pyclass_ref_mut` - --> src/impl_/extract_argument.rs - | - | pub fn extract_pyclass_ref_mut<'a, 'holder, T: PyClass>( - | ^^^^^^^^^^^^^^ required by this bound in `extract_pyclass_ref_mut` - = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) - error[E0271]: type mismatch resolving `::Frozen == False` --> tests/ui/invalid_frozen_pyclass_borrow.rs:9:1 | @@ -33,11 +15,11 @@ note: expected this to be `pyo3::pyclass::boolean_struct::False` | 3 | #[pyclass(frozen)] | ^^^^^^^^^^^^^^^^^^ -note: required by a bound in `PyClassGuardMut` - --> src/pyclass/guard.rs +note: required by a bound in `pyo3::impl_::extract_argument::extract_pyclass_ref_mut_trusted` + --> src/impl_/extract_argument.rs | - | pub struct PyClassGuardMut<'a, T: PyClass> { - | ^^^^^^^^^^^^^^ required by this bound in `PyClassGuardMut` + | pub unsafe fn extract_pyclass_ref_mut_trusted<'a, 'holder, T: PyClass>( + | ^^^^^^^^^^^^^^ required by this bound in `extract_pyclass_ref_mut_trusted` = note: this error originates in the attribute macro `pymethods` which comes from the expansion of the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0271]: type mismatch resolving `::Frozen == False` diff --git a/tests/ui/invalid_pymethod_enum.stderr b/tests/ui/invalid_pymethod_enum.stderr index 896a637460a..91aa046b2cd 100644 --- a/tests/ui/invalid_pymethod_enum.stderr +++ b/tests/ui/invalid_pymethod_enum.stderr @@ -1,21 +1,3 @@ -error[E0271]: type mismatch resolving `::Frozen == False` - --> tests/ui/invalid_pymethod_enum.rs:11:24 - | -11 | fn mutate_in_place(&mut self) { - | ^ type mismatch resolving `::Frozen == False` - | -note: expected this to be `pyo3::pyclass::boolean_struct::False` - --> tests/ui/invalid_pymethod_enum.rs:3:1 - | - 3 | #[pyclass] - | ^^^^^^^^^^ -note: required by a bound in `pyo3::impl_::extract_argument::extract_pyclass_ref_mut` - --> src/impl_/extract_argument.rs - | - | pub fn extract_pyclass_ref_mut<'a, 'holder, T: PyClass>( - | ^^^^^^^^^^^^^^ required by this bound in `extract_pyclass_ref_mut` - = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) - error[E0271]: type mismatch resolving `::Frozen == False` --> tests/ui/invalid_pymethod_enum.rs:9:1 | @@ -27,31 +9,13 @@ note: expected this to be `pyo3::pyclass::boolean_struct::False` | 3 | #[pyclass] | ^^^^^^^^^^ -note: required by a bound in `PyClassGuardMut` - --> src/pyclass/guard.rs +note: required by a bound in `pyo3::impl_::extract_argument::extract_pyclass_ref_mut_trusted` + --> src/impl_/extract_argument.rs | - | pub struct PyClassGuardMut<'a, T: PyClass> { - | ^^^^^^^^^^^^^^ required by this bound in `PyClassGuardMut` + | pub unsafe fn extract_pyclass_ref_mut_trusted<'a, 'holder, T: PyClass>( + | ^^^^^^^^^^^^^^ required by this bound in `extract_pyclass_ref_mut_trusted` = note: this error originates in the attribute macro `pymethods` which comes from the expansion of the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) -error[E0271]: type mismatch resolving `::Frozen == False` - --> tests/ui/invalid_pymethod_enum.rs:27:24 - | -27 | fn mutate_in_place(&mut self) { - | ^ type mismatch resolving `::Frozen == False` - | -note: expected this to be `pyo3::pyclass::boolean_struct::False` - --> tests/ui/invalid_pymethod_enum.rs:19:1 - | -19 | #[pyclass] - | ^^^^^^^^^^ -note: required by a bound in `pyo3::impl_::extract_argument::extract_pyclass_ref_mut` - --> src/impl_/extract_argument.rs - | - | pub fn extract_pyclass_ref_mut<'a, 'holder, T: PyClass>( - | ^^^^^^^^^^^^^^ required by this bound in `extract_pyclass_ref_mut` - = note: this error originates in the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) - error[E0271]: type mismatch resolving `::Frozen == False` --> tests/ui/invalid_pymethod_enum.rs:25:1 | @@ -63,9 +27,9 @@ note: expected this to be `pyo3::pyclass::boolean_struct::False` | 19 | #[pyclass] | ^^^^^^^^^^ -note: required by a bound in `PyClassGuardMut` - --> src/pyclass/guard.rs +note: required by a bound in `pyo3::impl_::extract_argument::extract_pyclass_ref_mut_trusted` + --> src/impl_/extract_argument.rs | - | pub struct PyClassGuardMut<'a, T: PyClass> { - | ^^^^^^^^^^^^^^ required by this bound in `PyClassGuardMut` + | pub unsafe fn extract_pyclass_ref_mut_trusted<'a, 'holder, T: PyClass>( + | ^^^^^^^^^^^^^^ required by this bound in `extract_pyclass_ref_mut_trusted` = note: this error originates in the attribute macro `pymethods` which comes from the expansion of the attribute macro `pyclass` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/tests/ui/not_send.stderr b/tests/ui/not_send.stderr index 0cb4039b186..b7915ee519f 100644 --- a/tests/ui/not_send.stderr +++ b/tests/ui/not_send.stderr @@ -9,9 +9,6 @@ error[E0277]: `*mut pyo3::Python<'static>` cannot be shared between threads safe = help: within `pyo3::Python<'_>`, the trait `Sync` is not implemented for `*mut pyo3::Python<'static>` note: required because it appears within the type `PhantomData<*mut pyo3::Python<'static>>` --> $RUST/core/src/marker.rs - | - | pub struct PhantomData; - | ^^^^^^^^^^^ note: required because it appears within the type `pyo3::marker::NotSend` --> src/marker.rs | @@ -19,9 +16,6 @@ note: required because it appears within the type `pyo3::marker::NotSend` | ^^^^^^^ note: required because it appears within the type `PhantomData` --> $RUST/core/src/marker.rs - | - | pub struct PhantomData; - | ^^^^^^^^^^^ note: required because it appears within the type `pyo3::Python<'_>` --> src/marker.rs | diff --git a/tests/ui/not_send2.stderr b/tests/ui/not_send2.stderr index 3d76b5ebc11..678d618d245 100644 --- a/tests/ui/not_send2.stderr +++ b/tests/ui/not_send2.stderr @@ -12,9 +12,6 @@ error[E0277]: `*mut pyo3::Python<'static>` cannot be shared between threads safe = help: within `pyo3::Bound<'_, PyString>`, the trait `Sync` is not implemented for `*mut pyo3::Python<'static>` note: required because it appears within the type `PhantomData<*mut pyo3::Python<'static>>` --> $RUST/core/src/marker.rs - | - | pub struct PhantomData; - | ^^^^^^^^^^^ note: required because it appears within the type `pyo3::marker::NotSend` --> src/marker.rs | @@ -22,9 +19,6 @@ note: required because it appears within the type `pyo3::marker::NotSend` | ^^^^^^^ note: required because it appears within the type `PhantomData` --> $RUST/core/src/marker.rs - | - | pub struct PhantomData; - | ^^^^^^^^^^^ note: required because it appears within the type `pyo3::Python<'_>` --> src/marker.rs |