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

Consider warning when comparing wide pointers with vtables (as their address identity is unstable) #69757

Open
jdm opened this issue Mar 6, 2020 · 27 comments
Labels
A-lint Area: Lints (warnings about flaws in source code) such as unused_mut. A-trait-system Area: Trait system T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-lang Relevant to the language team, which will review and decide on the PR/issue.

Comments

@jdm
Copy link
Contributor

jdm commented Mar 6, 2020

vtable addresses may differ cross codegen units. To mitigate this, it would be good to have a lint that warns against comparing wide pointers with vtables.

Original report

This is a regression from the 2/27 to 2/28 nightly.

pub trait Trait {}
impl Trait for i32 {}

pub struct Obj<'a, T: 'static + Trait> {
    ptr: &'a T,
    trait_ptr: *const dyn Trait,
}

impl<'a, T: Trait + 'static> Drop for Obj<'a, T> {
    fn drop(&mut self) {
        assert_eq!(self.trait_ptr, self.ptr as *const dyn Trait);
    }
}

fn main() {
    let ptr = 5;
    let _name = Obj {
        ptr: &ptr,
        trait_ptr: &ptr,
    };
}

When this program is built with rustc main.rs, it runs without any trouble. When it's built with rustc main.rs -C incremental=, I receive the following output:

thread 'main' panicked at 'assertion failed: `(left == right)`
  left: `0x7ffee7d23864`,
 right: `0x7ffee7d23864`', main.rs:11:9
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

From the regression window, I suspect #67332.

@jdm jdm added the regression-from-stable-to-nightly Performance or correctness regression from stable to nightly. label Mar 6, 2020
@jdm jdm changed the title Different vtables for trait object pointers in debug builds Different vtables for trait object pointers in incremental builds Mar 6, 2020
@RalfJung
Copy link
Member

RalfJung commented Mar 6, 2020

Something is odd about your example, you are asserting them to be equal and then the error is that they are equal? When I run your code on playground, it works fine on nightly.

But also, note that there is no guarantee that all vtables for the same type get the same address, or that they get different addresses. Vtable address identity is an unstable implementation detail (similar to, for example, address identity of functions, or of promoteable references). In other words, I see no bug here. Could you describe the expected behavior and how that differs from the actual behavior?

@Centril Centril added T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-lang Relevant to the language team, which will review and decide on the PR/issue. labels Mar 6, 2020
@Centril
Copy link
Contributor

Centril commented Mar 6, 2020

@RalfJung From my reading of the OP, it seems like there's something particular about incremental compilation here, so maybe it's not reproducible on the playground?

@RalfJung
Copy link
Member

RalfJung commented Mar 6, 2020

I guess one question is whether wide ptr equality should even compare vtables, given that they are "unstable" in a sense. I am not sure if it does, but your tests indicate they do?

FWIW, the example on playground also works on stable, and in release mode. Looks like playground cannot reproduce the problem.

@RalfJung RalfJung added the A-incr-comp Area: Incremental compilation label Mar 6, 2020
@RalfJung
Copy link
Member

RalfJung commented Mar 6, 2020

Ah, I missed that incremental compilation is involved. Incremental compilation having an effect on vtable identity seems odd... I am not sure if we want to consider that a bug or not.^^

@RalfJung
Copy link
Member

RalfJung commented Mar 6, 2020

@jdm could you adjust your example to print not just the data address but also the vtable address? That might also explain why the output looks so strange.^^
(You seem to have already diagnosed that this is about different vtable pointers, but do not explain how you arrived at that conclusion?)

Here's a totally not supported way to do that:

fn fmt_with_vtable(ptr: *const dyn Trait) -> String {
    let (ptr, vtable): (*const u8, *const u8) = unsafe { std::mem::transmute(ptr) };
    format!("{:?} (vtable: {:?})", ptr, vtable)
}

@jonas-schievink
Copy link
Contributor

Incremental may result in different codegen unit partitioning, and each codegen unit might get its own vtable, so that part seems entirely expected to me. However the assertion failure seems to show the same address here?

thread 'main' panicked at 'assertion failed: `(left == right)`
  left: `0x7ffee7d23864`,
 right: `0x7ffee7d23864`', main.rs:11:9

@RalfJung
Copy link
Member

RalfJung commented Mar 6, 2020

My guess is that the main problem @jdm has is that comparing wide pointers also compares their metadata. For trait objects, this basically means equal pointers might "randomly" not be equal any more due to vtable address differences.

So either @jdm could cast the raw ptr to a thin ptr in their codebase to avoid relying on vtable identities, or else comparing wide raw pointers could ignore metadata in general -- though I expect for slices we might want it to compare the length?

@jonas-schievink
Copy link
Contributor

Oh, right, the vtable pointer just isn't printed

@nox
Copy link
Contributor

nox commented Mar 6, 2020

