Skip to content

Commit c3618c8

Browse files
committed
Special-case Box in rustc_mir::borrow_check.
This should address issue 45696. Since we know dropping a box will not access any `&mut` or `&` references, it is safe to model its destructor as only touching the contents *owned* by the box. Note: At some point we may want to generalize this machinery to other reference and collection types that are "pure" in the same sense as box. If we add a `&move` reference type, it would probably also fall into this branch of code. But for the short term, we will be conservative and restrict this change to `Box<T>` alone. The code works by recursively descending a deref of the `Box`. We prevent `visit_terminator_drop` infinite-loop (which can arise in a very obscure scenario) via a linked-list of seen types. Note: A similar style stack-only linked-list definition can be found in `rustc_mir::borrow_check::places_conflict`. It might be good at some point in the future to unify the two types and put the resulting definition into `librustc_data_structures/`. ---- One final note: Review feedback led to significant simplification of logic here. During review, eddyb RalfJung and I uncovered the heart of why I needed a so-called "step 2" aka the Shallow Write to the Deref of the box. It was because the `visit_terminator_drop`, in its base case, will not emit any write at all (shallow or deep) to a place unless that place has a need_drop. So I was encoding a Shallow Write by hand for a `Box<T>`, as a separate step from recursively descending through `*a_box` (which was at the time known as "step 1"; it is now the *only* step, apart from the change to the base case for `visit_terminator_drop` that this commit now has encoded). eddyb aruged that *something* should be emitting some sort of write in the base case here (even a shallow one), of the dropped place, since by analogy we also emit a write when you *move* a place. That led to the revision here in this commit. * (Its possible that this desired write should be attached in some manner to StorageDead instead of Drop. But in this PR, I tried to leave the StorageDead logic alone and focus my attention solely on how Drop(x) is modelled in MIR-borrowck.)
1 parent 11f812a commit c3618c8

File tree

1 file changed

+146
-5
lines changed
  • src/librustc_mir/borrow_check

1 file changed

+146
-5
lines changed

src/librustc_mir/borrow_check/mod.rs

Lines changed: 146 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ use rustc::mir::{ClearCrossCrate, Local, Location, Mir, Mutability, Operand, Pla
2222
use rustc::mir::{Field, Projection, ProjectionElem, Rvalue, Statement, StatementKind};
2323
use rustc::mir::{Terminator, TerminatorKind};
2424
use rustc::ty::query::Providers;
25-
use rustc::ty::{self, ParamEnv, TyCtxt};
25+
use rustc::ty::{self, ParamEnv, TyCtxt, Ty};
2626

2727
use rustc_errors::{Diagnostic, DiagnosticBuilder, Level};
2828
use rustc_data_structures::graph::dominators::Dominators;
@@ -598,7 +598,12 @@ impl<'cx, 'gcx, 'tcx> DataflowResultsConsumer<'cx, 'tcx> for MirBorrowckCtxt<'cx
598598
// that is useful later.
599599
let drop_place_ty = gcx.lift(&drop_place_ty).unwrap();
600600

601-
self.visit_terminator_drop(loc, term, flow_state, drop_place, drop_place_ty, span);
601+
debug!("visit_terminator_drop \
602+
loc: {:?} term: {:?} drop_place: {:?} drop_place_ty: {:?} span: {:?}",
603+
loc, term, drop_place, drop_place_ty, span);
604+
605+
self.visit_terminator_drop(
606+
loc, term, flow_state, drop_place, drop_place_ty, span, SeenTy(None));
602607
}
603608
TerminatorKind::DropAndReplace {
604609
location: ref drop_place,
@@ -832,6 +837,35 @@ impl InitializationRequiringAction {
832837
}
833838
}
834839

