Skip to content

Commit f8f4ca9

Browse files
committed
Implement -Zmiri-tag-gc a garbage collector for tags
1 parent e0f0e1f commit f8f4ca9

File tree

11 files changed

+230
-4
lines changed

11 files changed

+230
-4
lines changed

miri

+1-1
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ fi
131131
# Enable rustc-specific lints (ignored without `-Zunstable-options`).
132132
export RUSTFLAGS="-Zunstable-options -Wrustc::internal $RUSTFLAGS"
133133
# We set the rpath so that Miri finds the private rustc libraries it needs.
134-
export RUSTFLAGS="-C link-args=-Wl,-rpath,$LIBDIR $RUSTFLAGS"
134+
export RUSTFLAGS="-C link-args=-Wl,-rpath,$LIBDIR $RUSTFLAGS -Cdebuginfo=1"
135135

136136
## Helper functions
137137

src/bin/miri.rs

+8
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,14 @@ 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 arg == "-Zmiri-tag-gc" {
525+
miri_config.gc_interval = Some(10_000);
526+
} else if let Some(param) = arg.strip_prefix("-Zmiri-tag-gc=") {
527+
let interval = match param.parse::<u32>() {
528+
Ok(i) => i,
529+
Err(err) => show_error!("-Zmiri-tag-gc requires a `u32`: {}", err),
530+
};
531+
miri_config.gc_interval = Some(interval);
524532
} else if let Some(param) = arg.strip_prefix("-Zmiri-measureme=") {
525533
miri_config.measureme_out = Some(param.to_string());
526534
} else if let Some(param) = arg.strip_prefix("-Zmiri-backtrace=") {

src/eval.rs

+4
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,8 @@ pub struct MiriConfig {
128128
pub report_progress: Option<u32>,
129129
/// Whether Stacked Borrows retagging should recurse into fields of datatypes.
130130
pub retag_fields: bool,
131+
/// Run a garbage collector for SbTags every N basic blocks.
132+
pub gc_interval: Option<u32>,
131133
}
132134

133135
impl Default for MiriConfig {
@@ -159,6 +161,7 @@ impl Default for MiriConfig {
159161
preemption_rate: 0.01, // 1%
160162
report_progress: None,
161163
retag_fields: false,
164+
gc_interval: None,
162165
}
163166
}
164167
}
@@ -342,6 +345,7 @@ pub fn eval_entry<'tcx>(
342345
match ecx.schedule()? {
343346
SchedulingAction::ExecuteStep => {
344347
assert!(ecx.step()?, "a terminated thread was scheduled for execution");
348+
//ecx.garbage_collect_tags()?;
345349
}
346350
SchedulingAction::ExecuteTimeoutCallback => {
347351
assert!(

src/intptrcast.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ pub struct GlobalStateInner {
3636
base_addr: FxHashMap<AllocId, u64>,
3737
/// Whether an allocation has been exposed or not. This cannot be put
3838
/// into `AllocExtra` for the same reason as `base_addr`.
39-
exposed: FxHashSet<AllocId>,
39+
pub exposed: FxHashSet<AllocId>,
4040
/// This is used as a memory address when a new pointer is casted to an integer. It
4141
/// is always larger than any address that was previously made part of a block.
4242
next_base_addr: u64,
@@ -246,6 +246,11 @@ impl<'mir, 'tcx> GlobalStateInner {
246246
rem => addr.checked_add(align).unwrap() - rem,
247247
}
248248
}
249+
250+
/// Returns whether the specified `AllocId` has been exposed.
251+
pub fn is_exposed(&self, id: AllocId) -> bool {
252+
self.exposed.contains(&id)
253+
}
249254
}
250255

251256
#[cfg(test)]

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
pub mod thread;
6667

6768
// Establish a "crate-wide prelude": we often import `crate::*`.
@@ -104,6 +105,7 @@ pub use crate::range_map::RangeMap;
104105
pub use crate::stacked_borrows::{
105106
CallId, EvalContextExt as StackedBorEvalContextExt, Item, Permission, SbTag, Stack, Stacks,
106107
};
108+
pub use crate::tag_gc::EvalContextExt as _;
107109
pub use crate::thread::{
108110
EvalContextExt as ThreadsEvalContextExt, SchedulingAction, ThreadId, ThreadManager, ThreadState,
109111
};

src/machine.rs

+19
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,12 @@ pub struct Evaluator<'mir, 'tcx> {
358358
pub(crate) report_progress: Option<u32>,
359359
/// The number of blocks that passed since the last progress report.
360360
pub(crate) since_progress_report: u32,
361+
362+
/// If `Some`, we will attempt to run a garbage collector on SbTags every N basic blocks.
363+
pub(crate) gc_interval: Option<u32>,
364+
/// The number of blocks that passed since the last SbTag GC pass.
365+
pub(crate) since_gc: u32,
366+
//pub(crate) hot_allocations: FxHashSet<AllocId>,
361367
}
362368

363369
impl<'mir, 'tcx> Evaluator<'mir, 'tcx> {
@@ -412,6 +418,8 @@ impl<'mir, 'tcx> Evaluator<'mir, 'tcx> {
412418
preemption_rate: config.preemption_rate,
413419
report_progress: config.report_progress,
414420
since_progress_report: 0,
421+
gc_interval: config.gc_interval,
422+
since_gc: 0,
415423
}
416424
}
417425

@@ -959,6 +967,17 @@ impl<'mir, 'tcx> Machine<'mir, 'tcx> for Evaluator<'mir, 'tcx> {
959967
// Cannot overflow, since it is strictly less than `report_progress`.
960968
ecx.machine.since_progress_report += 1;
961969
}
970+
971+
// Search all memory for SbTags, then use Stack::retain to drop all other tags.
972+
if let Some(gc_interval) = ecx.machine.gc_interval {
973+
if ecx.machine.since_gc >= gc_interval {
974+
ecx.machine.since_gc = 0;
975+
ecx.garbage_collect_tags()?;
976+
} else {
977+
ecx.machine.since_gc += 1;
978+
}
979+
}
980+
962981
// These are our preemption points.
963982
ecx.maybe_preempt_active_thread();
964983
Ok(())

src/shims/panic.rs

+7
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,13 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriEvalContextExt<'mir, 'tcx
5757
assert!(thread.panic_payload.is_none(), "the panic runtime should avoid double-panics");
5858
thread.panic_payload = Some(payload);
5959

60+
// Expose the allocation so we don't GC it
61+
if let Scalar::Ptr(Pointer { provenance: Provenance::Concrete { alloc_id, .. }, .. }, _) =
62+
payload
63+
{
64+
this.machine.intptrcast.borrow_mut().exposed.insert(alloc_id);
65+
}
66+
6067
// Jump to the unwind block to begin unwinding.
6168
this.unwind_to_block(unwind)?;
6269
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-1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ pub struct Stacks {
8080
history: AllocHistory,
8181
/// The set of tags that have been exposed inside this allocation.
8282
exposed_tags: FxHashSet<SbTag>,
83+
dirty: bool,
8384
}
8485

8586
/// Extra global state, available to the memory access hooks.
@@ -422,6 +423,7 @@ impl<'tcx> Stack {
422423
let item = self.get(idx).unwrap();
423424
Stack::item_popped(&item, global, dcx)?;
424425
}
426+
425427
Ok(())
426428
}
427429

