Skip to main content

miri/borrow_tracker/stacked_borrows/
diagnostics.rs

1use std::fmt;
2
3use rustc_abi::Size;
4use rustc_data_structures::fx::FxHashSet;
5use rustc_span::{Span, SpanData};
6use smallvec::SmallVec;
7
8use crate::borrow_tracker::{AccessKind, GlobalStateInner, ProtectorKind};
9use crate::*;
10
11/// Error reporting
12fn err_sb_ub<'tcx>(
13    msg: String,
14    help: Vec<String>,
15    history: Option<TagHistory>,
16) -> InterpErrorKind<'tcx> {
17    err_machine_stop!(TerminationInfo::StackedBorrowsUb { msg, help, history })
18}
19
20#[derive(Clone, Debug)]
21pub struct AllocHistory {
22    id: AllocId,
23    root: (Item, Span),
24    creations: smallvec::SmallVec<[Creation; 1]>,
25    invalidations: smallvec::SmallVec<[Invalidation; 1]>,
26    protectors: smallvec::SmallVec<[Protection; 1]>,
27}
28
29#[derive(Clone, Debug)]
30struct Creation {
31    retag: RetagOp,
32    span: Span,
33}
34
35impl Creation {
36    fn generate_diagnostic(&self) -> (String, SpanData) {
37        let tag = self.retag.new_tag;
38        if let Some(perm) = self.retag.permission {
39            (
40                format!("{tag:?} was created by a {perm:?} retag at offsets {}", self.retag.range),
41                self.span.data(),
42            )
43        } else {
44            assert!(self.retag.range.size == Size::ZERO);
45            (
46                format!(
47                    "{tag:?} would have been created here, but this is a zero-size retag ({}) so the tag in question does not exist anywhere",
48                    self.retag.range,
49                ),
50                self.span.data(),
51            )
52        }
53    }
54}
55
56#[derive(Clone, Debug)]
57struct Invalidation {
58    tag: BorTag,
59    range: AllocRange,
60    span: Span,
61    cause: InvalidationCause,
62}
63
64#[derive(Clone, Debug)]
65enum InvalidationCause {
66    Access(AccessKind),
67    Retag(Permission, RetagInfo),
68}
69
70impl Invalidation {
71    fn generate_diagnostic(&self) -> (String, SpanData) {
72        let message = if matches!(
73            self.cause,
74            InvalidationCause::Retag(_, RetagInfo { cause: RetagCause::FnEntry, .. })
75        ) {
76            // For a FnEntry retag, our Span points at the caller.
77            // See `DiagnosticCx::log_invalidation`.
78            format!(
79                "{:?} was later invalidated at offsets {} by a {} inside this call",
80                self.tag, self.range, self.cause
81            )
82        } else {
83            format!(
84                "{:?} was later invalidated at offsets {} by a {}",
85                self.tag, self.range, self.cause
86            )
87        };
88        (message, self.span.data())
89    }
90}
91
92impl fmt::Display for InvalidationCause {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        match self {
95            InvalidationCause::Access(kind) => write!(f, "{kind}"),
96            InvalidationCause::Retag(perm, info) =>
97                write!(f, "{perm:?} {retag}", retag = info.summary()),
98        }
99    }
100}
101
102#[derive(Clone, Debug)]
103struct Protection {
104    tag: BorTag,
105    span: Span,
106}
107
108#[derive(Clone)]
109pub struct TagHistory {
110    pub created: (String, SpanData),
111    pub invalidated: Option<(String, SpanData)>,
112    pub protected: Option<(String, SpanData)>,
113}
114
115pub struct DiagnosticCxBuilder<'ecx, 'tcx> {
116    operation: Operation,
117    machine: &'ecx MiriMachine<'tcx>,
118}
119
120pub struct DiagnosticCx<'history, 'ecx, 'tcx> {
121    operation: Operation,
122    machine: &'ecx MiriMachine<'tcx>,
123    history: &'history mut AllocHistory,
124    offset: Size,
125}
126
127impl<'ecx, 'tcx> DiagnosticCxBuilder<'ecx, 'tcx> {
128    pub fn build<'history>(
129        self,
130        history: &'history mut AllocHistory,
131        offset: Size,
132    ) -> DiagnosticCx<'history, 'ecx, 'tcx> {
133        DiagnosticCx { operation: self.operation, machine: self.machine, history, offset }
134    }
135
136    pub fn retag(
137        machine: &'ecx MiriMachine<'tcx>,
138        info: RetagInfo,
139        new_tag: BorTag,
140        orig_tag: ProvenanceExtra,
141        range: AllocRange,
142    ) -> Self {
143        let operation =
144            Operation::Retag(RetagOp { info, new_tag, orig_tag, range, permission: None });
145
146        DiagnosticCxBuilder { machine, operation }
147    }
148
149    pub fn read(machine: &'ecx MiriMachine<'tcx>, tag: ProvenanceExtra, range: AllocRange) -> Self {
150        let operation = Operation::Access(AccessOp { kind: AccessKind::Read, tag, range });
151        DiagnosticCxBuilder { machine, operation }
152    }
153
154    pub fn write(
155        machine: &'ecx MiriMachine<'tcx>,
156        tag: ProvenanceExtra,
157        range: AllocRange,
158    ) -> Self {
159        let operation = Operation::Access(AccessOp { kind: AccessKind::Write, tag, range });
160        DiagnosticCxBuilder { machine, operation }
161    }
162
163    pub fn dealloc(machine: &'ecx MiriMachine<'tcx>, tag: ProvenanceExtra) -> Self {
164        let operation = Operation::Dealloc(DeallocOp { tag });
165        DiagnosticCxBuilder { machine, operation }
166    }
167}
168
169impl<'history, 'ecx, 'tcx> DiagnosticCx<'history, 'ecx, 'tcx> {
170    pub fn unbuild(self) -> DiagnosticCxBuilder<'ecx, 'tcx> {
171        DiagnosticCxBuilder { machine: self.machine, operation: self.operation }
172    }
173}
174
175#[derive(Debug, Clone)]
176enum Operation {
177    Retag(RetagOp),
178    Access(AccessOp),
179    Dealloc(DeallocOp),
180}
181
182#[derive(Debug, Clone)]
183struct RetagOp {
184    info: RetagInfo,
185    new_tag: BorTag,
186    orig_tag: ProvenanceExtra,
187    range: AllocRange,
188    permission: Option<Permission>,
189}
190
191#[derive(Debug, Clone, Copy, PartialEq)]
192pub struct RetagInfo {
193    pub cause: RetagCause,
194}
195
196#[derive(Debug, Clone, Copy, PartialEq)]
197pub enum RetagCause {
198    Normal,
199    InPlaceFnPassing,
200    FnEntry,
201    TwoPhase,
202}
203
204#[derive(Debug, Clone)]
205struct AccessOp {
206    kind: AccessKind,
207    tag: ProvenanceExtra,
208    range: AllocRange,
209}
210
211#[derive(Debug, Clone)]
212struct DeallocOp {
213    tag: ProvenanceExtra,
214}
215
216impl AllocHistory {
217    pub fn new(id: AllocId, item: Item, machine: &MiriMachine<'_>) -> Self {
218        Self {
219            id,
220            root: (item, machine.current_user_relevant_span()),
221            creations: SmallVec::new(),
222            invalidations: SmallVec::new(),
223            protectors: SmallVec::new(),
224        }
225    }
226
227    pub fn retain(&mut self, live_tags: &FxHashSet<BorTag>) {
228        self.invalidations.retain(|event| live_tags.contains(&event.tag));
229        self.creations.retain(|event| live_tags.contains(&event.retag.new_tag));
230        self.protectors.retain(|event| live_tags.contains(&event.tag));
231    }
232}
233
234impl<'history, 'ecx, 'tcx> DiagnosticCx<'history, 'ecx, 'tcx> {
235    pub fn start_grant(&mut self, perm: Permission) {
236        let Operation::Retag(op) = &mut self.operation else {
237            unreachable!(
238                "start_grant must only be called during a retag, this is: {:?}",
239                self.operation
240            )
241        };
242        op.permission = Some(perm);
243
244        let last_creation = &mut self.history.creations.last_mut().unwrap();
245        match last_creation.retag.permission {
246            None => {
247                last_creation.retag.permission = Some(perm);
248            }
249            Some(previous) =>
250                if previous != perm {
251                    // 'Split up' the creation event.
252                    let previous_range = last_creation.retag.range;
253                    last_creation.retag.range = alloc_range(previous_range.start, self.offset);
254                    let mut new_event = last_creation.clone();
255                    new_event.retag.range = alloc_range(self.offset, previous_range.end());
256                    new_event.retag.permission = Some(perm);
257                    self.history.creations.push(new_event);
258                },
259        }
260    }
261
262    pub fn log_creation(&mut self) {
263        let Operation::Retag(op) = &self.operation else {
264            unreachable!("log_creation must only be called during a retag")
265        };
266        self.history
267            .creations
268            .push(Creation { retag: op.clone(), span: self.machine.current_user_relevant_span() });
269    }
270
271    pub fn log_invalidation(&mut self, tag: BorTag) {
272        let mut span = self.machine.current_user_relevant_span();
273        let (range, cause) = match &self.operation {
274            Operation::Retag(RetagOp { info, range, permission, .. }) => {
275                if info.cause == RetagCause::FnEntry {
276                    span = self.machine.caller_span();
277                }
278                (*range, InvalidationCause::Retag(permission.unwrap(), *info))
279            }
280            Operation::Access(AccessOp { kind, range, .. }) =>
281                (*range, InvalidationCause::Access(*kind)),
282            Operation::Dealloc(_) => {
283                // This can be reached, but never be relevant later since the entire allocation is
284                // gone now.
285                return;
286            }
287        };
288        self.history.invalidations.push(Invalidation { tag, range, span, cause });
289    }
290
291    pub fn log_protector(&mut self) {
292        let Operation::Retag(op) = &self.operation else {
293            unreachable!("Protectors can only be created during a retag")
294        };
295        self.history
296            .protectors
297            .push(Protection { tag: op.new_tag, span: self.machine.current_user_relevant_span() });
298    }
299
300    pub fn get_logs_relevant_to(
301        &self,
302        tag: BorTag,
303        protector_tag: Option<BorTag>,
304    ) -> Option<TagHistory> {
305        let Some(created) = self
306            .history
307            .creations
308            .iter()
309            .rev()
310            .find_map(|event| {
311                // First, look for a Creation event where the tag and the offset matches. This
312                // ensures that we pick the right Creation event when a retag isn't uniform due to
313                // Freeze.
314                let range = event.retag.range;
315                if event.retag.new_tag == tag
316                    && self.offset >= range.start
317                    && self.offset < (range.start + range.size)
318                {
319                    Some(event.generate_diagnostic())
320                } else {
321                    None
322                }
323            })
324            .or_else(|| {
325                // If we didn't find anything with a matching offset, just return the event where
326                // the tag was created. This branch is hit when we use a tag at an offset that
327                // doesn't have the tag.
328                self.history.creations.iter().rev().find_map(|event| {
329                    if event.retag.new_tag == tag {
330                        Some(event.generate_diagnostic())
331                    } else {
332                        None
333                    }
334                })
335            })
336            .or_else(|| {
337                // If we didn't find a retag that created this tag, it might be the root tag of
338                // this allocation.
339                if self.history.root.0.tag() == tag {
340                    Some((
341                        format!(
342                            "{tag:?} was created here, as the root tag for {}",
343                            self.history.id
344                        ),
345                        self.history.root.1.data(),
346                    ))
347                } else {
348                    None
349                }
350            })
351        else {
352            // But if we don't have a creation event, this is related to a wildcard, and there
353            // is really nothing we can do to help.
354            return None;
355        };
356
357        let invalidated = self.history.invalidations.iter().rev().find_map(|event| {
358            if event.tag == tag { Some(event.generate_diagnostic()) } else { None }
359        });
360
361        let protected = protector_tag
362            .and_then(|protector| {
363                self.history.protectors.iter().find(|protection| protection.tag == protector)
364            })
365            .map(|protection| {
366                let protected_tag = protection.tag;
367                (format!("{protected_tag:?} is this argument"), protection.span.data())
368            });
369
370        Some(TagHistory { created, invalidated, protected })
371    }
372
373    /// Report a descriptive error when `new` could not be granted from `derived_from`.
374    #[inline(never)] // This is only called on fatal code paths
375    pub(super) fn grant_error(&self, stack: &Stack) -> InterpErrorKind<'tcx> {
376        let Operation::Retag(op) = &self.operation else {
377            unreachable!("grant_error should only be called during a retag")
378        };
379        let perm =
380            op.permission.expect("`start_grant` must be called before calling `grant_error`");
381        let action = format!(
382            "trying to retag from {:?} for {:?} permission at {}[{:#x}]",
383            op.orig_tag,
384            perm,
385            self.history.id,
386            self.offset.bytes(),
387        );
388        let helps = vec![operation_summary(&op.info.summary(), self.history.id, op.range)];
389        err_sb_ub(
390            format!("{action}{}", error_cause(stack, op.orig_tag)),
391            helps,
392            op.orig_tag.and_then(|orig_tag| self.get_logs_relevant_to(orig_tag, None)),
393        )
394    }
395
396    /// Report a descriptive error when `access` is not permitted based on `tag`.
397    #[inline(never)] // This is only called on fatal code paths
398    pub(super) fn access_error(&self, stack: &Stack) -> InterpErrorKind<'tcx> {
399        // Deallocation and retagging also do an access as part of their thing, so handle that here, too.
400        let op = match &self.operation {
401            Operation::Access(op) => op,
402            Operation::Retag(_) => return self.grant_error(stack),
403            Operation::Dealloc(_) => return self.dealloc_error(stack),
404        };
405        let action = format!(
406            "attempting a {access} using {tag:?} at {alloc_id}[{offset:#x}]",
407            access = op.kind,
408            tag = op.tag,
409            alloc_id = self.history.id,
410            offset = self.offset.bytes(),
411        );
412        err_sb_ub(
413            format!("{action}{}", error_cause(stack, op.tag)),
414            vec![operation_summary("an access", self.history.id, op.range)],
415            op.tag.and_then(|tag| self.get_logs_relevant_to(tag, None)),
416        )
417    }
418
419    #[inline(never)] // This is only called on fatal code paths
420    pub(super) fn protector_error(
421        &self,
422        item: &Item,
423        kind: ProtectorKind,
424    ) -> InterpErrorKind<'tcx> {
425        let protected = match kind {
426            ProtectorKind::WeakProtector => "weakly protected",
427            ProtectorKind::StrongProtector => "strongly protected",
428        };
429        match self.operation {
430            Operation::Dealloc(_) =>
431                err_sb_ub(format!("deallocating while item {item:?} is {protected}",), vec![], None),
432            Operation::Retag(RetagOp { orig_tag: tag, .. })
433            | Operation::Access(AccessOp { tag, .. }) =>
434                err_sb_ub(
435                    format!(
436                        "not granting access to tag {tag:?} because that would remove {item:?} which is {protected}",
437                    ),
438                    vec![],
439                    tag.and_then(|tag| self.get_logs_relevant_to(tag, Some(item.tag()))),
440                ),
441        }
442    }
443
444    #[inline(never)] // This is only called on fatal code paths
445    pub fn dealloc_error(&self, stack: &Stack) -> InterpErrorKind<'tcx> {
446        let Operation::Dealloc(op) = &self.operation else {
447            unreachable!("dealloc_error should only be called during a deallocation")
448        };
449        err_sb_ub(
450            format!(
451                "attempting deallocation using {tag:?} at {alloc_id}{cause}",
452                tag = op.tag,
453                alloc_id = self.history.id,
454                cause = error_cause(stack, op.tag),
455            ),
456            vec![],
457            op.tag.and_then(|tag| self.get_logs_relevant_to(tag, None)),
458        )
459    }
460
461    #[inline(never)]
462    pub fn check_tracked_tag_popped(&self, item: &Item, global: &GlobalStateInner) {
463        if !global.tracked_pointer_tags.contains(&item.tag()) {
464            return;
465        }
466        let cause = match self.operation {
467            Operation::Dealloc(_) => format!(" due to deallocation"),
468            Operation::Access(AccessOp { kind, tag, .. }) =>
469                format!(" due to {kind:?} access for {tag:?}"),
470            Operation::Retag(RetagOp { orig_tag, permission, new_tag, .. }) => {
471                let permission = permission
472                    .expect("start_grant should set the current permission before popping a tag");
473                format!(
474                    " due to {permission:?} retag from {orig_tag:?} (that retag created {new_tag:?})"
475                )
476            }
477        };
478
479        self.machine.emit_diagnostic(NonHaltingDiagnostic::PoppedPointerTag(*item, cause));
480    }
481}
482
483fn operation_summary(operation: &str, alloc_id: AllocId, alloc_range: AllocRange) -> String {
484    format!("this error occurs as part of {operation} at {alloc_id}{alloc_range}")
485}
486
487fn error_cause(stack: &Stack, prov_extra: ProvenanceExtra) -> &'static str {
488    if let ProvenanceExtra::Concrete(tag) = prov_extra {
489        if (0..stack.len())
490            .map(|i| stack.get(i).unwrap())
491            .any(|item| item.tag() == tag && item.perm() != Permission::Disabled)
492        {
493            ", but that tag only grants SharedReadOnly permission for this location"
494        } else {
495            ", but that tag does not exist in the borrow stack for this location"
496        }
497    } else {
498        ", but no exposed tags have suitable permission in the borrow stack for this location"
499    }
500}
501
502impl RetagInfo {
503    fn summary(&self) -> String {
504        match self.cause {
505            RetagCause::Normal => "retag",
506            RetagCause::FnEntry => "function-entry retag",
507            RetagCause::InPlaceFnPassing => "in-place function argument/return passing protection",
508            RetagCause::TwoPhase => "two-phase retag",
509        }
510        .to_string()
511    }
512}