Skip to content

Commit 1b55b3b

Browse files
committed
Auto merge of rust-lang#2828 - Vanille-N:tb-diags, r=RalfJung
Tree Borrows: improved diagnostics Better diagnostics for Tree Borrows violations. - Shows where the conflicting tags (the one that was accessed and the one that had a permission or protector that caused UB) were reborrowed, which is more readable than only `<TAG>` - Shows a small history of what happened for the faulty tag to get there (which lines caused it to lose read/write permissions) - Explains permissions and transitions in natural language (e.g. "does not have read permissions" instead of "is Disabled") Not perfect, but at least testing TB will be less confusing. Lack of range information from `RangeMap` makes selection of relevant events nontrivial: we reconstruct history from knowledge of `(initial, final)` and `(offset, pi, p'i)` so that `initial -> final = p1 -> p1' = p2 -> p2' = p3 -> ... = final `
2 parents a050719 + 7566ed8 commit 1b55b3b

21 files changed

+802
-246
lines changed

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

Lines changed: 207 additions & 38 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,145 @@ 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, details, 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,
136-
)
271+
let conflicting_tag_name =
272+
if accessed_is_conflicting { "accessed" } else { "conflicting" };
273+
let title = format!("{kind} through {accessed} is forbidden");
274+
let mut details = Vec::new();
275+
if !accessed_is_conflicting {
276+
details.push(format!(
277+
"the accessed tag {accessed} is a child of the conflicting tag {conflicting}"
278+
));
279+
}
280+
details.push(format!(
281+
"the {conflicting_tag_name} tag {conflicting} has state {perm} which forbids child {kind}es"
282+
));
283+
(perm, title, details, conflicting_tag_name)
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,
146-
)
285+
ProtectedTransition(transition) => {
286+
let conflicting_tag_name = "protected";
287+
let title = format!("{kind} through {accessed} is forbidden");
288+
let details = vec![
289+
format!(
290+
"the accessed tag {accessed} is foreign to the {conflicting_tag_name} tag {conflicting} (i.e., it is not a child)"
291+
),
292+
format!(
293+
"the access would cause the {conflicting_tag_name} tag {conflicting} to transition {transition}"
294+
),
295+
format!(
296+
"this is {loss}, which is not allowed for protected tags",
297+
loss = transition.summary(),
298+
),
299+
];
300+
(transition.started(), title, details, conflicting_tag_name)
147301
}
148302
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,
153-
)
303+
let conflicting_tag_name = "strongly protected";
304+
let title = format!("deallocation through {accessed} is forbidden");
305+
let details = vec![
306+
format!(
307+
"the allocation of the accessed tag {accessed} also contains the {conflicting_tag_name} tag {conflicting}"
308+
),
309+
format!("the {conflicting_tag_name} tag {conflicting} disallows deallocations"),
310+
];
311+
(started_as, title, details, conflicting_tag_name)
154312
}
155-
}).into()
313+
};
314+
let pre_transition = PermTransition::from(started_as, pre_error).unwrap();
315+
let mut history = HistoryData::default();
316+
if !accessed_is_conflicting {
317+
history.extend(self.accessed_info.history.forget(), "accessed", false);
318+
}
319+
history.extend(
320+
self.conflicting_info.history.extract_relevant(pre_transition, self.error_offset),
321+
conflicting_tag_name,
322+
true,
323+
);
324+
err_machine_stop!(TerminationInfo::TreeBorrowsUb { title, details, history })
156325
}
157326
}
158327

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)