Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Array and Vec's Clone specialization is maybe unsound with conditionally Copy types. #132442

Open
theemathas opened this issue Nov 1, 2024 · 17 comments · May be fixed by #135634
Open

Array and Vec's Clone specialization is maybe unsound with conditionally Copy types. #132442

theemathas opened this issue Nov 1, 2024 · 17 comments · May be fixed by #135634
Labels
A-specialization Area: Trait impl specialization C-bug Category: This is a bug. E-needs-investigation Call for partcipation: This issues needs some investigation to determine current status I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness P-medium Medium priority T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-libs Relevant to the library team, which will review and decide on the PR/issue.

Comments

@theemathas
Copy link
Contributor

Currently, the Clone impl for [T; N] uses specialization. If T implements Copy, then cloning a [T; N] will do a memcpy (ignoring T's Clone impl). If T doesn't implement Copy, then it will iterate and call T's Clone impl.

However, specialization doesn't look at lifetimes. Therefore, even if T implements Copy for only some lifetimes, cloning a [T; N] will do a memcopy. This is incorrect in the case where T actually doesn't implement Copy. For example:

struct Weird<'a>(&'a i32);

impl Clone for Weird<'_> {
    fn clone(&self) -> Self {
        println!("clone() called");
        Weird(self.0)
    }
}

impl Copy for Weird<'static> {}

fn main() {
    let local = 1;
    let _ = [Weird(&local)].clone();
}

In the above code, Weird only implements Copy when its lifetime is 'static. Therefore, I think that cloning a [Weird<'a>; 1] should call the clone() method. However, running the above code in either stable (1.82.0) or nightly (1.84.0-nightly (2024-10-30 759e07f063fb8e6306ff)) rust doesn't print anything. I believe that this is incorrect.

I am unsure whether or not this can cause UB in purely safe code, (hence the "maybe" in the issue title). But maybe someone else could figure out how to do that?

However, I have written this code, which uses unsafe code to implement a WeirdCow type whose public API I believe is sound on its own, but can be used in combination with array's Clone specialization to cause use-after-free. Either this WeirdCow type is unsound, or the array's Clone implementation is unsound, and I can't figure out which.

@rustbot label +I-unsound

@theemathas theemathas added the C-bug Category: This is a bug. label Nov 1, 2024
@rustbot rustbot added needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness I-prioritize Issue: Indicates that prioritization has been requested for this issue. labels Nov 1, 2024
@ChayimFriedman2
Copy link
Contributor

I believe it was discussed before, and the conclusion was that the specialization is okay and it's invalid to implement Copy depending on lifetimes, and the compiler should probably lint/err at it.

@jieyouxu jieyouxu added T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-libs Relevant to the library team, which will review and decide on the PR/issue. labels Nov 1, 2024
@scottmcm
Copy link
Member

scottmcm commented Nov 1, 2024

@theemathas Is this at all specific to the array impl? Can you do the same thing with Vec, which has the same specialization?

impl<T: Copy> ConvertVec for T {

@theemathas
Copy link
Contributor Author

Can you do the same thing with Vec

Yes. This code also doesn't print anything.

struct Weird<'a>(&'a i32);

impl Clone for Weird<'_> {
    fn clone(&self) -> Self {
        println!("clone() called");
        Weird(self.0)
    }
}

impl Copy for Weird<'static> {}

fn main() {
    let local = 1;
    let _ = vec![Weird(&local)].clone();
}

@theemathas theemathas changed the title Array's Clone specialization is maybe unsound with conditionally Copy types. Array and Vec's Clone specialization is maybe unsound with conditionally Copy types. Nov 1, 2024
@Noratrieb
Copy link
Member

Noratrieb commented Nov 1, 2024

Implementing copy conditionally on lifetimes is nonsensical and unsupported. MIR building will lower all moves of such a value to copy's, which borrowck will then error about when it's not static.
This impl should be an error (which is probably super hard, the same way sound specialization is).

@hanna-kruppe
Copy link
Contributor

More discussion of impl Copy for Foo<'static>: #126179

@lcnr
Copy link
Contributor

lcnr commented Nov 1, 2024

Related to #89948. I think that until we actually forbid such Copy impls, I strongly feel like this is a std bug and your WeirdCow should be a sound abstraction. We should not specialize on Copy and doing so is unsound as it ignores lifetime constraints.

Whether Copy with lifetime constraints makes your type pretty much unusable (it prevents ever moving it as all moves get upgraded to use Copy) does not feel relevant to whether std should be allowed to do this. It does provide a good argument for linting/forbidding the Copy impls, at which point std can specialization on Copy without using the unsound (from a type system POV) #[rustc_unsafe_specialization_marker] trait:

//! ### rustc_unsafe_specialization_marker
//!
//! There are also some specialization on traits with no methods, including the
//! stable `FusedIterator` trait. We allow marking marker traits with an
//! unstable attribute that means we ignore them in point 3 of the checks
//! above. This is unsound, in the sense that the specialized impl may be used
//! when it doesn't apply, but we allow it in the short term since it can't
//! cause use after frees with purely safe code in the same way as specializing
//! on traits with methods can.

