Skip to content

Commit b8495e5

Browse files
committed
Auto merge of #130251 - saethlin:ptr-offset-preconditions, r=Amanieu
Add precondition checks to ptr::offset, ptr::add, ptr::sub All of `offset`, `add`, and `sub` (currently) have the trivial preconditions that the offset in bytes must be <= isize::MAX, and the computation of the new address must not wrap. This adds precondition checks for these, and like in slice indexing, we use intrinsics directly to implement unsafe APIs that have explicit checks, because we get a clearer error message that mentions the misused API not an implementation detail. Experimentation indicates these checks have 1-2% compile time overhead, due primarily to adding the checks for `add`. A crater run (#130251 (comment)) indicates some people currently have buggy calls to `ptr::offset` that apply a negative offset to a null pointer, but the crater run does not hit the `ptr::add` or `ptr::sub` checks, which seems like an argument for cfg'ing out those checks on account of their overhead.
2 parents 3ae715c + 8d562f6 commit b8495e5

4 files changed

Lines changed: 188 additions & 5 deletions

File tree

‎library/core/src/ptr/const_ptr.rs‎

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,36 @@ impl<T: ?Sized> *const T {
395395
where
396396
T:Sized,
397397
{
398+
#[inline]
399+
constfnruntime_offset_nowrap(this:*const(),count:isize,size:usize) -> bool{
400+
#[inline]
401+
fnruntime(this:*const(),count:isize,size:usize) -> bool{
402+
// We know `size <= isize::MAX` so the `as` cast here is not lossy.
403+
letSome(byte_offset) = count.checked_mul(size asisize)else{
404+
returnfalse;
405+
};
406+
let(_, overflow) = this.addr().overflowing_add_signed(byte_offset);
407+
!overflow
408+
}
409+
410+
constfncomptime(_:*const(), _:isize, _:usize) -> bool{
411+
true
412+
}
413+
414+
// We can use const_eval_select here because this is only for UB checks.
415+
intrinsics::const_eval_select((this, count, size), comptime, runtime)
416+
}
417+
418+
ub_checks::assert_unsafe_precondition!(
419+
check_language_ub,
420+
"ptr::offset requires the address calculation to not overflow",
421+
(
422+
this:*const() = selfas*const(),
423+
count:isize = count,
424+
size:usize = size_of::<T>(),
425+
) => runtime_offset_nowrap(this, count, size)
426+
);
427+
398428
// SAFETY: the caller must uphold the safety contract for `offset`.
399429
unsafe{ intrinsics::offset(self, count)}
400430
}
@@ -726,7 +756,6 @@ impl<T: ?Sized> *const T {
726756
true
727757
}
728758

729-
#[allow(unused_unsafe)]
730759
intrinsics::const_eval_select((this, origin), comptime, runtime)
731760
}
732761

@@ -858,6 +887,36 @@ impl<T: ?Sized> *const T {
858887
where
859888
T:Sized,
860889
{
890+
#[cfg(debug_assertions)]
891+
#[inline]
892+
constfnruntime_add_nowrap(this:*const(),count:usize,size:usize) -> bool{
893+
#[inline]
894+
fnruntime(this:*const(),count:usize,size:usize) -> bool{
895+
letSome(byte_offset) = count.checked_mul(size)else{
896+
returnfalse;
897+
};
898+
let(_, overflow) = this.addr().overflowing_add(byte_offset);
899+
byte_offset <= (isize::MAXasusize) && !overflow
900+
}
901+
902+
constfncomptime(_:*const(), _:usize, _:usize) -> bool{
903+
true
904+
}
905+
906+
intrinsics::const_eval_select((this, count, size), comptime, runtime)
907+
}
908+
909+
#[cfg(debug_assertions)]// Expensive, and doesn't catch much in the wild.
910+
ub_checks::assert_unsafe_precondition!(
911+
check_language_ub,
912+
"ptr::add requires that the address calculation does not overflow",
913+
(
914+
this:*const() = selfas*const(),
915+
count:usize = count,
916+
size:usize = size_of::<T>(),
917+
) => runtime_add_nowrap(this, count, size)
918+
);
919+
861920
// SAFETY: the caller must uphold the safety contract for `offset`.
862921
unsafe{ intrinsics::offset(self, count)}
863922
}
@@ -936,14 +995,43 @@ impl<T: ?Sized> *const T {
936995
where
937996
T:Sized,
938997
{
998+
#[cfg(debug_assertions)]
999+
#[inline]
1000+
constfnruntime_sub_nowrap(this:*const(),count:usize,size:usize) -> bool{
1001+
#[inline]
1002+
fnruntime(this:*const(),count:usize,size:usize) -> bool{
1003+
letSome(byte_offset) = count.checked_mul(size)else{
1004+
returnfalse;
1005+
};
1006+
byte_offset <= (isize::MAXasusize) && this.addr() >= byte_offset
1007+
}
1008+
1009+
constfncomptime(_:*const(), _:usize, _:usize) -> bool{
1010+
true
1011+
}
1012+
1013+
intrinsics::const_eval_select((this, count, size), comptime, runtime)
1014+
}
1015+
1016+
#[cfg(debug_assertions)]// Expensive, and doesn't catch much in the wild.
1017+
ub_checks::assert_unsafe_precondition!(
1018+
check_language_ub,
1019+
"ptr::sub requires that the address calculation does not overflow",
1020+
(
1021+
this:*const() = selfas*const(),
1022+
count:usize = count,
1023+
size:usize = size_of::<T>(),
1024+
) => runtime_sub_nowrap(this, count, size)
1025+
);
1026+
9391027
ifT::IS_ZST{
9401028
// Pointer arithmetic does nothing when the pointee is a ZST.
9411029
self
9421030
}else{
9431031
// SAFETY: the caller must uphold the safety contract for `offset`.
9441032
// Because the pointee is *not* a ZST, that means that `count` is
9451033
// at most `isize::MAX`, and thus the negation cannot overflow.
946-
unsafe{self.offset((count asisize).unchecked_neg())}
1034+
unsafe{intrinsics::offset(self, intrinsics::unchecked_sub(0,count asisize))}
9471035
}
9481036
}
9491037

‎library/core/src/ptr/mut_ptr.rs‎

Lines changed: 91 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,37 @@ impl<T: ?Sized> *mut T {
393393
where
394394
T:Sized,
395395
{
396+
#[inline]
397+
constfnruntime_offset_nowrap(this:*const(),count:isize,size:usize) -> bool{
398+
#[inline]
399+
fnruntime(this:*const(),count:isize,size:usize) -> bool{
400+
// `size` is the size of a Rust type, so we know that
401+
// `size <= isize::MAX` and thus `as` cast here is not lossy.
402+
letSome(byte_offset) = count.checked_mul(size asisize)else{
403+
returnfalse;
404+
};
405+
let(_, overflow) = this.addr().overflowing_add_signed(byte_offset);
406+
!overflow
407+
}
408+
409+
constfncomptime(_:*const(), _:isize, _:usize) -> bool{
410+
true
411+
}
412+
413+
// We can use const_eval_select here because this is only for UB checks.
414+
intrinsics::const_eval_select((this, count, size), comptime, runtime)
415+
}
416+
417+
ub_checks::assert_unsafe_precondition!(
418+
check_language_ub,
419+
"ptr::offset requires the address calculation to not overflow",
420+
(
421+
this:*const() = selfas*const(),
422+
count:isize = count,
423+
size:usize = size_of::<T>(),
424+
) => runtime_offset_nowrap(this, count, size)
425+
);
426+
396427
// SAFETY: the caller must uphold the safety contract for `offset`.
397428
// The obtained pointer is valid for writes since the caller must
398429
// guarantee that it points to the same allocated object as `self`.
@@ -940,6 +971,36 @@ impl<T: ?Sized> *mut T {
940971
where
941972
T:Sized,
942973
{
974+
#[cfg(debug_assertions)]
975+
#[inline]
976+
constfnruntime_add_nowrap(this:*const(),count:usize,size:usize) -> bool{
977+
#[inline]
978+
fnruntime(this:*const(),count:usize,size:usize) -> bool{
979+
letSome(byte_offset) = count.checked_mul(size)else{
980+
returnfalse;
981+
};
982+
let(_, overflow) = this.addr().overflowing_add(byte_offset);
983+
byte_offset <= (isize::MAXasusize) && !overflow
984+
}
985+
986+
constfncomptime(_:*const(), _:usize, _:usize) -> bool{
987+
true
988+
}
989+
990+
intrinsics::const_eval_select((this, count, size), comptime, runtime)
991+
}
992+
993+
#[cfg(debug_assertions)]// Expensive, and doesn't catch much in the wild.
994+
ub_checks::assert_unsafe_precondition!(
995+
check_language_ub,
996+
"ptr::add requires that the address calculation does not overflow",
997+
(
998+
this:*const() = selfas*const(),
999+
count:usize = count,
1000+
size:usize = size_of::<T>(),
1001+
) => runtime_add_nowrap(this, count, size)
1002+
);
1003+
9431004
// SAFETY: the caller must uphold the safety contract for `offset`.
9441005
unsafe{ intrinsics::offset(self, count)}
9451006
}
@@ -1018,14 +1079,43 @@ impl<T: ?Sized> *mut T {
10181079
where
10191080
T:Sized,
10201081
{
1082+
#[cfg(debug_assertions)]
1083+
#[inline]
1084+
constfnruntime_sub_nowrap(this:*const(),count:usize,size:usize) -> bool{
1085+
#[inline]
1086+
fnruntime(this:*const(),count:usize,size:usize) -> bool{
1087+
letSome(byte_offset) = count.checked_mul(size)else{
1088+
returnfalse;
1089+
};
1090+
byte_offset <= (isize::MAXasusize) && this.addr() >= byte_offset
1091+
}
1092+
1093+
constfncomptime(_:*const(), _:usize, _:usize) -> bool{
1094+
true
1095+
}
1096+
1097+
intrinsics::const_eval_select((this, count, size), comptime, runtime)
1098+
}
1099+
1100+
#[cfg(debug_assertions)]// Expensive, and doesn't catch much in the wild.
1101+
ub_checks::assert_unsafe_precondition!(
1102+
check_language_ub,
1103+
"ptr::sub requires that the address calculation does not overflow",
1104+
(
1105+
this:*const() = selfas*const(),
1106+
count:usize = count,
1107+
size:usize = size_of::<T>(),
1108+
) => runtime_sub_nowrap(this, count, size)
1109+
);
1110+
10211111
ifT::IS_ZST{
10221112
// Pointer arithmetic does nothing when the pointee is a ZST.
10231113
self
10241114
}else{
10251115
// SAFETY: the caller must uphold the safety contract for `offset`.
10261116
// Because the pointee is *not* a ZST, that means that `count` is
10271117
// at most `isize::MAX`, and thus the negation cannot overflow.
1028-
unsafe{self.offset((count asisize).unchecked_neg())}
1118+
unsafe{intrinsics::offset(self, intrinsics::unchecked_sub(0,count asisize))}
10291119
}
10301120
}
10311121

‎tests/codegen/option-as-slice.rs‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ pub fn u64_opt_as_slice(o: &Option<u64>) -> &[u64] {
1414
// CHECK-NOT: br
1515
// CHECK-NOT: switch
1616
// CHECK-NOT: icmp
17-
// CHECK: %[[LEN:.+]] = load i64,{{.+}} !range ![[META_U64:.+]], !noundef
17+
// CHECK: %[[LEN:.+]] = load i64
18+
// CHECK-SAME: !range ![[META_U64:[0-9]+]],
19+
// CHECK-SAME: !noundef
1820
// CHECK-NOT: select
1921
// CHECK-NOT: br
2022
// CHECK-NOT: switch
@@ -51,7 +53,9 @@ pub fn u8_opt_as_slice(o: &Option<u8>) -> &[u8] {
5153
// CHECK-NOT: br
5254
// CHECK-NOT: switch
5355
// CHECK-NOT: icmp
54-
// CHECK: %[[TAG:.+]] = load i8,{{.+}} !range ![[META_U8:.+]], !noundef
56+
// CHECK: %[[TAG:.+]] = load i8
57+
// CHECK-SAME: !range ![[META_U8:[0-9]+]],
58+
// CHECK-SAME: !noundef
5559
// CHECK: %[[LEN:.+]] = zext{{.*}} i8 %[[TAG]] to i64
5660
// CHECK-NOT: select
5761
// CHECK-NOT: br

‎tests/mir-opt/pre-codegen/slice_iter.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// skip-filecheck
22
//@ compile-flags: -O -C debuginfo=0 -Zmir-opt-level=2
33
//@ only-64bit (constants for `None::<&T>` show in the output)
4+
//@ ignore-debug: precondition checks on ptr::add are under cfg(debug_assertions)
45
// EMIT_MIR_FOR_EACH_PANIC_STRATEGY
56

67
#![crate_type = "lib"]

0 commit comments

Comments
 (0)