Skip to content

Commit e011dd4

Browse files
authored
Rollup merge of #144885 - zachs18:ptr_guaranteed_cmp_more, 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`: <#53020> This should not affect `is_null`, the only usage of this intrinsic on stable. Closes#144584
2 parents 2741508 + 25afbbc commit e011dd4

2 files changed

Lines changed: 295 additions & 45 deletions

File tree

‎compiler/rustc_const_eval/src/const_eval/machine.rs‎

Lines changed: 103 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -280,22 +280,110 @@ impl<'tcx> CompileTimeInterpCx<'tcx> {
280280
interp_ok(match(a, b){
281281
// Comparisons between integers are always known.
282282
(Scalar::Int(a),Scalar::Int(b)) => (a == b)asu8,
283-
// Comparisons of null with an arbitrary scalar can be known if `scalar_may_be_null`
284-
// indicates that the scalar can definitely *not* be null.
285-
(Scalar::Int(int), ptr) | (ptr,Scalar::Int(int))
286-
if int.is_null() && !self.scalar_may_be_null(ptr)? =>
287-
{
288-
0
283+
// Comparing a pointer `ptr` with an integer `int` is equivalent to comparing
284+
// `ptr-int` with null, so we can reduce this case to a `scalar_may_be_null` test.
285+
(Scalar::Int(int),Scalar::Ptr(ptr, _)) | (Scalar::Ptr(ptr, _),Scalar::Int(int)) => {
286+
let int = int.to_target_usize(*self.tcx);
287+
// The `wrapping_neg` here may produce a value that is not
288+
// a valid target usize any more... but `wrapping_offset` handles that correctly.
289+
let offset_ptr = ptr.wrapping_offset(Size::from_bytes(int.wrapping_neg()),self);
290+
if !self.scalar_may_be_null(Scalar::from_pointer(offset_ptr,self))? {
291+
// `ptr.wrapping_sub(int)` is definitely not equal to `0`, so `ptr != int`
292+
0
293+
}else{
294+
// `ptr.wrapping_sub(int)` could be equal to `0`, but might not be,
295+
// so we cannot know for sure if `ptr == int` or not
296+
2
297+
}
298+
}
299+
(Scalar::Ptr(a, _),Scalar::Ptr(b, _)) => {
300+
let(a_prov, a_offset) = a.prov_and_relative_offset();
301+
let(b_prov, b_offset) = b.prov_and_relative_offset();
302+
let a_allocid = a_prov.alloc_id();
303+
let b_allocid = b_prov.alloc_id();
304+
let a_info = self.get_alloc_info(a_allocid);
305+
let b_info = self.get_alloc_info(b_allocid);
306+
307+
// Check if the pointers cannot be equal due to alignment
308+
if a_info.align > Align::ONE && b_info.align > Align::ONE{
309+
let min_align = Ord::min(a_info.align.bytes(), b_info.align.bytes());
310+
let a_residue = a_offset.bytes() % min_align;
311+
let b_residue = b_offset.bytes() % min_align;
312+
if a_residue != b_residue {
313+
// If the two pointers have a different residue modulo their
314+
// common alignment, they cannot be equal.
315+
returninterp_ok(0);
316+
}
317+
// The pointers have the same residue modulo their common alignment,
318+
// so they could be equal. Try the other checks.
319+
}
320+
321+
iflet(Some(GlobalAlloc::Static(a_did)),Some(GlobalAlloc::Static(b_did))) = (
322+
self.tcx.try_get_global_alloc(a_allocid),
323+
self.tcx.try_get_global_alloc(b_allocid),
324+
){
325+
if a_allocid == b_allocid {
326+
debug_assert_eq!(
327+
a_did, b_did,
328+
"different static item DefIds had same AllocId? {a_allocid:?} == {b_allocid:?}, {a_did:?} != {b_did:?}"
329+
);
330+
// Comparing two pointers into the same static. As per
331+
// https://doc.rust-lang.org/nightly/reference/items/static-items.html#r-items.static.intro
332+
// a static cannot be duplicated, so if two pointers are into the same
333+
// static, they are equal if and only if their offsets are equal.
334+
(a_offset == b_offset)asu8
335+
}else{
336+
debug_assert_ne!(
337+
a_did, b_did,
338+
"same static item DefId had two different AllocIds? {a_allocid:?} != {b_allocid:?}, {a_did:?} == {b_did:?}"
339+
);
340+
// Comparing two pointers into the different statics.
341+
// We can never determine for sure that two pointers into different statics
342+
// are *equal*, but we can know that they are *inequal* if they are both
343+
// strictly in-bounds (i.e. in-bounds and not one-past-the-end) of
344+
// their respective static, as different non-zero-sized statics cannot
345+
// overlap or be deduplicated as per
346+
// https://doc.rust-lang.org/nightly/reference/items/static-items.html#r-items.static.intro
347+
// (non-deduplication), and
348+
// https://doc.rust-lang.org/nightly/reference/items/static-items.html#r-items.static.storage-disjointness
349+
// (non-overlapping).
350+
if a_offset < a_info.size && b_offset < b_info.size{
351+
0
352+
}else{
353+
// Otherwise, conservatively say we don't know.
354+
// There are some cases we could still return `0` for, e.g.
355+
// if the pointers being equal would require their statics to overlap
356+
// one or more bytes, but for simplicity we currently only check
357+
// strictly in-bounds pointers.
358+
2
359+
}
360+
}
361+
}else{
362+
// All other cases we conservatively say we don't know.
363+
//
364+
// For comparing statics to non-statics, as per https://doc.rust-lang.org/nightly/reference/items/static-items.html#r-items.static.storage-disjointness
365+
// immutable statics can overlap with other kinds of allocations sometimes.
366+
//
367+
// FIXME: We could be more decisive for (non-zero-sized) mutable statics,
368+
// which cannot overlap with other kinds of allocations.
369+
//
370+
// Functions and vtables can be duplicated and deduplicated, so we
371+
// cannot be sure of runtime equality of pointers to the same one, or the
372+
// runtime inequality of pointers to different ones (see e.g. #73722),
373+
// so comparing those should return 2, whether they are the same allocation
374+
// or not.
375+
//
376+
// `GlobalAlloc::TypeId` exists mostly to prevent consteval from comparing
377+
// `TypeId`s, so comparing those should always return 2, whether they are the
378+
// same allocation or not.
379+
//
380+
// FIXME: We could revisit comparing pointers into the same
381+
// `GlobalAlloc::Memory` once https://github.com/rust-lang/rust/issues/128775
382+
// is fixed (but they can be deduplicated, so comparing pointers into different
383+
// ones should return 2).
384+
2
385+
}
289386
}
290-
// Other ways of comparing integers and pointers can never be known for sure.
291-
(Scalar::Int{ .. },Scalar::Ptr(..)) | (Scalar::Ptr(..),Scalar::Int{ .. }) => 2,
292-
// FIXME: return a `1` for when both sides are the same pointer, *except* that
293-
// some things (like functions and vtables) do not have stable addresses
294-
// so we need to be careful around them (see e.g. #73722).
295-
// FIXME: return `0` for at least some comparisons where we can reliably
296-
// determine the result of runtime inequality tests at compile-time.
297-
// Examples include comparison of addresses in different static items.
298-
(Scalar::Ptr(..),Scalar::Ptr(..)) => 2,
299387
})
300388
}
301389
}

