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