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