@@ -496,6 +498,20 @@ impl<'tcx> Stack {
496498
}
497499
// # Stacked Borrows Core End
498500

501+
/// Integration with the SbTag garbage collector
502+
impl Stacks {
503+
pub fn collect(&mut self, live_tags: &FxHashSet<SbTag>) {
504+
if self.dirty {
505+
for stack in self.stacks.iter_mut_all() {
506+
if stack.len() > 64 {
507+
stack.retain(live_tags);
508+
}
509+
}
510+
self.dirty = false;
511+
}
512+
}
513+
}
514+
499515
/// Map per-stack operations to higher-level per-location-range operations.
500516
impl<'tcx> Stacks {
501517
/// Creates a new stack with an initial tag. For diagnostic purposes, we also need to know
@@ -508,6 +524,7 @@ impl<'tcx> Stacks {
508524
stacks: RangeMap::new(size, stack),
509525
history: AllocHistory::new(id),
510526
exposed_tags: FxHashSet::default(),
527+
dirty: false,
511528
}
512529
}
513530

@@ -522,6 +539,7 @@ impl<'tcx> Stacks {
522539
&mut FxHashSet<SbTag>,
523540
) -> InterpResult<'tcx>,
524541
) -> InterpResult<'tcx> {
542+
self.dirty = true;
525543
for (offset, stack) in self.stacks.iter_mut(range.start, range.size) {
526544
let mut dcx = dcx_builder.build(&mut self.history, offset);
527545
f(stack, &mut dcx, &mut self.exposed_tags)?;
@@ -614,7 +632,7 @@ impl Stacks {
614632
) -> InterpResult<'tcx> {
615633
trace!("deallocation with tag {:?}: {:?}, size {}", tag, alloc_id, range.size.bytes());
616634
let dcx = DiagnosticCxBuilder::dealloc(&mut current_span, threads, tag);
617-
let state = state.borrow();
635+
let state = state.borrow_mut();
618636
self.for_each(range, dcx, |stack, dcx, exposed_tags| {
619637
stack.dealloc(tag, &state, dcx, exposed_tags)
620638
})?;

src/stacked_borrows/stack.rs

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

42+
impl Stack {
43+
pub fn retain(&mut self, tags: &FxHashSet<SbTag>) {
44+
let prev_len = self.borrows.len();
45+
let mut read_idx = 1;
46+
let mut write_idx = 1;
47+
while read_idx < self.borrows.len() {
48+
let left = self.borrows[read_idx - 1];
49+
let this = self.borrows[read_idx];
50+
let should_keep = match this.perm() {
51+
// Always destroy adjacent Disabled tags, they only exist to separate SRW blocks so
52+
// two is just as good as one.
53+
Permission::Disabled => left.perm() != Permission::Disabled,
54+
// SRW and SRO don't do anything special, so we keep them if they are in use
55+
Permission::SharedReadWrite | Permission::SharedReadOnly =>
56+
tags.contains(&this.tag()),
57+
Permission::Unique =>
58+
left.perm() != Permission::Unique || tags.contains(&this.tag()),
59+
};
60+
61+
if should_keep {
62+
if read_idx != write_idx {
63+
self.borrows[write_idx] = self.borrows[read_idx];
64+
}
65+
write_idx += 1;
66+
}
67+
68+
read_idx += 1;
69+
}
70+
self.borrows.truncate(write_idx);
71+
72+
// Reset the cache if anything was changed
73+
#[cfg(feature = "stack-cache")]
74+
if self.borrows.len() != prev_len {
75+
if !self.unique_range.is_empty() {
76+
self.unique_range = 0..self.len();
77+
}
78+
for i in 0..CACHE_LEN {
79+
self.cache.items[i] = self.borrows[0];
80+
self.cache.idx[i] = 0;
81+
}
82+
}
83+
}
84+
}
85+
4286
/// A very small cache of searches of a borrow stack, mapping `Item`s to their position in said stack.
4387
///
4488
/// It may seem like maintaining this cache is a waste for small stacks, but
@@ -105,12 +149,14 @@ impl<'tcx> Stack {
105149

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

110154
// Check that the start of the unique_range is precise.
155+
/*
111156
if let Some(first_unique) = uniques.first() {
112157
assert_eq!(first_unique.perm(), Permission::Unique);
113158
}
159+
*/
114160
// We cannot assert that the unique range is exact on the upper end.
115161
// When we pop items within the unique range, setting the end of the range precisely
116162
// requires doing a linear search of the borrow stack, which is exactly the kind of

src/tag_gc.rs

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
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+
self.find_pointers_in_tls(&mut tags);
16+
self.find_pointers_in_memory(&mut tags);
17+
self.find_pointers_in_locals(&mut tags)?;
18+
19+
self.remove_extraneous_tags(tags);
20+
21+
Ok(())
22+
}
23+
24+
fn find_pointers_in_tls(&mut self, tags: &mut FxHashSet<SbTag>) {
25+
let this = self.eval_context_mut();
26+
this.machine.tls.iter(|scalar| {
27+
if let Scalar::Ptr(Pointer { provenance: Provenance::Concrete { sb, .. }, .. }, _) =
28+
scalar
29+
{
30+
tags.insert(*sb);
31+
}
32+
});
33+
}
34+
35+
fn find_pointers_in_memory(&mut self, tags: &mut FxHashSet<SbTag>) {
36+
let this = self.eval_context_mut();
37+
this.memory.alloc_map().iter(|it| {
38+
for (_id, (_kind, alloc)) in it {
39+
for (_size, prov) in alloc.relocations().iter() {
40+
if let Provenance::Concrete { sb, .. } = prov {
41+
tags.insert(*sb);
42+
}
43+
}
44+
}
45+
});
46+
}
47+
48+
fn find_pointers_in_locals(&mut self, tags: &mut FxHashSet<SbTag>) -> InterpResult<'tcx> {
49+
let this = self.eval_context_mut();
50+
for frame in this.machine.threads.all_stacks().flatten() {
51+
// Handle the return place of each frame
52+
if let Ok(return_place) = frame.return_place.try_as_mplace() {
53+
if let Some(Provenance::Concrete { sb, .. }) = return_place.ptr.provenance {
54+
tags.insert(sb);
55+
}
56+
}
57+
58+
for local in frame.locals.iter() {
59+
let LocalValue::Live(value) = local.value else {
60+
continue;
61+
};
62+
match value {
63+
Operand::Immediate(Immediate::Scalar(ScalarMaybeUninit::Scalar(
64+
Scalar::Ptr(ptr, _),
65+
))) =>
66+
if let Provenance::Concrete { sb, .. } = ptr.provenance {
67+
tags.insert(sb);
68+
},
69+
Operand::Immediate(Immediate::ScalarPair(s1, s2)) => {
70+
if let ScalarMaybeUninit::Scalar(Scalar::Ptr(ptr, _)) = s1 {
71+
if let Provenance::Concrete { sb, .. } = ptr.provenance {
72+
tags.insert(sb);
73+
}
74+
}
75+
if let ScalarMaybeUninit::Scalar(Scalar::Ptr(ptr, _)) = s2 {
76+
if let Provenance::Concrete { sb, .. } = ptr.provenance {
77+
tags.insert(sb);
78+
}
79+
}
80+
}
81+
Operand::Indirect(MemPlace { ptr, .. }) => {
82+
if let Some(Provenance::Concrete { sb, .. }) = ptr.provenance {
83+
tags.insert(sb);
84+
}
85+
}
86+
Operand::Immediate(Immediate::Uninit)
87+
| Operand::Immediate(Immediate::Scalar(ScalarMaybeUninit::Uninit))
88+
| Operand::Immediate(Immediate::Scalar(ScalarMaybeUninit::Scalar(
89+
Scalar::Int(_),
90+
))) => {}
91+
}
92+
}
93+
}
94+
95+
Ok(())
96+
}
97+
98+
fn remove_extraneous_tags(&mut self, tags: FxHashSet<SbTag>) {
99+
let this = self.eval_context_mut();
100+
this.memory.alloc_map().iter(|it| {
101+
for (id, (_kind, alloc)) in it {
102+
// Don't GC any allocation which has been exposed
103+
if this.machine.intptrcast.borrow().is_exposed(*id) {
104+
continue;
105+
}
106+
107+
alloc.extra.stacked_borrows.as_ref().unwrap().borrow_mut().collect(&tags);
108+
}
109+
});
110+
}
111+
}

0 commit comments

Comments
 (0)