Skip to content

Commit 26089ba

Browse files
committed
Auto merge of #114483 - RalfJung:unsized-fields, r=oli-obk
interpret: fix projecting into an unsized field of a local See the new Miri testcase that didn't work before. r? `@oli-obk`
2 parents 61efe9d + 6d1ce9b commit 26089ba

File tree

17 files changed

+374
-213
lines changed

17 files changed

+374
-213
lines changed

compiler/rustc_const_eval/messages.ftl

+1-1
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,7 @@ const_eval_unreachable_unwind =
384384
385385
const_eval_unsigned_offset_from_overflow =
386386
`ptr_offset_from_unsigned` called when first pointer has smaller offset than second: {$a_offset} < {$b_offset}
387-
387+
const_eval_unsized_local = unsized locals are not supported
388388
const_eval_unstable_const_fn = `{$def_path}` is not yet stable as a const fn
389389
390390
const_eval_unstable_in_stable =

compiler/rustc_const_eval/src/const_eval/eval_queries.rs

+1
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ fn eval_body_using_ecx<'mir, 'tcx>(
6161
&ret.clone().into(),
6262
StackPopCleanup::Root { cleanup: false },
6363
)?;
64+
ecx.storage_live_for_always_live_locals()?;
6465

6566
// The main interpreter loop.
6667
while ecx.step()? {}

