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