Skip to content

Commit

Permalink
Auto merge of rust-lang#135634 - joboet:trivial-clone, r=<try>
Browse files Browse the repository at this point in the history
stop specializing on `Copy`

fixes rust-lang#132442

`std` specializes on `Copy` to optimize certain library functions such as `clone_from_slice`. This is unsound, however, as the `Copy` implementation may not be always applicable because of lifetime bounds, which specialization does not take into account; the result being that values are copied even though they are not `Copy`. For instance, this code:
```rust
struct SometimesCopy<'a>(&'a Cell<bool>);

impl<'a> Clone for SometimesCopy<'a> {
    fn clone(&self) -> Self {
        self.0.set(true);
        Self(self.0)
    }
}

impl Copy for SometimesCopy<'static> {}

let clone_called = Cell::new(false);
// As SometimesCopy<'clone_called> is not 'static, this must run `clone`,
// setting the value to `true`.
let _ = [SometimesCopy(&clone_called)].clone();
assert!(clone_called.get());
```
should not panic, but does ([playground](https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=6be7a48cad849d8bd064491616fdb43c)).

To solve this, this PR introduces a new `unsafe` trait: `TrivialClone`. This trait may be implemented whenever the `Clone` implementation is equivalent to copying the value (so e.g. `fn clone(&self) -> Self { *self }`). Because of lifetime erasure, there is no way for the `Clone` implementation to observe lifetime bounds, meaning that even if the `TrivialClone` has stricter bounds than the `Clone` implementation, its invariant still holds. Therefore, it is sound to specialize on `TrivialClone`.

I've changed all `Copy` specializations in the standard library to specialize on `TrivialClone` instead. Unfortunately, the unsound `#[rustc_unsafe_specialization_marker]` attribute on `Copy` cannot be removed in this PR as `hashbrown` still depends on it. I'll make a PR updating `hashbrown` once this lands.

With `Copy` no longer being considered for specialization, this change alone would result in the standard library optimizations not being applied for user types unaware of `TrivialClone`. To avoid this and restore the optimizations in most cases, I have changed the expansion of `#[derive(Clone)]`: Currently, whenever both `Clone` and `Copy` are derived, the `clone` method performs a copy of the value. With this PR, the derive macro also adds a `TrivialClone` implementation to make this case observable using specialization. I anticipate that most users will use `#[derive(Clone, Copy)]` whenever both are applicable, so most users will still profit from the library optimizations.

Unfortunately, Hyrum's law applies to this PR: there are some popular crates which rely on the precise specialization behaviour of `core` to implement "specialization at home", e.g. [`libAFL`](https://github.com/AFLplusplus/LibAFL/blob/89cff637025c1652c24e8d97a30a2e3d01f187a4/libafl_bolts/src/tuples.rs#L27-L49). I have no remorse for breaking such horrible code, but perhaps we should open other, better ways to satisfy their needs – for example by dropping the `'static` bound on `TypeId::of`...
  • Loading branch information
bors committed Feb 12, 2025
2 parents 021fb9c + d31fbe3 commit 3e31775
Show file tree
Hide file tree
Showing 28 changed files with 223 additions and 81 deletions.
6 changes: 5 additions & 1 deletion compiler/rustc_builtin_macros/src/deriving/bounds.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use rustc_ast::MetaItem;
use rustc_ast::{MetaItem, Safety};
use rustc_expand::base::{Annotatable, ExtCtxt};
use rustc_span::Span;

Expand All @@ -23,6 +23,7 @@ pub(crate) fn expand_deriving_copy(
methods: Vec::new(),
associated_types: Vec::new(),
is_const,
safety: Safety::Default,
};

trait_def.expand(cx, mitem, item, push);
Expand All @@ -46,6 +47,7 @@ pub(crate) fn expand_deriving_const_param_ty(
methods: Vec::new(),
associated_types: Vec::new(),
is_const,
safety: Safety::Default,
};

trait_def.expand(cx, mitem, item, push);
Expand All @@ -60,6 +62,7 @@ pub(crate) fn expand_deriving_const_param_ty(
methods: Vec::new(),
associated_types: Vec::new(),
is_const,
safety: Safety::Default,
};

trait_def.expand(cx, mitem, item, push);
Expand All @@ -83,6 +86,7 @@ pub(crate) fn expand_deriving_unsized_const_param_ty(
methods: Vec::new(),
associated_types: Vec::new(),
is_const,
safety: Safety::Default,
};

trait_def.expand(cx, mitem, item, push);
Expand Down
24 changes: 22 additions & 2 deletions compiler/rustc_builtin_macros/src/deriving/clone.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use rustc_ast::{self as ast, Generics, ItemKind, MetaItem, VariantData};
use rustc_ast::{self as ast, Generics, ItemKind, MetaItem, Safety, VariantData};
use rustc_data_structures::fx::FxHashSet;
use rustc_expand::base::{Annotatable, ExtCtxt};
use rustc_span::{Ident, Span, kw, sym};
use rustc_span::{DUMMY_SP, Ident, Span, kw, sym};
use thin_vec::{ThinVec, thin_vec};

use crate::deriving::generic::ty::*;
Expand Down Expand Up @@ -68,6 +68,25 @@ pub(crate) fn expand_deriving_clone(
_ => cx.dcx().span_bug(span, "`#[derive(Clone)]` on trait item or impl item"),
}