compiler/rustc_const_eval/src/errors.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -795,6 +795,7 @@ impl ReportErrorExt for UnsupportedOpInfo {
795795
use crate::fluent_generated::*;
796796
match self {
797797
UnsupportedOpInfo::Unsupported(s) => s.clone().into(),
798+
UnsupportedOpInfo::UnsizedLocal => const_eval_unsized_local,
798799
UnsupportedOpInfo::OverwritePartialPointer(_) => const_eval_partial_pointer_overwrite,
799800
UnsupportedOpInfo::ReadPartialPointer(_) => const_eval_partial_pointer_copy,
800801
UnsupportedOpInfo::ReadPointerAsInt(_) => const_eval_read_pointer_as_int,
@@ -814,7 +815,7 @@ impl ReportErrorExt for UnsupportedOpInfo {
814815
// `ReadPointerAsInt(Some(info))` is never printed anyway, it only serves as an error to
815816
// be further processed by validity checking which then turns it into something nice to
816817
// print. So it's not worth the effort of having diagnostics that can print the `info`.
817-
Unsupported(_) | ReadPointerAsInt(_) => {}
818+
UnsizedLocal | Unsupported(_) | ReadPointerAsInt(_) => {}
818819
OverwritePartialPointer(ptr) | ReadPartialPointer(ptr) => {
819820
builder.set_arg("ptr", ptr);
820821
}

compiler/rustc_const_eval/src/interpret/eval_context.rs

+102-23
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,8 @@ pub enum StackPopCleanup {
158158
#[derive(Clone, Debug)]
159159
pub struct LocalState<'tcx, Prov: Provenance = AllocId> {
160160
pub value: LocalValue<Prov>,
161-
/// Don't modify if `Some`, this is only used to prevent computing the layout twice
161+
/// Don't modify if `Some`, this is only used to prevent computing the layout twice.
162+
/// Avoids computing the layout of locals that are never actually initialized.
162163
pub layout: Cell<Option<TyAndLayout<'tcx>>>,
163164
}
164165

@@ -177,7 +178,7 @@ pub enum LocalValue<Prov: Provenance = AllocId> {
177178

178179
impl<'tcx, Prov: Provenance + 'static> LocalState<'tcx, Prov> {
179180
/// Read the local's value or error if the local is not yet live or not live anymore.
180-
#[inline]
181+
#[inline(always)]
181182
pub fn access(&self) -> InterpResult<'tcx, &Operand<Prov>> {
182183
match &self.value {
183184
LocalValue::Dead => throw_ub!(DeadLocal), // could even be "invalid program"?
@@ -190,7 +191,7 @@ impl<'tcx, Prov: Provenance + 'static> LocalState<'tcx, Prov> {
190191
///
191192
/// Note: This may only be invoked from the `Machine::access_local_mut` hook and not from
192193
/// anywhere else. You may be invalidating machine invariants if you do!
193-
#[inline]
194+
#[inline(always)]
194195
pub fn access_mut(&mut self) -> InterpResult<'tcx, &mut Operand<Prov>> {
195196
match &mut self.value {
196197
LocalValue::Dead => throw_ub!(DeadLocal), // could even be "invalid program"?
@@ -483,7 +484,7 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
483484
}
484485

485486
#[inline(always)]
486-
pub(super) fn body(&self) -> &'mir mir::Body<'tcx> {
487+
pub fn body(&self) -> &'mir mir::Body<'tcx> {
487488
self.frame().body
488489
}
489490

@@ -705,15 +706,15 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
705706
return_to_block: StackPopCleanup,
706707
) -> InterpResult<'tcx> {
707708
trace!("body: {:#?}", body);
709+
let dead_local = LocalState { value: LocalValue::Dead, layout: Cell::new(None) };
710+
let locals = IndexVec::from_elem(dead_local, &body.local_decls);
708711
// First push a stack frame so we have access to the local args
709712
let pre_frame = Frame {
710713
body,
711714
loc: Right(body.span), // Span used for errors caused during preamble.
712715
return_to_block,
713716
return_place: return_place.clone(),
714-
// empty local array, we fill it in below, after we are inside the stack frame and
715-
// all methods actually know about the frame
716-
locals: IndexVec::new(),
717+
locals,
717718
instance,
718719
tracing_span: SpanGuard::new(),
719720
extra: (),
@@ -728,19 +729,7 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
728729
self.eval_mir_constant(&ct, Some(span), None)?;
729730
}
730731

731-
// Most locals are initially dead.
732-
let dummy = LocalState { value: LocalValue::Dead, layout: Cell::new(None) };
733-
let mut locals = IndexVec::from_elem(dummy, &body.local_decls);
734-
735-
// Now mark those locals as live that have no `Storage*` annotations.
736-
let always_live = always_storage_live_locals(self.body());
737-
for local in locals.indices() {
738-
if always_live.contains(local) {
739-
locals[local].value = LocalValue::Live(Operand::Immediate(Immediate::Uninit));
740-
}
741-
}
742732
// done
743-
self.frame_mut().locals = locals;
744733
M::after_stack_push(self)?;
745734
self.frame_mut().loc = Left(mir::Location::START);
746735

@@ -907,12 +896,96 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
907896
}
908897
}
909898

910-
/// Mark a storage as live, killing the previous content.
911-
pub fn storage_live(&mut self, local: mir::Local) -> InterpResult<'tcx> {
912-
assert!(local != mir::RETURN_PLACE, "Cannot make return place live");
899+
/// In the current stack frame, mark all locals as live that are not arguments and don't have
900+
/// `Storage*` annotations (this includes the return place).
901+
pub fn storage_live_for_always_live_locals(&mut self) -> InterpResult<'tcx> {
902+
self.storage_live(mir::RETURN_PLACE)?;
903+
904+
let body = self.body();
905+
let always_live = always_storage_live_locals(body);
906+
for local in body.vars_and_temps_iter() {
907+
if always_live.contains(local) {
908+
self.storage_live(local)?;
909+
}
910+
}
911+
Ok(())
912+
}
913+
914+
pub fn storage_live_dyn(
915+
&mut self,
916+
local: mir::Local,
917+
meta: MemPlaceMeta<M::Provenance>,
918+
) -> InterpResult<'tcx> {
913919
trace!("{:?} is now live", local);
914920

915-
let local_val = LocalValue::Live(Operand::Immediate(Immediate::Uninit));
921+
// We avoid `ty.is_trivially_sized` since that (a) cannot assume WF, so it recurses through
922+
// all fields of a tuple, and (b) does something expensive for ADTs.
923+
fn is_very_trivially_sized(ty: Ty<'_>) -> bool {
924+
match ty.kind() {
925+
ty::Infer(ty::IntVar(_) | ty::FloatVar(_))
926+
| ty::Uint(_)
927+
| ty::Int(_)
928+
| ty::Bool
929+
| ty::Float(_)
930+
| ty::FnDef(..)
931+
| ty::FnPtr(_)
932+
| ty::RawPtr(..)
933+
| ty::Char
934+
| ty::Ref(..)
935+
| ty::Generator(..)
936+
| ty::GeneratorWitness(..)
937+
| ty::GeneratorWitnessMIR(..)
938+
| ty::Array(..)
939+
| ty::Closure(..)
940+
| ty::Never
941+
| ty::Error(_) => true,
942+
943+
ty::Str | ty::Slice(_) | ty::Dynamic(..) | ty::Foreign(..) => false,
944+
945+
ty::Tuple(tys) => tys.last().iter().all(|ty| is_very_trivially_sized(**ty)),
946+
947+
// We don't want to do any queries, so there is not much we can do with ADTs.
948+
ty::Adt(..) => false,
949+
950+
ty::Alias(..) | ty::Param(_) | ty::Placeholder(..) => false,
951+
952+
ty::Infer(ty::TyVar(_)) => false,
953+
954+
ty::Bound(..)
955+
| ty::Infer(ty::FreshTy(_) | ty::FreshIntTy(_) | ty::FreshFloatTy(_)) => {
956+
bug!("`is_very_trivially_sized` applied to unexpected type: {:?}", ty)
957+
}
958+
}
959+
}
960+
961+
// This is a hot function, we avoid computing the layout when possible.
962+
// `unsized_` will be `None` for sized types and `Some(layout)` for unsized types.
963+
let unsized_ = if is_very_trivially_sized(self.body().local_decls[local].ty) {
964+
None
965+
} else {
966+
// We need the layout.
967+
let layout = self.layout_of_local(self.frame(), local, None)?;
968+
if layout.is_sized() { None } else { Some(layout) }
969+
};
970+
971+
let local_val = LocalValue::Live(if let Some(layout) = unsized_ {
972+
if !meta.has_meta() {
973+
throw_unsup!(UnsizedLocal);
974+
}
975+
// Need to allocate some memory, since `Immediate::Uninit` cannot be unsized.
976+
let dest_place = self.allocate_dyn(layout, MemoryKind::Stack, meta)?;
977+
Operand::Indirect(*dest_place)
978+
} else {
979+
assert!(!meta.has_meta()); // we're dropping the metadata
980+
// Just make this an efficient immediate.
981+
// Note that not calling `layout_of` here does have one real consequence:
982+
// if the type is too big, we'll only notice this when the local is actually initialized,
983+
// which is a bit too late -- we should ideally notice this alreayd here, when the memory
984+
// is conceptually allocated. But given how rare that error is and that this is a hot function,
985+
// we accept this downside for now.
986+
Operand::Immediate(Immediate::Uninit)
987+
});
988+
916989
// StorageLive expects the local to be dead, and marks it live.
917990
let old = mem::replace(&mut self.frame_mut().locals[local].value, local_val);
918991
if !matches!(old, LocalValue::Dead) {
@@ -921,6 +994,12 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
921994
Ok(())
922995
}
923996

997+
/// Mark a storage as live, killing the previous content.
998+
#[inline(always)]
999+
pub fn storage_live(&mut self, local: mir::Local) -> InterpResult<'tcx> {
1000+
self.storage_live_dyn(local, MemPlaceMeta::None)
1001+
}
1002+
9241003
pub fn storage_dead(&mut self, local: mir::Local) -> InterpResult<'tcx> {
9251004
assert!(local != mir::RETURN_PLACE, "Cannot make return place dead");
9261005
trace!("{:?} is now dead", local);

compiler/rustc_const_eval/src/interpret/operand.rs

+42-47
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ pub enum Immediate<Prov: Provenance = AllocId> {
3333
/// A pair of two scalar value (must have `ScalarPair` ABI where both fields are
3434
/// `Scalar::Initialized`).
3535
ScalarPair(Scalar<Prov>, Scalar<Prov>),
36-
/// A value of fully uninitialized memory. Can have arbitrary size and layout.
36+
/// A value of fully uninitialized memory. Can have arbitrary size and layout, but must be sized.
3737
Uninit,
3838
}
3939

@@ -190,16 +190,19 @@ impl<'tcx, Prov: Provenance> From<ImmTy<'tcx, Prov>> for OpTy<'tcx, Prov> {
190190
impl<'tcx, Prov: Provenance> ImmTy<'tcx, Prov> {
191191
#[inline]
192192
pub fn from_scalar(val: Scalar<Prov>, layout: TyAndLayout<'tcx>) -> Self {
193+
debug_assert!(layout.abi.is_scalar(), "`ImmTy::from_scalar` on non-scalar layout");
193194
ImmTy { imm: val.into(), layout }
194195
}
195196

196-
#[inline]
197+
#[inline(always)]
197198
pub fn from_immediate(imm: Immediate<Prov>, layout: TyAndLayout<'tcx>) -> Self {
199+
debug_assert!(layout.is_sized(), "immediates must be sized");
198200
ImmTy { imm, layout }
199201
}
200202

201203
#[inline]
202204
pub fn uninit(layout: TyAndLayout<'tcx>) -> Self {
205+
debug_assert!(layout.is_sized(), "immediates must be sized");
203206
ImmTy { imm: Immediate::Uninit, layout }
204207
}
205208

@@ -291,23 +294,21 @@ impl<'tcx, Prov: Provenance> Projectable<'tcx, Prov> for ImmTy<'tcx, Prov> {
291294
self.layout
292295
}
293296

294-
fn meta<'mir, M: Machine<'mir, 'tcx, Provenance = Prov>>(
295-
&self,
296-
_ecx: &InterpCx<'mir, 'tcx, M>,
297-
) -> InterpResult<'tcx, MemPlaceMeta<M::Provenance>> {
298-
assert!(self.layout.is_sized()); // unsized ImmTy can only exist temporarily and should never reach this here
299-
Ok(MemPlaceMeta::None)
297+
#[inline(always)]
298+
fn meta(&self) -> MemPlaceMeta<Prov> {
299+
debug_assert!(self.layout.is_sized()); // unsized ImmTy can only exist temporarily and should never reach this here
300+
MemPlaceMeta::None
300301
}
301302

