Skip to content

Commit 0dde6f3

Browse files
committed
Implement -Zmiri-tag-gc a garbage collector for tags
1 parent b8a97c4 commit 0dde6f3

File tree

10 files changed

+241
-7
lines changed

10 files changed

+241
-7
lines changed

README.md

+4
Original file line numberDiff line numberDiff line change
@@ -323,6 +323,10 @@ environment variable. We first document the most relevant and most commonly used
323323
ensure alignment. (The standard library `align_to` method works fine in both modes; under
324324
symbolic alignment it only fills the middle slice when the allocation guarantees sufficient
325325
alignment.)
326+
* `-Zmiri-tag-gc=<blocks>` configures how often the pointer tag garbage collector runs. The default
327+
is to search for and remove unreachable tags once every `10,000` basic blocks. Setting this to
328+
`0` disables the garbage collector, which causes some programs to have explosive memory usage
329+
and/or super-linear runtime.
326330

327331
The remaining flags are for advanced use only, and more likely to change or be removed.
328332
Some of these are **unsound**, which means they can lead

src/bin/miri.rs

+6
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,12 @@ fn main() {
521521
Err(err) => show_error!("-Zmiri-report-progress requires a `u32`: {}", err),
522522
};
523523
miri_config.report_progress = Some(interval);
524+
} else if let Some(param) = arg.strip_prefix("-Zmiri-tag-gc=") {
525+
let interval = match param.parse::<u32>() {
526+
Ok(i) => i,
527+
Err(err) => show_error!("-Zmiri-tag-gc requires a `u32`: {}", err),
528+
};
529+
miri_config.gc_interval = interval;
524530
} else if let Some(param) = arg.strip_prefix("-Zmiri-measureme=") {
525531
miri_config.measureme_out = Some(param.to_string());
526532
} else if let Some(param) = arg.strip_prefix("-Zmiri-backtrace=") {

src/concurrency/thread.rs

+4
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,10 @@ impl<'mir, 'tcx: 'mir> ThreadManager<'mir, 'tcx> {
289289
&mut self.threads[self.active_thread].stack
290290
}
291291

292+
pub fn iter(&self) -> impl Iterator<Item = &Thread<'mir, 'tcx>> {
293+
self.threads.iter()
294+
}
295+
292296
pub fn all_stacks(
293297
&self,
294298
) -> impl Iterator<Item = &[Frame<'mir, 'tcx, Provenance, FrameData<'tcx>>]> {

src/eval.rs

+3
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,8 @@ pub struct MiriConfig {
132132
/// The location of a shared object file to load when calling external functions
133133
/// FIXME! consider allowing users to specify paths to multiple SO files, or to a directory
134134
pub external_so_file: Option<PathBuf>,
135+
/// Run a garbage collector for SbTags every N basic blocks.
136+
pub gc_interval: u32,
135137
}
136138

137139
impl Default for MiriConfig {
@@ -164,6 +166,7 @@ impl Default for MiriConfig {
164166
report_progress: None,
165167
retag_fields: false,
166168
external_so_file: None,
169+
gc_interval: 10_000,
167170
}
168171
}
169172
}

src/lib.rs

+2
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ mod operator;
6262
mod range_map;
6363
mod shims;
6464
mod stacked_borrows;
65+
mod tag_gc;
6566

6667
// Establish a "crate-wide prelude": we often import `crate::*`.
6768

@@ -110,6 +111,7 @@ pub use crate::range_map::RangeMap;
110111
pub use crate::stacked_borrows::{
111112
CallId, EvalContextExt as StackedBorEvalContextExt, Item, Permission, SbTag, Stack, Stacks,
112113
};
114+
pub use crate::tag_gc::EvalContextExt as _;
113115

114116
/// Insert rustc arguments at the beginning of the argument list that Miri wants to be
115117
/// set per default, for maximal validation power.

src/machine.rs

+21
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,11 @@ pub struct Evaluator<'mir, 'tcx> {
394394

395395
/// Handle of the optional shared object file for external functions.
396396
pub external_so_lib: Option<(libloading::Library, std::path::PathBuf)>,
397+
398+
/// Run a garbage collector for SbTags every N basic blocks.
399+
pub(crate) gc_interval: u32,
400+
/// The number of blocks that passed since the last SbTag GC pass.
401+
pub(crate) since_gc: u32,
397402
}
398403

