Skip to content

Commit 9ce37dc

Browse files
committed
Auto merge of #122240 - RalfJung:miri-addr-reuse, r=oli-obk
miri: add some chance to reuse addresses of previously freed allocations The hope is that this can help us find ABA issues. Unfortunately this needs rustc changes so I can't easily run the regular benchmark suite. I used `src/tools/miri/tests/pass/float_nan.rs` as a substitute: ``` Before: Benchmark 1: ./x.py run miri --stage 0 --args src/tools/miri/tests/pass/float_nan.rs --args --edition=2021 Time (mean ± σ): 9.570 s ± 0.013 s [User: 9.279 s, System: 0.290 s] Range (min … max): 9.561 s … 9.579 s 2 runs After: Benchmark 1: ./x.py run miri --stage 0 --args src/tools/miri/tests/pass/float_nan.rs --args --edition=2021 Time (mean ± σ): 9.698 s ± 0.046 s [User: 9.413 s, System: 0.279 s] Range (min … max): 9.666 s … 9.731 s 2 runs ``` That's a ~1.3% slowdown, which seems fine to me. I have seen a lot of noise in this style of benchmarking so I don't quite trust this anyway; we can make further experiments in the Miri repo after this migrated there. r? `@oli-obk`
2 parents 762d317 + c016768 commit 9ce37dc

File tree

14 files changed

+214
-65
lines changed

14 files changed

+214
-65
lines changed

compiler/rustc_const_eval/src/interpret/machine.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -443,7 +443,8 @@ pub trait Machine<'mir, 'tcx: 'mir>: Sized {
443443
_machine: &mut Self,
444444
_alloc_extra: &mut Self::AllocExtra,
445445
_prov: (AllocId, Self::ProvenanceExtra),
446-
_range: AllocRange,
446+
_size: Size,
447+
_align: Align,
447448
) -> InterpResult<'tcx> {
448449
Ok(())
449450
}

compiler/rustc_const_eval/src/interpret/memory.rs

+2-1
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,8 @@ impl<'mir, 'tcx: 'mir, M: Machine<'mir, 'tcx>> InterpCx<'mir, 'tcx, M> {
353353
&mut self.machine,
354354
&mut alloc.extra,
355355
(alloc_id, prov),
356-
alloc_range(Size::ZERO, size),
356+
size,
357+
alloc.align,
357358
)?;
358359

359360
// Don't forget to remember size and align of this now-dead allocation

src/tools/miri/src/intptrcast.rs src/tools/miri/src/alloc_addresses/mod.rs

+76-38
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
//! This module is responsible for managing the absolute addresses that allocations are located at,
2+
//! and for casting between pointers and integers based on those addresses.
3+
4+
mod reuse_pool;
5+
16
use std::cell::RefCell;
27
use std::cmp::max;
38
use std::collections::hash_map::Entry;
@@ -6,9 +11,10 @@ use rand::Rng;
611

712
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
813
use rustc_span::Span;
9-
use rustc_target::abi::{HasDataLayout, Size};
14+
use rustc_target::abi::{Align, HasDataLayout, Size};
1015

1116
use crate::*;
17+
use reuse_pool::ReusePool;
1218

