Skip to content

Commit 21b3b5e

Browse files
committed
add separate allocator for MiriMachine
1 parent 6e01f6e commit 21b3b5e

File tree

6 files changed

+328
-3
lines changed

6 files changed

+328
-3
lines changed

Cargo.lock

Lines changed: 19 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ features = ['unprefixed_malloc_on_supported_platforms']
3939
libc = "0.2"
4040
libffi = "4.0.0"
4141
libloading = "0.8"
42+
nix = { version = "0.30.1", features = ["mman", "ptrace", "signal"] }
4243

4344
[target.'cfg(target_family = "windows")'.dependencies]
4445
windows-sys = { version = "0.59", features = [

src/alloc_bytes.rs

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ use std::{alloc, slice};
55
use rustc_abi::{Align, Size};
66
use rustc_middle::mir::interpret::AllocBytes;
77

8+
#[cfg(all(unix, any(target_arch = "x86", target_arch = "x86_64")))]
9+
use crate::discrete_alloc::MachineAlloc;
810
use crate::helpers::ToU64 as _;
911

1012
/// Allocation bytes that explicitly handle the layout of the data they're storing.
@@ -37,8 +39,14 @@ impl Drop for MiriAllocBytes {
3739
} else {
3840
self.layout
3941
};
42+
4043
// SAFETY: Invariant, `self.ptr` points to memory allocated with `self.layout`.
41-
unsafe { alloc::dealloc(self.ptr, alloc_layout) }
44+
unsafe {
45+
#[cfg(all(unix, any(target_arch = "x86", target_arch = "x86_64")))]
46+
MachineAlloc::dealloc(self.ptr, alloc_layout);
47+
#[cfg(not(all(unix, any(target_arch = "x86", target_arch = "x86_64"))))]
48+
alloc::dealloc(self.ptr, alloc_layout);
49+
}
4250
}
4351
}
4452

