You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
dyn Trait comparison should not include the vtable pointer #106447
This was previously raised in #103763 (comment). Currently, comparing a pair of *const dyn Trait raw pointers for equality will check that both the address and vtable are equal.
This is problematic because the vtable of a type is not guaranteed to be unique for each type: different types can share the same vtable and the same type can have its vtable instantiated multiple times.
To illustrate this issue, consider the following program which prints true in release mode and false in debug mode because LLVM is merging the two identical vtables (playground).
structA;structB;traitT{}implTforA{}implTforB{}fnmain(){let ab = (A,B);let a = &ab.0as*constdynT;let b = &ab.1as*constdynT;println!("{}", a == b);println!("{}", a as*constu8 == b as*constu8);}
It is also possible to achieve the reverse effect where two dyn Trait pointers to the same value (with the same base type) compare unequal because a separate vtable was instantiated with each pointer (e.g. due to codegen units or separate crates).
In conclusion, vtables are completely useless as a source of truth for pointer equality, so we should only consider the pointer address when comparing dyn Trait raw pointers. We currently document this as a footgun (#103567), but it would be better to eliminate this footgun entirely from the language.
That's only true because u8 and Foo have different Debug impls. However it is perfectly valid for the compiler to merge 2 vtables if they are identical:
traitT{}implTforu8{}implTforFoo{}#[repr(transparent)]structFoo(u8);let f = Foo(1);let p1:*constdynT = &f;let p2:*constdynT = &f.0;assert_ne!(p1, p2);// FAILS IN RELEASE MODE, passes in debug mode
True, but that doesn't solve the issue where multiple instances of the same vtable are emitted in different codegen units. I feel that overall this is a footgun in the language which people are already having to work around by explicitly casting to *mut u8.
The proposal as stated (just compare data pointers) means that x == y being true no longer means "these values are semantically equivalent". We feel that would be a breaking change, in that there could be sound code that checks whether x == y that would be broken.
We agree that the existing semantics of x == y (basically: if true, it means equivalent, but it may fail "randomly") are not desirable.
We think the desired semantics are that the data pointer is the same and the vtables are for the same type, but we understand that making vtables unique may not be viable across dynamic linking boundaries and so forth.
Are there other ways forward here? Do vtables always contain type-id, for example?
x == y can return true even if the values are not semantically equivalent. See my example in #106447 (comment) where the values compare equal despite having different types.
So today x == y provides no information: it can have false positives if the vtables are merged, and false negatives if multiple instances of the same vtable are emitted.
The type might be different, but the behaviour is since the code is the same.
I'm not sure that's true at the semantic level. The behavior is the same given the same bit patterns, but (if I understand correctly) hypothetically you could have two types with the same bit patterns that have differing meanings, and the same machine code would therefore have differing semantic meanings. Plus, we are now exposing implementation details of an impl at the level of language semantics.
For trait objects I can't come up with a realistic example other than what @Amanieu showed above, though.
I would add that merging of functions is not a purely hypothetical concern. For instance, I've seen it break backtraces where a function that calls abort() gets merged with a function in a completely unrelated file (another part of libc, or something written in another language) that does the same thing. So you get something like this:
Abort
1: abort in libc.so
2: some_random_function in some_random_lib.so
3: function_that_never_calls_the_above in your_code
4: something_else in your_code
...
N: main
This can be super confusing, especially when combined with inlining, when lacking line numbers (or they're unreliable) in a backtrace, and when (3) is a large function! And while it doesn't threaten language semantics to break backtraces (they're always best effort), it's an example of a situation where reasoning only about a function's behavior is not sufficient to recover the semantic meaning.
So today x == y provides no information: it can have false positives if the vtables are merged, and false negatives if multiple instances of the same vtable are emitted.
Fair, but I think it is close to the sense that I specified. It seems like we could prevent this merging by including e.g. type-id in the vtable.
I think that x == y should be entirely deterministic (assuming both x and y point to live objects). This means that it should always produce a correct result, no matter what optimization level is used or how the compiler splits codegen units.
Practically speaking, this leaves 2 options:
Only compare the data pointer, not the vtable. This is what I am proposing here.
Include type-ids in the vtable and do type comparison by comparing the type-ids instead of the vtable pointer. This comes at a cost in vtable size though.
Either option is fine and would make dyn trait comparison have actually sane results.
Activity
nbdd0121 commentedon Apr 6, 2023
This was already discussed in lang triage meeting on 2022-11-08 (notes: https://hackmd.io/@rust-lang-team/SyDavQuri) when
Arc::ptr_eqwas discussed.scottmcm commentedon Apr 18, 2023
I agree with this in the narrow, since for equality it's not useful.
But for inequality, one can rely on it today, no? For example,
So is it possible that people could be relying on this side of it already?
Amanieu commentedon Apr 21, 2023
That's only true because
u8andFoohave differentDebugimpls. However it is perfectly valid for the compiler to merge 2 vtables if they are identical:nbdd0121 commentedon Apr 21, 2023
We could change that by making vtable not
unnamed_addr.Amanieu commentedon Apr 21, 2023
True, but that doesn't solve the issue where multiple instances of the same vtable are emitted in different codegen units. I feel that overall this is a footgun in the language which people are already having to work around by explicitly casting to
*mut u8.nikomatsakis commentedon May 2, 2023
Discussed in @rust-lang/lang today. Conclusions:
x == ybeing true no longer means "these values are semantically equivalent". We feel that would be a breaking change, in that there could be sound code that checks whetherx == ythat would be broken.x == y(basically: if true, it means equivalent, but it may fail "randomly") are not desirable.Amanieu commentedon May 3, 2023
x == ycan return true even if the values are not semantically equivalent. See my example in #106447 (comment) where the values compare equal despite having different types.So today
x == yprovides no information: it can have false positives if the vtables are merged, and false negatives if multiple instances of the same vtable are emitted.nbdd0121 commentedon May 3, 2023
Well, x == y does mean that they will behave equivalently. The type might be different, but the behaviour is since the code is the same.
tmandry commentedon May 3, 2023
I'm not sure that's true at the semantic level. The behavior is the same given the same bit patterns, but (if I understand correctly) hypothetically you could have two types with the same bit patterns that have differing meanings, and the same machine code would therefore have differing semantic meanings. Plus, we are now exposing implementation details of an impl at the level of language semantics.
For trait objects I can't come up with a realistic example other than what @Amanieu showed above, though.
tmandry commentedon May 3, 2023
I would add that merging of functions is not a purely hypothetical concern. For instance, I've seen it break backtraces where a function that calls
abort()gets merged with a function in a completely unrelated file (another part of libc, or something written in another language) that does the same thing. So you get something like this:This can be super confusing, especially when combined with inlining, when lacking line numbers (or they're unreliable) in a backtrace, and when (3) is a large function! And while it doesn't threaten language semantics to break backtraces (they're always best effort), it's an example of a situation where reasoning only about a function's behavior is not sufficient to recover the semantic meaning.
nikomatsakis commentedon May 9, 2023
Fair, but I think it is close to the sense that I specified. It seems like we could prevent this merging by including e.g. type-id in the vtable.
Amanieu commentedon May 11, 2023
I think that
x == yshould be entirely deterministic (assuming bothxandypoint to live objects). This means that it should always produce a correct result, no matter what optimization level is used or how the compiler splits codegen units.Practically speaking, this leaves 2 options:
Either option is fine and would make dyn trait comparison have actually sane results.
28 remaining items
nikomatsakis commentedon Nov 8, 2023
@scottmcm is opening a new issue to track the lint.
nikomatsakis commentedon Nov 8, 2023
On a personal note, I would much prefer to fix the existing behavior by adding more info into the vtable.
scottmcm commentedon Nov 8, 2023
Issue for adding a lint: #117717
Auto merge of rust-lang#117758 - Urgau:lint_pointer_trait_comparisons…
Auto merge of #117758 - Urgau:lint_pointer_trait_comparisons, r=david…
Auto merge of #117758 - Urgau:lint_pointer_trait_comparisons, r=david…
Auto merge of #117758 - Urgau:lint_pointer_trait_comparisons, r=david…
fix(opcode/Ifacmp): compare objects by data pointer only
Auto merge of #117758 - Urgau:lint_pointer_trait_comparisons, r=david…
Auto merge of #117758 - Urgau:lint_pointer_trait_comparisons, r=david…
Auto merge of #117758 - Urgau:lint_pointer_trait_comparisons, r=david…
Auto merge of #117758 - Urgau:lint_pointer_trait_comparisons, r=david…
Auto merge of #117758 - Urgau:lint_pointer_trait_comparisons, r=david…