302-
fn offset_with_meta(
303+
fn offset_with_meta<'mir, M: Machine<'mir, 'tcx, Provenance = Prov>>(
303304
&self,
304305
offset: Size,
305306
meta: MemPlaceMeta<Prov>,
306307
layout: TyAndLayout<'tcx>,
307-
cx: &impl HasDataLayout,
308+
ecx: &InterpCx<'mir, 'tcx, M>,
308309
) -> InterpResult<'tcx, Self> {
309310
assert_matches!(meta, MemPlaceMeta::None); // we can't store this anywhere anyway
310-
Ok(self.offset_(offset, layout, cx))
311+
Ok(self.offset_(offset, layout, ecx))
311312
}
312313

313314
fn to_op<'mir, M: Machine<'mir, 'tcx, Provenance = Prov>>(
@@ -318,49 +319,37 @@ impl<'tcx, Prov: Provenance> Projectable<'tcx, Prov> for ImmTy<'tcx, Prov> {
318319
}
319320
}
320321

321-
impl<'tcx, Prov: Provenance> OpTy<'tcx, Prov> {
322-
// Provided as inherent method since it doesn't need the `ecx` of `Projectable::meta`.
323-
pub fn meta(&self) -> InterpResult<'tcx, MemPlaceMeta<Prov>> {
324-
Ok(if self.layout.is_unsized() {
325-
if matches!(self.op, Operand::Immediate(_)) {
326-
// Unsized immediate OpTy cannot occur. We create a MemPlace for all unsized locals during argument passing.
327-
// However, ConstProp doesn't do that, so we can run into this nonsense situation.
328-
throw_inval!(ConstPropNonsense);
329-
}
330-
// There are no unsized immediates.
331-
self.assert_mem_place().meta
332-
} else {
333-
MemPlaceMeta::None
334-
})
335-
}
336-
}
337-
338322
impl<'tcx, Prov: Provenance + 'static> Projectable<'tcx, Prov> for OpTy<'tcx, Prov> {
339323
#[inline(always)]
340324
fn layout(&self) -> TyAndLayout<'tcx> {
341325
self.layout
342326
}
343327