840+
/// A simple linked-list threaded up the stack of recursive calls in `visit_terminator_drop`.
841+
#[derive(Copy, Clone, Debug)]
842+
struct SeenTy<'a, 'gcx: 'a>(Option<(Ty<'gcx>, &'a SeenTy<'a, 'gcx>)>);
843+
844+
impl<'a, 'gcx> SeenTy<'a, 'gcx> {
845+
/// Return a new list with `ty` prepended to the front of `self`.
846+
fn cons(&'a self, ty: Ty<'gcx>) -> Self {
847+
SeenTy(Some((ty, self)))
848+
}
849+
850+
/// True if and only if `ty` occurs on the linked list `self`.
851+
fn have_seen(self, ty: Ty) -> bool {
852+
let mut this = self.0;
853+
loop {
854+
match this {
855+
None => return false,
856+
Some((seen_ty, recur)) => {
857+
if seen_ty == ty {
858+
return true;
859+
} else {
860+
this = recur.0;
861+
continue;
862+
}
863+
}
864+
}
865+
}
866+
}
867+
}
868+
835869
impl<'cx, 'gcx, 'tcx> MirBorrowckCtxt<'cx, 'gcx, 'tcx> {
836870
/// Invokes `access_place` as appropriate for dropping the value
837871
/// at `drop_place`. Note that the *actual* `Drop` in the MIR is
@@ -847,14 +881,57 @@ impl<'cx, 'gcx, 'tcx> MirBorrowckCtxt<'cx, 'gcx, 'tcx> {
847881
drop_place: &Place<'tcx>,
848882
erased_drop_place_ty: ty::Ty<'gcx>,
849883
span: Span,
884+
prev_seen: SeenTy<'_, 'gcx>,
850885
) {
886+
if prev_seen.have_seen(erased_drop_place_ty) {
887+
// if we have directly seen the input ty `T`, then we must
888+
// have had some *direct* ownership loop between `T` and
889+
// some directly-owned (as in, actually traversed by
890+
// recursive calls below) part that is also of type `T`.
891+
//
892+
// Note: in *all* such cases, the data in question cannot
893+
// be constructed (nor destructed) in finite time/space.
894+
//
895+
// Proper examples, some of which are statically rejected:
896+
//
897+
// * `struct A { field: A, ... }`:
898+
// statically rejected as infinite size
899+
//
900+
// * `type B = (B, ...);`:
901+
// statically rejected as cyclic
902+
//
903+
// * `struct C { field: Box<C>, ... }`
904+
// * `struct D { field: Box<(D, D)>, ... }`:
905+
// *accepted*, though impossible to construct
906+
//
907+
// Here is *NOT* an example:
908+
// * `struct Z { field: Option<Box<Z>>, ... }`:
909+
// Here, the type is both representable in finite space (due to the boxed indirection)
910+
// and constructable in finite time (since the recursion can bottom out with `None`).
911+
// This is an obvious instance of something the compiler must accept.
912+
//
913+
// Since some of the above impossible cases like `C` and
914+
// `D` are accepted by the compiler, we must take care not
915+
// to infinite-loop while processing them. But since such
916+
// cases cannot actually arise, it is sound for us to just
917+
// skip them during drop. If the developer uses unsafe
918+
// code to construct them, they should not be surprised by
919+
// weird drop behavior in their resulting code.
920+
debug!("visit_terminator_drop previously seen \
921+
erased_drop_place_ty: {:?} on prev_seen: {:?}; returning early.",
922+
erased_drop_place_ty, prev_seen);
923+
return;
924+
}
925+
851926
let gcx = self.tcx.global_tcx();
852927
let drop_field = |mir: &mut MirBorrowckCtxt<'cx, 'gcx, 'tcx>,
853928
(index, field): (usize, ty::Ty<'gcx>)| {
854929
let field_ty = gcx.normalize_erasing_regions(mir.param_env, field);
855930
let place = drop_place.clone().field(Field::new(index), field_ty);
856931

857-
mir.visit_terminator_drop(loc, term, flow_state, &place, field_ty, span);
932+
debug!("visit_terminator_drop drop_field place: {:?} field_ty: {:?}", place, field_ty);
933+
let seen = prev_seen.cons(erased_drop_place_ty);
934+
mir.visit_terminator_drop(loc, term, flow_state, &place, field_ty, span, seen);
858935
};
859936

860937
match erased_drop_place_ty.sty {
@@ -899,20 +976,84 @@ impl<'cx, 'gcx, 'tcx> MirBorrowckCtxt<'cx, 'gcx, 'tcx> {
899976
.enumerate()
900977
.for_each(|field| drop_field(self, field));
901978
}
979+
980+
// #45696: special-case Box<T> by treating its dtor as
981+
// only deep *across owned content*. Namely, we know
982+
// dropping a box does not touch data behind any
983+
// references it holds; if we were to instead fall into
984+
// the base case below, we would have a Deep Write due to
985+
// the box being `needs_drop`, and that Deep Write would
986+
// touch `&mut` data in the box.
987+
ty::TyAdt(def, _) if def.is_box() => {
988+
// When/if we add a `&own T` type, this action would
989+
// be like running the destructor of the `&own T`.
990+
// (And the owner of backing storage referenced by the
991+
// `&own T` would be responsible for deallocating that
992+
// backing storage.)
993+
994+
// we model dropping any content owned by the box by
995+
// recurring on box contents. This catches cases like
996+
// `Box<Box<ScribbleWhenDropped<&mut T>>>`, while
997+
// still restricting Write to *owned* content.
998+
let ty = erased_drop_place_ty.boxed_ty();
999+
let deref_place = drop_place.clone().deref();
1000+
debug!("visit_terminator_drop drop-box-content deref_place: {:?} ty: {:?}",
1001+
deref_place, ty);
1002+
let seen = prev_seen.cons(erased_drop_place_ty);
1003+
self.visit_terminator_drop(
1004+
loc, term, flow_state, &deref_place, ty, span, seen);
1005+
}
1006+
9021007
_ => {
9031008
// We have now refined the type of the value being
9041009
// dropped (potentially) to just the type of a
9051010
// subfield; so check whether that field's type still
906-
// "needs drop". If so, we assume that the destructor
907-
// may access any data it likes (i.e., a Deep Write).
1011+
// "needs drop".
9081012
if erased_drop_place_ty.needs_drop(gcx, self.param_env) {
1013+
// If so, we assume that the destructor may access
1014+
// any data it likes (i.e., a Deep Write).
9091015
self.access_place(
9101016
ContextKind::Drop.new(loc),
9111017
(drop_place, span),
9121018
(Deep, Write(WriteKind::StorageDeadOrDrop)),
9131019
LocalMutationIsAllowed::Yes,
9141020
flow_state,
9151021
);
1022+
} else {
1023+
// If there is no destructor, we still include a
1024+
// *shallow* write. This essentially ensures that
1025+
// borrows of the memory directly at `drop_place`
1026+
// cannot continue to be borrowed across the drop.
1027+
//
1028+
// If we were to use a Deep Write here, then any
1029+
// `&mut T` that is reachable from `drop_place`
1030+
// would get invalidated; fixing that is the
1031+
// essence of resolving issue #45696.
1032+
//
1033+
// * Note: In the compiler today, doing a Deep
1034+
// Write here would not actually break
1035+
// anything beyond #45696; for example it does not
1036+
// break this example:
1037+
//
1038+
// ```rust
1039+
// fn reborrow(x: &mut i32) -> &mut i32 { &mut *x }
1040+
// ```
1041+
//
1042+
// Why? Because we do not schedule/emit
1043+
// `Drop(x)` in the MIR unless `x` needs drop in
1044+
// the first place.
1045+
//
1046+
// FIXME: Its possible this logic actually should
1047+
// be attached to the `StorageDead` statement
1048+
// rather than the `Drop`. See discussion on PR
1049+
// #52782.
1050+
self.access_place(
1051+
ContextKind::Drop.new(loc),
1052+
(drop_place, span),
1053+
(Shallow(None), Write(WriteKind::StorageDeadOrDrop)),
1054+
LocalMutationIsAllowed::Yes,
1055+
flow_state,
1056+
);
9161057
}
9171058
}
9181059
}

0 commit comments

Comments
 (0)