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