Skip to content

Commit 67ecc0e

Browse files
authored
Unrolled build for #148967
Rollup merge of #148967 - RalfJung:const-eval-preserve-src-padding, r=JonathanBrouwer,traviscross const-eval: always do mem-to-mem copies if there might be padding involved This is the final piece of the puzzle for #148470: when copying data of a type that has padding, always do a mem-to-mem copy, so that we always preserve the source padding exactly. That prevents rustc implementation choices from leaking into user-visible behavior. This is technically a breaking change: the example at the top of #148470 no longer compiles with this. However, it seems very unlikely that anyone would have depended on this. My main concern is not backwards compatibility, it is performance. Fixes #148470 --- > Actually that seems to be entirely fine, it even helps with some benchmarks! I guess the mem-to-mem codepath is actually faster than the scalar pair codepath for the copy itself. It can slow things down later since now we have to do everything bytewise, but that doesn't show up in our benchmarks and might not be very relevant after all (in particular, it only affects types with padding, so the rather common wide pointers still always use the efficient scalar representation). > > So that would be my proposal to for resolving this issue then: to make const-eval behavior consistent, we always copy the padding from the source to the target. IOW, potentially pre-existing provenance in the target always gets overwritten (that part is already in #148259), and potentially existing provenance in padding in the source always gets carried over (that's #148967). If there's provenance elsewhere in the source our existing handling is fine: > - If it's in an integer, that's UB during const-eval so we can do whatever. > - If it's in a pointer, the the fragments must combine back together to a pointer or else we have UB. > - If it's in a union we just carry it over unchanged. > > @traviscross we should check that this special const-eval-only UB is properly reflected in the reference. Currently we have [this](https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html#r-undefined.const-transmute-ptr2int) but that only talks about int2ptr, not about invalid pointer fragments at pointer type. I also wonder if this shouldn't rather be part of ["invalid values"](https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html#r-undefined.validity) to make it clear that this applies recursively inside fields as well. > EDIT: Reference PR is up at rust-lang/reference#2091. _Originally posted by @RalfJung in [#148470](#148470 (comment) > Worth noting that this does not resolve the concerns @theemathas had about `-Zextra-const-ub-checks` sometimes causing *more* code to compile. Specifically, with that flag, the behavior changes to "potentially existing provenance in padding in the source never gets carried over". However, it's a nightly-only flag (used by Miri) so while the behavior is odd, I don't think this is a problem. _Originally posted by @RalfJung in [#148470](#148470 (comment) --- Related: - #148470 - rust-lang/reference#2091
2 parents 0c40f5b + 5a76a60 commit 67ecc0e

File tree

5 files changed

+79
-6
lines changed

5 files changed

+79
-6
lines changed

compiler/rustc_const_eval/src/interpret/place.rs

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
use either::{Either, Left, Right};
66
use rustc_abi::{BackendRepr, HasDataLayout, Size};
77
use rustc_data_structures::assert_matches;
8-
use rustc_middle::ty::Ty;
98
use rustc_middle::ty::layout::TyAndLayout;
9+
use rustc_middle::ty::{self, Ty};
1010
use rustc_middle::{bug, mir, span_bug};
1111
use tracing::field::Empty;
1212
use tracing::{instrument, trace};
@@ -884,10 +884,38 @@ where
884884
dest.layout().ty,
885885
);
886886
}
887+
// If the source has padding, we want to always do a mem-to-mem copy to ensure consistent
888+
// padding in the target independent of layout choices.
889+
let src_has_padding = match src.layout().backend_repr {
890+
BackendRepr::Scalar(_) => false,
891+
BackendRepr::ScalarPair(left, right)
892+
if matches!(src.layout().ty.kind(), ty::Ref(..) | ty::RawPtr(..)) =>
893+
{
894+
// Wide pointers never have padding, so we can avoid calling `size()`.
895+
debug_assert_eq!(left.size(self) + right.size(self), src.layout().size);
896+
false
897+
}
898+
BackendRepr::ScalarPair(left, right) => {
899+
let left_size = left.size(self);
900+
let right_size = right.size(self);
901+
// We have padding if the sizes don't add up to the total.
902+
left_size + right_size != src.layout().size
903+
}
904+
// Everything else can only exist in memory anyway, so it doesn't matter.
905+
BackendRepr::SimdVector { .. }
906+
| BackendRepr::ScalableVector { .. }
907+
| BackendRepr::Memory { .. } => true,
908+
};
887909