@RalfJung Do you mean that as a temporary workaround? It is definitely a regression that this broke. How codegen units are generated shouldn't influence whether two identical trait objects are indeed identical.

@nox
Copy link
Contributor

nox commented Mar 6, 2020

Or should PartialEq be implemented for *const T and *mut T only if T: Sized?

@tmiasko
Copy link
Contributor

tmiasko commented Mar 6, 2020

See also #46139, #48795 and #63021.

@nox
Copy link
Contributor

nox commented Mar 6, 2020 via email

@RalfJung
Copy link
Member

RalfJung commented Mar 6, 2020

Given there is ample precedence for "vtable identity is unstable and allowed to change with codegen units" (and there are three open issues about it so we don't need a 4th), should this issue be turned into one of the following discussions?

  • Better linting to prevent people from running into this.
  • Changing pointer equality test to not compare vtables any more when comparing wide pointers.
  • Making an effort to actually make vtables unique across the entire compilation unit.

@nox
Copy link
Contributor

nox commented Mar 6, 2020

Changing pointer equality test to not compare vtables any more when comparing wide pointers.

That seems to be the worst solution (among the three you suggested I mean). In Servo, there are two different tracing objects for a given T, depending on whether the T has been fully initialised or not. Granted, we can add safety guards on our side, but if we naively assumed that the vtable pointers were indeed inspected for equality, we could end up removing the tracing object for a fully initialised T when we meant to remove the older one, for when the T was not initialised yet.

@RalfJung
Copy link
Member

RalfJung commented Mar 6, 2020

@nox that also sounds like the lint would have a high risk of false positives, though.

@nox
Copy link
Contributor

nox commented Mar 6, 2020

@RalfJung Why? Our problem in Servo does not involve any generic T, it's all about *const dyn JSTraceable comparisons.

@RalfJung
Copy link
Member

RalfJung commented Mar 6, 2020

It just sounded like you actually do want to compare vtables in that case?

I was imagining the lint would propose to cast the pointer the *const u8 before comparing if vtables ought to be ignored, at least that part seems not to be what you want.

@nox
Copy link
Contributor

nox commented Mar 6, 2020

I was imagining the lint would propose to cast the pointer the *const u8 before comparing if vtables ought to be ignored, at least that part seems not to be what you want.

My reply was assuming that when you said "changing pointer equality test to not compare vtables any more when comparing wide pointers" you meant to change the test in the very implementationn of PartialEq in the standard library, which would lead us to not even notice there was a problem.

If you meant in our code in Servo, yeah, we should probably do that.

@RalfJung
Copy link
Member

RalfJung commented Mar 6, 2020

If you meant in our code in Servo, yeah, we should probably do that.

Wait didn't you just say that in Server you don't want to ignore the vtable? I am confused. Or did you want to say that Servo is being careful enough but hypothetically it would be easy to get wrong?

But anyway I just take you word for it that ignoring vtables would be a bad idea. There are other arguments for this as well (not least being implementation difficulty, as for slices we likely do want to compare the metadata).

@nox
Copy link
Contributor

nox commented Mar 6, 2020

Let's walk back a bit:

  • In Servo we currently rely on the vtable pointers being stable.
  • I thought you were saying you want PartialEq to ignore the vtable pointers.
  • That would lead to issues in current Servo because the vtable pointers are currently important, and ignoring the vtables in PartialEq itself would be a silent change for us.

So I'm saying that we should lint against trait object comparisons, and that in Servo we should stop relying on vtable pointers being stable and change the thing I mentioned earlier.

@nox
Copy link
Contributor

nox commented Mar 6, 2020

For the record you convinced me earlier that we need to change the code in Servo anyway because those vtable pointers were never stable, so I made the necessary changes etc in Servo. I guess this issue could be repurposed as needing a lint to report such comparisons.

@RalfJung RalfJung added A-trait-system Area: Trait system and removed regression-from-stable-to-nightly Performance or correctness regression from stable to nightly. labels Mar 7, 2020
@RalfJung RalfJung changed the title Different vtables for trait object pointers in incremental builds Consider warning when comparing wide pointers with vtables (as their address identitiy is unstable) Mar 7, 2020
@RalfJung RalfJung added A-lint Area: Lints (warnings about flaws in source code) such as unused_mut. and removed A-incr-comp Area: Incremental compilation labels Mar 7, 2020
@RalfJung
Copy link
Member

RalfJung commented Mar 7, 2020

All right I edited the issue title and description accordingly.

@nox nox changed the title Consider warning when comparing wide pointers with vtables (as their address identitiy is unstable) Consider warning when comparing wide pointers with vtables (as their address identity is unstable) Mar 7, 2020
@tmiasko
Copy link
Contributor

tmiasko commented Mar 9, 2020

There is also an opposite issue: addresses of virtual tables associated with
different impls might compare equal, since they are marked with unnamed_addr.

It is possible to produce such cases after repeating some optimization passes.
First all methods from vtables are merged together, and then vtables themselves are.

bors added a commit to rust-lang/rust-clippy that referenced this issue Mar 30, 2020
Lint unnamed address comparisons

Functions and vtables have an insignificant address. Attempts to compare such addresses will lead to very surprising behaviour. For example: addresses of different functions could compare equal; two trait object pointers representing the same object and the same type could be unequal.

Lint against unnamed address comparisons to avoid issues like those in rust-lang/rust#69757 and rust-lang/rust#54685.

Changelog: New lints: [`fn_address_comparisons`] [#5294](#5294), [`vtable_address_comparisons`] [#5294](#5294)
bors added a commit to rust-lang/rust-clippy that referenced this issue Mar 30, 2020
Lint unnamed address comparisons

Functions and vtables have an insignificant address. Attempts to compare such addresses will lead to very surprising behaviour. For example: addresses of different functions could compare equal; two trait object pointers representing the same object and the same type could be unequal.

Lint against unnamed address comparisons to avoid issues like those in rust-lang/rust#69757 and rust-lang/rust#54685.

changelog: New lints: [`fn_address_comparisons`] [#5294](#5294), [`vtable_address_comparisons`] [#5294](#5294)
@redradist
Copy link

redradist commented May 11, 2020

From my point of view this issue should be fixed, because it is completely not obvious that the same trait object has different fat pointers ... :(

https://users.rust-lang.org/t/rwlock-hashmap-no-entry-found/42428/15

@Aaron1011
Copy link
Member

If we don't want to guarantee that vtables are unique, then I think a lint would need to effectively "deprecate" the Eq/PartialEq impls for *[const/mut] T and [&/&mut] T when T: !Sized (assuming that we want to lint on something like HashSet<*const T> where T: !Sized). The only alternative I could see would be to do some kind of post-monomorphization check, which I think could lead to very confusing error messages.

I'm not sure how noisy this would end up being, or how difficult it would be to implement.

@redradist
Copy link

redradist commented May 12, 2020

@Aaron1011

If we don't want to guarantee that vtables are unique, then I think a lint would need to effectively "deprecate" the Eq/PartialEq impls for *[const/mut] T and [&/&mut] T when T: !Sized (assuming that we want to lint on something like HashSet<*const T> where T: !Sized). The only alternative I could see would be to do some kind of post-monomorphization check, which I think could lead to very confusing error messages.

I'm not sure how noisy this would end up being, or how difficult it would be to implement.

I am not sure if rust should to have different vtables for implementation of trait for the same object
because it does not seems like Zero-Overhead Abstraction !!
We have two vtables instead of one and for lots of traits it increases the binary size !!

I think the best that could be done is to stabilize trait object vtable address for the same object ...

matklad added a commit to matklad/nearcore that referenced this issue Apr 11, 2022
* use consistent spelling for `io::Reslut`
* repplace `e.into()` with more explicit `io::Error::from`
* implement From rather than Into
* reduce minore code duplication by delegating to existing functions
* use Arc::clone to make it explicit flow around store's identity
* use buffered writer when dumping store to a file
* fix latent bug around comparing wide pointers rust-lang/rust#69757.
  I thing we should not be hit by that due to full LTO, but that was a scary one.
* remove some extra clones when working with cache
near-bulldozer bot pushed a commit to near/nearcore that referenced this issue Apr 11, 2022
* use consistent spelling for `io::Reslut`
* replace `e.into()` with more explicit `io::Error::from`
* implement From rather than Into
* reduce minor code duplication by delegating to existing functions
* use Arc::clone to make it explicit flow around store's identity
* use buffered writer when dumping store to a file
* fix latent bug around comparing wide pointers rust-lang/rust#69757.
  I thing we should not be hit by that due to full LTO, but that was a scary one.
* remove some extra clones when working with cache
pompon0 pushed a commit to near/nearcore that referenced this issue Apr 15, 2022
* use consistent spelling for `io::Reslut`
* replace `e.into()` with more explicit `io::Error::from`
* implement From rather than Into
* reduce minor code duplication by delegating to existing functions
* use Arc::clone to make it explicit flow around store's identity
* use buffered writer when dumping store to a file
* fix latent bug around comparing wide pointers rust-lang/rust#69757.
  I thing we should not be hit by that due to full LTO, but that was a scary one.
* remove some extra clones when working with cache
@RalfJung
Copy link
Member

RalfJung commented Jan 9, 2024

I think this has been closed by #117758, or is there anything left to do?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-lint Area: Lints (warnings about flaws in source code) such as unused_mut. A-trait-system Area: Trait system T-compiler Relevant to the compiler team, which will review and decide on the PR/issue. T-lang Relevant to the language team, which will review and decide on the PR/issue.
Projects
None yet
Development

No branches or pull requests

8 participants