Skip to content

Commit c016768

Browse files
committed
miri: add some chance to reuse addresses of previously freed allocations
1 parent 092bd53 commit c016768

File tree

6 files changed

+182
-39
lines changed

6 files changed

+182
-39
lines changed

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

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

1012
use rustc_data_structures::fx::{FxHashMap, FxHashSet};
1113
use rustc_span::Span;
12-
use rustc_target::abi::{HasDataLayout, Size};
14+
use rustc_target::abi::{Align, HasDataLayout, Size};
1315

1416
use crate::*;
17+
use reuse_pool::ReusePool;
1518

1619
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1720
pub enum ProvenanceMode {
@@ -26,7 +29,7 @@ pub enum ProvenanceMode {
2629

2730
pub type GlobalState = RefCell<GlobalStateInner>;
2831

29-
#[derive(Clone, Debug)]
32+
#[derive(Debug)]
3033
pub struct GlobalStateInner {
3134
/// This is used as a map between the address of each allocation and its `AllocId`. It is always
3235
/// sorted by address. We cannot use a `HashMap` since we can be given an address that is offset
@@ -38,6 +41,8 @@ pub struct GlobalStateInner {
3841
/// they do not have an `AllocExtra`.
3942
/// This is the inverse of `int_to_ptr_map`.
4043
base_addr: FxHashMap<AllocId, u64>,
44+
/// A pool of addresses we can reuse for future allocations.
45+
reuse: ReusePool,
4146
/// Whether an allocation has been exposed or not. This cannot be put
4247
/// into `AllocExtra` for the same reason as `base_addr`.
4348
exposed: FxHashSet<AllocId>,
@@ -53,6 +58,7 @@ impl VisitProvenance for GlobalStateInner {
5358
let GlobalStateInner {
5459
int_to_ptr_map: _,
5560
base_addr: _,
61+
reuse: _,
5662
exposed: _,
5763
next_base_addr: _,
5864
provenance_mode: _,
@@ -71,6 +77,7 @@ impl GlobalStateInner {
7177
GlobalStateInner {
7278
int_to_ptr_map: Vec::default(),
7379
base_addr: FxHashMap::default(),
80+
reuse: ReusePool::new(),
7481
exposed: FxHashSet::default(),
7582
next_base_addr: stack_addr,
7683
provenance_mode: config.provenance_mode,
@@ -142,6 +149,7 @@ trait EvalContextExtPriv<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
142149
Ok(match global_state.base_addr.entry(alloc_id) {
143150
Entry::Occupied(entry) => *entry.get(),
144151
Entry::Vacant(entry) => {
152+
let mut rng = ecx.machine.rng.borrow_mut();
145153
let (size, align, kind) = ecx.get_alloc_info(alloc_id);
146154
// This is either called immediately after allocation (and then cached), or when
147155
// adjusting `tcx` pointers (which never get freed). So assert that we are looking
@@ -150,44 +158,63 @@ trait EvalContextExtPriv<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
150158
// information was removed.
151159
assert!(!matches!(kind, AllocKind::Dead));
152160

153-
// This allocation does not have a base address yet, pick one.
154-
// Leave some space to the previous allocation, to give it some chance to be less aligned.
155-
let slack = {
156-
let mut rng = ecx.machine.rng.borrow_mut();
157-
// This means that `(global_state.next_base_addr + slack) % 16` is uniformly distributed.
158-
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
159191
};
160-
// From next_base_addr + slack, round up to adjust for alignment.
161-
let base_addr = global_state
162-
.next_base_addr
163-
.checked_add(slack)
164-
.ok_or_else(|| err_exhaust!(AddressSpaceFull))?;
165-
let base_addr = align_addr(base_addr, align.bytes());
166-
entry.insert(base_addr);
167192
trace!(
168-
"Assigning base address {:#x} to allocation {:?} (size: {}, align: {}, slack: {})",
193+
"Assigning base address {:#x} to allocation {:?} (size: {}, align: {})",
169194
base_addr,
170195
alloc_id,
171196
size.bytes(),
172197
align.bytes(),
173-
slack,
174198
);
175199

176-
// Remember next base address. If this allocation is zero-sized, leave a gap
177-
// of at least 1 to avoid two allocations having the same base address.
178-
// (The logic in `alloc_id_from_addr` assumes unique addresses, and different
179-
// function/vtable pointers need to be distinguishable!)
180-
global_state.next_base_addr = base_addr
181-
.checked_add(max(size.bytes(), 1))
182-
.ok_or_else(|| err_exhaust!(AddressSpaceFull))?;
183-
// Even if `Size` didn't overflow, we might still have filled up the address space.
184-
if global_state.next_base_addr > ecx.target_usize_max() {
185-
throw_exhaust!(AddressSpaceFull);
186-
}
187-
// Also maintain the opposite mapping in `int_to_ptr_map`.
188-
// Given that `next_base_addr` increases in each allocation, pushing the
189-
// corresponding tuple keeps `int_to_ptr_map` sorted
190-
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));
191218

192219
base_addr
193220
}
@@ -302,7 +329,13 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
302329
}
303330

304331
impl GlobalStateInner {
305-
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+
) {
306339
// We can *not* remove this from `base_addr`, since the interpreter design requires that we
307340
// be able to retrieve an AllocId + offset for any memory access *before* we check if the
308341
// access is valid. Specifically, `ptr_get_alloc` is called on each attempt at a memory
@@ -322,6 +355,8 @@ impl GlobalStateInner {
322355
// We can also remove it from `exposed`, since this allocation can anyway not be returned by
323356
// `alloc_id_from_addr` any more.
324357
self.exposed.remove(&dead_id);
358+
// Also remember this address for future reuse.
359+
self.reuse.add_addr(rng, addr, size, align)
325360
}
326361
}
327362

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/helpers.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ use std::iter;
33
use std::num::NonZero;
44
use std::time::Duration;
55

6+
use rand::RngCore;
7+
68
use rustc_apfloat::ieee::{Double, Single};
79
use rustc_apfloat::Float;
810
use rustc_hir::def::{DefKind, Namespace};
@@ -18,8 +20,6 @@ use rustc_span::{def_id::CrateNum, sym, Span, Symbol};
1820
use rustc_target::abi::{Align, FieldIdx, FieldsShape, Size, Variants};
1921
use rustc_target::spec::abi::Abi;
2022

21-
use rand::RngCore;
22-
2323
use crate::*;
2424

2525
/// Indicates which kind of access is being performed.

src/tools/miri/src/machine.rs

+7-2
Original file line numberDiff line numberDiff line change
@@ -1289,7 +1289,7 @@ impl<'mir, 'tcx> Machine<'mir, 'tcx> for MiriMachine<'mir, 'tcx> {
12891289
alloc_extra: &mut AllocExtra<'tcx>,
12901290
(alloc_id, prove_extra): (AllocId, Self::ProvenanceExtra),
12911291
size: Size,
1292-
_align: Align,
1292+
align: Align,
12931293
) -> InterpResult<'tcx> {
12941294
if machine.tracked_alloc_ids.contains(&alloc_id) {
12951295
machine.emit_diagnostic(NonHaltingDiagnostic::FreedAlloc(alloc_id));
@@ -1304,7 +1304,12 @@ impl<'mir, 'tcx> Machine<'mir, 'tcx> for MiriMachine<'mir, 'tcx> {
13041304
{
13051305
*deallocated_at = Some(machine.current_span());
13061306
}
1307-
machine.alloc_addresses.get_mut().free_alloc_id(alloc_id);
1307+
machine.alloc_addresses.get_mut().free_alloc_id(
1308+
machine.rng.get_mut(),
1309+
alloc_id,
1310+
size,
1311+
align,
1312+
);
13081313
Ok(())
13091314
}
13101315

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//! Check that we do sometimes reuse addresses.
2+
use std::collections::HashSet;
3+
4+
fn main() {
5+
let count = 100;
6+
let mut addrs = HashSet::<usize>::new();
7+
for _ in 0..count {
8+
// We make a `Box` with a layout that's hopefully not used by tons of things inside the
9+
// allocator itself, so that we are more likely to get reuse. (With `i32` or `usize`, on
10+
// Windows the reuse chances are very low.)
11+
let b = Box::new([42usize; 4]);
12+
addrs.insert(&*b as *const [usize; 4] as usize);
13+
}
14+
// dbg!(addrs.len());
15+
assert!(addrs.len() > 1 && addrs.len() < count);
16+
}

src/tools/miri/tests/pass/intptrcast.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ fn ptr_eq_dangling() {
6767
drop(b);
6868
let b = Box::new(0);
6969
let y = &*b as *const i32; // different allocation
70-
// They *could* be equal if memory was reused, but probably are not.
71-
assert!(x != y);
70+
// They *could* be equal if memory is reused...
71+
assert!(x != y || x == y);
7272
}
7373

7474
fn ptr_eq_out_of_bounds() {

0 commit comments

Comments
 (0)