However, given that std does specialize on Copy and I am quite confident this that won't change in the medium-term, anything relying on lifetime constraints of Copy for soundness is not a sound abstraction until then. I do think we should keep this open as a bug until either specialization is sound wrt to lifetimes or we've changed these Copy impls to be an error.

@theemathas
Copy link
Contributor Author

theemathas commented Nov 1, 2024

I found a somewhat popular crate (2K stars) that specifically depends on this Clone specialization existing, and depends on a Copy impl that is conditional on type equality.

https://github.com/AFLplusplus/LibAFL/blob/89cff637025c1652c24e8d97a30a2e3d01f187a4/libafl_bolts/src/tuples.rs#L27-L49

/// Returns if the type `T` is equal to `U`, ignoring lifetimes.

@purplesyringa
Copy link
Contributor

purplesyringa commented Nov 1, 2024

Note that it's possible to avoid this specialization by using the typeid crate in that case (without losing optimizations). But code like this might easily be duplicated all over the ecosystem.

@theemathas
Copy link
Contributor Author

lol, this issue has been known since 2020 in #71321

// FIXME(matthewjasper) This allows copying a type that doesn't implement
// `Copy` because of unsatisfied lifetime bounds (copying `A<'_>` when only
// `A<'static>: Copy` and `A<'_>: Clone`).
// We have this attribute here for now only because there are quite a few
// existing specializations on `Copy` that already exist in the standard
// library, and there's no way to safely have this behavior right now.

@purplesyringa
Copy link
Contributor

purplesyringa commented Nov 1, 2024

Note that there's plenty other uses of #[rustc_unsafe_specialization_marker]. None of the specializations call user-supplied code, but neither does the Copy spec and this issue still exists, so it might be time to consider the bigger picture. Removing one spec isn't going to fix all other misspecs.

@jieyouxu jieyouxu added A-specialization Area: Trait impl specialization and removed needs-triage This issue may need triage. Remove it if it has been sufficiently triaged. labels Nov 1, 2024
@lolbinarycat
Copy link
Contributor

It's also worth noting that there's nothing in the docs that says Copy and Clone shouldn't diverge. there probably should be a warning that this will cause weird problems.

@hanna-kruppe
Copy link
Contributor

https://doc.rust-lang.org/std/clone/trait.Clone.html#how-can-i-implement-clone

Types that are Copy should have a trivial implementation of Clone. More formally: if T: Copy, x: T, and y: &T, then let x = y.clone(); is equivalent to let x = *y;. Manual implementations should be careful to uphold this invariant; however, unsafe code must not rely on it to ensure memory safety.

@the8472
Copy link
Member

the8472 commented Nov 2, 2024

I found a somewhat popular crate (2K stars) that specifically depends on this Clone specialization existing, and depends on a Copy impl that is conditional on type equality.

https://github.com/AFLplusplus/LibAFL/blob/89cff637025c1652c24e8d97a30a2e3d01f187a4/libafl_bolts/src/tuples.rs#L27-L49

/// Returns if the type T is equal to U, ignoring lifetimes.

We don't need to accommodate that though, since it's not part of our stable API. I have asked people to not propagate this hack without attaching the appropriate warnings, but it appears that I'm shouting into the void.

@purplesyringa
Copy link
Contributor

People do that because it solves a problem other methods can't solve. Adding an intended solution to core would let people switch away from this workaround. Now, I'm not saying that's easy, and it'd require a better design and an RFC, but personally, I'd be fine with

macro_rules! implements_for_static {
    ($type:ty: $($trait:tt)*) => { /* compiler intrinsic */ };
};

such that implements_for_static!(T: Trait) if substituting all lifetimes in T for 'static results in a type that implements Trait. (I think this exactly matches the present specialization behavior, but I'm not 100% sure.)

@RalfJung
Copy link
Member

RalfJung commented Nov 3, 2024

it's invalid to implement Copy depending on lifetimes, and the compiler should probably lint/err at it.

I have not heard this as an official t-types opinion. If it is, we should start linting against this.

@apiraino
Copy link
Contributor

apiraino commented Nov 4, 2024

WG-prioritization assigning priority (Zulip discussion).

@rustbot label -I-prioritize +P-medium

@rustbot rustbot added P-medium Medium priority and removed I-prioritize Issue: Indicates that prioritization has been requested for this issue. labels Nov 4, 2024
@jieyouxu jieyouxu added the E-needs-investigation Call for partcipation: This issues needs some investigation to determine current status label Nov 4, 2024
@jieyouxu
Copy link
Member

jieyouxu commented Nov 4, 2024

E-needs-investigation: should figure out if a decision was made previously and why, and if this should be linted against going forward.

@joboet joboet linked a pull request Jan 17, 2025 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-specialization Area: Trait impl specialization C-bug Category: This is a bug. E-needs-investigation Call for partcipation: This issues needs some investigation to determine current status I-unsound Issue: A soundness hole (worst kind of bug), see: https://en.wikipedia.org/wiki/Soundness P-medium Medium priority T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-libs Relevant to the library team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging a pull request may close this issue.