// If the clone method is just copying the value, also mark the type as
// `TrivialClone` to allow some library optimizations.
if is_simple {
let trivial_def = TraitDef {
span,
path: path_std!(clone::TrivialClone),
skip_path_as_bound: false,
needs_copy_as_bound_if_packed: true,
additional_bounds: bounds.clone(),
supports_unions: true,
methods: Vec::new(),
associated_types: Vec::new(),
is_const,
safety: Safety::Unsafe(DUMMY_SP),
};

trivial_def.expand_ext(cx, mitem, item, push, true);
}

let trait_def = TraitDef {
span,
path: path_std!(clone::Clone),
Expand All @@ -87,6 +106,7 @@ pub(crate) fn expand_deriving_clone(
}],
associated_types: Vec::new(),
is_const,
safety: Safety::Default,
};

trait_def.expand_ext(cx, mitem, item, push, is_simple)
Expand Down
3 changes: 2 additions & 1 deletion compiler/rustc_builtin_macros/src/deriving/cmp/eq.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use rustc_ast::{self as ast, MetaItem};
use rustc_ast::{self as ast, MetaItem, Safety};
use rustc_data_structures::fx::FxHashSet;
use rustc_expand::base::{Annotatable, ExtCtxt};
use rustc_span::{Span, sym};
Expand Down Expand Up @@ -43,6 +43,7 @@ pub(crate) fn expand_deriving_eq(
}],
associated_types: Vec::new(),
is_const,
safety: Safety::Default,
};
trait_def.expand_ext(cx, mitem, item, push, true)
}
Expand Down
3 changes: 2 additions & 1 deletion compiler/rustc_builtin_macros/src/deriving/cmp/ord.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use rustc_ast::MetaItem;
use rustc_ast::{MetaItem, Safety};
use rustc_expand::base::{Annotatable, ExtCtxt};
use rustc_span::{Ident, Span, sym};
use thin_vec::thin_vec;
Expand Down Expand Up @@ -34,6 +34,7 @@ pub(crate) fn expand_deriving_ord(
}],
associated_types: Vec::new(),
is_const,
safety: Safety::Default,
};

trait_def.expand(cx, mitem, item, push)
Expand Down
4 changes: 3 additions & 1 deletion compiler/rustc_builtin_macros/src/deriving/cmp/partial_eq.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use rustc_ast::ptr::P;
use rustc_ast::{BinOpKind, BorrowKind, Expr, ExprKind, MetaItem, Mutability};
use rustc_ast::{BinOpKind, BorrowKind, Expr, ExprKind, MetaItem, Mutability, Safety};
use rustc_expand::base::{Annotatable, ExtCtxt};
use rustc_span::{Span, sym};
use thin_vec::thin_vec;
Expand Down Expand Up @@ -84,6 +84,7 @@ pub(crate) fn expand_deriving_partial_eq(
methods: Vec::new(),
associated_types: Vec::new(),
is_const: false,
safety: Safety::Default,
};
structural_trait_def.expand(cx, mitem, item, push);