888-
// Let us see if the layout is simple so we take a shortcut,
889-
// avoid force_allocation.
890-
let src = match self.read_immediate_raw(src)? {
910+
let src_val = if src_has_padding {
911+
// Do our best to get an mplace. If there's no mplace, then this is stored as an
912+
// "optimized" local, so its padding is definitely uninitialized and we are fine.
913+
src.to_op(self)?.as_mplace_or_imm()
914+
} else {
915+
// Do our best to get an immediate, to avoid having to force_allocate the destination.
916+
self.read_immediate_raw(src)?
917+
};
918+
let src = match src_val {
891919
Right(src_val) => {
892920
assert!(!src.layout().is_unsized());
893921
assert!(!dest.layout().is_unsized());

compiler/rustc_const_eval/src/interpret/projection.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ pub trait Projectable<'tcx, Prov: Provenance>: Sized + std::fmt::Debug {
9797
}
9898

9999
/// Convert this to an `OpTy`. This might be an irreversible transformation, but is useful for
100-
/// reading from this thing.
100+
/// reading from this thing. This will never actually do a read from memory!
101101
fn to_op<M: Machine<'tcx, Provenance = Prov>>(
102102
&self,
103103
ecx: &InterpCx<'tcx, M>,

tests/ui/consts/const-eval/ptr_fragments.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ const _PARTIAL_OVERWRITE: () = {
7070
#[allow(dead_code)]
7171
#[cfg(not(target_arch = "s390x"))] // u128 is less aligned on s390x, removing the padding
7272
fn fragment_in_dst_padding_gets_overwritten() {
73+
// We can't use `repr(align)` here as that would make this not a `ScalarPair` any more.
7374
#[repr(C)]
7475
struct Pair {
7576
x: u128,

tests/ui/consts/const-eval/ptr_fragments_in_final.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,40 @@ const MIXED_PTR: MaybeUninit<*const u8> = { //~ERROR: partial pointer in final v
3737
}
3838
};
3939

40+
/// This has pointer bytes in the padding of the memory that the final value is read from.
41+
/// To ensure consistent behavior, we want to *always* copy that padding, even if the value
42+
/// could be represented as a more efficient ScalarPair. Hence this must fail to compile.
43+
fn fragment_in_padding() -> impl Copy {
44+
// We can't use `repr(align)` here as that would make this not a `ScalarPair` any more.
45+
#[repr(C)]
46+
#[derive(Clone, Copy)]
47+
struct Thing {
48+
x: u128,
49+
y: usize,
50+
// at least one pointer worth of padding
51+
}
52+
// Ensure there is indeed padding.
53+
const _: () = assert!(mem::size_of::<Thing>() > 16 + mem::size_of::<usize>());
54+
55+
#[derive(Clone, Copy)]
56+
union PreservePad {
57+
thing: Thing,
58+
bytes: [u8; mem::size_of::<Thing>()],
59+
}
60+
61+
const A: Thing = unsafe { //~ERROR: partial pointer in final value
62+
let mut buffer = [PreservePad { bytes: [0u8; mem::size_of::<Thing>()] }; 2];
63+
// The offset half a pointer from the end, so that copying a `Thing` copies exactly
64+
// half the pointer.
65+
let offset = mem::size_of::<Thing>() - mem::size_of::<usize>()/2;
66+
// Ensure this is inside the padding.
67+
assert!(offset >= std::mem::offset_of!(Thing, y) + mem::size_of::<usize>());
68+
69+
(&raw mut buffer).cast::<&i32>().byte_add(offset).write_unaligned(&1);
70+
buffer[0].thing
71+
};
72+
73+
A
74+
}
75+
4076
fn main() {}

tests/ui/consts/const-eval/ptr_fragments_in_final.stderr

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,13 @@ LL | const MIXED_PTR: MaybeUninit<*const u8> = {
1414
|
1515
= note: while pointers can be broken apart into individual bytes during const-evaluation, only complete pointers (with all their bytes in the right order) are supported in the final value
1616

17-
error: aborting due to 2 previous errors
17+
error: encountered partial pointer in final value of constant
18+
--> $DIR/ptr_fragments_in_final.rs:61:5
19+
|
20+
LL | const A: Thing = unsafe {
21+
| ^^^^^^^^^^^^^^
22+
|
23+
= note: while pointers can be broken apart into individual bytes during const-evaluation, only complete pointers (with all their bytes in the right order) are supported in the final value
24+
25+
error: aborting due to 3 previous errors
1826

0 commit comments

Comments
 (0)