blob: 10467fa79d9e78674b60fa12f4a10b427704b709 [file] [log] [blame]
use std::fmt::{self, Write};
use std::num::NonZero;
use std::sync::Mutex;
use rustc_abi::{Align, Size};
use rustc_data_structures::fx::{FxBuildHasher, FxHashSet};
use rustc_errors::{Diag, DiagMessage, Level};
use rustc_span::{DUMMY_SP, Span, SpanData, Symbol};
use crate::borrow_tracker::stacked_borrows::diagnostics::TagHistory;
use crate::borrow_tracker::tree_borrows::diagnostics as tree_diagnostics;
use crate::*;
/// Details of premature program termination.
pub enum TerminationInfo {
Exit {
code: i32,
leak_check: bool,
},
Abort(String),
/// Miri was interrupted by a Ctrl+C from the user
Interrupted,
UnsupportedInIsolation(String),
StackedBorrowsUb {
msg: String,
help: Vec<String>,
history: Option<TagHistory>,
},
TreeBorrowsUb {
title: String,
details: Vec<String>,
history: tree_diagnostics::HistoryData,
},
Int2PtrWithStrictProvenance,
/// All threads are blocked.
GlobalDeadlock,
/// Some thread discovered a deadlock condition (e.g. in a mutex with reentrancy checking).
LocalDeadlock,
MultipleSymbolDefinitions {
link_name: Symbol,
first: SpanData,
first_crate: Symbol,
second: SpanData,
second_crate: Symbol,
},
SymbolShimClashing {
link_name: Symbol,
span: SpanData,
},
DataRace {
involves_non_atomic: bool,
ptr: interpret::Pointer<AllocId>,
op1: RacingOp,
op2: RacingOp,
extra: Option<&'static str>,
retag_explain: bool,
},
UnsupportedForeignItem(String),
}
pub struct RacingOp {
pub action: String,
pub thread_info: String,
pub span: SpanData,
}
impl fmt::Display for TerminationInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use TerminationInfo::*;
match self {
Exit { code, .. } => write!(f, "the evaluated program completed with exit code {code}"),
Abort(msg) => write!(f, "{msg}"),
Interrupted => write!(f, "interpretation was interrupted"),
UnsupportedInIsolation(msg) => write!(f, "{msg}"),
Int2PtrWithStrictProvenance =>
write!(
f,
"integer-to-pointer casts and `ptr::with_exposed_provenance` are not supported with `-Zmiri-strict-provenance`"
),
StackedBorrowsUb { msg, .. } => write!(f, "{msg}"),
TreeBorrowsUb { title, .. } => write!(f, "{title}"),
GlobalDeadlock => write!(f, "the evaluated program deadlocked"),
LocalDeadlock => write!(f, "a thread deadlocked"),
MultipleSymbolDefinitions { link_name, .. } =>
write!(f, "multiple definitions of symbol `{link_name}`"),
SymbolShimClashing { link_name, .. } =>
write!(f, "found `{link_name}` symbol definition that clashes with a built-in shim",),
DataRace { involves_non_atomic, ptr, op1, op2, .. } =>
write!(
f,
"{} detected between (1) {} on {} and (2) {} on {} at {ptr:?}",
if *involves_non_atomic { "Data race" } else { "Race condition" },
op1.action,
op1.thread_info,
op2.action,
op2.thread_info
),
UnsupportedForeignItem(msg) => write!(f, "{msg}"),
}
}
}
impl fmt::Debug for TerminationInfo {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{self}")
}
}
impl MachineStopType for TerminationInfo {
fn diagnostic_message(&self) -> DiagMessage {
self.to_string().into()
}
fn add_args(
self: Box<Self>,
_: &mut dyn FnMut(std::borrow::Cow<'static, str>, rustc_errors::DiagArgValue),
) {
}
}
/// Miri specific diagnostics
pub enum NonHaltingDiagnostic {
/// (new_tag, new_perm, (alloc_id, base_offset, orig_tag))
///
/// new_perm is `None` for base tags.
CreatedPointerTag(NonZero<u64>, Option<String>, Option<(AllocId, AllocRange, ProvenanceExtra)>),
/// This `Item` was popped from the borrow stack. The string explains the reason.
PoppedPointerTag(Item, String),
TrackingAlloc(AllocId, Size, Align),
FreedAlloc(AllocId),
AccessedAlloc(AllocId, AllocRange, borrow_tracker::AccessKind),
RejectedIsolatedOp(String),
ProgressReport {
block_count: u64, // how many basic blocks have been run so far
},
Int2Ptr {
details: bool,
},
NativeCallSharedMem {
tracing: bool,
},
NativeCallFnPtr,
WeakMemoryOutdatedLoad {
ptr: Pointer,
},
ExternTypeReborrow,
GenmcCompareExchangeWeak,
GenmcCompareExchangeOrderingMismatch {
success_ordering: AtomicRwOrd,
upgraded_success_ordering: AtomicRwOrd,
failure_ordering: AtomicReadOrd,
effective_failure_ordering: AtomicReadOrd,
},
}
/// Level of Miri specific diagnostics
pub enum DiagLevel {
Error,
Warning,
Note,
}
/// Generate a note/help text without a span.
macro_rules! note {
($($tt:tt)*) => { (None, format!($($tt)*)) };
}
/// Generate a note/help text with a span.
macro_rules! note_span {
($span:expr, $($tt:tt)*) => { (Some($span), format!($($tt)*)) };
}
/// Attempts to prune a stacktrace to omit the Rust runtime, and returns a bool indicating if any
/// frames were pruned. If the stacktrace does not have any local frames, we conclude that it must
/// be pointing to a problem in the Rust runtime itself, and do not prune it at all.
pub fn prune_stacktrace<'tcx>(
mut stacktrace: Vec<FrameInfo<'tcx>>,
machine: &MiriMachine<'tcx>,
) -> (Vec<FrameInfo<'tcx>>, bool) {
match machine.backtrace_style {
BacktraceStyle::Off => {
// Remove all frames marked with `caller_location` -- that attribute indicates we
// usually want to point at the caller, not them.
stacktrace.retain(|frame| !frame.instance.def.requires_caller_location(machine.tcx));
// Retain one frame so that we can print a span for the error itself
stacktrace.truncate(1);
(stacktrace, false)
}
BacktraceStyle::Short => {
let original_len = stacktrace.len();
// Remove all frames marked with `caller_location` -- that attribute indicates we
// usually want to point at the caller, not them.
stacktrace.retain(|frame| !frame.instance.def.requires_caller_location(machine.tcx));
// Only prune further frames if there is at least one local frame. This check ensures
// that if we get a backtrace that never makes it to the user code because it has
// detected a bug in the Rust runtime, we don't prune away every frame.
let has_local_frame = stacktrace.iter().any(|frame| machine.is_local(frame.instance));
if has_local_frame {
// This is part of the logic that `std` uses to select the relevant part of a
// backtrace. But here, we only look for __rust_begin_short_backtrace, not
// __rust_end_short_backtrace because the end symbol comes from a call to the default
// panic handler.
stacktrace = stacktrace
.into_iter()
.take_while(|frame| {
let def_id = frame.instance.def_id();
let path = machine.tcx.def_path_str(def_id);
!path.contains("__rust_begin_short_backtrace")
})
.collect::<Vec<_>>();
// After we prune frames from the bottom, there are a few left that are part of the
// Rust runtime. So we remove frames until we get to a local symbol, which should be
// main or a test.
// This len check ensures that we don't somehow remove every frame, as doing so breaks
// the primary error message.
while stacktrace.len() > 1
&& stacktrace.last().is_some_and(|frame| !machine.is_local(frame.instance))
{
stacktrace.pop();
}
}
let was_pruned = stacktrace.len() != original_len;
(stacktrace, was_pruned)
}
BacktraceStyle::Full => (stacktrace, false),
}
}
/// Report the result of a Miri execution.
///
/// Returns `Some` if this was regular program termination with a given exit code and a `bool`
/// indicating whether a leak check should happen; `None` otherwise.
pub fn report_result<'tcx>(
ecx: &InterpCx<'tcx, MiriMachine<'tcx>>,
res: InterpErrorInfo<'tcx>,
) -> Option<(i32, bool)> {
use InterpErrorKind::*;
use UndefinedBehaviorInfo::*;
let mut labels = vec![];
let (title, helps) = if let MachineStop(info) = res.kind() {
let info = info.downcast_ref::<TerminationInfo>().expect("invalid MachineStop payload");
use TerminationInfo::*;
let title = match info {
&Exit { code, leak_check } => return Some((code, leak_check)),
Abort(_) => Some("abnormal termination"),
Interrupted => None,
UnsupportedInIsolation(_) | Int2PtrWithStrictProvenance | UnsupportedForeignItem(_) =>
Some("unsupported operation"),
StackedBorrowsUb { .. } | TreeBorrowsUb { .. } | DataRace { .. } =>
Some("Undefined Behavior"),
LocalDeadlock => {
labels.push(format!("this thread got stuck here"));
None
}
GlobalDeadlock => {
// Global deadlocks are reported differently: just show all blocked threads.
// The "active" thread might actually be terminated, so we ignore it.
let mut any_pruned = false;
for (thread, stack) in ecx.machine.threads.all_blocked_stacks() {
let stacktrace = Frame::generate_stacktrace_from_stack(stack);
let (stacktrace, was_pruned) = prune_stacktrace(stacktrace, &ecx.machine);
any_pruned |= was_pruned;
report_msg(
DiagLevel::Error,
format!("the evaluated program deadlocked"),
vec![format!(
"thread `{}` got stuck here",
ecx.machine.threads.get_thread_display_name(thread)
)],
vec![],
vec![],
&stacktrace,
Some(thread),
&ecx.machine,
)
}
if any_pruned {
ecx.tcx.dcx().note(
"some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace"
);
}
return None;
}
MultipleSymbolDefinitions { .. } | SymbolShimClashing { .. } => None,
};
#[rustfmt::skip]
let helps = match info {
UnsupportedInIsolation(_) =>
vec![
note!("set `MIRIFLAGS=-Zmiri-disable-isolation` to disable isolation;"),
note!("or set `MIRIFLAGS=-Zmiri-isolation-error=warn` to make Miri return an error code from isolated operations (if supported for that operation) and continue with a warning"),
],
UnsupportedForeignItem(_) => {
vec![
note!("this means the program tried to do something Miri does not support; it does not indicate a bug in the program"),
]
}
StackedBorrowsUb { help, history, .. } => {
labels.extend(help.clone());
let mut helps = vec![
note!("this indicates a potential bug in the program: it performed an invalid operation, but the Stacked Borrows rules it violated are still experimental"),
note!("see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/stacked-borrows.md for further information"),
];
if let Some(TagHistory {created, invalidated, protected}) = history.clone() {
helps.push((Some(created.1), created.0));
if let Some((msg, span)) = invalidated {
helps.push(note_span!(span, "{msg}"));
}
if let Some((protector_msg, protector_span)) = protected {
helps.push(note_span!(protector_span, "{protector_msg}"));
}
}
helps
},
TreeBorrowsUb { title: _, details, history } => {
let mut helps = vec![
note!("this indicates a potential bug in the program: it performed an invalid operation, but the Tree Borrows rules it violated are still experimental"),
note!("see https://github.com/rust-lang/unsafe-code-guidelines/blob/master/wip/tree-borrows.md for further information"),
];
for m in details {
helps.push(note!("{m}"));
}
for event in history.events.clone() {
helps.push(event);
}
helps
}
MultipleSymbolDefinitions { first, first_crate, second, second_crate, .. } =>
vec![
note_span!(*first, "it's first defined here, in crate `{first_crate}`"),
note_span!(*second, "then it's defined here again, in crate `{second_crate}`"),
],
SymbolShimClashing { link_name, span } =>
vec![note_span!(*span, "the `{link_name}` symbol is defined here")],
Int2PtrWithStrictProvenance =>
vec![note!("use Strict Provenance APIs (https://doc.rust-lang.org/nightly/std/ptr/index.html#strict-provenance, https://crates.io/crates/sptr) instead")],
DataRace { op1, extra, retag_explain, .. } => {
labels.push(format!("(2) just happened here"));
let mut helps = vec![note_span!(op1.span, "and (1) occurred earlier here")];
if let Some(extra) = extra {
helps.push(note!("{extra}"));
helps.push(note!("see https://doc.rust-lang.org/nightly/std/sync/atomic/index.html#memory-model-for-atomic-accesses for more information about the Rust memory model"));
}
if *retag_explain {
helps.push(note!("retags occur on all (re)borrows and as well as when references are copied or moved"));
helps.push(note!("retags permit optimizations that insert speculative reads or writes"));
helps.push(note!("therefore from the perspective of data races, a retag has the same implications as a read or write"));
}
helps.push(note!("this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior"));
helps.push(note!("see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information"));
helps
}
,
_ => vec![],
};
(title, helps)
} else {
let title = match res.kind() {
UndefinedBehavior(ValidationError(validation_err))
if matches!(
validation_err.kind,
ValidationErrorKind::PointerAsInt { .. } | ValidationErrorKind::PartialPointer
) =>
{
ecx.handle_ice(); // print interpreter backtrace (this is outside the eval `catch_unwind`)
bug!(
"This validation error should be impossible in Miri: {}",
format_interp_error(ecx.tcx.dcx(), res)
);
}
UndefinedBehavior(_) => "Undefined Behavior",
ResourceExhaustion(_) => "resource exhaustion",
Unsupported(
// We list only the ones that can actually happen.
UnsupportedOpInfo::Unsupported(_)
| UnsupportedOpInfo::UnsizedLocal
| UnsupportedOpInfo::ExternTypeField,
) => "unsupported operation",
InvalidProgram(
// We list only the ones that can actually happen.
InvalidProgramInfo::AlreadyReported(_) | InvalidProgramInfo::Layout(..),
) => "post-monomorphization error",
_ => {
ecx.handle_ice(); // print interpreter backtrace (this is outside the eval `catch_unwind`)
bug!(
"This error should be impossible in Miri: {}",
format_interp_error(ecx.tcx.dcx(), res)
);
}
};
#[rustfmt::skip]
let helps = match res.kind() {
Unsupported(_) =>
vec![
note!("this is likely not a bug in the program; it indicates that the program performed an operation that Miri does not support"),
],
ResourceExhaustion(ResourceExhaustionInfo::AddressSpaceFull) if ecx.machine.data_race.as_genmc_ref().is_some() =>
vec![
note!("in GenMC mode, the address space is limited to 4GB per thread, and addresses cannot be reused")
],
UndefinedBehavior(AlignmentCheckFailed { .. })
if ecx.machine.check_alignment == AlignmentCheck::Symbolic
=>
vec![
note!("this usually indicates that your program performed an invalid operation and caused Undefined Behavior"),
note!("but due to `-Zmiri-symbolic-alignment-check`, alignment errors can also be false positives"),
],
UndefinedBehavior(info) => {
let mut helps = vec![
note!("this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior"),
note!("see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information"),
];
match info {
PointerUseAfterFree(alloc_id, _) | PointerOutOfBounds { alloc_id, .. } => {
if let Some(span) = ecx.machine.allocated_span(*alloc_id) {
helps.push(note_span!(span, "{:?} was allocated here:", alloc_id));
}
if let Some(span) = ecx.machine.deallocated_span(*alloc_id) {
helps.push(note_span!(span, "{:?} was deallocated here:", alloc_id));
}
}
AbiMismatchArgument { .. } | AbiMismatchReturn { .. } => {
helps.push(note!("this means these two types are not *guaranteed* to be ABI-compatible across all targets"));
helps.push(note!("if you think this code should be accepted anyway, please report an issue with Miri"));
}
_ => {},
}
helps
}
InvalidProgram(
InvalidProgramInfo::AlreadyReported(_)
) => {
// This got already reported. No point in reporting it again.
return None;
}
_ =>
vec![],
};
(Some(title), helps)
};
let stacktrace = ecx.generate_stacktrace();
let (stacktrace, pruned) = prune_stacktrace(stacktrace, &ecx.machine);
// We want to dump the allocation if this is `InvalidUninitBytes`.
// Since `format_interp_error` consumes `e`, we compute the outut early.
let mut extra = String::new();
match res.kind() {
UndefinedBehavior(InvalidUninitBytes(Some((alloc_id, access)))) => {
writeln!(
extra,
"Uninitialized memory occurred at {alloc_id:?}{range:?}, in this allocation:",
range = access.bad,
)
.unwrap();
writeln!(extra, "{:?}", ecx.dump_alloc(*alloc_id)).unwrap();
}
_ => {}
}
let mut primary_msg = String::new();
if let Some(title) = title {
write!(primary_msg, "{title}: ").unwrap();
}
write!(primary_msg, "{}", format_interp_error(ecx.tcx.dcx(), res)).unwrap();
if labels.is_empty() {
labels.push(format!(
"{} occurred {}",
title.unwrap_or("error"),
if stacktrace.is_empty() { "due to this code" } else { "here" }
));
}
report_msg(
DiagLevel::Error,
primary_msg,
labels,
vec![],
helps,
&stacktrace,
Some(ecx.active_thread()),
&ecx.machine,
);
eprint!("{extra}"); // newlines are already in the string
// Include a note like `std` does when we omit frames from a backtrace
if pruned {
ecx.tcx.dcx().note(
"some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace",
);
}
// Debug-dump all locals.
for (i, frame) in ecx.active_thread_stack().iter().enumerate() {
trace!("-------------------");
trace!("Frame {}", i);
trace!(" return: {:?}", frame.return_place);
for (i, local) in frame.locals.iter().enumerate() {
trace!(" local {}: {:?}", i, local);
}
}
None
}
pub fn report_leaks<'tcx>(
ecx: &InterpCx<'tcx, MiriMachine<'tcx>>,
leaks: Vec<(AllocId, MemoryKind, Allocation<Provenance, AllocExtra<'tcx>, MiriAllocBytes>)>,
) {
let mut any_pruned = false;
for (id, kind, alloc) in leaks {
let mut title = format!(
"memory leaked: {id:?} ({}, size: {:?}, align: {:?})",
kind,
alloc.size().bytes(),
alloc.align.bytes()
);
let Some(backtrace) = alloc.extra.backtrace else {
ecx.tcx.dcx().err(title);
continue;
};
title.push_str(", allocated here:");
let (backtrace, pruned) = prune_stacktrace(backtrace, &ecx.machine);
any_pruned |= pruned;
report_msg(
DiagLevel::Error,
title,
vec![],
vec![],
vec![],
&backtrace,
None, // we don't know the thread this is from
&ecx.machine,
);
}
if any_pruned {
ecx.tcx.dcx().note(
"some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace",
);
}
}
/// Report an error or note (depending on the `error` argument) with the given stacktrace.
/// Also emits a full stacktrace of the interpreter stack.
/// We want to present a multi-line span message for some errors. Diagnostics do not support this
/// directly, so we pass the lines as a `Vec<String>` and display each line after the first with an
/// additional `span_label` or `note` call.
fn report_msg<'tcx>(
diag_level: DiagLevel,
title: String,
span_msg: Vec<String>,
notes: Vec<(Option<SpanData>, String)>,
helps: Vec<(Option<SpanData>, String)>,
stacktrace: &[FrameInfo<'tcx>],
thread: Option<ThreadId>,
machine: &MiriMachine<'tcx>,
) {
let span = match stacktrace.first() {
Some(fi) => fi.span,
None =>
match thread {
Some(thread_id) => machine.threads.thread_ref(thread_id).origin_span,
None => DUMMY_SP,
},
};
let sess = machine.tcx.sess;
let level = match diag_level {
DiagLevel::Error => Level::Error,
DiagLevel::Warning => Level::Warning,
DiagLevel::Note => Level::Note,
};
let mut err = Diag::<()>::new(sess.dcx(), level, title);
err.span(span);
// Show main message.
if !span.is_dummy() {
for line in span_msg {
err.span_label(span, line);
}
} else {
// Make sure we show the message even when it is a dummy span.
for line in span_msg {
err.note(line);
}
err.note("(no span available)");
}
// Show note and help messages.
let mut extra_span = false;
for (span_data, note) in notes {
if let Some(span_data) = span_data {
err.span_note(span_data.span(), note);
extra_span = true;
} else {
err.note(note);
}
}
for (span_data, help) in helps {
if let Some(span_data) = span_data {
err.span_help(span_data.span(), help);
extra_span = true;
} else {
err.help(help);
}
}
// Add backtrace
if stacktrace.len() > 1 {
let mut backtrace_title = String::from("BACKTRACE");
if extra_span {
write!(backtrace_title, " (of the first span)").unwrap();
}
if let Some(thread) = thread {
let thread_name = machine.threads.get_thread_display_name(thread);
if thread_name != "main" {
// Only print thread name if it is not `main`.
write!(backtrace_title, " on thread `{thread_name}`").unwrap();
};
}
write!(backtrace_title, ":").unwrap();
err.note(backtrace_title);
for (idx, frame_info) in stacktrace.iter().enumerate() {
let is_local = machine.is_local(frame_info.instance);
// No span for non-local frames and the first frame (which is the error site).
if is_local && idx > 0 {
err.subdiagnostic(frame_info.as_note(machine.tcx));
} else {
let sm = sess.source_map();
let span = sm.span_to_diagnostic_string(frame_info.span);
err.note(format!("{frame_info} at {span}"));
}
}
} else if stacktrace.len() == 0 && !span.is_dummy() {
err.note(format!(
"this {} occurred while pushing a call frame onto an empty stack",
level.to_str()
));
err.note("the span indicates which code caused the function to be called, but may not be the literal call site");
}
err.emit();
}
impl<'tcx> MiriMachine<'tcx> {
pub fn emit_diagnostic(&self, e: NonHaltingDiagnostic) {
use NonHaltingDiagnostic::*;
let stacktrace = Frame::generate_stacktrace_from_stack(self.threads.active_thread_stack());
let (stacktrace, _was_pruned) = prune_stacktrace(stacktrace, self);
let (label, diag_level) = match &e {
RejectedIsolatedOp(_) =>
("operation rejected by isolation".to_string(), DiagLevel::Warning),
Int2Ptr { .. } => ("integer-to-pointer cast".to_string(), DiagLevel::Warning),
NativeCallSharedMem { .. } =>
("sharing memory with a native function".to_string(), DiagLevel::Warning),
NativeCallFnPtr =>
(
"sharing a function pointer with a native function".to_string(),
DiagLevel::Warning,
),
ExternTypeReborrow =>
("reborrow of reference to `extern type`".to_string(), DiagLevel::Warning),
GenmcCompareExchangeWeak | GenmcCompareExchangeOrderingMismatch { .. } =>
("GenMC might miss possible behaviors of this code".to_string(), DiagLevel::Warning),
CreatedPointerTag(..)
| PoppedPointerTag(..)
| TrackingAlloc(..)
| AccessedAlloc(..)
| FreedAlloc(..)
| ProgressReport { .. }
| WeakMemoryOutdatedLoad { .. } =>
("tracking was triggered here".to_string(), DiagLevel::Note),
};
let title = match &e {
CreatedPointerTag(tag, None, _) => format!("created base tag {tag:?}"),
CreatedPointerTag(tag, Some(perm), None) =>
format!("created {tag:?} with {perm} derived from unknown tag"),
CreatedPointerTag(tag, Some(perm), Some((alloc_id, range, orig_tag))) =>
format!(
"created tag {tag:?} with {perm} at {alloc_id:?}{range:?} derived from {orig_tag:?}"
),
PoppedPointerTag(item, cause) => format!("popped tracked tag for item {item:?}{cause}"),
TrackingAlloc(id, size, align) =>
format!(
"now tracking allocation {id:?} of {size} bytes (alignment {align} bytes)",
size = size.bytes(),
align = align.bytes(),
),
AccessedAlloc(id, range, access_kind) =>
format!("{access_kind} at {id:?}[{}..{}]", range.start.bytes(), range.end().bytes()),
FreedAlloc(id) => format!("freed allocation {id:?}"),
RejectedIsolatedOp(op) => format!("{op} was made to return an error due to isolation"),
ProgressReport { .. } =>
format!("progress report: current operation being executed is here"),
Int2Ptr { .. } => format!("integer-to-pointer cast"),
NativeCallSharedMem { .. } =>
format!("sharing memory with a native function called via FFI"),
NativeCallFnPtr =>
format!("sharing a function pointer with a native function called via FFI"),
WeakMemoryOutdatedLoad { ptr } =>
format!("weak memory emulation: outdated value returned from load at {ptr}"),
ExternTypeReborrow =>
format!("reborrow of a reference to `extern type` is not properly supported"),
GenmcCompareExchangeWeak =>
"GenMC currently does not model spurious failures of `compare_exchange_weak`. Miri with GenMC might miss bugs related to spurious failures."
.to_string(),
GenmcCompareExchangeOrderingMismatch {
success_ordering,
upgraded_success_ordering,
failure_ordering,
effective_failure_ordering,
} => {
let was_upgraded_msg = if success_ordering != upgraded_success_ordering {
format!("Success ordering '{success_ordering:?}' was upgraded to '{upgraded_success_ordering:?}' to match failure ordering '{failure_ordering:?}'")
} else {
assert_ne!(failure_ordering, effective_failure_ordering);
format!("Due to success ordering '{success_ordering:?}', the failure ordering '{failure_ordering:?}' is treated like '{effective_failure_ordering:?}'")
};
format!("GenMC currently does not model the failure ordering for `compare_exchange`. {was_upgraded_msg}. Miri with GenMC might miss bugs related to this memory access.")
}
};
let notes = match &e {
ProgressReport { block_count } => {
vec![note!("so far, {block_count} basic blocks have been executed")]
}
_ => vec![],
};
let helps = match &e {
Int2Ptr { details: true } => {
let mut v = vec![
note!(
"this program is using integer-to-pointer casts or (equivalently) `ptr::with_exposed_provenance`, which means that Miri might miss pointer bugs in this program"
),
note!(
"see https://doc.rust-lang.org/nightly/std/ptr/fn.with_exposed_provenance.html for more details on that operation"
),
note!(
"to ensure that Miri does not miss bugs in your program, use Strict Provenance APIs (https://doc.rust-lang.org/nightly/std/ptr/index.html#strict-provenance, https://crates.io/crates/sptr) instead"
),
note!(
"you can then set `MIRIFLAGS=-Zmiri-strict-provenance` to ensure you are not relying on `with_exposed_provenance` semantics"
),
];
if self.borrow_tracker.as_ref().is_some_and(|b| {
matches!(
b.borrow().borrow_tracker_method(),
BorrowTrackerMethod::TreeBorrows { .. }
)
}) {
v.push(
note!("Tree Borrows does not support integer-to-pointer casts, so the program is likely to go wrong when this pointer gets used")
);
} else {
v.push(
note!("alternatively, `MIRIFLAGS=-Zmiri-permissive-provenance` disables this warning")
);
}
v
}
NativeCallSharedMem { tracing } =>
if *tracing {
vec![
note!(
"when memory is shared with a native function call, Miri can only track initialisation and provenance on a best-effort basis"
),
note!(
"in particular, Miri assumes that the native call initializes all memory it has written to"
),
note!(
"Miri also assumes that any part of this memory may be a pointer that is permitted to point to arbitrary exposed memory"
),
note!(
"what this means is that Miri will easily miss Undefined Behavior related to incorrect usage of this shared memory, so you should not take a clean Miri run as a signal that your FFI code is UB-free"
),
note!(
"tracing memory accesses in native code is not yet fully implemented, so there can be further imprecisions beyond what is documented here"
),
]
} else {
vec![
note!(
"when memory is shared with a native function call, Miri stops tracking initialization and provenance for that memory"
),
note!(
"in particular, Miri assumes that the native call initializes all memory it has access to"
),
note!(
"Miri also assumes that any part of this memory may be a pointer that is permitted to point to arbitrary exposed memory"
),
note!(
"what this means is that Miri will easily miss Undefined Behavior related to incorrect usage of this shared memory, so you should not take a clean Miri run as a signal that your FFI code is UB-free"
),
]
},
NativeCallFnPtr => {
vec![note!(
"calling Rust functions from C is not supported and will, in the best case, crash the program"
)]
}
ExternTypeReborrow => {
assert!(self.borrow_tracker.as_ref().is_some_and(|b| {
matches!(
b.borrow().borrow_tracker_method(),
BorrowTrackerMethod::StackedBorrows
)
}));
vec![
note!(
"`extern type` are not compatible with the Stacked Borrows aliasing model implemented by Miri; Miri may miss bugs in this code"
),
note!(
"try running with `MIRIFLAGS=-Zmiri-tree-borrows` to use the more permissive but also even more experimental Tree Borrows aliasing checks instead"
),
]
}
_ => vec![],
};
report_msg(
diag_level,
title,
vec![label],
notes,
helps,
&stacktrace,
Some(self.threads.active_thread()),
self,
);
}
}
impl<'tcx> EvalContextExt<'tcx> for crate::MiriInterpCx<'tcx> {}
pub trait EvalContextExt<'tcx>: crate::MiriInterpCxExt<'tcx> {
fn emit_diagnostic(&self, e: NonHaltingDiagnostic) {
let this = self.eval_context_ref();
this.machine.emit_diagnostic(e);
}
/// We had a panic in Miri itself, try to print something useful.
fn handle_ice(&self) {
eprintln!();
eprintln!(
"Miri caused an ICE during evaluation. Here's the interpreter backtrace at the time of the panic:"
);
let this = self.eval_context_ref();
let stacktrace = this.generate_stacktrace();
report_msg(
DiagLevel::Note,
"the place in the program where the ICE was triggered".to_string(),
vec![],
vec![],
vec![],
&stacktrace,
Some(this.active_thread()),
&this.machine,
);
}
/// Call `f` only if this is the first time we are seeing this span.
/// The `first` parameter indicates whether this is the first time *ever* that this diagnostic
/// is emitted.
fn dedup_diagnostic(
&self,
dedup: &SpanDedupDiagnostic,
f: impl FnOnce(/*first*/ bool) -> NonHaltingDiagnostic,
) {
let this = self.eval_context_ref();
// We want to deduplicate both based on where the error seems to be located "from the user
// perspective", and the location of the actual operation (to avoid warning about the same
// operation called from different places in the local code).
let span1 = this.machine.current_user_relevant_span();
// For the "location of the operation", we still skip `track_caller` frames, to match the
// span that the diagnostic will point at.
let span2 = this
.active_thread_stack()
.iter()
.rev()
.find(|frame| !frame.instance().def.requires_caller_location(*this.tcx))
.map(|frame| frame.current_span())
.unwrap_or(span1);
let mut lock = dedup.0.lock().unwrap();
let first = lock.is_empty();
// Avoid mutating the hashset unless both spans are new.
if !lock.contains(&span2) && lock.insert(span1) && (span1 == span2 || lock.insert(span2)) {
// Both of the two spans were newly inserted.
this.emit_diagnostic(f(first));
}
}
}
/// Helps deduplicate a diagnostic to ensure it is only shown once per span.
pub struct SpanDedupDiagnostic(Mutex<FxHashSet<Span>>);
impl SpanDedupDiagnostic {
pub const fn new() -> Self {
Self(Mutex::new(FxHashSet::with_hasher(FxBuildHasher)))
}
}