1319
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1420
pub enum ProvenanceMode {
@@ -23,7 +29,7 @@ pub enum ProvenanceMode {
2329

2430
pub type GlobalState = RefCell<GlobalStateInner>;
2531

26-
#[derive(Clone, Debug)]
32+
#[derive(Debug)]
2733
pub struct GlobalStateInner {
2834
/// This is used as a map between the address of each allocation and its `AllocId`. It is always
2935
/// sorted by address. We cannot use a `HashMap` since we can be given an address that is offset
@@ -35,6 +41,8 @@ pub struct GlobalStateInner {
3541
/// they do not have an `AllocExtra`.
3642
/// This is the inverse of `int_to_ptr_map`.
3743
base_addr: FxHashMap<AllocId, u64>,
44+
/// A pool of addresses we can reuse for future allocations.
45+
reuse: ReusePool,
3846
/// Whether an allocation has been exposed or not. This cannot be put
3947
/// into `AllocExtra` for the same reason as `base_addr`.
4048
exposed: FxHashSet<AllocId>,
@@ -50,6 +58,7 @@ impl VisitProvenance for GlobalStateInner {
5058
let GlobalStateInner {
5159
int_to_ptr_map: _,
5260
base_addr: _,
61+
reuse: _,
5362
exposed: _,
5463
next_base_addr: _,
5564
provenance_mode: _,
@@ -68,6 +77,7 @@ impl GlobalStateInner {
6877
GlobalStateInner {
6978
int_to_ptr_map: Vec::default(),
7079
base_addr: FxHashMap::default(),
80+
reuse: ReusePool::new(),
7181
exposed: FxHashSet::default(),
7282
next_base_addr: stack_addr,
7383
provenance_mode: config.provenance_mode,
@@ -96,7 +106,7 @@ trait EvalContextExtPriv<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
96106
// or `None` if the addr is out of bounds
97107
fn alloc_id_from_addr(&self, addr: u64) -> Option<AllocId> {
98108
let ecx = self.eval_context_ref();
99-
let global_state = ecx.machine.intptrcast.borrow();
109+
let global_state = ecx.machine.alloc_addresses.borrow();
100110
assert!(global_state.provenance_mode != ProvenanceMode::Strict);
101111

102112
let pos = global_state.int_to_ptr_map.binary_search_by_key(&addr, |(addr, _)| *addr);
@@ -133,12 +143,13 @@ trait EvalContextExtPriv<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
133143

134144
fn addr_from_alloc_id(&self, alloc_id: AllocId) -> InterpResult<'tcx, u64> {
135145
let ecx = self.eval_context_ref();
136-
let mut global_state = ecx.machine.intptrcast.borrow_mut();
146+
let mut global_state = ecx.machine.alloc_addresses.borrow_mut();
137147
let global_state = &mut *global_state;
138148

139149
Ok(match global_state.base_addr.entry(alloc_id) {
140150
Entry::Occupied(entry) => *entry.get(),
141151
Entry::Vacant(entry) => {
152+
let mut rng = ecx.machine.rng.borrow_mut();
142153
let (size, align, kind) = ecx.get_alloc_info(alloc_id);
143154
// This is either called immediately after allocation (and then cached), or when
144155
// adjusting `tcx` pointers (which never get freed). So assert that we are looking
@@ -147,44 +158,63 @@ trait EvalContextExtPriv<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
147158
// information was removed.
148159
assert!(!matches!(kind, AllocKind::Dead));
149160

150-
// This allocation does not have a base address yet, pick one.
151-
// Leave some space to the previous allocation, to give it some chance to be less aligned.
152-
let slack = {
153-
let mut rng = ecx.machine.rng.borrow_mut();
154-
// This means that `(global_state.next_base_addr + slack) % 16` is uniformly distributed.
155-
rng.gen_range(0..16)
161+
// This allocation does not have a base address yet, pick or reuse one.
162+
let base_addr = if let Some(reuse_addr) =
163+
global_state.reuse.take_addr(&mut *rng, size, align)
164+
{
165+
reuse_addr
166+
} else {
167+
// We have to pick a fresh address.
168+
// Leave some space to the previous allocation, to give it some chance to be less aligned.
169+
// We ensure that `(global_state.next_base_addr + slack) % 16` is uniformly distributed.
170+
let slack = rng.gen_range(0..16);
171+
// From next_base_addr + slack, round up to adjust for alignment.
172+
let base_addr = global_state
173+
.next_base_addr
174+
.checked_add(slack)
175+
.ok_or_else(|| err_exhaust!(AddressSpaceFull))?;
176+
let base_addr = align_addr(base_addr, align.bytes());
177+
178+
// Remember next base address. If this allocation is zero-sized, leave a gap
179+
// of at least 1 to avoid two allocations having the same base address.
180+
// (The logic in `alloc_id_from_addr` assumes unique addresses, and different
181+
// function/vtable pointers need to be distinguishable!)
182+
global_state.next_base_addr = base_addr
183+
.checked_add(max(size.bytes(), 1))
184+
.ok_or_else(|| err_exhaust!(AddressSpaceFull))?;
185+
// Even if `Size` didn't overflow, we might still have filled up the address space.
186+
if global_state.next_base_addr > ecx.target_usize_max() {
187+
throw_exhaust!(AddressSpaceFull);
188+
}
189+
190+
base_addr
156191
};
157-
// From next_base_addr + slack, round up to adjust for alignment.
158-
let base_addr = global_state
159-
.next_base_addr
160-
.checked_add(slack)
161-
.ok_or_else(|| err_exhaust!(AddressSpaceFull))?;
162-
let base_addr = align_addr(base_addr, align.bytes());
163-
entry.insert(base_addr);
164192
trace!(
165-
"Assigning base address {:#x} to allocation {:?} (size: {}, align: {}, slack: {})",
193+
"Assigning base address {:#x} to allocation {:?} (size: {}, align: {})",
166194
base_addr,
167195
alloc_id,
168196
size.bytes(),
169197
align.bytes(),
170-
slack,
171198
);
172199

173-
// Remember next base address. If this allocation is zero-sized, leave a gap
174-
// of at least 1 to avoid two allocations having the same base address.
175-
// (The logic in `alloc_id_from_addr` assumes unique addresses, and different
176-
// function/vtable pointers need to be distinguishable!)
177-
global_state.next_base_addr = base_addr
178-
.checked_add(max(size.bytes(), 1))
179-
.ok_or_else(|| err_exhaust!(AddressSpaceFull))?;
180-
// Even if `Size` didn't overflow, we might still have filled up the address space.
181-
if global_state.next_base_addr > ecx.target_usize_max() {
182-
throw_exhaust!(AddressSpaceFull);
183-
}
184-
// Also maintain the opposite mapping in `int_to_ptr_map`.
185-
// Given that `next_base_addr` increases in each allocation, pushing the
186-
// corresponding tuple keeps `int_to_ptr_map` sorted
187-
global_state.int_to_ptr_map.push((base_addr, alloc_id));
200+
// Store address in cache.
201+
entry.insert(base_addr);
202+
203+
// Also maintain the opposite mapping in `int_to_ptr_map`, ensuring we keep it sorted.
204+
// We have a fast-path for the common case that this address is bigger than all previous ones.
205+
let pos = if global_state
206+
.int_to_ptr_map
207+
.last()
208+
.is_some_and(|(last_addr, _)| *last_addr < base_addr)
209+
{
210+
global_state.int_to_ptr_map.len()
211+
} else {
212+
global_state
213+
.int_to_ptr_map
214+
.binary_search_by_key(&base_addr, |(addr, _)| *addr)
215+
.unwrap_err()
216+
};
217+
global_state.int_to_ptr_map.insert(pos, (base_addr, alloc_id));
188218

189219
base_addr
190220
}
@@ -196,7 +226,7 @@ impl<'mir, 'tcx: 'mir> EvalContextExt<'mir, 'tcx> for crate::MiriInterpCx<'mir,
196226
pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
197227
fn expose_ptr(&mut self, alloc_id: AllocId, tag: BorTag) -> InterpResult<'tcx> {
198228
let ecx = self.eval_context_mut();
199-
let global_state = ecx.machine.intptrcast.get_mut();
229+
let global_state = ecx.machine.alloc_addresses.get_mut();
200230
// In strict mode, we don't need this, so we can save some cycles by not tracking it.
201231
if global_state.provenance_mode == ProvenanceMode::Strict {
202232
return Ok(());
@@ -207,7 +237,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
207237
return Ok(());
208238
}
209239
trace!("Exposing allocation id {alloc_id:?}");
210-
let global_state = ecx.machine.intptrcast.get_mut();
240+
let global_state = ecx.machine.alloc_addresses.get_mut();
211241
global_state.exposed.insert(alloc_id);
212242
if ecx.machine.borrow_tracker.is_some() {
213243
ecx.expose_tag(alloc_id, tag)?;
@@ -219,7 +249,7 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
219249
trace!("Casting {:#x} to a pointer", addr);
220250

221251
let ecx = self.eval_context_ref();
222-
let global_state = ecx.machine.intptrcast.borrow();
252+
let global_state = ecx.machine.alloc_addresses.borrow();
223253

224254
// Potentially emit a warning.
225255
match global_state.provenance_mode {
@@ -299,7 +329,13 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
299329
}
300330

301331
impl GlobalStateInner {
302-
pub fn free_alloc_id(&mut self, dead_id: AllocId) {
332+
pub fn free_alloc_id(
333+
&mut self,
334+
rng: &mut impl Rng,
335+
dead_id: AllocId,
336+
size: Size,
337+
align: Align,
338+
) {
303339
// We can *not* remove this from `base_addr`, since the interpreter design requires that we
304340
// be able to retrieve an AllocId + offset for any memory access *before* we check if the
305341
// access is valid. Specifically, `ptr_get_alloc` is called on each attempt at a memory
@@ -319,6 +355,8 @@ impl GlobalStateInner {
319355
// We can also remove it from `exposed`, since this allocation can anyway not be returned by
320356
// `alloc_id_from_addr` any more.
321357
self.exposed.remove(&dead_id);
358+
// Also remember this address for future reuse.
359+
self.reuse.add_addr(rng, addr, size, align)
322360
}
323361
}
324362

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
//! Manages a pool of addresses that can be reused.
2+
3+
use rand::Rng;
4+
5+
use rustc_target::abi::{Align, Size};
6+
7+
const MAX_POOL_SIZE: usize = 64;
8+
9+
// Just use fair coins, until we have evidence that other numbers are better.
10+
const ADDR_REMEMBER_CHANCE: f64 = 0.5;
11+
const ADDR_TAKE_CHANCE: f64 = 0.5;
12+
13+
/// The pool strikes a balance between exploring more possible executions and making it more likely
14+
/// to find bugs. The hypothesis is that bugs are more likely to occur when reuse happens for
15+
/// allocations with the same layout, since that can trigger e.g. ABA issues in a concurrent data
16+
/// structure. Therefore we only reuse allocations when size and alignment match exactly.
17+
#[derive(Debug)]
18+
pub struct ReusePool {
19+
/// The i-th element in `pool` stores allocations of alignment `2^i`. We store these reusable
20+
/// allocations as address-size pairs, the list must be sorted by the size.
21+
///
22+
/// Each of these maps has at most MAX_POOL_SIZE elements, and since alignment is limited to
23+
/// less than 64 different possible value, that bounds the overall size of the pool.
24+
pool: Vec<Vec<(u64, Size)>>,
25+
}
26+
27+
impl ReusePool {
28+
pub fn new() -> Self {
29+
ReusePool { pool: vec![] }
30+
}
31+
32+
fn subpool(&mut self, align: Align) -> &mut Vec<(u64, Size)> {
33+
let pool_idx: usize = align.bytes().trailing_zeros().try_into().unwrap();
34+
if self.pool.len() <= pool_idx {
35+
self.pool.resize(pool_idx + 1, Vec::new());
36+
}
37+
&mut self.pool[pool_idx]
38+
}
39+
40+
pub fn add_addr(&mut self, rng: &mut impl Rng, addr: u64, size: Size, align: Align) {
41+
// Let's see if we even want to remember this address.
42+
if !rng.gen_bool(ADDR_REMEMBER_CHANCE) {
43+
return;
44+
}
45+
// Determine the pool to add this to, and where in the pool to put it.
46+
let subpool = self.subpool(align);
47+
let pos = subpool.partition_point(|(_addr, other_size)| *other_size < size);
48+
// Make sure the pool does not grow too big.
49+
if subpool.len() >= MAX_POOL_SIZE {
50+
// Pool full. Replace existing element, or last one if this would be even bigger.
51+
let clamped_pos = pos.min(subpool.len() - 1);
52+
subpool[clamped_pos] = (addr, size);
53+
return;
54+
}
55+
// Add address to pool, at the right position.
56+
subpool.insert(pos, (addr, size));
57+
}
58+
59+
pub fn take_addr(&mut self, rng: &mut impl Rng, size: Size, align: Align) -> Option<u64> {
60+
// Determine whether we'll even attempt a reuse.
61+
if !rng.gen_bool(ADDR_TAKE_CHANCE) {
62+
return None;
63+
}
64+
// Determine the pool to take this from.
65+
let subpool = self.subpool(align);
66+
// Let's see if we can find something of the right size. We want to find the full range of
67+
// such items, beginning with the first, so we can't use `binary_search_by_key`.
68+
let begin = subpool.partition_point(|(_addr, other_size)| *other_size < size);
69+
let mut end = begin;
70+
while let Some((_addr, other_size)) = subpool.get(end) {
71+
if *other_size != size {
72+
break;
73+
}
74+
end += 1;
75+
}
76+
if end == begin {
77+
// Could not find any item of the right size.
78+
return None;
79+
}
80+
// Pick a random element with the desired size.
81+
let idx = rng.gen_range(begin..end);
82+
// Remove it from the pool and return.
83+
let (chosen_addr, chosen_size) = subpool.remove(idx);
84+
debug_assert!(chosen_size >= size && chosen_addr % align.bytes() == 0);
85+
Some(chosen_addr)
86+
}
87+
}

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -485,14 +485,14 @@ impl AllocState {
485485
&mut self,
486486
alloc_id: AllocId,
487487
prov_extra: ProvenanceExtra,
488-
range: AllocRange,
488+
size: Size,
489489
machine: &MiriMachine<'_, 'tcx>,
490490
) -> InterpResult<'tcx> {
491491
match self {
492492
AllocState::StackedBorrows(sb) =>
493-
sb.get_mut().before_memory_deallocation(alloc_id, prov_extra, range, machine),
493+
sb.get_mut().before_memory_deallocation(alloc_id, prov_extra, size, machine),
494494
AllocState::TreeBorrows(tb) =>
495-
tb.get_mut().before_memory_deallocation(alloc_id, prov_extra, range, machine),
495+
tb.get_mut().before_memory_deallocation(alloc_id, prov_extra, size, machine),
496496
}
497497
}
498498

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

+3-3
Original file line numberDiff line numberDiff line change
@@ -574,13 +574,13 @@ impl Stacks {
574574
&mut self,
575575
alloc_id: AllocId,
576576
tag: ProvenanceExtra,
577-
range: AllocRange,
577+
size: Size,
578578
machine: &MiriMachine<'_, 'tcx>,
579579
) -> InterpResult<'tcx> {
580-
trace!("deallocation with tag {:?}: {:?}, size {}", tag, alloc_id, range.size.bytes());
580+
trace!("deallocation with tag {:?}: {:?}, size {}", tag, alloc_id, size.bytes());
581581
let dcx = DiagnosticCxBuilder::dealloc(machine, tag);
582582
let state = machine.borrow_tracker.as_ref().unwrap().borrow();
583-
self.for_each(range, dcx, |stack, dcx, exposed_tags| {
583+
self.for_each(alloc_range(Size::ZERO, size), dcx, |stack, dcx, exposed_tags| {
584584
stack.dealloc(tag, &state, dcx, exposed_tags)
585585
})?;
586586
Ok(())

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ impl<'tcx> Tree {
8080
&mut self,
8181
alloc_id: AllocId,
8282
prov: ProvenanceExtra,
83-
range: AllocRange,
83+
size: Size,
8484
machine: &MiriMachine<'_, 'tcx>,
8585
) -> InterpResult<'tcx> {
8686
// TODO: for now we bail out on wildcard pointers. Eventually we should
@@ -91,7 +91,7 @@ impl<'tcx> Tree {
9191
};
9292
let global = machine.borrow_tracker.as_ref().unwrap();
9393
let span = machine.current_span();
94-
self.dealloc(tag, range, global, alloc_id, span)
94+
self.dealloc(tag, alloc_range(Size::ZERO, size), global, alloc_id, span)
9595
}
9696

9797
pub fn expose_tag(&mut self, _tag: BorTag) {

src/tools/miri/src/concurrency/data_race.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -1071,10 +1071,10 @@ impl VClockAlloc {
10711071
pub fn deallocate<'tcx>(
10721072
&mut self,
10731073
alloc_id: AllocId,
1074-
range: AllocRange,
1074+
size: Size,
10751075
machine: &mut MiriMachine<'_, '_>,
10761076
) -> InterpResult<'tcx> {
1077-
self.unique_access(alloc_id, range, NaWriteType::Deallocate, machine)
1077+
self.unique_access(alloc_id, alloc_range(Size::ZERO, size), NaWriteType::Deallocate, machine)
10781078
}
10791079
}
10801080

0 commit comments

Comments
 (0)