Expand All @@ -110,6 +111,7 @@ pub(crate) fn expand_deriving_partial_eq(
methods,
associated_types: Vec::new(),
is_const,
safety: Safety::Default,
};
trait_def.expand(cx, mitem, item, push)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use rustc_ast::{ExprKind, ItemKind, MetaItem, PatKind};
use rustc_ast::{ExprKind, ItemKind, MetaItem, PatKind, Safety};
use rustc_expand::base::{Annotatable, ExtCtxt};
use rustc_span::{Ident, Span, sym};
use thin_vec::thin_vec;
Expand Down Expand Up @@ -64,6 +64,7 @@ pub(crate) fn expand_deriving_partial_ord(
methods: vec![partial_cmp_def],
associated_types: Vec::new(),
is_const,
safety: Safety::Default,
};
trait_def.expand(cx, mitem, item, push)
}
Expand Down
3 changes: 2 additions & 1 deletion compiler/rustc_builtin_macros/src/deriving/debug.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use rustc_ast::{self as ast, EnumDef, MetaItem};
use rustc_ast::{self as ast, EnumDef, MetaItem, Safety};
use rustc_expand::base::{Annotatable, ExtCtxt};
use rustc_session::config::FmtDebug;
use rustc_span::{Ident, Span, Symbol, sym};
Expand Down Expand Up @@ -41,6 +41,7 @@ pub(crate) fn expand_deriving_debug(
}],
associated_types: Vec::new(),
is_const,
safety: Safety::Default,
};
trait_def.expand(cx, mitem, item, push)
}
Expand Down
4 changes: 2 additions & 2 deletions compiler/rustc_builtin_macros/src/deriving/default.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
use core::ops::ControlFlow;

use rustc_ast as ast;
use rustc_ast::visit::visit_opt;
use rustc_ast::{EnumDef, VariantData, attr};
use rustc_ast::{self as ast, EnumDef, Safety, VariantData, attr};
use rustc_expand::base::{Annotatable, DummyResult, ExtCtxt};
use rustc_span::{ErrorGuaranteed, Ident, Span, kw, sym};
use smallvec::SmallVec;
Expand Down Expand Up @@ -51,6 +50,7 @@ pub(crate) fn expand_deriving_default(
}],
associated_types: Vec::new(),
is_const,
safety: Safety::Default,
};
trait_def.expand(cx, mitem, item, push)
}
Expand Down
7 changes: 5 additions & 2 deletions compiler/rustc_builtin_macros/src/deriving/generic/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ pub(crate) use SubstructureFields::*;
use rustc_ast::ptr::P;
use rustc_ast::{
self as ast, AnonConst, BindingMode, ByRef, EnumDef, Expr, GenericArg, GenericParamKind,
Generics, Mutability, PatKind, VariantData,
Generics, Mutability, PatKind, Safety, VariantData,
};
use rustc_attr_parsing as attr;
use rustc_expand::base::{Annotatable, ExtCtxt};
Expand Down Expand Up @@ -220,6 +220,9 @@ pub(crate) struct TraitDef<'a> {
pub associated_types: Vec<(Ident, Ty)>,

pub is_const: bool,

/// The safety of the `impl`.
pub safety: Safety,
}