344-
fn meta<'mir, M: Machine<'mir, 'tcx, Provenance = Prov>>(
345-
&self,
346-
_ecx: &InterpCx<'mir, 'tcx, M>,
347-
) -> InterpResult<'tcx, MemPlaceMeta<M::Provenance>> {
348-
self.meta()
328+
#[inline]
329+
fn meta(&self) -> MemPlaceMeta<Prov> {
330+
match self.as_mplace_or_imm() {
331+
Left(mplace) => mplace.meta,
332+
Right(_) => {
333+
debug_assert!(self.layout.is_sized(), "unsized immediates are not a thing");
334+
MemPlaceMeta::None
335+
}
336+
}
349337
}
350338

351-
fn offset_with_meta(
339+
fn offset_with_meta<'mir, M: Machine<'mir, 'tcx, Provenance = Prov>>(
352340
&self,
353341
offset: Size,
354342
meta: MemPlaceMeta<Prov>,
355343
layout: TyAndLayout<'tcx>,
356-
cx: &impl HasDataLayout,
344+
ecx: &InterpCx<'mir, 'tcx, M>,
357345
) -> InterpResult<'tcx, Self> {
358346
match self.as_mplace_or_imm() {
359-
Left(mplace) => Ok(mplace.offset_with_meta(offset, meta, layout, cx)?.into()),
347+
Left(mplace) => Ok(mplace.offset_with_meta(offset, meta, layout, ecx)?.into()),
360348
Right(imm) => {
361-
assert!(!meta.has_meta()); // no place to store metadata here
349+
debug_assert!(layout.is_sized(), "unsized immediates are not a thing");
350+
assert_matches!(meta, MemPlaceMeta::None); // no place to store metadata here
362351
// Every part of an uninit is uninit.
363-
Ok(imm.offset(offset, layout, cx)?.into())
352+
Ok(imm.offset_(offset, layout, ecx).into())
364353
}
365354
}
366355
}
@@ -588,6 +577,13 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
588577
) -> InterpResult<'tcx, OpTy<'tcx, M::Provenance>> {
589578
let layout = self.layout_of_local(frame, local, layout)?;
590579
let op = *frame.locals[local].access()?;
580+
if matches!(op, Operand::Immediate(_)) {
581+
if layout.is_unsized() {
582+
// ConstProp marks *all* locals as `Immediate::Uninit` since it cannot
583+
// efficiently check whether they are sized. We have to catch that case here.
584+
throw_inval!(ConstPropNonsense);
585+
}
586+
}
591587
Ok(OpTy { op, layout, align: Some(layout.align.abi) })
592588
}
593589

@@ -601,16 +597,15 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
601597
match place.as_mplace_or_local() {
602598
Left(mplace) => Ok(mplace.into()),
603599
Right((frame, local, offset)) => {
600+
debug_assert!(place.layout.is_sized()); // only sized locals can ever be `Place::Local`.
604601
let base = self.local_to_op(&self.stack()[frame], local, None)?;
605-
let mut field = if let Some(offset) = offset {
606-
// This got offset. We can be sure that the field is sized.
607-
base.offset(offset, place.layout, self)?
608-
} else {
609-
assert_eq!(place.layout, base.layout);
610-
// Unsized cases are possible here since an unsized local will be a
611-
// `Place::Local` until the first projection calls `place_to_op` to extract the
612-
// underlying mplace.
613-
base
602+
let mut field = match offset {
603+
Some(offset) => base.offset(offset, place.layout, self)?,
604+
None => {
605+
// In the common case this hasn't been projected.
606+
debug_assert_eq!(place.layout, base.layout);
607+
base
608+
}
614609
};
615610
field.align = Some(place.align);
616611
Ok(field)

0 commit comments

Comments
 (0)