399404
impl<'mir, 'tcx> Evaluator<'mir, 'tcx> {
@@ -469,6 +474,8 @@ impl<'mir, 'tcx> Evaluator<'mir, 'tcx> {
469474
lib_file_path.clone(),
470475
)
471476
}),
477+
gc_interval: config.gc_interval,
478+
since_gc: 0,
472479
}
473480
}
474481

@@ -1016,6 +1023,20 @@ impl<'mir, 'tcx> Machine<'mir, 'tcx> for Evaluator<'mir, 'tcx> {
10161023
});
10171024
}
10181025
}
1026+
1027+
// Search for SbTags to find all live pointers, then remove all other tags from borrow
1028+
// stacks.
1029+
// When debug assertions are enabled, run the GC as often as possible so that any cases
1030+
// where it mistakenly removes an important tag become visible.
1031+
if cfg!(debug_assertions)
1032+
|| (ecx.machine.gc_interval > 0 && ecx.machine.since_gc >= ecx.machine.gc_interval)
1033+
{
1034+
ecx.machine.since_gc = 0;
1035+
ecx.garbage_collect_tags()?;
1036+
} else {
1037+
ecx.machine.since_gc += 1;
1038+
}
1039+
10191040
// These are our preemption points.
10201041
ecx.maybe_preempt_active_thread();
10211042
Ok(())

src/shims/tls.rs

+6
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,12 @@ impl<'tcx> TlsData<'tcx> {
233233
data.remove(&thread_id);
234234
}
235235
}
236+
237+
pub fn iter(&self, mut visitor: impl FnMut(&Scalar<Provenance>)) {
238+
for scalar in self.keys.values().flat_map(|v| v.data.values()) {
239+
visitor(scalar);
240+
}
241+
}
236242
}
237243

238244
impl<'mir, 'tcx: 'mir> EvalContextPrivExt<'mir, 'tcx> for crate::MiriEvalContext<'mir, 'tcx> {}

src/stacked_borrows/mod.rs

+19
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ pub struct Stacks {
8080
history: AllocHistory,
8181
/// The set of tags that have been exposed inside this allocation.
8282
exposed_tags: FxHashSet<SbTag>,
83+
/// Whether this memory has been modified since the last time the tag GC ran
84+
modified_since_last_gc: bool,
8385
}
8486

8587
/// Extra global state, available to the memory access hooks.
@@ -422,6 +424,7 @@ impl<'tcx> Stack {
422424
let item = self.get(idx).unwrap();
423425
Stack::item_popped(&item, global, dcx)?;
424426
}
427+
425428
Ok(())
426429
}
427430

@@ -496,6 +499,20 @@ impl<'tcx> Stack {
496499
}
497500
// # Stacked Borrows Core End
498501

502+
/// Integration with the SbTag garbage collector
503+
impl Stacks {
504+
pub fn remove_unreachable_tags(&mut self, live_tags: &FxHashSet<SbTag>) {
505+
if self.modified_since_last_gc {
506+
for stack in self.stacks.iter_mut_all() {
507+
if stack.len() > 64 {
508+
stack.retain(live_tags);
509+
}
510+
}
511+
self.modified_since_last_gc = false;
512+
}
513+
}
514+
}
515+
499516
/// Map per-stack operations to higher-level per-location-range operations.
500517
impl<'tcx> Stacks {
501518
/// Creates a new stack with an initial tag. For diagnostic purposes, we also need to know
@@ -514,6 +531,7 @@ impl<'tcx> Stacks {
514531
stacks: RangeMap::new(size, stack),
515532
history: AllocHistory::new(id, item, current_span),
516533
exposed_tags: FxHashSet::default(),
534+
modified_since_last_gc: false,
517535
}
518536
}
519537

