Skip to content

Commit 21c0251

Browse files
committed
Miri: add a flag to do recursive validity checking
1 parent 2cec7a8 commit 21c0251

File tree

14 files changed

+186
-107
lines changed

14 files changed

+186
-107
lines changed

Diff for: compiler/rustc_const_eval/src/const_eval/eval_queries.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -396,7 +396,7 @@ fn const_validate_mplace<'tcx>(
396396
let alloc_id = mplace.ptr().provenance.unwrap().alloc_id();
397397
let mut ref_tracking = RefTracking::new(mplace.clone());
398398
let mut inner = false;
399-
while let Some((mplace, path)) = ref_tracking.todo.pop() {
399+
while let Some((mplace, path)) = ref_tracking.next() {
400400
let mode = match ecx.tcx.static_mutability(cid.instance.def_id()) {
401401
_ if cid.promoted.is_some() => CtfeValidationMode::Promoted,
402402
Some(mutbl) => CtfeValidationMode::Static { mutbl }, // a `static`

Diff for: compiler/rustc_const_eval/src/interpret/machine.rs

+7
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,13 @@ pub trait Machine<'tcx>: Sized {
165165

166166
/// Whether to enforce the validity invariant for a specific layout.
167167
fn enforce_validity(ecx: &InterpCx<'tcx, Self>, layout: TyAndLayout<'tcx>) -> bool;
168+
/// Whether to enforce the validity invariant *recursively*.
169+
fn enforce_validity_recursively(
170+
_ecx: &InterpCx<'tcx, Self>,
171+
_layout: TyAndLayout<'tcx>,
172+
) -> bool {
173+
false
174+
}
168175

169176
/// Whether function calls should be [ABI](CallAbi)-checked.
170177
fn enforce_abi(_ecx: &InterpCx<'tcx, Self>) -> bool {

Diff for: compiler/rustc_const_eval/src/interpret/memory.rs

+4-1
Original file line numberDiff line numberDiff line change
@@ -1006,8 +1006,11 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
10061006
})
10071007
}
10081008

1009-
/// Runs the close in "validation" mode, which means the machine's memory read hooks will be
1009+
/// Runs the closure in "validation" mode, which means the machine's memory read hooks will be
10101010
/// suppressed. Needless to say, this must only be set with great care! Cannot be nested.
1011+
///
1012+
/// We do this so Miri's allocation access tracking does not show the validation
1013+
/// reads as spurious accesses.
10111014
pub(super) fn run_for_validation<R>(&self, f: impl FnOnce() -> R) -> R {
10121015
// This deliberately uses `==` on `bool` to follow the pattern
10131016
// `assert!(val.replace(new) == old)`.

Diff for: compiler/rustc_const_eval/src/interpret/place.rs

+12-3
Original file line numberDiff line numberDiff line change
@@ -572,7 +572,10 @@ where
572572

573573
if M::enforce_validity(self, dest.layout()) {
574574
// Data got changed, better make sure it matches the type!
575-
self.validate_operand(&dest.to_op(self)?)?;
575+
self.validate_operand(
576+
&dest.to_op(self)?,
577+
M::enforce_validity_recursively(self, dest.layout()),
578+
)?;
576579
}
577580

578581
Ok(())
@@ -811,15 +814,21 @@ where
811814
// Generally for transmutation, data must be valid both at the old and new type.
812815
// But if the types are the same, the 2nd validation below suffices.
813816
if src.layout().ty != dest.layout().ty && M::enforce_validity(self, src.layout()) {
814-
self.validate_operand(&src.to_op(self)?)?;
817+
self.validate_operand(
818+
&src.to_op(self)?,
819+
M::enforce_validity_recursively(self, src.layout()),
820+
)?;
815821
}
816822

817823
// Do the actual copy.
818824
self.copy_op_no_validate(src, dest, allow_transmute)?;
819825

820826
if validate_dest && M::enforce_validity(self, dest.layout()) {
821827
// Data got changed, better make sure it matches the type!
822-
self.validate_operand(&dest.to_op(self)?)?;
828+
self.validate_operand(
829+
&dest.to_op(self)?,
830+
M::enforce_validity_recursively(self, dest.layout()),
831+
)?;
823832
}
824833

825834
Ok(())

Diff for: compiler/rustc_const_eval/src/interpret/validity.rs

+104-72
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,8 @@ impl CtfeValidationMode {
155155

156156
/// State for tracking recursive validation of references
157157
pub struct RefTracking<T, PATH = ()> {
158-
pub seen: FxHashSet<T>,
159-
pub todo: Vec<(T, PATH)>,
158+
seen: FxHashSet<T>,
159+
todo: Vec<(T, PATH)>,
160160
}
161161

162162
impl<T: Clone + Eq + Hash + std::fmt::Debug, PATH: Default> RefTracking<T, PATH> {
@@ -169,8 +169,11 @@ impl<T: Clone + Eq + Hash + std::fmt::Debug, PATH: Default> RefTracking<T, PATH>
169169
ref_tracking_for_consts.seen.insert(op);
170170
ref_tracking_for_consts
171171
}
172+
pub fn next(&mut self) -> Option<(T, PATH)> {
173+
self.todo.pop()
174+
}
172175

173-
pub fn track(&mut self, op: T, path: impl FnOnce() -> PATH) {
176+
fn track(&mut self, op: T, path: impl FnOnce() -> PATH) {
174177
if self.seen.insert(op.clone()) {
175178
trace!("Recursing below ptr {:#?}", op);
176179
let path = path();
@@ -435,95 +438,112 @@ impl<'rt, 'tcx, M: Machine<'tcx>> ValidityVisitor<'rt, 'tcx, M> {
435438
if self.ecx.scalar_may_be_null(Scalar::from_maybe_pointer(place.ptr(), self.ecx))? {
436439
throw_validation_failure!(self.path, NullPtr { ptr_kind })
437440
}
438-
// Do not allow pointers to uninhabited types.
441+
// Do not allow references to uninhabited types.
439442
if place.layout.abi.is_uninhabited() {
440443
let ty = place.layout.ty;
441444
throw_validation_failure!(self.path, PtrToUninhabited { ptr_kind, ty })
442445
}
443446
// Recursive checking
444447
if let Some(ref_tracking) = self.ref_tracking.as_deref_mut() {
445-
// Determine whether this pointer expects to be pointing to something mutable.
446-
let ptr_expected_mutbl = match ptr_kind {
447-
PointerKind::Box => Mutability::Mut,
448-
PointerKind::Ref(mutbl) => {
449-
// We do not take into account interior mutability here since we cannot know if
450-
// there really is an `UnsafeCell` inside `Option<UnsafeCell>` -- so we check
451-
// that in the recursive descent behind this reference (controlled by
452-
// `allow_immutable_unsafe_cell`).
453-
mutbl
454-
}
455-
};
456448
// Proceed recursively even for ZST, no reason to skip them!
457449
// `!` is a ZST and we want to validate it.
458-
if let Ok((alloc_id, _offset, _prov)) = self.ecx.ptr_try_get_alloc_id(place.ptr(), 0) {
450+
if let Some(ctfe_mode) = self.ctfe_mode {
459451
let mut skip_recursive_check = false;
460-
if let Some(GlobalAlloc::Static(did)) = self.ecx.tcx.try_get_global_alloc(alloc_id)
452+
// CTFE imposes restrictions on what references can point to.
453+
if let Ok((alloc_id, _offset, _prov)) =
454+
self.ecx.ptr_try_get_alloc_id(place.ptr(), 0)
461455
{
462-
let DefKind::Static { nested, .. } = self.ecx.tcx.def_kind(did) else { bug!() };
463-
// Special handling for pointers to statics (irrespective of their type).
464-
assert!(!self.ecx.tcx.is_thread_local_static(did));
465-
assert!(self.ecx.tcx.is_static(did));
466-
// Mode-specific checks
467-
match self.ctfe_mode {
468-
Some(
469-
CtfeValidationMode::Static { .. } | CtfeValidationMode::Promoted { .. },
470-
) => {
471-
// We skip recursively checking other statics. These statics must be sound by
472-
// themselves, and the only way to get broken statics here is by using
473-
// unsafe code.
474-
// The reasons we don't check other statics is twofold. For one, in all
475-
// sound cases, the static was already validated on its own, and second, we
476-
// trigger cycle errors if we try to compute the value of the other static
477-
// and that static refers back to us (potentially through a promoted).
478-
// This could miss some UB, but that's fine.
479-
// We still walk nested allocations, as they are fundamentally part of this validation run.
480-
// This means we will also recurse into nested statics of *other*
481-
// statics, even though we do not recurse into other statics directly.
482-
// That's somewhat inconsistent but harmless.
483-
skip_recursive_check = !nested;
484-
}
485-
Some(CtfeValidationMode::Const { .. }) => {
486-
// We can't recursively validate `extern static`, so we better reject them.
487-
if self.ecx.tcx.is_foreign_item(did) {
488-
throw_validation_failure!(self.path, ConstRefToExtern);
456+
if let Some(GlobalAlloc::Static(did)) =
457+
self.ecx.tcx.try_get_global_alloc(alloc_id)
458+
{
459+
let DefKind::Static { nested, .. } = self.ecx.tcx.def_kind(did) else {
460+
bug!()
461+
};
462+
// Special handling for pointers to statics (irrespective of their type).
463+
assert!(!self.ecx.tcx.is_thread_local_static(did));
464+
assert!(self.ecx.tcx.is_static(did));
465+
// Mode-specific checks
466+
match ctfe_mode {
467+
CtfeValidationMode::Static { .. }
468+
| CtfeValidationMode::Promoted { .. } => {
469+
// We skip recursively checking other statics. These statics must be sound by
470+
// themselves, and the only way to get broken statics here is by using
471+
// unsafe code.
472+
// The reasons we don't check other statics is twofold. For one, in all
473+
// sound cases, the static was already validated on its own, and second, we
474+
// trigger cycle errors if we try to compute the value of the other static
475+
// and that static refers back to us (potentially through a promoted).
476+
// This could miss some UB, but that's fine.
477+
// We still walk nested allocations, as they are fundamentally part of this validation run.
478+
// This means we will also recurse into nested statics of *other*
479+
// statics, even though we do not recurse into other statics directly.
480+
// That's somewhat inconsistent but harmless.
481+
skip_recursive_check = !nested;
482+
}
483+
CtfeValidationMode::Const { .. } => {
484+
// We can't recursively validate `extern static`, so we better reject them.
485+
if self.ecx.tcx.is_foreign_item(did) {
486+
throw_validation_failure!(self.path, ConstRefToExtern);
487+
}
489488
}
490489
}
491-
None => {}
492490
}
493-
}
494491

495-
// Dangling and Mutability check.
496-
let (size, _align, alloc_kind) = self.ecx.get_alloc_info(alloc_id);
497-
if alloc_kind == AllocKind::Dead {
498-
// This can happen for zero-sized references. We can't have *any* references to non-existing
499-
// allocations though, interning rejects them all as the rest of rustc isn't happy with them...
500-
// so we throw an error, even though this isn't really UB.
501-
// A potential future alternative would be to resurrect this as a zero-sized allocation
502-
// (which codegen will then compile to an aligned dummy pointer anyway).
503-
throw_validation_failure!(self.path, DanglingPtrUseAfterFree { ptr_kind });
504-
}
505-
// If this allocation has size zero, there is no actual mutability here.
506-
if size != Size::ZERO {
507-
let alloc_actual_mutbl = mutability(self.ecx, alloc_id);
508-
// Mutable pointer to immutable memory is no good.
509-
if ptr_expected_mutbl == Mutability::Mut
510-
&& alloc_actual_mutbl == Mutability::Not
511-
{
512-
throw_validation_failure!(self.path, MutableRefToImmutable);
492+
// Dangling and Mutability check.
493+
let (size, _align, alloc_kind) = self.ecx.get_alloc_info(alloc_id);
494+
if alloc_kind == AllocKind::Dead {
495+
// This can happen for zero-sized references. We can't have *any* references to
496+
// non-existing allocations in const-eval though, interning rejects them all as
497+
// the rest of rustc isn't happy with them... so we throw an error, even though
498+
// this isn't really UB.
499+
// A potential future alternative would be to resurrect this as a zero-sized allocation
500+
// (which codegen will then compile to an aligned dummy pointer anyway).
501+
throw_validation_failure!(self.path, DanglingPtrUseAfterFree { ptr_kind });
513502
}
514-
// In a const, everything must be completely immutable.
515-
if matches!(self.ctfe_mode, Some(CtfeValidationMode::Const { .. })) {
503+
// If this allocation has size zero, there is no actual mutability here.
504+
if size != Size::ZERO {
505+
// Determine whether this pointer expects to be pointing to something mutable.
506+
let ptr_expected_mutbl = match ptr_kind {
507+
PointerKind::Box => Mutability::Mut,
508+
PointerKind::Ref(mutbl) => {
509+
// We do not take into account interior mutability here since we cannot know if
510+
// there really is an `UnsafeCell` inside `Option<UnsafeCell>` -- so we check
511+
// that in the recursive descent behind this reference (controlled by
512+
// `allow_immutable_unsafe_cell`).
513+
mutbl
514+
}
515+
};
516+
// Determine what it actually points to.
517+
let alloc_actual_mutbl = mutability(self.ecx, alloc_id);
518+
// Mutable pointer to immutable memory is no good.
516519
if ptr_expected_mutbl == Mutability::Mut
517-
|| alloc_actual_mutbl == Mutability::Mut
520+
&& alloc_actual_mutbl == Mutability::Not
518521
{
519-
throw_validation_failure!(self.path, ConstRefToMutable);
522+
throw_validation_failure!(self.path, MutableRefToImmutable);
523+
}
524+
// In a const, everything must be completely immutable.
525+
if matches!(self.ctfe_mode, Some(CtfeValidationMode::Const { .. })) {
526+
if ptr_expected_mutbl == Mutability::Mut
527+
|| alloc_actual_mutbl == Mutability::Mut
528+
{
529+
throw_validation_failure!(self.path, ConstRefToMutable);
530+
}
520531
}
521532
}
522533
}
523534
// Potentially skip recursive check.
524535
if skip_recursive_check {
525536
return Ok(());
526537
}
538+
} else {
539+
// This is not CTFE, so it's Miri with recursive checking.
540+
// FIXME: we do *not* check behind boxes, since creating a new box first creates it uninitialized
541+
// and then puts the value in there, so briefly we have a box with uninit contents.
542+
// FIXME: should we also skip `UnsafeCell` behind shared references? Currently that is not
543+
// needed since validation reads bypass Stacked Borrows and data race checks.
544+
if matches!(ptr_kind, PointerKind::Box) {
545+
return Ok(());
546+
}
527547
}
528548
let path = &self.path;
529549
ref_tracking.track(place, || {
@@ -1072,11 +1092,23 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> {
10721092
/// `op` is assumed to cover valid memory if it is an indirect operand.
10731093
/// It will error if the bits at the destination do not match the ones described by the layout.
10741094
#[inline(always)]
1075-
pub fn validate_operand(&self, op: &OpTy<'tcx, M::Provenance>) -> InterpResult<'tcx> {
1095+
pub fn validate_operand(
1096+
&self,
1097+
op: &OpTy<'tcx, M::Provenance>,
1098+
recursive: bool,
1099+
) -> InterpResult<'tcx> {
10761100
// Note that we *could* actually be in CTFE here with `-Zextra-const-ub-checks`, but it's
10771101
// still correct to not use `ctfe_mode`: that mode is for validation of the final constant
1078-
// value, it rules out things like `UnsafeCell` in awkward places. It also can make checking
1079-
// recurse through references which, for now, we don't want here, either.
1080-
self.validate_operand_internal(op, vec![], None, None)
1102+
// value, it rules out things like `UnsafeCell` in awkward places.
1103+
if !recursive {
1104+
return self.validate_operand_internal(op, vec![], None, None);
1105+
}
1106+
// Do a recursive check.
1107+
let mut ref_tracking = RefTracking::empty();
1108+
self.validate_operand_internal(op, vec![], Some(&mut ref_tracking), None)?;
1109+
while let Some((mplace, path)) = ref_tracking.todo.pop() {
1110+
self.validate_operand_internal(&mplace.into(), path, Some(&mut ref_tracking), None)?;
1111+
}
1112+
Ok(())
10811113
}
10821114
}

Diff for: compiler/rustc_const_eval/src/util/check_validity_requirement.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ fn might_permit_raw_init_strict<'tcx>(
6767
// This does *not* actually check that references are dereferenceable, but since all types that
6868
// require dereferenceability also require non-null, we don't actually get any false negatives
6969
// due to this.
70-
Ok(cx.validate_operand(&ot).is_ok())
70+
Ok(cx.validate_operand(&ot, /*recursive*/ false).is_ok())
7171
}
7272

7373
/// Implements the 'lax' (default) version of the `might_permit_raw_init` checks; see that function for

Diff for: src/tools/miri/README.md

+2
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,8 @@ to Miri failing to detect cases of undefined behavior in a program.
398398
application instead of raising an error within the context of Miri (and halting
399399
execution). Note that code might not expect these operations to ever panic, so
400400
this flag can lead to strange (mis)behavior.
401+
* `-Zmiri-recursive-validation` is a *highly experimental* flag that makes validity checking
402+
recurse below references.
401403
* `-Zmiri-retag-fields[=<all|none|scalar>]` controls when Stacked Borrows retagging recurses into
402404
fields. `all` means it always recurses (the default, and equivalent to `-Zmiri-retag-fields`
403405
without an explicit value), `none` means it never recurses, `scalar` means it only recurses for

Diff for: src/tools/miri/src/bin/miri.rs

+4-2
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ use rustc_session::config::{CrateType, ErrorOutputType, OptLevel};
4444
use rustc_session::search_paths::PathKind;
4545
use rustc_session::{CtfeBacktrace, EarlyDiagCtxt};
4646

47-
use miri::{BacktraceStyle, BorrowTrackerMethod, ProvenanceMode, RetagFields};
47+
use miri::{BacktraceStyle, BorrowTrackerMethod, ProvenanceMode, RetagFields, ValidationMode};
4848

4949
struct MiriCompilerCalls {
5050
miri_config: miri::MiriConfig,
@@ -421,7 +421,9 @@ fn main() {
421421
} else if arg == "--" {
422422
after_dashdash = true;
423423
} else if arg == "-Zmiri-disable-validation" {
424-
miri_config.validate = false;
424+
miri_config.validation = ValidationMode::No;
425+
} else if arg == "-Zmiri-recursive-validation" {
426+
miri_config.validation = ValidationMode::Deep;
425427
} else if arg == "-Zmiri-disable-stacked-borrows" {
426428
miri_config.borrow_tracker = None;
427429
} else if arg == "-Zmiri-tree-borrows" {

0 commit comments

Comments
 (0)