Skip to content

Commit 10fde9e

Browse files
zachs18RalfJung
andcommitted
Implement some more checks for ptr_guaranteed_cmp in consteval:
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 <post@ralfj.de>
1 parent 07b7dc9 commit 10fde9e

File tree

2 files changed

+311
-45
lines changed

2 files changed

+311
-45
lines changed

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) as u8,
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+
return interp_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+
if let (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) as u8
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: 208 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,221 @@
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!(<*const u8>::guaranteed_eq(a.cast(), b.cast()), $expected));
16+
};
17+
};
18+
}
819

9-
const FOO: &usize = &42;
20+
#[repr(align(2))]
21+
struct T(#[allow(unused)] u16);
1022

11-
macro_rules! check {
12-
(eq, $a:expr, $b:expr) => {
13-
pub const _: () =
14-
assert!(std::intrinsics::ptr_guaranteed_cmp($a as *const u8, $b as *const u8) == 1);
15-
};
16-
(ne, $a:expr, $b:expr) => {
17-
pub const _: () =
18-
assert!(std::intrinsics::ptr_guaranteed_cmp($a as *const u8, $b as *const u8) == 0);
23+
#[repr(align(2))]
24+
struct AlignedZst;
25+
26+
static A: T = T(42);
27+
static B: T = T(42);
28+
static mut MUT_STATIC: T = T(42);
29+
static ZST: () = ();
30+
static ALIGNED_ZST: AlignedZst = AlignedZst;
31+
static LARGE_WORD_ALIGNED: [usize; 2] = [0, 1];
32+
static mut MUT_LARGE_WORD_ALIGNED: [usize; 2] = [0, 1];
33+
34+
const FN_PTR: *const () = {
35+
fn foo() {}
36+
unsafe { std::mem::transmute(foo as fn()) }
37+
};
38+
39+
const ALIGNED_FN_PTR: *const () = {
40+
#[rustc_align(2)]
41+
fn aligned_foo() {}
42+
unsafe { std::mem::transmute(aligned_foo as fn()) }
43+
};
44+
45+
// Only on armv5te-* and armv4t-*
46+
#[cfg(all(
47+
target_arch = "arm",
48+
not(target_feature = "v6"),
49+
))]
50+
const ALIGNED_THUMB_FN_PTR: *const () = {
51+
#[rustc_align(2)]
52+
#[instruction_set(arm::t32)]
53+
fn aligned_thumb_foo() {}
54+
unsafe { std::mem::transmute(aligned_thumb_foo as fn()) }
55+
};
56+
57+
trait Trait {
58+
#[allow(unused)]
59+
fn method(&self) -> u8;
60+
}
61+
impl Trait for u32 {
62+
fn method(&self) -> u8 { 1 }
63+
}
64+
impl Trait for i32 {
65+
fn method(&self) -> u8 { 2 }
66+
}
67+
68+
const VTABLE_PTR_1: *const () = {
69+
let [_data, vtable] = unsafe {
70+
std::mem::transmute::<&dyn Trait, [*const (); 2]>(&42_u32 as &dyn Trait)
1971
};
20-
(!, $a:expr, $b:expr) => {
21-
pub const _: () =
22-
assert!(std::intrinsics::ptr_guaranteed_cmp($a as *const u8, $b as *const u8) == 2);
72+
vtable
73+
};
74+
const VTABLE_PTR_2: *const () = {
75+
let [_data, vtable] = unsafe {
76+
std::mem::transmute::<&dyn Trait, [*const (); 2]>(&42_i32 as &dyn Trait)
2377
};
24-
}
78+
vtable
79+
};
2580

26-
check!(eq, 0, 0);
27-
check!(ne, 0, 1);
28-
check!(ne, FOO as *const _, 0);
29-
check!(ne, unsafe { (FOO as *const usize).offset(1) }, 0);
30-
check!(ne, unsafe { (FOO as *const usize as *const u8).offset(3) }, 0);
81+
// Cannot be `None`: `is_null` is stable with strong guarantees about integer-valued pointers.
82+
do_test!(0 as *const u8, 0 as *const u8, Some(true));
83+
do_test!(0 as *const u8, 1 as *const u8, Some(false));
3184

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!(!, FOO as *const _, FOO as *const _);
85+
// Integer-valued pointers can always be compared.
86+
do_test!(1 as *const u8, 1 as *const u8, Some(true));
87+
do_test!(1 as *const u8, 2 as *const u8, Some(false));
88+
89+
// Cannot be `None`: `static`s' addresses, references, (and within and one-past-the-end of those),
90+
// and `fn` pointers cannot be null, and `is_null` is stable with strong guarantees, and
91+
// `is_null` is implemented using `guaranteed_cmp`.
92+
do_test!(&A, 0 as *const u8, Some(false));
93+
do_test!((&raw const A).cast::<u8>().wrapping_add(1), 0 as *const u8, Some(false));
94+
do_test!((&raw const A).wrapping_add(1), 0 as *const u8, Some(false));
95+
do_test!(&ZST, 0 as *const u8, Some(false));
96+
do_test!(&(), 0 as *const u8, Some(false));
97+
do_test!(const { &() }, 0 as *const u8, Some(false));
98+
do_test!(FN_PTR, 0 as *const u8, Some(false));
99+
100+
// This pointer is out-of-bounds, but still cannot be equal to 0 because of alignment.
101+
do_test!((&raw const A).cast::<u8>().wrapping_add(size_of::<T>() + 1), 0 as *const u8, Some(false));
36102

37103
// aside from 0, these pointers might end up pretty much anywhere.
38-
check!(!, FOO as *const _, 1); // this one could be `ne` by taking into account alignment
39-
check!(!, FOO as *const _, 1024);
104+
do_test!(&A, align_of::<T>() as *const u8, None);
105+
do_test!((&raw const A).wrapping_byte_add(1), (align_of::<T>() + 1) as *const u8, None);
106+
107+
// except that they must still be aligned
108+
do_test!(&A, 1 as *const u8, Some(false));
109+
do_test!((&raw const A).wrapping_byte_add(1), align_of::<T>() as *const u8, Some(false));
110+
111+
// If `ptr.wrapping_sub(int)` cannot be null (because it is in-bounds or one-past-the-end of
112+
// `ptr`'s allocation, or because it is misaligned from `ptr`'s allocation), then we know that
113+
// `ptr != int`, even if `ptr` itself is out-of-bounds or one-past-the-end of its allocation.
114+
do_test!((&raw const A).wrapping_byte_add(1), 1 as *const u8, Some(false));
115+
do_test!((&raw const A).wrapping_byte_add(2), 2 as *const u8, Some(false));
116+
do_test!((&raw const A).wrapping_byte_add(3), 1 as *const u8, Some(false));
117+
do_test!((&raw const ZST).wrapping_byte_add(1), 1 as *const u8, Some(false));
118+
do_test!(VTABLE_PTR_1.wrapping_byte_add(1), 1 as *const u8, Some(false));
119+
do_test!(FN_PTR.wrapping_byte_add(1), 1 as *const u8, Some(false));
120+
do_test!(&A, size_of::<T>().wrapping_neg() as *const u8, Some(false));
121+
do_test!(&LARGE_WORD_ALIGNED, size_of::<usize>().wrapping_neg() as *const u8, Some(false));
122+
// (`ptr - int != 0` due to misalignment)
123+
do_test!((&raw const A).wrapping_byte_add(2), 1 as *const u8, Some(false));
124+
do_test!((&raw const ALIGNED_ZST).wrapping_byte_add(2), 1 as *const u8, Some(false));
40125

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

0 commit comments

Comments
 (0)