@@ -528,6 +546,7 @@ impl<'tcx> Stacks {
528546
&mut FxHashSet<SbTag>,
529547
) -> InterpResult<'tcx>,
530548
) -> InterpResult<'tcx> {
549+
self.modified_since_last_gc = true;
531550
for (offset, stack) in self.stacks.iter_mut(range.start, range.size) {
532551
let mut dcx = dcx_builder.build(&mut self.history, offset);
533552
f(stack, &mut dcx, &mut self.exposed_tags)?;

src/stacked_borrows/stack.rs

+59-7
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,61 @@ pub struct Stack {
3939
unique_range: Range<usize>,
4040
}
4141

42+
impl Stack {
43+
pub fn retain(&mut self, tags: &FxHashSet<SbTag>) {
44+
let mut first_removed = None;
45+
46+
let mut read_idx = 1;
47+
let mut write_idx = 1;
48+
while read_idx < self.borrows.len() {
49+
let left = self.borrows[read_idx - 1];
50+
let this = self.borrows[read_idx];
51+
let should_keep = match this.perm() {
52+
// SharedReadWrite is the simplest case, if it's unreachable we can just remove it.
53+
Permission::SharedReadWrite => tags.contains(&this.tag()),
54+
// Only retain a Disabled tag if it is terminating a SharedReadWrite block.
55+
Permission::Disabled => left.perm() == Permission::SharedReadWrite,
56+
// Unique and SharedReadOnly can terminate a SharedReadWrite block, so only remove
57+
// them if they are both unreachable and not directly after a SharedReadWrite.
58+
Permission::Unique | Permission::SharedReadOnly =>
59+
left.perm() == Permission::SharedReadWrite || tags.contains(&this.tag()),
60+
};
61+
62+
if should_keep {
63+
if read_idx != write_idx {
64+
self.borrows[write_idx] = self.borrows[read_idx];
65+
}
66+
write_idx += 1;
67+
} else if first_removed.is_none() {
68+
first_removed = Some(read_idx);
69+
}
70+
71+
read_idx += 1;
72+
}
73+
self.borrows.truncate(write_idx);
74+
75+
#[cfg(not(feature = "stack-cache"))]
76+
drop(first_removed); // This is only needed for the stack-cache
77+
78+
#[cfg(feature = "stack-cache")]
79+
if let Some(first_removed) = first_removed {
80+
// Either end of unique_range may have shifted, all we really know is that we can't
81+
// have introduced a new Unique.
82+
if !self.unique_range.is_empty() {
83+
self.unique_range = 0..self.len();
84+
}
85+
86+
// Replace any Items which have been collected with the base item, a known-good value.
87+
for i in 0..CACHE_LEN {
88+
if self.cache.idx[i] >= first_removed {
89+
self.cache.items[i] = self.borrows[0];
90+
self.cache.idx[i] = 0;
91+
}
92+
}
93+
}
94+
}
95+
}
96+
4297
/// A very small cache of searches of a borrow stack, mapping `Item`s to their position in said stack.
4398
///
4499
/// It may seem like maintaining this cache is a waste for small stacks, but
@@ -105,14 +160,11 @@ impl<'tcx> Stack {
105160

106161
// Check that the unique_range is a valid index into the borrow stack.
107162
// This asserts that the unique_range's start <= end.
108-
let uniques = &self.borrows[self.unique_range.clone()];
163+
let _uniques = &self.borrows[self.unique_range.clone()];
109164

110-
// Check that the start of the unique_range is precise.
111-
if let Some(first_unique) = uniques.first() {
112-
assert_eq!(first_unique.perm(), Permission::Unique);
113-
}
114-
// We cannot assert that the unique range is exact on the upper end.
115-
// When we pop items within the unique range, setting the end of the range precisely
165+
// We cannot assert that the unique range is precise.
166+
// Both ends may shift around when `Stack::retain` is called. Additionally,
167+
// when we pop items within the unique range, setting the end of the range precisely
116168
// requires doing a linear search of the borrow stack, which is exactly the kind of
117169
// operation that all this caching exists to avoid.
118170
}

src/tag_gc.rs