@@ -91,7 +99,16 @@ impl AllocBytes for MiriAllocBytes {
9199
let size = slice.len();
92100
let align = align.bytes();
93101
// SAFETY: `alloc_fn` will only be used with `size != 0`.
94-
let alloc_fn = |layout| unsafe { alloc::alloc(layout) };
102+
let alloc_fn = |layout| unsafe {
103+
#[cfg(all(unix, any(target_arch = "x86", target_arch = "x86_64")))]
104+
{
105+
MachineAlloc::alloc(layout)
106+
}
107+
#[cfg(not(all(unix, any(target_arch = "x86", target_arch = "x86_64"))))]
108+
{
109+
alloc::alloc(layout)
110+
}
111+
};
95112
let alloc_bytes = MiriAllocBytes::alloc_with(size.to_u64(), align, alloc_fn)
96113
.unwrap_or_else(|()| {
97114
panic!("Miri ran out of memory: cannot create allocation of {size} bytes")
@@ -106,7 +123,16 @@ impl AllocBytes for MiriAllocBytes {
106123
let size = size.bytes();
107124
let align = align.bytes();
108125
// SAFETY: `alloc_fn` will only be used with `size != 0`.
109-
let alloc_fn = |layout| unsafe { alloc::alloc_zeroed(layout) };
126+
let alloc_fn = |layout| unsafe {
127+
#[cfg(all(unix, any(target_arch = "x86", target_arch = "x86_64")))]
128+
{
129+
MachineAlloc::alloc_zeroed(layout)
130+
}
131+
#[cfg(not(all(unix, any(target_arch = "x86", target_arch = "x86_64"))))]
132+
{
133+
alloc::alloc_zeroed(layout)
134+
}
135+
};
110136
MiriAllocBytes::alloc_with(size, align, alloc_fn).ok()
111137
}
112138

src/discrete_alloc.rs

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
use std::alloc::{self, Layout};
2+
use std::sync;
3+
4+
use nix::sys::mman::ProtFlags;
5+
6+
use crate::helpers::ToU64;
7+
8+
static ALLOCATOR: sync::Mutex<MachineAlloc> = sync::Mutex::new(MachineAlloc::empty());
9+
10+
/// A distinct allocator for the `MiriMachine`, allowing us to manage its
11+
/// memory separately from that of Miri itself.
12+
#[derive(Debug)]
13+
pub struct MachineAlloc {
14+
pages: Vec<*mut u8>,
15+
huge_allocs: Vec<(*mut u8, usize)>,
16+
allocated: Vec<Box<[u8]>>,
17+
page_size: usize,
18+
enabled: bool,
19+
ffi_mode: bool,
20+
}
21+
22+
// SAFETY: We only point to heap-allocated data
23+
unsafe impl Send for MachineAlloc {}
24+
25+
impl MachineAlloc {
26+
// Allocation-related methods
27+
28+
/// Initializes the allocator with placeholder 4k pages.
29+
const fn empty() -> Self {
30+
Self {
31+
pages: Vec::new(),
32+
huge_allocs: Vec::new(),
33+
allocated: Vec::new(),
34+
page_size: 4096,
35+
enabled: false,
36+
ffi_mode: false,
37+
}
38+
}
39+
40+
/// SAFETY: There must be no existing `MiriAllocBytes`
41+
pub unsafe fn enable() {
42+
let mut alloc = ALLOCATOR.lock().unwrap();
43+
alloc.enabled = true;
44+
// This needs to specifically be the system pagesize!
45+
alloc.page_size = unsafe {
46+
let ret = libc::sysconf(libc::_SC_PAGE_SIZE);
47+
if ret > 0 {
48+
ret.try_into().unwrap()
49+
} else {
50+
4096 // fallback
51+
}
52+
}
53+
}
54+
55+
/// Returns a vector of page addresses managed by the allocator.
56+
#[expect(dead_code)]
57+
pub fn pages() -> Vec<u64> {
58+
let alloc = ALLOCATOR.lock().unwrap();
59+
alloc.pages.clone().into_iter().map(|p| p.addr().to_u64()).collect()
60+
}
61+
62+
fn add_page(&mut self) {
63+
let page_layout =
64+
unsafe { Layout::from_size_align_unchecked(self.page_size, self.page_size) };
65+
let page_ptr = unsafe { alloc::alloc(page_layout) };
66+
if page_ptr.is_null() {
67+
panic!("aligned_alloc failed!!!")
68+
}
69+
self.allocated.push(vec![0u8; self.page_size / 8].into_boxed_slice());
70+
self.pages.push(page_ptr);
71+
}
72+
73+
#[inline]
74+
fn normalized_layout(layout: Layout) -> (usize, usize) {
75+
let align = if layout.align() < 8 { 8 } else { layout.align() };
76+
let size = layout.size().next_multiple_of(8);
77+
(size, align)
78+
}
79+
80+
#[inline]
81+
fn huge_normalized_layout(&self, layout: Layout) -> (usize, usize) {
82+
let size = layout.size().next_multiple_of(self.page_size);
83+
let align = std::cmp::max(layout.align(), self.page_size);
84+
(size, align)
85+
}
86+
87+
/// SAFETY: See alloc::alloc()
88+
#[inline]
89+
pub unsafe fn alloc(layout: Layout) -> *mut u8 {
90+
let mut alloc = ALLOCATOR.lock().unwrap();
91+
unsafe { if alloc.enabled { alloc.alloc_inner(layout) } else { alloc::alloc(layout) } }
92+
}
93+
94+
/// SAFETY: See alloc::alloc_zeroed()
95+
pub unsafe fn alloc_zeroed(layout: Layout) -> *mut u8 {
96+
let mut alloc = ALLOCATOR.lock().unwrap();
97+
if alloc.enabled {
98+
let ptr = unsafe { alloc.alloc_inner(layout) };
99+
if !ptr.is_null() {
100+
unsafe {
101+
ptr.write_bytes(0, layout.size());
102+
}
103+
}
104+
ptr
105+
} else {
106+
unsafe { alloc::alloc_zeroed(layout) }
107+
}
108+
}
109+
110+
/// SAFETY: See alloc::alloc()
111+
unsafe fn alloc_inner(&mut self, layout: Layout) -> *mut u8 {
112+
let (size, align) = MachineAlloc::normalized_layout(layout);
113+
114+
if align > self.page_size || size > self.page_size {
115+
unsafe { self.alloc_multi_page(layout) }
116+
} else {
117+
for (page, pinfo) in std::iter::zip(&mut self.pages, &mut self.allocated) {
118+
for idx in (0..self.page_size).step_by(align) {
119+
if pinfo.len() < idx / 8 + size / 8 {
120+
break;
121+
}
122+
if pinfo[idx / 8..idx / 8 + size / 8].iter().all(|v| *v == 0) {
123+
pinfo[idx / 8..idx / 8 + size / 8].fill(255);
124+
unsafe {
125+
let ret = page.offset(idx.try_into().unwrap());
126+
if ret.addr() >= page.addr() + self.page_size {
127+
panic!("Returing {} from page {}", ret.addr(), page.addr());
128+
}
129+
return page.offset(idx.try_into().unwrap());
130+
}
131+
}
132+
}
133+
}
134+
135+
// We get here only if there's no space in our existing pages
136+
self.add_page();
137+
unsafe { self.alloc_inner(layout) }
138+
}
139+
}
140+
141+
/// SAFETY: See alloc::alloc()
142+
unsafe fn alloc_multi_page(&mut self, layout: Layout) -> *mut u8 {
143+
let (size, align) = self.huge_normalized_layout(layout);
144+
145+
let layout = unsafe { Layout::from_size_align_unchecked(size, align) };
146+
let ret = unsafe { alloc::alloc(layout) };
147+
self.huge_allocs.push((ret, size));
148+
ret
149+
}
150+
151+
/// Safety: see alloc::dealloc()
152+
pub unsafe fn dealloc(ptr: *mut u8, layout: Layout) {
153+
let mut alloc = ALLOCATOR.lock().unwrap();
154+
unsafe {
155+
if alloc.enabled {
156+
alloc.dealloc_inner(ptr, layout);
157+
} else {
158+
alloc::dealloc(ptr, layout);
159+
}
160+
}
161+
}
162+
163+
/// SAFETY: See alloc::dealloc()
164+
unsafe fn dealloc_inner(&mut self, ptr: *mut u8, layout: Layout) {
165+
let (size, align) = MachineAlloc::normalized_layout(layout);
166+
167+
if size == 0 || ptr.is_null() {
168+
return;
169+
}
170+
171+
let ptr_idx = ptr.addr() % self.page_size;
172+
let page_addr = ptr.addr() - ptr_idx;
173+
174+
if align > self.page_size || size > self.page_size {
175+
unsafe {
176+
self.dealloc_multi_page(ptr, layout);
177+
}
178+
} else {
179+
let pinfo = std::iter::zip(&mut self.pages, &mut self.allocated)
180+
.find(|(page, _)| page.addr() == page_addr);
181+
let Some((_, pinfo)) = pinfo else {
182+
panic!("Freeing in an unallocated page: {ptr:?}\nHolding pages {:?}", self.pages)
183+
};
184+
185+
// Everything is always aligned to at least 8 bytes so this is ok
186+
pinfo[ptr_idx / 8..ptr_idx / 8 + size / 8].fill(0);
187+
}
188+
189+
let mut free = vec![];
190+
let page_layout =
191+
unsafe { Layout::from_size_align_unchecked(self.page_size, self.page_size) };
192+
for (idx, pinfo) in self.allocated.iter().enumerate() {
193+
if pinfo.iter().all(|p| *p == 0) {
194+
free.push(idx);
195+
}
196+
}
197+
free.reverse();
198+
for idx in free {
199+
let _ = self.allocated.remove(idx);
200+
unsafe {
201+
alloc::dealloc(self.pages.remove(idx), page_layout);
202+
}
203+
}
204+
}
205+
206+
/// SAFETY: See alloc::dealloc()
207+
unsafe fn dealloc_multi_page(&mut self, ptr: *mut u8, layout: Layout) {
208+
let (idx, _) = self
209+
.huge_allocs
210+
.iter()
211+
.enumerate()
212+
.find(|pg| ptr.addr() == pg.1.0.addr())
213+
.expect("Freeing unallocated pages");
214+
let ptr = self.huge_allocs.remove(idx).0;
215+
let (size, align) = self.huge_normalized_layout(layout);
216+
unsafe {
217+
let layout = Layout::from_size_align_unchecked(size, align);
218+
alloc::dealloc(ptr, layout);
219+
}
220+
}
221+
222+
// Protection-related methods
223+
224+
/// Protects all owned memory, preventing accesses.
225+
///
226+
/// SAFETY: Accessing memory after this point will result in a segfault
227+
/// unless it is first unprotected.
228+
#[expect(dead_code)]
229+
pub unsafe fn prepare_ffi() -> Result<(), nix::errno::Errno> {
230+
let mut alloc = ALLOCATOR.lock().unwrap();
231+
unsafe {
232+
alloc.mprotect(ProtFlags::PROT_NONE)?;
233+
}
234+
alloc.ffi_mode = true;
235+
Ok(())
236+
}
237+
238+
/// Deprotects all owned memory by setting it to RW. Erroring here is very
239+
/// likely unrecoverable, so it may panic if applying those permissions
240+
/// fails.
241+
#[expect(dead_code)]
242+
pub fn unprep_ffi() {
243+
let mut alloc = ALLOCATOR.lock().unwrap();
244+
let default_flags = ProtFlags::PROT_READ | ProtFlags::PROT_WRITE;
245+
unsafe {
246+
alloc.mprotect(default_flags).unwrap();
247+
}
248+
alloc.ffi_mode = false;
249+
}
250+
251+
/// Applies `prot` to every page managed by the allocator.
252+
///
253+
/// SAFETY: Accessing memory in violation of the protection flags will
254+
/// trigger a segfault.
255+
unsafe fn mprotect(&mut self, prot: ProtFlags) -> Result<(), nix::errno::Errno> {
256+
for &pg in &self.pages {
257+
unsafe {
258+
// We already know only non-null ptrs are pushed to self.pages
259+
let addr: std::ptr::NonNull<std::ffi::c_void> =
260+
std::ptr::NonNull::new_unchecked(pg.cast());
261+
nix::sys::mman::mprotect(addr, self.page_size, prot)?;
262+
}
263+
}
264+
for &(hpg, size) in &self.huge_allocs {
265+
unsafe {
266+
let addr = std::ptr::NonNull::new_unchecked(hpg.cast());
267+
nix::sys::mman::mprotect(addr, size, prot)?;
268+
}
269+
}
270+
Ok(())
271+
}
272+
}
273+
274+
// TODO: Add tests for... all of this?

0 commit comments

Comments
 (0)