pub(crate) struct MethodDef<'a> {
Expand Down Expand Up @@ -788,7 +791,7 @@ impl<'a> TraitDef<'a> {
Ident::empty(),
attrs,
ast::ItemKind::Impl(Box::new(ast::Impl {
safety: ast::Safety::Default,
safety: self.safety,
polarity: ast::ImplPolarity::Positive,
defaultness: ast::Defaultness::Final,
constness: if self.is_const { ast::Const::Yes(DUMMY_SP) } else { ast::Const::No },
Expand Down
3 changes: 2 additions & 1 deletion compiler/rustc_builtin_macros/src/deriving/hash.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use rustc_ast::{MetaItem, Mutability};
use rustc_ast::{MetaItem, Mutability, Safety};
use rustc_expand::base::{Annotatable, ExtCtxt};
use rustc_span::{Span, sym};
use thin_vec::thin_vec;
Expand Down Expand Up @@ -41,6 +41,7 @@ pub(crate) fn expand_deriving_hash(
}],
associated_types: Vec::new(),
is_const,
safety: Safety::Default,
};

hash_trait_def.expand(cx, mitem, item, push);
Expand Down
1 change: 1 addition & 0 deletions compiler/rustc_span/src/symbol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ symbols! {
ToString,
TokenStream,
Trait,
TrivialClone,
Try,
TryCaptureGeneric,
TryCapturePrintable,
Expand Down
6 changes: 5 additions & 1 deletion library/alloc/src/boxed/convert.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use core::any::Any;
#[cfg(not(no_global_oom_handling))]
use core::clone::TrivialClone;
use core::error::Error;
use core::mem;
use core::pin::Pin;
Expand Down Expand Up @@ -75,11 +77,13 @@ impl<T: Clone> BoxFromSlice<T> for Box<[T]> {
}

#[cfg(not(no_global_oom_handling))]
impl<T: Copy> BoxFromSlice<T> for Box<[T]> {
impl<T: TrivialClone> BoxFromSlice<T> for Box<[T]> {
#[inline]
fn from_slice(slice: &[T]) -> Self {
let len = slice.len();
let buf = RawVec::with_capacity(len);
// SAFETY: since `T` implements `TrivialClone`, this is sound and
// equivalent to the above.
unsafe {
ptr::copy_nonoverlapping(slice.as_ptr(), buf.ptr(), len);
buf.into_box(slice.len()).assume_init()
Expand Down
1 change: 1 addition & 0 deletions library/alloc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,7 @@
#![feature(std_internals)]
#![feature(str_internals)]
#![feature(temporary_niche_types)]
#![feature(trivial_clone)]
#![feature(trusted_fused)]
#![feature(trusted_len)]
#![feature(trusted_random_access)]
Expand Down
9 changes: 6 additions & 3 deletions library/alloc/src/rc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,7 @@
use core::any::Any;
use core::cell::Cell;
#[cfg(not(no_global_oom_handling))]
use core::clone::CloneToUninit;
use core::clone::{CloneToUninit, TrivialClone};
use core::cmp::Ordering;
use core::hash::{Hash, Hasher};
use core::intrinsics::abort;
Expand Down Expand Up @@ -2143,7 +2143,8 @@ impl<T> Rc<[T]> {

/// Copy elements from slice into newly allocated `Rc<[T]>`
///
/// Unsafe because the caller must either take ownership or bind `T: Copy`
/// Unsafe because the caller must either take ownership, bind `T: Copy` or
/// bind `T: TrivialClone`.
#[cfg(not(no_global_oom_handling))]
unsafe fn copy_from_slice(v: &[T]) -> Rc<[T]> {
unsafe {
Expand Down Expand Up @@ -2233,9 +2234,11 @@ impl<T: Clone> RcFromSlice<T> for Rc<[T]> {
}

#[cfg(not(no_global_oom_handling))]
impl<T: Copy> RcFromSlice<T> for Rc<[T]> {
impl<T: TrivialClone> RcFromSlice<T> for Rc<[T]> {
#[inline]
fn from_slice(v: &[T]) -> Self {
// SAFETY: `T` implements `TrivialClone`, so this is sound and equivalent
// to the above.
unsafe { Rc::copy_from_slice(v) }
}
}
Expand Down
8 changes: 6 additions & 2 deletions library/alloc/src/slice.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

use core::borrow::{Borrow, BorrowMut};
#[cfg(not(no_global_oom_handling))]
use core::clone::TrivialClone;
#[cfg(not(no_global_oom_handling))]
use core::cmp::Ordering::{self, Less};
#[cfg(not(no_global_oom_handling))]
use core::mem::{self, MaybeUninit};
Expand Down Expand Up @@ -88,6 +90,8 @@ use crate::vec::Vec;
#[allow(unreachable_pub)] // cfg(test) pub above
pub(crate) mod hack {
use core::alloc::Allocator;
#[cfg(not(no_global_oom_handling))]
use core::clone::TrivialClone;

use crate::boxed::Box;
use crate::vec::Vec;
Expand Down Expand Up @@ -156,7 +160,7 @@ pub(crate) mod hack {
}

#[cfg(not(no_global_oom_handling))]
impl<T: Copy> ConvertVec for T {
impl<T: TrivialClone> ConvertVec for T {
#[inline]
fn to_vec<A: Allocator>(s: &[Self], alloc: A) -> Vec<Self, A> {
let mut v = Vec::with_capacity_in(s.len(), alloc);
Expand Down Expand Up @@ -872,7 +876,7 @@ impl<T: Clone, A: Allocator> SpecCloneIntoVec<T, A> for [T] {
}

#[cfg(not(no_global_oom_handling))]
impl<T: Copy, A: Allocator> SpecCloneIntoVec<T, A> for [T] {
impl<T: TrivialClone, A: Allocator> SpecCloneIntoVec<T, A> for [T] {
fn clone_into(&self, target: &mut Vec<T, A>) {
target.clear();
target.extend_from_slice(self);
Expand Down
9 changes: 7 additions & 2 deletions library/alloc/src/sync.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
use core::any::Any;
#[cfg(not(no_global_oom_handling))]
use core::clone::CloneToUninit;
#[cfg(not(no_global_oom_handling))]
use core::clone::TrivialClone;
use core::cmp::Ordering;
use core::hash::{Hash, Hasher};
use core::intrinsics::abort;
Expand Down Expand Up @@ -2044,7 +2046,8 @@ impl<T> Arc<[T]> {

/// Copy elements from slice into newly allocated `Arc<[T]>`
///
/// Unsafe because the caller must either take ownership or bind `T: Copy`.
/// Unsafe because the caller must either take ownership, bind `T: Copy` or
/// bind `T: TrivialClone`.
#[cfg(not(no_global_oom_handling))]
unsafe fn copy_from_slice(v: &[T]) -> Arc<[T]> {
unsafe {
Expand Down Expand Up @@ -2136,9 +2139,11 @@ impl<T: Clone> ArcFromSlice<T> for Arc<[T]> {
}

#[cfg(not(no_global_oom_handling))]
impl<T: Copy> ArcFromSlice<T> for Arc<[T]> {
impl<T: TrivialClone> ArcFromSlice<T> for Arc<[T]> {
#[inline]
fn from_slice(v: &[T]) -> Self {
// SAFETY: `T` implements `TrivialClone`, so this is sound and equivalent
// to the above.
unsafe { Arc::copy_from_slice(v) }
}
}
Expand Down
8 changes: 5 additions & 3 deletions library/alloc/src/vec/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@
#![stable(feature = "rust1", since = "1.0.0")]

#[cfg(not(no_global_oom_handling))]
use core::clone::TrivialClone;
#[cfg(not(no_global_oom_handling))]
use core::cmp;
use core::cmp::Ordering;
Expand Down Expand Up @@ -3225,7 +3227,7 @@ impl<T: Clone, A: Allocator> ExtendFromWithinSpec for Vec<T, A> {
}

#[cfg(not(no_global_oom_handling))]
impl<T: Copy, A: Allocator> ExtendFromWithinSpec for Vec<T, A> {
impl<T: TrivialClone, A: Allocator> ExtendFromWithinSpec for Vec<T, A> {
unsafe fn spec_extend_from_within(&mut self, src: Range<usize>) {
let count = src.len();
{
Expand All @@ -3238,8 +3240,8 @@ impl<T: Copy, A: Allocator> ExtendFromWithinSpec for Vec<T, A> {
// SAFETY:
// - Both pointers are created from unique slice references (`&mut [_]`)
// so they are valid and do not overlap.
// - Elements are :Copy so it's OK to copy them, without doing
// anything with the original values
// - Elements implement `TrivialClone` so this is equivalent to calling
// `clone` on every one of them.
// - `count` is equal to the len of `source`, so source is valid for
// `count` reads
// - `.reserve(count)` guarantees that `spare.len() >= count` so spare
Expand Down
Loading

0 comments on commit 3e31775

Please sign in to comment.