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