-
Notifications
You must be signed in to change notification settings - Fork 13.7k
Implement some more checks in ptr_guaranteed_cmp
.
#144885
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
base: master
Are you sure you want to change the base?
Conversation
r? @davidtwco rustbot has assigned @davidtwco. Use |
Are two different immutable statics required to have distinct addresses? In other words, is the compiler allowed to optimize two immutable statics with identical contents to be stored at the same place? |
I don't think it is, but this also restricts people doing their own custom linking shenanigans, so we should have some stable docs to point to before having logic of this sort in const-eval. That sounds like something t-lang should sign off on. |
If you just want to fix #144584, please reduce the PR to only affect the case where both pointers are based on the same static. |
Yes, they are required to have distinct addresses (if non-zero-sized). No, the compiler is not allowed to coalesce two immutable statics with the same contents.
This is guaranteed for non-zero-sized statics, in the Reference:
This (non-zero-sized statics not overlapping) has been FCP'd by T-lang here (though not anything about consteval specifically) |
r? @RalfJung |
I would prefer to land all of these changes, but if "Pointers to the same static allocation are equal if and only if they have the same offset" is more straightforward to land than the other two, then I can split them into a followup PR. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's an impressive test, thanks a lot for writing it!
// runtime inequality of pointers to different ones) (see e.g. #73722). | ||
Some(GlobalAlloc::Function { .. } | GlobalAlloc::VTable(..)) => 2, | ||
// FIXME: Can these be duplicated (or deduplicated)? | ||
Some(GlobalAlloc::Memory(..) | GlobalAlloc::TypeId { .. }) => 2, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently Memory
definitely can be deduplicated. TypeId
is its own thing that exists mostly to prevent const-eval from comparing it so we should definitely always return 2
there.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, I'll split and update the comment for TypeId
here.
I'll update the comment in the different-AllocId
branch to state that Memory
can be deduplicated (and Vtable
and Function
too).
Can Memory
be duplicated? That is, should two pointers to the same Memory
allocation (at the same offset) compare equal, or be uncomparable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can Memory be duplicated? That is, should two pointers to the same Memory allocation (at the same offset) compare equal, or be uncomparable?
Until we fix #128775, yes that can happen.
Latest push addresses some parts of review comments, moves the test to |
I would have preferred if you didn't extend the scope of the PR after I already did most of my review. Now I'll have to re-review, so back on the queue it goes. |
This is a separate commit. If you prefer I can split this into a separate PR. |
This comment has been minimized.
This comment has been minimized.
if a_allocid == b_allocid { | ||
match self.tcx.try_get_global_alloc(a_allocid) { | ||
None => 2, | ||
// A static cannot be duplicated, so if two pointers are into the same | ||
// static, they are equal if and only if their offsets into the static | ||
// are equal | ||
Some(GlobalAlloc::Static(_)) => (a_offset == b_offset) as u8, | ||
// Functions and vtables can be duplicated (and deduplicated), so we | ||
// cannot be sure of runtime equality of pointers to the same one, (or the | ||
// runtime inequality of pointers to different ones) (see e.g. #73722). | ||
Some(GlobalAlloc::Function { .. } | GlobalAlloc::VTable(..)) => 2, | ||
// FIXME: Revisit this once https://github.com/rust-lang/rust/issues/128775 | ||
// is fixed. | ||
Some(GlobalAlloc::Memory(..)) => 2, | ||
// `GlobalAlloc::TypeId` exists mostly to prevent consteval from comparing | ||
// `TypeId`s, always return 2 | ||
Some(GlobalAlloc::TypeId { .. }) => 2, | ||
} | ||
} else { | ||
if let (Some(GlobalAlloc::Static(a_did)), Some(GlobalAlloc::Static(b_did))) = ( | ||
self.tcx.try_get_global_alloc(a_allocid), | ||
self.tcx.try_get_global_alloc(b_allocid), | ||
) { | ||
debug_assert_ne!( | ||
a_did, b_did, | ||
"same static item DefId had two different AllocIds? {a_allocid:?} != {b_allocid:?}, {a_did:?} == {b_did:?}" | ||
); | ||
|
||
if a_info.size == Size::ZERO || b_info.size == Size::ZERO { | ||
// One or both allocations is zero-sized, so we can't know if the | ||
// pointers are (in)equal. | ||
// FIXME: Can zero-sized static be "within" non-zero-sized statics? | ||
// Conservatively we say yes, since that doesn't cause them to | ||
// "overlap" any bytes, but if not, then we could delete this branch; | ||
// the other branches would already handle ZST allocations correctly. | ||
2 | ||
} else if a_offset > a_info.size || b_offset > b_info.size { | ||
// One or both pointers are out of bounds of their allocation, | ||
// so conservatively say we don't know. | ||
// FIXME: we could reason about how far out of bounds the pointers are, | ||
// e.g. two pointers cannot be equal if them being equal would require | ||
// their statics to overlap. | ||
2 | ||
} else if (a_offset == Size::ZERO && b_offset == b_info.size) | ||
|| (a_offset == a_info.size && b_offset == Size::ZERO) | ||
{ | ||
// The pointers are on opposite ends of different allocations, we | ||
// cannot know if they are equal, since the allocations may end up | ||
// adjacent at runtime. | ||
2 | ||
} else { | ||
// The pointers are within (or one past the end of) different | ||
// non-zero-sized static allocations, and they are not at oppotiste | ||
// ends, so we know they are not equal because non-zero-sized statics | ||
// cannot overlap or be deduplicated, as per | ||
// https://doc.rust-lang.org/nightly/reference/items/static-items.html#r-items.static.intro | ||
// (non-deduplication), and | ||
// https://doc.rust-lang.org/nightly/reference/items/static-items.html#r-items.static.storage-disjointness | ||
// (non-overlapping) | ||
0 | ||
} | ||
} else { | ||
// Even if one of them is a static, as per https://doc.rust-lang.org/nightly/reference/items/static-items.html#r-items.static.storage-disjointness | ||
// immutable statics can overlap with other kinds of allocations sometimes. | ||
// FIXME: We could be more decisive for (non-zero-sized) mutable statics, | ||
// which cannot overlap with other kinds of allocations. | ||
// `GlobalAlloc::{Memory, Function, Vtable}` can at least be deduplicated with | ||
// the same kind, so comparing two of the same kind of those should return 2. | ||
// `GlobalAlloc::TypeId` exists mostly to prevent consteval from comparing | ||
// `TypeId`s, so comparing two of those should always return 2. | ||
// FIXME: Can we determine any other cases? | ||
2 | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In this whole discussion, we only return !=2 if both allocations are statics. Could this be simplified a lot by first checking that allocations are statics, and only then compare alloc ids?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, that would work. I think I did it this way to have the comments be more precise to which cases they cover, but the comments could definitely be combined, especially for the cases that can both be duplicated and de-duplicated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Latest push does this.
if a_info.size == Size::ZERO || b_info.size == Size::ZERO { | ||
// One or both allocations is zero-sized, so we can't know if the | ||
// pointers are (in)equal. | ||
// FIXME: Can zero-sized static be "within" non-zero-sized statics? | ||
// Conservatively we say yes, since that doesn't cause them to | ||
// "overlap" any bytes, but if not, then we could delete this branch; | ||
// the other branches would already handle ZST allocations correctly. | ||
2 | ||
} else if a_offset > a_info.size || b_offset > b_info.size { | ||
// One or both pointers are out of bounds of their allocation, | ||
// so conservatively say we don't know. | ||
// FIXME: we could reason about how far out of bounds the pointers are, | ||
// e.g. two pointers cannot be equal if them being equal would require | ||
// their statics to overlap. | ||
2 | ||
} else if (a_offset == Size::ZERO && b_offset == b_info.size) | ||
|| (a_offset == a_info.size && b_offset == Size::ZERO) | ||
{ | ||
// The pointers are on opposite ends of different allocations, we | ||
// cannot know if they are equal, since the allocations may end up | ||
// adjacent at runtime. | ||
2 | ||
} else { | ||
// The pointers are within (or one past the end of) different | ||
// non-zero-sized static allocations, and they are not at oppotiste | ||
// ends, so we know they are not equal because non-zero-sized statics | ||
// cannot overlap or be deduplicated, as per | ||
// https://doc.rust-lang.org/nightly/reference/items/static-items.html#r-items.static.intro | ||
// (non-deduplication), and | ||
// https://doc.rust-lang.org/nightly/reference/items/static-items.html#r-items.static.storage-disjointness | ||
// (non-overlapping) | ||
0 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we simplify this whole discussion with as "if both offsets are strictly in bounds, then return 0, else return 2"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If by "strictly in bounds", you are excluding the one-past-the-end address, then yes that would work, but would return 2
in some cases where we can know the answer, e.g. comparing one-past-the-end pointers to two non-zero-sized statics.
Thinking some more about this though, I think the fact that "pointers to different statics cannot be equal if them being equal would require their statics to overlap" could be used to make this section simpler, while still returning 0
for two one-past-the-end pointers; e.g:
if a_info.size == Size::ZERO || b_info.size == Size::ZERO {
2 // as above, but remove the "we could delete this branch", since the below branches assume non-zero-sized allocations
} else if a_offset > b_offset {
// Implement this using a match on Ord::cmp instead of chained ifs
// assuming the two pointers had equal addresses, this would be the differece in address of
// the base of their allocations, with `a`'s allocation at lower address.
let diff = a_offset - b_offset;
if a_info.size > diff {
// The pointers cannot be equal, because that would imply their non-zero-sized allocations overlap
0
} else {
2
}
} else if a_offset < b_offset {
// assuming the two pointers had equal addresses, this would be the difference in address of
// the base of their allocations, with `b`'s allocation at lower address.
let diff = b_offset - a_offset;
if b_info.size > diff {
// The pointers cannot be equal, because that would imply their non-zero-sized allocations overlap
0
} else {
2
}
} else if a_offset == b_offset {
// assuming the two pointers had equal addresses, their allocations would have the same base address.
// The pointers cannot be equal, because that would imply their non-zero-sized allocations overlap
0
} else { unreachable!{} }
I think this handles every case currently handled, and more cases where a (or both) pointer is out-of-bounds of its allocation, but in such a way that they still cannot be equal.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please try to do as simple as possible. As a rule of thumb, it's better to list cases where we ensure stuff and return 0 or 1, and have the fallback to "I don't know". A "both inbound then 1", else "some special case then 1" else 2 is the best way.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a lot harder to think about, and only makes a difference when comparing out-of-bounds pointers... I don't think it's worth it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Latest push only returns unequal if both pointers are strictly in-bounds of their own allocations for simplicity.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't actually mind the old logic you proposed first (with the "different ends of the allocation" check), not sure why you removed that...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess you got mixed signaling here, I don't agree with @cjgillot.
So let's just go with the simple thing now, this constant back-and-forth is a terrible waste of review time.
tests/ui/consts/ptr_comparisons.rs
Outdated
// aside from 0, these pointers might end up pretty much anywhere. | ||
check!(!, FOO as *const _, 1); // this one could be `ne` by taking into account alignment | ||
check!(!, FOO as *const _, 1024); | ||
do_test!(&A, align_of::<T>() as *const u8, None); | ||
do_test!(&A, 1 as *const u8, Some(false)); // this one takes into account alignment, so we know that |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This grouping no longer makes sense... you first say "can end up anywhere" but then still have a Some
test.
Please also ensure that (&raw const A).wrapping_byte_add(1)
might be equal to 1. (I guess technically we could exclude that since statics cannot live at address 0.... but at some point we have to stop adding more cases to this PR.^^)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please also ensure that
(&raw const A).wrapping_byte_add(1)
might be equal to 1. (I guess technically we could exclude that since statics cannot live at address 0.... but at some point we have to stop adding more cases to this PR.^^)
(The change added in 2a24220 makes this known to never occur)
// Other ways of comparing integers and pointers can never be known for sure, | ||
// except for alignment, e.g. `1 as *const _` can never be equal to an even offset | ||
// in an `align(2)` allocation. | ||
(Scalar::Int(int), Scalar::Ptr(ptr, _)) | (Scalar::Ptr(ptr, _), Scalar::Int(int)) => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another possible implementation here would be to compute ptr.wrapping_sub(int)
and then compare that against null. scalar_may_be_null
already handles alignment so that should cover the same cases and more, I think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added in 2a24220
// and `fn` pointers cannot be null, and `is_null` is stable with strong guarantees, and | ||
// `is_null` is implemented using `guaranteed_cmp`. | ||
do_test!(&A, 0 as *const u8, Some(false)); | ||
do_test!((&raw const A).cast::<u8>().wrapping_add(1), 0 as *const u8, Some(false)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please also add a case that adds 11
or so to ensure we hit the alignment code path, not the inbounds code path.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added in edbafa2 (as size_of::<T>() + 1
)
// This pointer is out-of-bounds, but still cannot be equal to 0 because of alignment.
do_test!((&raw const A).cast::<u8>().wrapping_add(size_of::<T>() + 1), 0 as *const u8, Some(false));
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, let's go with this logic for now. :)
Just some comment nits.
This looks great, thanks! Please squash the commits, then we can land this. Then write @rustbot author |
Reminder, once the PR becomes ready for a review, use |
Pointers with different residues modulo their least common allocation alignment are never equal. Pointers to the same static allocation are equal if and only if they have the same offset. Strictly in-bounds (in-bounds and not one-past-the-end) pointers to different static allocations are always unequal. A pointer cannot be equal to an integer if `ptr-int` cannot be null. Also adds more tests for `ptr_guaranteed_cmp`. Co-authored-by: Ralf Jung <[email protected]>
5e706b9
to
10fde9e
Compare
@rustbot ready |
@bors r+ |
…=RalfJung Implement some more checks in `ptr_guaranteed_cmp`. * Pointers with different residues modulo their allocations' least common alignment are never equal. * Pointers to the same static allocation are equal if and only if they have the same offset. * Pointers to different non-zero-sized static allocations are unequal if both point within their allocation, and not on opposite ends. Tracking issue for `const_raw_ptr_comparison`: <rust-lang#53020> This should not affect `is_null`, the only usage of this intrinsic on stable. Closes rust-lang#144584
…=RalfJung Implement some more checks in `ptr_guaranteed_cmp`. * Pointers with different residues modulo their allocations' least common alignment are never equal. * Pointers to the same static allocation are equal if and only if they have the same offset. * Pointers to different non-zero-sized static allocations are unequal if both point within their allocation, and not on opposite ends. Tracking issue for `const_raw_ptr_comparison`: <rust-lang#53020> This should not affect `is_null`, the only usage of this intrinsic on stable. Closes rust-lang#144584
…=RalfJung Implement some more checks in `ptr_guaranteed_cmp`. * Pointers with different residues modulo their allocations' least common alignment are never equal. * Pointers to the same static allocation are equal if and only if they have the same offset. * Pointers to different non-zero-sized static allocations are unequal if both point within their allocation, and not on opposite ends. Tracking issue for `const_raw_ptr_comparison`: <rust-lang#53020> This should not affect `is_null`, the only usage of this intrinsic on stable. Closes rust-lang#144584
Rollup of 6 pull requests Successful merges: - #144531 (Add lint against integer to pointer transmutes) - #144885 (Implement some more checks in `ptr_guaranteed_cmp`. ) - #145307 (Fix `LazyLock` poison panic message) - #145554 (rustc-dev-guide subtree update) - #145798 (Use unnamed lifetime spans as primary spans for `MISMATCHED_LIFETIME_SYNTAXES`) - #145799 (std/src/lib.rs: mention "search button" instead of "search bar") r? `@ghost` `@rustbot` modify labels: rollup
All function pointers are currently treated as unaligned anyway; any change implementing function pointer alignment during consteval should add tests that it works properly on arm::t32 functions.
Latest commit removes the failing @rustbot ready |
Tracking issue for
const_raw_ptr_comparison
: #53020This should not affect
is_null
, the only usage of this intrinsic on stable.Closes #144584