Skip to content

Commit fe178cc

Browse files
Vanille-NRalfJung
authored andcommitted
Tree Borrows: improved diagnostics
1 parent db73863 commit fe178cc

21 files changed

+799
-244
lines changed

src/tools/miri/src/borrow_tracker/tree_borrows/diagnostics.rs

Lines changed: 205 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,90 @@
1-
use rustc_data_structures::fx::FxHashMap;
2-
31
use std::fmt;
42
use std::ops::Range;
53

4+
use rustc_data_structures::fx::FxHashMap;
5+
use rustc_span::{Span, SpanData};
6+
use rustc_target::abi::Size;
7+
68
use crate::borrow_tracker::tree_borrows::{
7-
err_tb_ub, perms::Permission, tree::LocationState, unimap::UniIndex,
9+
perms::{PermTransition, Permission},
10+
tree::LocationState,
11+
unimap::UniIndex,
812
};
913
use crate::borrow_tracker::{AccessKind, ProtectorKind};
1014
use crate::*;
1115

16+
/// Complete data for an event:
17+
/// - `kind` is what happened to the permissions
18+
/// - `access_kind` and `access_range` describe the access that caused the event
19+
/// - `offset` allows filtering only the relevant events for a given memory location
20+
/// (see how we perform the filtering in `History::extract_relevant`.
21+
/// - `span` is the line of code in question
22+
#[derive(Clone, Debug)]
23+
pub struct Event {
24+
pub transition: PermTransition,
25+
pub access_kind: AccessKind,
26+
pub is_foreign: bool,
27+
pub access_range: AllocRange,
28+
pub offset: Size,
29+
pub span: Span,
30+
}
31+
32+
/// List of all events that affected a tag.
33+
/// NOTE: not all of these events are relevant for a particular location,
34+
/// the events should be filtered before the generation of diagnostics.
35+
/// Available filtering methods include `History::forget` and `History::extract_relevant`.
36+
#[derive(Clone, Debug)]
37+
pub struct History {
38+
pub tag: BorTag,
39+
pub created: (Span, Permission),
40+
pub events: Vec<Event>,
41+
}
42+
43+
/// History formatted for use by `src/diagnostics.rs`.
44+
///
45+
/// NOTE: needs to be `Send` because of a bound on `MachineStopType`, hence
46+
/// the use of `SpanData` rather than `Span`.
47+
#[derive(Debug, Clone, Default)]
48+
pub struct HistoryData {
49+
pub events: Vec<(Option<SpanData>, String)>, // includes creation
50+
}
51+
52+
impl History {
53+
/// Record an additional event to the history.
54+
pub fn push(&mut self, event: Event) {
55+
self.events.push(event);
56+
}
57+
}
58+
59+
impl HistoryData {
60+
// Format events from `new_history` into those recorded by `self`.
61+
//
62+
// NOTE: also converts `Span` to `SpanData`.
63+
pub fn extend(
64+
&mut self,
65+
new_history: History,
66+
tag_name: &'static str,
67+
show_initial_state: bool,
68+
) {
69+
let History { tag, created, events } = new_history;
70+
let this = format!("the {tag_name} tag {tag:?}");
71+
let msg_initial_state = format!(", in the initial state {}", created.1);
72+
let msg_creation = format!(
73+
"{this} was created here{maybe_msg_initial_state}.",
74+
maybe_msg_initial_state = if show_initial_state { &msg_initial_state } else { "" },
75+
);
76+
77+
self.events.push((Some(created.0.data()), msg_creation));
78+
for &Event { transition, access_kind, is_foreign, access_range, span, offset: _ } in &events
79+
{
80+
// NOTE: `offset` is explicitly absent from the error message, it has no significance
81+
// to the user. The meaningful one is `access_range`.
82+
self.events.push((Some(span.data()), format!("{this} then transitioned {transition} due to a {rel} {access_kind} at offsets {access_range:?}.", rel = if is_foreign { "foreign" } else { "child" })));
83+
self.events.push((None, format!("this corresponds to {}.", transition.summary())));
84+
}
85+
}
86+
}
87+
1288
/// Some information that is irrelevant for the algorithm but very
1389
/// convenient to know about a tag for debugging and testing.
1490
#[derive(Clone, Debug)]
@@ -20,18 +96,29 @@ pub struct NodeDebugInfo {
2096
/// pointer in the source code.
2197
/// Helps match tag numbers to human-readable names.
2298
pub name: Option<String>,
99+
/// Notable events in the history of this tag, used for
100+
/// diagnostics.
101+
///
102+
/// NOTE: by virtue of being part of `NodeDebugInfo`,
103+
/// the history is automatically cleaned up by the GC.
104+
/// NOTE: this is `!Send`, it needs to be converted before displaying
105+
/// the actual diagnostics because `src/diagnostics.rs` requires `Send`.
106+
pub history: History,
23107
}
108+
24109
impl NodeDebugInfo {
25-
/// New node info with a name.
26-
pub fn new(tag: BorTag) -> Self {
27-
Self { tag, name: None }
110+
/// Information for a new node. By default it has no
111+
/// name and an empty history.
112+
pub fn new(tag: BorTag, initial: Permission, span: Span) -> Self {
113+
let history = History { tag, created: (span, initial), events: Vec::new() };
114+
Self { tag, name: None, history }
28115
}
29116

30117
/// Add a name to the tag. If a same tag is associated to several pointers,
31118
/// it can have several names which will be separated by commas.
32-
fn add_name(&mut self, name: &str) {
119+
pub fn add_name(&mut self, name: &str) {
33120
if let Some(ref mut prev_name) = &mut self.name {
34-
prev_name.push(',');
121+
prev_name.push_str(", ");
35122
prev_name.push_str(name);
36123
} else {
37124
self.name = Some(String::from(name));
@@ -42,7 +129,7 @@ impl NodeDebugInfo {
42129
impl fmt::Display for NodeDebugInfo {
43130
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44131
if let Some(ref name) = self.name {
45-
write!(f, "{tag:?} (also named '{name}')", tag = self.tag)
132+
write!(f, "{tag:?} ({name})", tag = self.tag)
46133
} else {
47134
write!(f, "{tag:?}", tag = self.tag)
48135
}
@@ -86,7 +173,7 @@ impl<'tcx> Tree {
86173
}
87174
}
88175

89-
#[derive(Debug, Clone, Copy)]
176+
#[derive(Debug, Clone, Copy, PartialEq)]
90177
pub(super) enum TransitionError {
91178
/// This access is not allowed because some parent tag has insufficient permissions.
92179
/// For example, if a tag is `Frozen` and encounters a child write this will
@@ -96,63 +183,146 @@ pub(super) enum TransitionError {
96183
/// A protector was triggered due to an invalid transition that loses
97184
/// too much permissions.
98185
/// For example, if a protected tag goes from `Active` to `Frozen` due
99-
/// to a foreign write this will produce a `ProtectedTransition(Active, Frozen)`.
186+
/// to a foreign write this will produce a `ProtectedTransition(PermTransition(Active, Frozen))`.
100187
/// This kind of error can only occur on foreign accesses.
101-
ProtectedTransition(Permission, Permission),
188+
ProtectedTransition(PermTransition),
102189
/// Cannot deallocate because some tag in the allocation is strongly protected.
103190
/// This kind of error can only occur on deallocations.
104191
ProtectedDealloc,
105192
}
106193

194+
impl History {
195+
/// Keep only the tag and creation
196+
fn forget(&self) -> Self {
197+
History { events: Vec::new(), created: self.created, tag: self.tag }
198+
}
199+
200+
/// Reconstruct the history relevant to `error_offset` knowing that
201+
/// its permission followed `complete_transition`.
202+
///
203+
/// Here's how we do this:
204+
/// - we know `full := complete_transition` the transition of the permission from
205+
/// its initialization to the state just before the error was caused,
206+
/// we want to find a chain of events that produces `full`
207+
/// - we decompose `full` into `pre o post` where
208+
/// `pre` is the best applicable transition from recorded events
209+
/// - we select the event that caused `pre` and iterate
210+
/// to find the chain of events that produces `full := post`
211+
///
212+
/// To find the "best applicable transition" for full:
213+
/// - eliminate events that cannot be applied because their offset is too big
214+
/// - eliminate events that cannot be applied because their starting point is wrong
215+
/// - select the one that happened closest to the range of interest
216+
fn extract_relevant(&self, complete_transition: PermTransition, error_offset: Size) -> Self {
217+
let mut selected_events: Vec<Event> = Vec::new();
218+
let mut full = complete_transition;
219+
while !full.is_noop() {
220+
let (pre, post) = self
221+
.events
222+
.iter()
223+
.filter(|e| e.offset <= error_offset)
224+
.filter_map(|pre_canditate| {
225+
full.apply_start(pre_canditate.transition)
226+
.map(|post_canditate| (pre_canditate, post_canditate))
227+
})
228+
.max_by_key(|(pre_canditate, _post_candidate)| pre_canditate.offset)
229+
.unwrap();
230+
// If this occurs we will loop infinitely !
231+
// Make sure to only put non-noop transitions in `History`.
232+
assert!(!pre.transition.is_noop());
233+
full = post;
234+
selected_events.push(pre.clone());
235+
}
236+
237+
History { events: selected_events, created: self.created, tag: self.tag }
238+
}
239+
}
240+
107241
/// Failures that can occur during the execution of Tree Borrows procedures.
108242
pub(super) struct TbError<'node> {
109243
/// What failure occurred.
110244
pub error_kind: TransitionError,
245+
/// The byte at which the conflict occured.
246+
pub error_offset: Size,
111247
/// The tag on which the error was triggered.
112248
/// On protector violations, this is the tag that was protected.
113249
/// On accesses rejected due to insufficient permissions, this is the
114250
/// tag that lacked those permissions.
115-
pub faulty_tag: &'node NodeDebugInfo,
251+
pub conflicting_info: &'node NodeDebugInfo,
116252
/// Whether this was a Read or Write access. This field is ignored
117253
/// when the error was triggered by a deallocation.
118254
pub access_kind: AccessKind,
119255
/// Which tag the access that caused this error was made through, i.e.
120256
/// which tag was used to read/write/deallocate.
121-
pub tag_of_access: &'node NodeDebugInfo,
257+
pub accessed_info: &'node NodeDebugInfo,
122258
}
123259

124260
impl TbError<'_> {
125261
/// Produce a UB error.
126-
pub fn build<'tcx>(self) -> InterpErrorInfo<'tcx> {
262+
pub fn build<'tcx>(self) -> InterpError<'tcx> {
127263
use TransitionError::*;
128-
err_tb_ub(match self.error_kind {
264+
let started_as = self.conflicting_info.history.created.1;
265+
let kind = self.access_kind;
266+
let accessed = self.accessed_info;
267+
let conflicting = self.conflicting_info;
268+
let accessed_is_conflicting = accessed.tag == conflicting.tag;
269+
let (pre_error, title, relation, problem, conflicting_tag_name) = match self.error_kind {
129270
ChildAccessForbidden(perm) => {
130-
format!(
131-
"{kind} through {initial} is forbidden because it is a child of {current} which is {perm}.",
132-
kind=self.access_kind,
133-
initial=self.tag_of_access,
134-
current=self.faulty_tag,
135-
perm=perm,
271+
let conflicting_tag_name =
272+
if accessed_is_conflicting { "accessed" } else { "conflicting" };
273+
(
274+
perm,
275+
format!("{kind} through {accessed} is forbidden."),
276+
(!accessed_is_conflicting).then_some(format!(
277+
"the accessed tag {accessed} is a child of the conflicting tag {conflicting}."
278+
)),
279+
format!(
280+
"the {conflicting_tag_name} tag {conflicting} has state {perm} which forbids child {kind}es."
281+
),
282+
conflicting_tag_name,
136283
)
137284
}
138-
ProtectedTransition(start, end) => {
139-
format!(
140-
"{kind} through {initial} is forbidden because it is a foreign tag for {current}, which would hence change from {start} to {end}, but {current} is protected",
141-
current=self.faulty_tag,
142-
start=start,
143-
end=end,
144-
kind=self.access_kind,
145-
initial=self.tag_of_access,
285+
ProtectedTransition(transition) => {
286+
let conflicting_tag_name = "protected";
287+
(
288+
transition.started(),
289+
format!("{kind} through {accessed} is forbidden."),
290+
Some(format!(
291+
"the accessed tag {accessed} is a foreign tag for the {conflicting_tag_name} tag {conflicting}."
292+
)),
293+
format!(
294+
"the access would cause the {conflicting_tag_name} tag {conflicting} to transition {transition}. This is {loss}, which is not allowed for protected tags.",
295+
loss = transition.summary(),
296+
),
297+
conflicting_tag_name,
146298
)
147299
}
148300
ProtectedDealloc => {
149-
format!(
150-
"the allocation of {initial} also contains {current} which is strongly protected, cannot deallocate",
151-
initial=self.tag_of_access,
152-
current=self.faulty_tag,
301+
let conflicting_tag_name = "strongly protected";
302+
(
303+
started_as,
304+
format!("deallocation through {accessed} is forbidden."),
305+
Some(format!(
306+
"the allocation of the accessed tag {accessed} also contains the {conflicting_tag_name} tag {conflicting}."
307+
)),
308+
format!(
309+
"the {conflicting_tag_name} tag {conflicting} disallows deallocations."
310+
),
311+
conflicting_tag_name,
153312
)
154313
}
155-
}).into()
314+
};
315+
let pre_transition = PermTransition::from(started_as, pre_error).unwrap();
316+
let mut history = HistoryData::default();
317+
if !accessed_is_conflicting {
318+
history.extend(self.accessed_info.history.forget(), "accessed", false);
319+
}
320+
history.extend(
321+
self.conflicting_info.history.extract_relevant(pre_transition, self.error_offset),
322+
conflicting_tag_name,
323+
true,
324+
);
325+
err_machine_stop!(TerminationInfo::TreeBorrowsUb { title, relation, problem, history })
156326
}
157327
}
158328

src/tools/miri/src/borrow_tracker/tree_borrows/mod.rs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use rustc_middle::{
1414

1515
use crate::*;
1616

17-
mod diagnostics;
17+
pub mod diagnostics;
1818
mod perms;
1919
mod tree;
2020
mod unimap;
@@ -23,10 +23,6 @@ pub use tree::Tree;
2323

2424
pub type AllocState = Tree;
2525

26-
pub fn err_tb_ub<'tcx>(msg: String) -> InterpError<'tcx> {
27-
err_machine_stop!(TerminationInfo::TreeBorrowsUb { msg })
28-
}
29-
3026
impl<'tcx> Tree {
3127
/// Create a new allocation, i.e. a new tree
3228
pub fn new_allocation(
@@ -37,7 +33,8 @@ impl<'tcx> Tree {
3733
machine: &MiriMachine<'_, 'tcx>,
3834
) -> Self {
3935
let tag = state.base_ptr_tag(id, machine); // Fresh tag for the root
40-
Tree::new(tag, size)
36+
let span = machine.current_span();
37+
Tree::new(tag, size, span)
4138
}
4239

4340
/// Check that an access on the entire range is permitted, and update
@@ -64,7 +61,8 @@ impl<'tcx> Tree {
6461
ProvenanceExtra::Wildcard => return Ok(()),
6562
};
6663
let global = machine.borrow_tracker.as_ref().unwrap();
67-
self.perform_access(access_kind, tag, range, global)
64+
let span = machine.current_span();
65+
self.perform_access(access_kind, tag, range, global, span)
6866
}
6967

7068
/// Check that this pointer has permission to deallocate this range.
@@ -82,7 +80,8 @@ impl<'tcx> Tree {
8280
ProvenanceExtra::Wildcard => return Ok(()),
8381
};
8482
let global = machine.borrow_tracker.as_ref().unwrap();
85-
self.dealloc(tag, range, global)
83+
let span = machine.current_span();
84+
self.dealloc(tag, range, global, span)
8685
}
8786

8887
pub fn expose_tag(&mut self, _tag: BorTag) {
@@ -265,21 +264,23 @@ trait EvalContextPrivExt<'mir: 'ecx, 'tcx: 'mir, 'ecx>: crate::MiriInterpCxExt<'
265264
.insert(new_tag, protect);
266265
}
267266

267+
let span = this.machine.current_span();
268268
let alloc_extra = this.get_alloc_extra(alloc_id)?;
269269
let range = alloc_range(base_offset, ptr_size);
270270
let mut tree_borrows = alloc_extra.borrow_tracker_tb().borrow_mut();
271271

272272
if new_perm.perform_read_access {
273273
// Count this reborrow as a read access
274274
let global = &this.machine.borrow_tracker.as_ref().unwrap();
275-
tree_borrows.perform_access(AccessKind::Read, orig_tag, range, global)?;
275+
let span = this.machine.current_span();
276+
tree_borrows.perform_access(AccessKind::Read, orig_tag, range, global, span)?;
276277
if let Some(data_race) = alloc_extra.data_race.as_ref() {
277278
data_race.read(alloc_id, range, &this.machine)?;
278279
}
279280
}
280281

281282
// Record the parent-child pair in the tree.
282-
tree_borrows.new_child(orig_tag, new_tag, new_perm.initial_state, range)?;
283+
tree_borrows.new_child(orig_tag, new_tag, new_perm.initial_state, range, span)?;
283284
Ok(Some((alloc_id, new_tag)))
284285
}
285286

0 commit comments

Comments
 (0)