‎tests/ui/consts/ptr_comparisons.rs‎

Lines changed: 192 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,205 @@
11
//@ compile-flags: --crate-type=lib
22
//@ check-pass
3+
//@ edition: 2024
4+
#![feature(const_raw_ptr_comparison)]
5+
#![feature(fn_align)]
6+
// Generally:
7+
// For any `Some` return, `None` would also be valid, unless otherwise noted.
8+
// For any `None` return, only `None` is valid, unless otherwise noted.
39

4-
#![feature(
5-
core_intrinsics,
6-
const_raw_ptr_comparison,
7-
)]
10+
macro_rules! do_test {
11+
($a:expr, $b:expr, $expected:pat) => {
12+
const _:() = {
13+
let a:*const _ = $a;
14+
let b:*const _ = $b;
15+
assert!(matches!(<*constu8>::guaranteed_eq(a.cast(), b.cast()), $expected));
16+
};
17+
};
18+
}
819

9-
constFOO:&usize = &42;
20+
#[repr(align(2))]
21+
structT(#[allow(unused)]u16);
1022

11-
macro_rules! check {
12-
(eq, $a:expr, $b:expr) => {
13-
pubconst _:() =
14-
assert!(std::intrinsics::ptr_guaranteed_cmp($a as*constu8, $b as*constu8) == 1);
15-
};
16-
(ne, $a:expr, $b:expr) => {
17-
pubconst _:() =
18-
assert!(std::intrinsics::ptr_guaranteed_cmp($a as*constu8, $b as*constu8) == 0);
23+
#[repr(align(2))]
24+
structAlignedZst;
25+
26+
staticA:T = T(42);
27+
staticB:T = T(42);
28+
staticmutMUT_STATIC:T = T(42);
29+
staticZST:() = ();
30+
staticALIGNED_ZST:AlignedZst = AlignedZst;
31+
staticLARGE_WORD_ALIGNED:[usize;2] = [0,1];
32+
staticmutMUT_LARGE_WORD_ALIGNED:[usize;2] = [0,1];
33+
34+
constFN_PTR:*const() = {
35+
fnfoo(){}
36+
unsafe{ std::mem::transmute(foo asfn())}
37+
};
38+
39+
constALIGNED_FN_PTR:*const() = {
40+
#[rustc_align(2)]
41+
fnaligned_foo(){}
42+
unsafe{ std::mem::transmute(aligned_foo asfn())}
43+
};
44+
45+
traitTrait{
46+
#[allow(unused)]
47+
fnmethod(&self) -> u8;
48+
}
49+
implTraitforu32{
50+
fnmethod(&self) -> u8{1}
51+
}
52+
implTraitfori32{
53+
fnmethod(&self) -> u8{2}
54+
}
55+
56+
constVTABLE_PTR_1:*const() = {
57+
let[_data, vtable] = unsafe{
58+
std::mem::transmute::<&dynTrait,[*const();2]>(&42_u32as&dynTrait)
1959
};
20-
(!, $a:expr, $b:expr) => {
21-
pubconst _:() =
22-
assert!(std::intrinsics::ptr_guaranteed_cmp($a as*constu8, $b as*constu8) == 2);
60+
vtable
61+
};
62+
constVTABLE_PTR_2:*const() = {
63+
let[_data, vtable] = unsafe{
64+
std::mem::transmute::<&dynTrait,[*const();2]>(&42_i32as&dynTrait)
2365
};
24-
}
66+
vtable
67+
};
2568

26-
check!(eq,0,0);
27-
check!(ne,0,1);
28-
check!(ne,FOOas*const _,0);
29-
check!(ne,unsafe{(FOOas*constusize).offset(1)},0);
30-
check!(ne,unsafe{(FOOas*constusizeas*constu8).offset(3)},0);
69+
// Cannot be `None`: `is_null` is stable with strong guarantees about integer-valued pointers.
70+
do_test!(0as*constu8,0as*constu8,Some(true));
71+
do_test!(0as*constu8,1as*constu8,Some(false));
3172

32-
// We want pointers to be equal to themselves, but aren't checking this yet because
33-
// there are some open questions (e.g. whether function pointers to the same function
34-
// compare equal: they don't necessarily do at runtime).
35-
check!(!,FOOas*const _,FOOas*const _);
73+
// Integer-valued pointers can always be compared.
74+
do_test!(1as*constu8,1as*constu8,Some(true));
75+
do_test!(1as*constu8,2as*constu8,Some(false));
76+
77+
// Cannot be `None`: `static`s' addresses, references, (and within and one-past-the-end of those),
78+
// and `fn` pointers cannot be null, and `is_null` is stable with strong guarantees, and
79+
// `is_null` is implemented using `guaranteed_cmp`.
80+
do_test!(&A,0as*constu8,Some(false));
81+
do_test!((&raw constA).cast::<u8>().wrapping_add(1),0as*constu8,Some(false));
82+
do_test!((&raw constA).wrapping_add(1),0as*constu8,Some(false));
83+
do_test!(&ZST,0as*constu8,Some(false));
84+
do_test!(&(),0as*constu8,Some(false));
85+
do_test!(const{&()},0as*constu8,Some(false));
86+
do_test!(FN_PTR,0as*constu8,Some(false));
87+
88+
// This pointer is out-of-bounds, but still cannot be equal to 0 because of alignment.
89+
do_test!((&raw constA).cast::<u8>().wrapping_add(size_of::<T>() + 1),0as*constu8,Some(false));
3690

3791
// aside from 0, these pointers might end up pretty much anywhere.
38-
check!(!,FOOas*const _,1);// this one could be `ne` by taking into account alignment
39-
check!(!,FOOas*const _,1024);
92+
do_test!(&A, align_of::<T>()as*constu8,None);
93+
do_test!((&raw constA).wrapping_byte_add(1),(align_of::<T>() + 1)as*constu8,None);
94+
95+
// except that they must still be aligned
96+
do_test!(&A,1as*constu8,Some(false));
97+
do_test!((&raw constA).wrapping_byte_add(1), align_of::<T>()as*constu8,Some(false));
98+
99+
// If `ptr.wrapping_sub(int)` cannot be null (because it is in-bounds or one-past-the-end of
100+
// `ptr`'s allocation, or because it is misaligned from `ptr`'s allocation), then we know that
101+
// `ptr != int`, even if `ptr` itself is out-of-bounds or one-past-the-end of its allocation.
102+
do_test!((&raw constA).wrapping_byte_add(1),1as*constu8,Some(false));
103+
do_test!((&raw constA).wrapping_byte_add(2),2as*constu8,Some(false));
104+
do_test!((&raw constA).wrapping_byte_add(3),1as*constu8,Some(false));
105+
do_test!((&raw constZST).wrapping_byte_add(1),1as*constu8,Some(false));
106+
do_test!(VTABLE_PTR_1.wrapping_byte_add(1),1as*constu8,Some(false));
107+
do_test!(FN_PTR.wrapping_byte_add(1),1as*constu8,Some(false));
108+
do_test!(&A, size_of::<T>().wrapping_neg()as*constu8,Some(false));
109+
do_test!(&LARGE_WORD_ALIGNED, size_of::<usize>().wrapping_neg()as*constu8,Some(false));
110+
// (`ptr - int != 0` due to misalignment)
111+
do_test!((&raw constA).wrapping_byte_add(2),1as*constu8,Some(false));
112+
do_test!((&raw constALIGNED_ZST).wrapping_byte_add(2),1as*constu8,Some(false));
40113

41114
// When pointers go out-of-bounds, they *might* become null, so these comparions cannot work.
42-
check!(!,unsafe{(FOOas*constusize).wrapping_add(2)},0);
43-
check!(!,unsafe{(FOOas*constusize).wrapping_sub(1)},0);
115+
do_test!((&raw constA).wrapping_add(2),0as*constu8,None);
116+
do_test!((&raw constA).wrapping_sub(1),0as*constu8,None);
117+
118+
// Statics cannot be duplicated
119+
do_test!(&A,&A,Some(true));
120+
121+
// Two non-ZST statics cannot have the same address
122+
do_test!(&A,&B,Some(false));
123+
do_test!(&A,&raw constMUT_STATIC,Some(false));
124+
125+
// One-past-the-end of one static can be equal to the address of another static.
126+
do_test!(&A,(&raw constB).wrapping_add(1),None);
127+
128+
// Cannot know if ZST static is at the same address with anything non-null (if alignment allows).
129+
do_test!(&A,&ZST,None);
130+
do_test!(&A,&ALIGNED_ZST,None);
131+
132+
// Unclear if ZST statics can be placed "in the middle of" non-ZST statics.
133+
// For now, we conservatively say they could, and return None here.
134+
do_test!(&ZST,(&raw constA).wrapping_byte_add(1),None);
135+
136+
// As per https://doc.rust-lang.org/nightly/reference/items/static-items.html#r-items.static.storage-disjointness
137+
// immutable statics are allowed to overlap with const items and promoteds.
138+
do_test!(&A,&T(42),None);
139+
do_test!(&A,const{&T(42)},None);
140+
do_test!(&A,{constX:T = T(42);&X},None);
141+
142+
// These could return Some(false), since only immutable statics can overlap with const items
143+
// and promoteds.
144+
do_test!(&raw constMUT_STATIC,&T(42),None);
145+
do_test!(&raw constMUT_STATIC,const{&T(42)},None);
146+
do_test!(&raw constMUT_STATIC,{constX:T = T(42);&X},None);
147+
148+
// An odd offset from a 2-aligned allocation can never be equal to an even offset from a
149+
// 2-aligned allocation, even if the offsets are out-of-bounds.
150+
do_test!(&A,(&raw constB).wrapping_byte_add(1),Some(false));
151+
do_test!(&A,(&raw constB).wrapping_byte_add(5),Some(false));
152+
do_test!(&A,(&raw constALIGNED_ZST).wrapping_byte_add(1),Some(false));
153+
do_test!(&ALIGNED_ZST,(&raw constA).wrapping_byte_add(1),Some(false));
154+
do_test!(&A,(&T(42)as*constT).wrapping_byte_add(1),Some(false));
155+
do_test!(&A,(const{&T(42)}as*constT).wrapping_byte_add(1),Some(false));
156+
do_test!(&A,({constX:T = T(42);&X}as*constT).wrapping_byte_add(1),Some(false));
157+
158+
// We could return `Some(false)` for these, as pointers to different statics can never be equal if
159+
// that would require the statics to overlap, even if the pointers themselves are offset out of
160+
// bounds or one-past-the-end. We currently only check strictly in-bounds pointers when comparing
161+
// pointers to different statics, however.
162+
do_test!((&raw constA).wrapping_add(1),(&raw constB).wrapping_add(1),None);
163+
do_test!(
164+
(&raw constLARGE_WORD_ALIGNED).cast::<usize>().wrapping_add(2),
165+
(&raw constMUT_LARGE_WORD_ALIGNED).cast::<usize>().wrapping_add(1),
166+
None
167+
);
168+
169+
// Pointers into the same static are equal if and only if their offset is the same,
170+
// even if either is out-of-bounds.
171+
do_test!(&A,&A,Some(true));
172+
do_test!(&A,&A.0,Some(true));
173+
do_test!(&A,(&raw constA).wrapping_byte_add(1),Some(false));
174+
do_test!(&A,(&raw constA).wrapping_byte_add(2),Some(false));
175+
do_test!(&A,(&raw constA).wrapping_byte_add(51),Some(false));
176+
do_test!((&raw constA).wrapping_byte_add(51),(&raw constA).wrapping_byte_add(51),Some(true));
177+
178+
// Pointers to the same fn may be unequal, since `fn`s can be duplicated.
179+
do_test!(FN_PTR,FN_PTR,None);
180+
do_test!(ALIGNED_FN_PTR,ALIGNED_FN_PTR,None);
181+
182+
// Pointers to different fns may be equal, since `fn`s can be deduplicated.
183+
do_test!(FN_PTR,ALIGNED_FN_PTR,None);
184+
185+
// Pointers to the same vtable may be unequal, since vtables can be duplicated.
186+
do_test!(VTABLE_PTR_1,VTABLE_PTR_1,None);
187+
188+
// Pointers to different vtables may be equal, since vtables can be deduplicated.
189+
do_test!(VTABLE_PTR_1,VTABLE_PTR_2,None);
190+
191+
// Function pointers to aligned function allocations are not necessarily actually aligned,
192+
// due to platform-specific semantics.
193+
// See https://github.com/rust-lang/rust/issues/144661
194+
// FIXME: This could return `Some` on platforms where function pointers' addresses actually
195+
// correspond to function addresses including alignment, or on platforms where all functions
196+
// are aligned to some amount (e.g. ARM where a32 function pointers are at least 4-aligned,
197+
// and t32 function pointers are 2-aligned-offset-by-1).
198+
do_test!(ALIGNED_FN_PTR,ALIGNED_FN_PTR.wrapping_byte_offset(1),None);
199+
200+
// Conservatively say we don't know.
201+
do_test!(FN_PTR,VTABLE_PTR_1,None);
202+
do_test!((&raw constLARGE_WORD_ALIGNED).cast::<usize>().wrapping_add(1),VTABLE_PTR_1,None);
203+
do_test!((&raw constMUT_LARGE_WORD_ALIGNED).cast::<usize>().wrapping_add(1),VTABLE_PTR_1,None);
204+
do_test!((&raw constLARGE_WORD_ALIGNED).cast::<usize>().wrapping_add(1),FN_PTR,None);
205+
do_test!((&raw constMUT_LARGE_WORD_ALIGNED).cast::<usize>().wrapping_add(1),FN_PTR,None);

0 commit comments

Comments
 (0)