+117
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
use crate::*;
2+
use rustc_data_structures::fx::FxHashSet;
3+
4+
impl<'mir, 'tcx: 'mir> EvalContextExt<'mir, 'tcx> for crate::MiriEvalContext<'mir, 'tcx> {}
5+
pub trait EvalContextExt<'mir, 'tcx: 'mir>: MiriEvalContextExt<'mir, 'tcx> {
6+
fn garbage_collect_tags(&mut self) -> InterpResult<'tcx> {
7+
let this = self.eval_context_mut();
8+
// No reason to do anything at all if stacked borrows is off.
9+
if this.machine.stacked_borrows.is_none() {
10+
return Ok(());
11+
}
12+
13+
let mut tags = FxHashSet::default();
14+
15+
for thread in this.machine.threads.iter() {
16+
if let Some(Scalar::Ptr(
17+
Pointer { provenance: Provenance::Concrete { sb, .. }, .. },
18+
_,
19+
)) = thread.panic_payload
20+
{
21+
tags.insert(sb);
22+
}
23+
}
24+
25+
self.find_tags_in_tls(&mut tags);
26+
self.find_tags_in_memory(&mut tags);
27+
self.find_tags_in_locals(&mut tags)?;
28+
29+
self.remove_unreachable_tags(tags);
30+
31+
Ok(())
32+
}
33+
34+
fn find_tags_in_tls(&mut self, tags: &mut FxHashSet<SbTag>) {
35+
let this = self.eval_context_mut();
36+
this.machine.tls.iter(|scalar| {
37+
if let Scalar::Ptr(Pointer { provenance: Provenance::Concrete { sb, .. }, .. }, _) =
38+
scalar
39+
{
40+
tags.insert(*sb);
41+
}
42+
});
43+
}
44+
45+
fn find_tags_in_memory(&mut self, tags: &mut FxHashSet<SbTag>) {
46+
let this = self.eval_context_mut();
47+
this.memory.alloc_map().iter(|it| {
48+
for (_id, (_kind, alloc)) in it {
49+
for (_size, prov) in alloc.provenance().iter() {
50+
if let Provenance::Concrete { sb, .. } = prov {
51+
tags.insert(*sb);
52+
}
53+
}
54+
}
55+
});
56+
}
57+
58+
fn find_tags_in_locals(&mut self, tags: &mut FxHashSet<SbTag>) -> InterpResult<'tcx> {
59+
let this = self.eval_context_mut();
60+
for frame in this.machine.threads.all_stacks().flatten() {
61+
// Handle the return place of each frame
62+
if let Ok(return_place) = frame.return_place.try_as_mplace() {
63+
if let Some(Provenance::Concrete { sb, .. }) = return_place.ptr.provenance {
64+
tags.insert(sb);
65+
}
66+
}
67+
68+
for local in frame.locals.iter() {
69+
let LocalValue::Live(value) = local.value else {
70+
continue;
71+
};
72+
match value {
73+
Operand::Immediate(Immediate::Scalar(Scalar::Ptr(ptr, _))) =>
74+
if let Provenance::Concrete { sb, .. } = ptr.provenance {
75+
tags.insert(sb);
76+
},
77+
Operand::Immediate(Immediate::ScalarPair(s1, s2)) => {
78+
if let Scalar::Ptr(ptr, _) = s1 {
79+
if let Provenance::Concrete { sb, .. } = ptr.provenance {
80+
tags.insert(sb);
81+
}
82+
}
83+
if let Scalar::Ptr(ptr, _) = s2 {
84+
if let Provenance::Concrete { sb, .. } = ptr.provenance {
85+
tags.insert(sb);
86+
}
87+
}
88+
}
89+
Operand::Indirect(MemPlace { ptr, .. }) => {
90+
if let Some(Provenance::Concrete { sb, .. }) = ptr.provenance {
91+
tags.insert(sb);
92+
}
93+
}
94+
Operand::Immediate(Immediate::Uninit)
95+
| Operand::Immediate(Immediate::Scalar(Scalar::Int(_))) => {}
96+
}
97+
}
98+
}
99+
100+
Ok(())
101+
}
102+
103+
fn remove_unreachable_tags(&mut self, tags: FxHashSet<SbTag>) {
104+
let this = self.eval_context_mut();
105+
this.memory.alloc_map().iter(|it| {
106+
for (_id, (_kind, alloc)) in it {
107+
alloc
108+
.extra
109+
.stacked_borrows
110+
.as_ref()
111+
.unwrap()
112+
.borrow_mut()
113+
.remove_unreachable_tags(&tags);
114+
}
115+
});
116+
}
117+
}

0 commit comments

Comments
 (0)