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    pub in_field: bool,
195}
196
197#[derive(Debug, Clone, Copy, PartialEq)]
198pub enum RetagCause {
199    Normal,
200    InPlaceFnPassing,
201    FnEntry,
202    TwoPhase,
203}
204
205#[derive(Debug, Clone)]
206struct AccessOp {
207    kind: AccessKind,
208    tag: ProvenanceExtra,
209    range: AllocRange,
210}
211
212#[derive(Debug, Clone)]
213struct DeallocOp {
214    tag: ProvenanceExtra,
215}
216
217impl AllocHistory {
218    pub fn new(id: AllocId, item: Item, machine: &MiriMachine<'_>) -> Self {
219        Self {
220            id,
221            root: (item, machine.current_user_relevant_span()),
222            creations: SmallVec::new(),
223            invalidations: SmallVec::new(),
224            protectors: SmallVec::new(),
225        }
226    }
227
228    pub fn retain(&mut self, live_tags: &FxHashSet<BorTag>) {
229        self.invalidations.retain(|event| live_tags.contains(&event.tag));
230        self.creations.retain(|event| live_tags.contains(&event.retag.new_tag));
231        self.protectors.retain(|event| live_tags.contains(&event.tag));
232    }
233}
234
235impl<'history, 'ecx, 'tcx> DiagnosticCx<'history, 'ecx, 'tcx> {
236    pub fn start_grant(&mut self, perm: Permission) {
237        let Operation::Retag(op) = &mut self.operation else {
238            unreachable!(
239                "start_grant must only be called during a retag, this is: {:?}",
240                self.operation
241            )
242        };
243        op.permission = Some(perm);
244
245        let last_creation = &mut self.history.creations.last_mut().unwrap();
246        match last_creation.retag.permission {
247            None => {
248                last_creation.retag.permission = Some(perm);
249            }
250            Some(previous) =>
251                if previous != perm {
252                    // 'Split up' the creation event.
253                    let previous_range = last_creation.retag.range;
254                    last_creation.retag.range = alloc_range(previous_range.start, self.offset);
255                    let mut new_event = last_creation.clone();
256                    new_event.retag.range = alloc_range(self.offset, previous_range.end());
257                    new_event.retag.permission = Some(perm);
258                    self.history.creations.push(new_event);
259                },
260        }
261    }
262
263    pub fn log_creation(&mut self) {
264        let Operation::Retag(op) = &self.operation else {
265            unreachable!("log_creation must only be called during a retag")
266        };
267        self.history
268            .creations
269            .push(Creation { retag: op.clone(), span: self.machine.current_user_relevant_span() });
270    }
271
272    pub fn log_invalidation(&mut self, tag: BorTag) {
273        let mut span = self.machine.current_user_relevant_span();
274        let (range, cause) = match &self.operation {
275            Operation::Retag(RetagOp { info, range, permission, .. }) => {
276                if info.cause == RetagCause::FnEntry {
277                    span = self.machine.caller_span();
278                }
279                (*range, InvalidationCause::Retag(permission.unwrap(), *info))
280            }
281            Operation::Access(AccessOp { kind, range, .. }) =>
282                (*range, InvalidationCause::Access(*kind)),
283            Operation::Dealloc(_) => {
284                // This can be reached, but never be relevant later since the entire allocation is
285                // gone now.
286                return;
287            }
288        };
289        self.history.invalidations.push(Invalidation { tag, range, span, cause });
290    }
291
292    pub fn log_protector(&mut self) {
293        let Operation::Retag(op) = &self.operation else {
294            unreachable!("Protectors can only be created during a retag")
295        };
296        self.history
297            .protectors
298            .push(Protection { tag: op.new_tag, span: self.machine.current_user_relevant_span() });
299    }
300
301    pub fn get_logs_relevant_to(
302        &self,
303        tag: BorTag,
304        protector_tag: Option<BorTag>,
305    ) -> Option<TagHistory> {
306        let Some(created) = self
307            .history
308            .creations
309            .iter()
310            .rev()
311            .find_map(|event| {
312                // First, look for a Creation event where the tag and the offset matches. This
313                // ensures that we pick the right Creation event when a retag isn't uniform due to
314                // Freeze.
315                let range = event.retag.range;
316                if event.retag.new_tag == tag
317                    && self.offset >= range.start
318                    && self.offset < (range.start + range.size)
319                {
320                    Some(event.generate_diagnostic())
321                } else {
322                    None
323                }
324            })
325            .or_else(|| {
326                // If we didn't find anything with a matching offset, just return the event where
327                // the tag was created. This branch is hit when we use a tag at an offset that
328                // doesn't have the tag.
329                self.history.creations.iter().rev().find_map(|event| {
330                    if event.retag.new_tag == tag {
331                        Some(event.generate_diagnostic())
332                    } else {
333                        None
334                    }
335                })
336            })
337            .or_else(|| {
338                // If we didn't find a retag that created this tag, it might be the root tag of
339                // this allocation.
340                if self.history.root.0.tag() == tag {
341                    Some((
342                        format!(
343                            "{tag:?} was created here, as the root tag for {}",
344                            self.history.id
345                        ),
346                        self.history.root.1.data(),
347                    ))
348                } else {
349                    None
350                }
351            })
352        else {
353            // But if we don't have a creation event, this is related to a wildcard, and there
354            // is really nothing we can do to help.
355            return None;
356        };
357
358        let invalidated = self.history.invalidations.iter().rev().find_map(|event| {
359            if event.tag == tag { Some(event.generate_diagnostic()) } else { None }
360        });
361
362        let protected = protector_tag
363            .and_then(|protector| {
364                self.history.protectors.iter().find(|protection| protection.tag == protector)
365            })
366            .map(|protection| {
367                let protected_tag = protection.tag;
368                (format!("{protected_tag:?} is this argument"), protection.span.data())
369            });
370
371        Some(TagHistory { created, invalidated, protected })
372    }
373
374    /// Report a descriptive error when `new` could not be granted from `derived_from`.
375    #[inline(never)] // This is only called on fatal code paths
376    pub(super) fn grant_error(&self, stack: &Stack) -> InterpErrorKind<'tcx> {
377        let Operation::Retag(op) = &self.operation else {
378            unreachable!("grant_error should only be called during a retag")
379        };
380        let perm =
381            op.permission.expect("`start_grant` must be called before calling `grant_error`");
382        let action = format!(
383            "trying to retag from {:?} for {:?} permission at {}[{:#x}]",
384            op.orig_tag,
385            perm,
386            self.history.id,
387            self.offset.bytes(),
388        );
389        let mut helps = vec![operation_summary(&op.info.summary(), self.history.id, op.range)];
390        if op.info.in_field {
391            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"));
392        }
393        err_sb_ub(
394            format!("{action}{}", error_cause(stack, op.orig_tag)),
395            helps,
396            op.orig_tag.and_then(|orig_tag| self.get_logs_relevant_to(orig_tag, None)),
397        )
398    }
399
400    /// Report a descriptive error when `access` is not permitted based on `tag`.
401    #[inline(never)] // This is only called on fatal code paths
402    pub(super) fn access_error(&self, stack: &Stack) -> InterpErrorKind<'tcx> {
403        // Deallocation and retagging also do an access as part of their thing, so handle that here, too.
404        let op = match &self.operation {
405            Operation::Access(op) => op,
406            Operation::Retag(_) => return self.grant_error(stack),
407            Operation::Dealloc(_) => return self.dealloc_error(stack),
408        };
409        let action = format!(
410            "attempting a {access} using {tag:?} at {alloc_id}[{offset:#x}]",
411            access = op.kind,
412            tag = op.tag,
413            alloc_id = self.history.id,
414            offset = self.offset.bytes(),
415        );
416        err_sb_ub(
417            format!("{action}{}", error_cause(stack, op.tag)),
418            vec![operation_summary("an access", self.history.id, op.range)],
419            op.tag.and_then(|tag| self.get_logs_relevant_to(tag, None)),
420        )
421    }
422
423    #[inline(never)] // This is only called on fatal code paths
424    pub(super) fn protector_error(
425        &self,
426        item: &Item,
427        kind: ProtectorKind,
428    ) -> InterpErrorKind<'tcx> {
429        let protected = match kind {
430            ProtectorKind::WeakProtector => "weakly protected",
431            ProtectorKind::StrongProtector => "strongly protected",
432        };
433        match self.operation {
434            Operation::Dealloc(_) =>
435                err_sb_ub(format!("deallocating while item {item:?} is {protected}",), vec![], None),
436            Operation::Retag(RetagOp { orig_tag: tag, .. })
437            | Operation::Access(AccessOp { tag, .. }) =>
438                err_sb_ub(
439                    format!(
440                        "not granting access to tag {tag:?} because that would remove {item:?} which is {protected}",
441                    ),
442                    vec![],
443                    tag.and_then(|tag| self.get_logs_relevant_to(tag, Some(item.tag()))),
444                ),
445        }
446    }
447
448    #[inline(never)] // This is only called on fatal code paths
449    pub fn dealloc_error(&self, stack: &Stack) -> InterpErrorKind<'tcx> {
450        let Operation::Dealloc(op) = &self.operation else {
451            unreachable!("dealloc_error should only be called during a deallocation")
452        };
453        err_sb_ub(
454            format!(
455                "attempting deallocation using {tag:?} at {alloc_id}{cause}",
456                tag = op.tag,
457                alloc_id = self.history.id,
458                cause = error_cause(stack, op.tag),
459            ),
460            vec![],
461            op.tag.and_then(|tag| self.get_logs_relevant_to(tag, None)),
462        )
463    }
464
465    #[inline(never)]
466    pub fn check_tracked_tag_popped(&self, item: &Item, global: &GlobalStateInner) {
467        if !global.tracked_pointer_tags.contains(&item.tag()) {
468            return;
469        }
470        let cause = match self.operation {
471            Operation::Dealloc(_) => format!(" due to deallocation"),
472            Operation::Access(AccessOp { kind, tag, .. }) =>
473                format!(" due to {kind:?} access for {tag:?}"),
474            Operation::Retag(RetagOp { orig_tag, permission, new_tag, .. }) => {
475                let permission = permission
476                    .expect("start_grant should set the current permission before popping a tag");
477                format!(
478                    " due to {permission:?} retag from {orig_tag:?} (that retag created {new_tag:?})"
479                )
480            }
481        };
482
483        self.machine.emit_diagnostic(NonHaltingDiagnostic::PoppedPointerTag(*item, cause));
484    }
485}
486
487fn operation_summary(operation: &str, alloc_id: AllocId, alloc_range: AllocRange) -> String {
488    format!("this error occurs as part of {operation} at {alloc_id}{alloc_range}")
489}
490
491fn error_cause(stack: &Stack, prov_extra: ProvenanceExtra) -> &'static str {
492    if let ProvenanceExtra::Concrete(tag) = prov_extra {
493        if (0..stack.len())
494            .map(|i| stack.get(i).unwrap())
495            .any(|item| item.tag() == tag && item.perm() != Permission::Disabled)
496        {
497            ", but that tag only grants SharedReadOnly permission for this location"
498        } else {
499            ", but that tag does not exist in the borrow stack for this location"
500        }
501    } else {
502        ", but no exposed tags have suitable permission in the borrow stack for this location"
503    }
504}
505
506impl RetagInfo {
507    fn summary(&self) -> String {
508        let mut s = match self.cause {
509            RetagCause::Normal => "retag",
510            RetagCause::FnEntry => "function-entry retag",
511            RetagCause::InPlaceFnPassing => "in-place function argument/return passing protection",
512            RetagCause::TwoPhase => "two-phase retag",
513        }
514        .to_string();
515        if self.in_field {
516            s.push_str(" (of a reference/box inside this compound value)");
517        }
518        s
519    }
520}