blob: 1a11879fa9012814b6c87333c58c030cf42f6131 [file] [log] [blame]
use smallvec::SmallVec;
use std::fmt;
use rustc_data_structures::fx::FxHashSet;
use rustc_middle::mir::interpret::{alloc_range, AllocId, AllocRange, InterpError};
use rustc_span::{Span, SpanData};
use rustc_target::abi::Size;
use crate::borrow_tracker::{
stacked_borrows::Permission, AccessKind, GlobalStateInner, ProtectorKind,
};
use crate::*;
/// Error reporting
fn err_sb_ub<'tcx>(
msg: String,
help: Vec<String>,
history: Option<TagHistory>,
) -> InterpError<'tcx> {
err_machine_stop!(TerminationInfo::StackedBorrowsUb { msg, help, history })
}
#[derive(Clone, Debug)]
pub struct AllocHistory {
id: AllocId,
base: (Item, Span),
creations: smallvec::SmallVec<[Creation; 1]>,
invalidations: smallvec::SmallVec<[Invalidation; 1]>,
protectors: smallvec::SmallVec<[Protection; 1]>,
}
#[derive(Clone, Debug)]
struct Creation {
retag: RetagOp,
span: Span,
}
impl Creation {
fn generate_diagnostic(&self) -> (String, SpanData) {
let tag = self.retag.new_tag;
if let Some(perm) = self.retag.permission {
(
format!(
"{tag:?} was created by a {:?} retag at offsets {:?}",
perm, self.retag.range,
),
self.span.data(),
)
} else {
assert!(self.retag.range.size == Size::ZERO);
(
format!(
"{tag:?} would have been created here, but this is a zero-size retag ({:?}) so the tag in question does not exist anywhere",
self.retag.range,
),
self.span.data(),
)
}
}
}
#[derive(Clone, Debug)]
struct Invalidation {
tag: BorTag,
range: AllocRange,
span: Span,
cause: InvalidationCause,
}
#[derive(Clone, Debug)]
enum InvalidationCause {
Access(AccessKind),
Retag(Permission, RetagInfo),
}
impl Invalidation {
fn generate_diagnostic(&self) -> (String, SpanData) {
let message = if matches!(
self.cause,
InvalidationCause::Retag(_, RetagInfo { cause: RetagCause::FnEntry, .. })
) {
// For a FnEntry retag, our Span points at the caller.
// See `DiagnosticCx::log_invalidation`.
format!(
"{:?} was later invalidated at offsets {:?} by a {} inside this call",
self.tag, self.range, self.cause
)
} else {
format!(
"{:?} was later invalidated at offsets {:?} by a {}",
self.tag, self.range, self.cause
)
};
(message, self.span.data())
}
}
impl fmt::Display for InvalidationCause {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
InvalidationCause::Access(kind) => write!(f, "{kind}"),
InvalidationCause::Retag(perm, info) =>
write!(f, "{perm:?} {retag}", retag = info.summary()),
}
}
}
#[derive(Clone, Debug)]
struct Protection {
tag: BorTag,
span: Span,
}
#[derive(Clone)]
pub struct TagHistory {
pub created: (String, SpanData),
pub invalidated: Option<(String, SpanData)>,
pub protected: Option<(String, SpanData)>,
}
pub struct DiagnosticCxBuilder<'ecx, 'mir, 'tcx> {
operation: Operation,
machine: &'ecx MiriMachine<'mir, 'tcx>,
}
pub struct DiagnosticCx<'history, 'ecx, 'mir, 'tcx> {
operation: Operation,
machine: &'ecx MiriMachine<'mir, 'tcx>,
history: &'history mut AllocHistory,
offset: Size,
}
impl<'ecx, 'mir, 'tcx> DiagnosticCxBuilder<'ecx, 'mir, 'tcx> {
pub fn build<'history>(
self,
history: &'history mut AllocHistory,
offset: Size,
) -> DiagnosticCx<'history, 'ecx, 'mir, 'tcx> {
DiagnosticCx { operation: self.operation, machine: self.machine, history, offset }
}
pub fn retag(
machine: &'ecx MiriMachine<'mir, 'tcx>,
info: RetagInfo,
new_tag: BorTag,
orig_tag: ProvenanceExtra,
range: AllocRange,
) -> Self {
let operation =
Operation::Retag(RetagOp { info, new_tag, orig_tag, range, permission: None });
DiagnosticCxBuilder { machine, operation }
}
pub fn read(
machine: &'ecx MiriMachine<'mir, 'tcx>,
tag: ProvenanceExtra,
range: AllocRange,
) -> Self {
let operation = Operation::Access(AccessOp { kind: AccessKind::Read, tag, range });
DiagnosticCxBuilder { machine, operation }
}
pub fn write(
machine: &'ecx MiriMachine<'mir, 'tcx>,
tag: ProvenanceExtra,
range: AllocRange,
) -> Self {
let operation = Operation::Access(AccessOp { kind: AccessKind::Write, tag, range });
DiagnosticCxBuilder { machine, operation }
}
pub fn dealloc(machine: &'ecx MiriMachine<'mir, 'tcx>, tag: ProvenanceExtra) -> Self {
let operation = Operation::Dealloc(DeallocOp { tag });
DiagnosticCxBuilder { machine, operation }
}
}
impl<'history, 'ecx, 'mir, 'tcx> DiagnosticCx<'history, 'ecx, 'mir, 'tcx> {
pub fn unbuild(self) -> DiagnosticCxBuilder<'ecx, 'mir, 'tcx> {
DiagnosticCxBuilder { machine: self.machine, operation: self.operation }
}
}
#[derive(Debug, Clone)]
enum Operation {
Retag(RetagOp),
Access(AccessOp),
Dealloc(DeallocOp),
}
#[derive(Debug, Clone)]
struct RetagOp {
info: RetagInfo,
new_tag: BorTag,
orig_tag: ProvenanceExtra,
range: AllocRange,
permission: Option<Permission>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RetagInfo {
pub cause: RetagCause,
pub in_field: bool,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum RetagCause {
Normal,
InPlaceFnPassing,
FnEntry,
TwoPhase,
}
#[derive(Debug, Clone)]
struct AccessOp {
kind: AccessKind,
tag: ProvenanceExtra,
range: AllocRange,
}
#[derive(Debug, Clone)]
struct DeallocOp {
tag: ProvenanceExtra,
}
impl AllocHistory {
pub fn new(id: AllocId, item: Item, machine: &MiriMachine<'_, '_>) -> Self {
Self {
id,
base: (item, machine.current_span()),
creations: SmallVec::new(),
invalidations: SmallVec::new(),
protectors: SmallVec::new(),
}
}
pub fn retain(&mut self, live_tags: &FxHashSet<BorTag>) {
self.invalidations.retain(|event| live_tags.contains(&event.tag));
self.creations.retain(|event| live_tags.contains(&event.retag.new_tag));
self.protectors.retain(|event| live_tags.contains(&event.tag));
}
}
impl<'history, 'ecx, 'mir, 'tcx> DiagnosticCx<'history, 'ecx, 'mir, 'tcx> {
pub fn start_grant(&mut self, perm: Permission) {
let Operation::Retag(op) = &mut self.operation else {
unreachable!(
"start_grant must only be called during a retag, this is: {:?}",
self.operation
)
};
op.permission = Some(perm);
let last_creation = &mut self.history.creations.last_mut().unwrap();
match last_creation.retag.permission {
None => {
last_creation.retag.permission = Some(perm);
}
Some(previous) =>
if previous != perm {
// 'Split up' the creation event.
let previous_range = last_creation.retag.range;
last_creation.retag.range = alloc_range(previous_range.start, self.offset);
let mut new_event = last_creation.clone();
new_event.retag.range = alloc_range(self.offset, previous_range.end());
new_event.retag.permission = Some(perm);
self.history.creations.push(new_event);
},
}
}
pub fn log_creation(&mut self) {
let Operation::Retag(op) = &self.operation else {
unreachable!("log_creation must only be called during a retag")
};
self.history
.creations
.push(Creation { retag: op.clone(), span: self.machine.current_span() });
}
pub fn log_invalidation(&mut self, tag: BorTag) {
let mut span = self.machine.current_span();
let (range, cause) = match &self.operation {
Operation::Retag(RetagOp { info, range, permission, .. }) => {
if info.cause == RetagCause::FnEntry {
span = self.machine.caller_span();
}
(*range, InvalidationCause::Retag(permission.unwrap(), *info))
}
Operation::Access(AccessOp { kind, range, .. }) =>
(*range, InvalidationCause::Access(*kind)),
Operation::Dealloc(_) => {
// This can be reached, but never be relevant later since the entire allocation is
// gone now.
return;
}
};
self.history.invalidations.push(Invalidation { tag, range, span, cause });
}
pub fn log_protector(&mut self) {
let Operation::Retag(op) = &self.operation else {
unreachable!("Protectors can only be created during a retag")
};
self.history
.protectors
.push(Protection { tag: op.new_tag, span: self.machine.current_span() });
}
pub fn get_logs_relevant_to(
&self,
tag: BorTag,
protector_tag: Option<BorTag>,
) -> Option<TagHistory> {
let Some(created) = self
.history
.creations
.iter()
.rev()
.find_map(|event| {
// First, look for a Creation event where the tag and the offset matches. This
// ensures that we pick the right Creation event when a retag isn't uniform due to
// Freeze.
let range = event.retag.range;
if event.retag.new_tag == tag
&& self.offset >= range.start
&& self.offset < (range.start + range.size)
{
Some(event.generate_diagnostic())
} else {
None
}
})
.or_else(|| {
// If we didn't find anything with a matching offset, just return the event where
// the tag was created. This branch is hit when we use a tag at an offset that
// doesn't have the tag.
self.history.creations.iter().rev().find_map(|event| {
if event.retag.new_tag == tag {
Some(event.generate_diagnostic())
} else {
None
}
})
})
.or_else(|| {
// If we didn't find a retag that created this tag, it might be the base tag of
// this allocation.
if self.history.base.0.tag() == tag {
Some((
format!(
"{tag:?} was created here, as the base tag for {:?}",
self.history.id
),
self.history.base.1.data(),
))
} else {
None
}
})
else {
// But if we don't have a creation event, this is related to a wildcard, and there
// is really nothing we can do to help.
return None;
};
let invalidated = self.history.invalidations.iter().rev().find_map(|event| {
if event.tag == tag { Some(event.generate_diagnostic()) } else { None }
});
let protected = protector_tag
.and_then(|protector| {
self.history.protectors.iter().find(|protection| protection.tag == protector)
})
.map(|protection| {
let protected_tag = protection.tag;
(format!("{protected_tag:?} is this argument"), protection.span.data())
});
Some(TagHistory { created, invalidated, protected })
}
/// Report a descriptive error when `new` could not be granted from `derived_from`.
#[inline(never)] // This is only called on fatal code paths
pub(super) fn grant_error(&self, stack: &Stack) -> InterpError<'tcx> {
let Operation::Retag(op) = &self.operation else {
unreachable!("grant_error should only be called during a retag")
};
let perm =
op.permission.expect("`start_grant` must be called before calling `grant_error`");
let action = format!(
"trying to retag from {:?} for {:?} permission at {:?}[{:#x}]",
op.orig_tag,
perm,
self.history.id,
self.offset.bytes(),
);
let mut helps = vec![operation_summary(&op.info.summary(), self.history.id, op.range)];
if op.info.in_field {
helps.push(format!("errors for retagging in fields are fairly new; please reach out to us (e.g. at <https://rust-lang.zulipchat.com/#narrow/stream/269128-miri>) if you find this error troubling"));
}
err_sb_ub(
format!("{action}{}", error_cause(stack, op.orig_tag)),
helps,
op.orig_tag.and_then(|orig_tag| self.get_logs_relevant_to(orig_tag, None)),
)
}
/// Report a descriptive error when `access` is not permitted based on `tag`.
#[inline(never)] // This is only called on fatal code paths
pub(super) fn access_error(&self, stack: &Stack) -> InterpError<'tcx> {
// Deallocation and retagging also do an access as part of their thing, so handle that here, too.
let op = match &self.operation {
Operation::Access(op) => op,
Operation::Retag(_) => return self.grant_error(stack),
Operation::Dealloc(_) => return self.dealloc_error(stack),
};
let action = format!(
"attempting a {access} using {tag:?} at {alloc_id:?}[{offset:#x}]",
access = op.kind,
tag = op.tag,
alloc_id = self.history.id,
offset = self.offset.bytes(),
);
err_sb_ub(
format!("{action}{}", error_cause(stack, op.tag)),
vec![operation_summary("an access", self.history.id, op.range)],
op.tag.and_then(|tag| self.get_logs_relevant_to(tag, None)),
)
}
#[inline(never)] // This is only called on fatal code paths
pub(super) fn protector_error(&self, item: &Item, kind: ProtectorKind) -> InterpError<'tcx> {
let protected = match kind {
ProtectorKind::WeakProtector => "weakly protected",
ProtectorKind::StrongProtector => "strongly protected",
};
let item_tag = item.tag();
let call_id = self
.machine
.threads
.all_stacks()
.flatten()
.map(|frame| {
frame.extra.borrow_tracker.as_ref().expect("we should have borrow tracking data")
})
.find(|frame| frame.protected_tags.iter().any(|(_, tag)| tag == &item_tag))
.map(|frame| frame.call_id)
.unwrap(); // FIXME: Surely we should find something, but a panic seems wrong here?
match self.operation {
Operation::Dealloc(_) =>
err_sb_ub(
format!("deallocating while item {item:?} is {protected} by call {call_id:?}",),
vec![],
None,
),
Operation::Retag(RetagOp { orig_tag: tag, .. })
| Operation::Access(AccessOp { tag, .. }) =>
err_sb_ub(
format!(
"not granting access to tag {tag:?} because that would remove {item:?} which is {protected} because it is an argument of call {call_id:?}",
),
vec![],
tag.and_then(|tag| self.get_logs_relevant_to(tag, Some(item.tag()))),
),
}
}
#[inline(never)] // This is only called on fatal code paths
pub fn dealloc_error(&self, stack: &Stack) -> InterpError<'tcx> {
let Operation::Dealloc(op) = &self.operation else {
unreachable!("dealloc_error should only be called during a deallocation")
};
err_sb_ub(
format!(
"attempting deallocation using {tag:?} at {alloc_id:?}{cause}",
tag = op.tag,
alloc_id = self.history.id,
cause = error_cause(stack, op.tag),
),
vec![],
op.tag.and_then(|tag| self.get_logs_relevant_to(tag, None)),
)
}
#[inline(never)]
pub fn check_tracked_tag_popped(&self, item: &Item, global: &GlobalStateInner) {
if !global.tracked_pointer_tags.contains(&item.tag()) {
return;
}
let cause = match self.operation {
Operation::Dealloc(_) => format!(" due to deallocation"),
Operation::Access(AccessOp { kind, tag, .. }) =>
format!(" due to {kind:?} access for {tag:?}"),
Operation::Retag(RetagOp { orig_tag, permission, new_tag, .. }) => {
let permission = permission
.expect("start_grant should set the current permission before popping a tag");
format!(
" due to {permission:?} retag from {orig_tag:?} (that retag created {new_tag:?})"
)
}
};
self.machine.emit_diagnostic(NonHaltingDiagnostic::PoppedPointerTag(*item, cause));
}
}
fn operation_summary(operation: &str, alloc_id: AllocId, alloc_range: AllocRange) -> String {
format!("this error occurs as part of {operation} at {alloc_id:?}{alloc_range:?}")
}
fn error_cause(stack: &Stack, prov_extra: ProvenanceExtra) -> &'static str {
if let ProvenanceExtra::Concrete(tag) = prov_extra {
if (0..stack.len())
.map(|i| stack.get(i).unwrap())
.any(|item| item.tag() == tag && item.perm() != Permission::Disabled)
{
", but that tag only grants SharedReadOnly permission for this location"
} else {
", but that tag does not exist in the borrow stack for this location"
}
} else {
", but no exposed tags have suitable permission in the borrow stack for this location"
}
}
impl RetagInfo {
fn summary(&self) -> String {
let mut s = match self.cause {
RetagCause::Normal => "retag",
RetagCause::FnEntry => "function-entry retag",
RetagCause::InPlaceFnPassing => "in-place function argument/return passing protection",
RetagCause::TwoPhase => "two-phase retag",
}
.to_string();
if self.in_field {
s.push_str(" (of a reference/box inside this compound value)");
}
s
}
}