Skip to content

Commit 0e5835c

Browse files
committed
mmap/munmap/mremamp shims
1 parent 2ece95a commit 0e5835c

File tree

9 files changed

+324
-12
lines changed

9 files changed

+324
-12
lines changed

src/concurrency/data_race.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -697,7 +697,8 @@ impl VClockAlloc {
697697
MiriMemoryKind::Rust
698698
| MiriMemoryKind::Miri
699699
| MiriMemoryKind::C
700-
| MiriMemoryKind::WinHeap,
700+
| MiriMemoryKind::WinHeap
701+
| MiriMemoryKind::Mmap,
701702
)
702703
| MemoryKind::Stack => {
703704
let (alloc_index, clocks) = global.current_thread_state(thread_mgr);

src/machine.rs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ pub enum MiriMemoryKind {
104104
/// Memory for thread-local statics.
105105
/// This memory may leak.
106106
Tls,
107+
/// Memory mapped directly by the program
108+
Mmap,
107109
}
108110

109111
impl From<MiriMemoryKind> for MemoryKind<MiriMemoryKind> {
@@ -119,7 +121,7 @@ impl MayLeak for MiriMemoryKind {
119121
use self::MiriMemoryKind::*;
120122
match self {
121123
Rust | Miri | C | WinHeap | Runtime => false,
122-
Machine | Global | ExternStatic | Tls => true,
124+
Machine | Global | ExternStatic | Tls | Mmap => true,
123125
}
124126
}
125127
}
@@ -137,6 +139,7 @@ impl fmt::Display for MiriMemoryKind {
137139
Global => write!(f, "global (static or const)"),
138140
ExternStatic => write!(f, "extern static"),
139141
Tls => write!(f, "thread-local static"),
142+
Mmap => write!(f, "mmap"),
140143
}
141144
}
142145
}
@@ -674,6 +677,15 @@ impl<'mir, 'tcx> MiriMachine<'mir, 'tcx> {
674677
let def_id = frame.instance.def_id();
675678
def_id.is_local() || self.local_crates.contains(&def_id.krate)
676679
}
680+
681+
pub(crate) fn round_up_to_multiple_of_page_size(&self, length: u64) -> Option<u64> {
682+
#[allow(clippy::integer_arithmetic)] // page size is nonzero
683+
(length.checked_add(self.page_size - 1)? / self.page_size).checked_mul(self.page_size)
684+
}
685+
686+
pub(crate) fn page_align(&self) -> Align {
687+
Align::from_bytes(self.page_size).unwrap()
688+
}
677689
}
678690

679691
impl VisitTags for MiriMachine<'_, '_> {

src/shims/unix/foreign_items.rs

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use rustc_target::spec::abi::Abi;
1010
use crate::*;
1111
use shims::foreign_items::EmulateByNameResult;
1212
use shims::unix::fs::EvalContextExt as _;
13+
use shims::unix::mem::EvalContextExt as _;
1314
use shims::unix::sync::EvalContextExt as _;
1415
use shims::unix::thread::EvalContextExt as _;
1516

@@ -213,6 +214,22 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
213214
}
214215
}
215216

217+
"mmap" => {
218+
let [addr, length, prot, flags, fd, offset] = this.check_shim(abi, Abi::C {unwind: false}, link_name, args)?;
219+
let ptr = this.mmap(addr, length, prot, flags, fd, offset)?;
220+
this.write_scalar(ptr, dest)?;
221+
}
222+
"mremap" => {
223+
let [old_address, old_size, new_size, flags] = this.check_shim(abi, Abi::C {unwind: false}, link_name, args)?;
224+
let ptr = this.mremap(old_address, old_size, new_size, flags)?;
225+
this.write_scalar(ptr, dest)?;
226+
}
227+
"munmap" => {
228+
let [addr, length] = this.check_shim(abi, Abi::C {unwind: false}, link_name, args)?;
229+
let result = this.munmap(addr, length)?;
230+
this.write_scalar(result, dest)?;
231+
}
232+
216233
// Dynamic symbol loading
217234
"dlsym" => {
218235
let [handle, symbol] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;

src/shims/unix/macos/foreign_items.rs

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -197,16 +197,6 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
197197
this.write_scalar(res, dest)?;
198198
}
199199

200-
// Incomplete shims that we "stub out" just to get pre-main initialization code to work.
201-
// These shims are enabled only when the caller is in the standard library.
202-
"mmap" if this.frame_in_std() => {
203-
// This is a horrible hack, but since the guard page mechanism calls mmap and expects a particular return value, we just give it that value.
204-
let [addr, _, _, _, _, _] =
205-
this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
206-
let addr = this.read_scalar(addr)?;
207-
this.write_scalar(addr, dest)?;
208-
}
209-
210200
_ => return Ok(EmulateByNameResult::NotSupported),
211201
};
212202

src/shims/unix/mem.rs

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
1+
//! This is an incomplete implementation of mmap/mremap/munmap which is restricted in order to be
2+
//! implementable on top of the existing memory system. The point of these function as-written is
3+
//! to allow memory allocators written entirely in Rust to be executed by Miri. This implementation
4+
//! does not support other uses of mmap such as file mappings.
5+
//!
6+
//! mmap/mremap/munmap behave a lot like alloc/realloc/dealloc, and for simple use they are exactly
7+
//! equivalent. But the memory-mapping API provides more control. For example:
8+
//!
9+
//! * It is possible to munmap a single page in the middle of a mapped region. We do not have a way
10+
//! to express non-contiguous allocations.
11+
//!
12+
//! * With MAP_FIXED it is possible to call mmap multiple times, but create a single contiguous
13+
//! range of mapped virtual addresses. A memory allocator can then choose to carve this up into
14+
//! allocations in arbitrary ways.
15+
16+
use crate::*;
17+
use rustc_target::abi::{Align, Size};
18+
19+
impl<'mir, 'tcx: 'mir> EvalContextExt<'mir, 'tcx> for crate::MiriInterpCx<'mir, 'tcx> {}
20+
pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
21+
fn mmap(
22+
&mut self,
23+
addr: &OpTy<'tcx, Provenance>,
24+
length: &OpTy<'tcx, Provenance>,
25+
prot: &OpTy<'tcx, Provenance>,
26+
flags: &OpTy<'tcx, Provenance>,
27+
fd: &OpTy<'tcx, Provenance>,
28+
offset: &OpTy<'tcx, Provenance>,
29+
) -> InterpResult<'tcx, Scalar<Provenance>> {
30+
let this = self.eval_context_mut();
31+
32+
// We do not support MAP_FIXED, so the addr argument is always ignored
33+
let addr = this.read_pointer(addr)?;
34+
let length = this.read_scalar(length)?.to_machine_usize(this)?;
35+
let prot = this.read_scalar(prot)?.to_i32()?;
36+
let flags = this.read_scalar(flags)?.to_i32()?;
37+
let fd = this.read_scalar(fd)?.to_i32()?;
38+
let offset = this.read_scalar(offset)?.to_machine_usize(this)?;
39+
40+
let map_private = this.eval_libc_i32("MAP_PRIVATE");
41+
let map_anonymous = this.eval_libc_i32("MAP_ANONYMOUS");
42+
let map_shared = this.eval_libc_i32("MAP_SHARED");
43+
let map_fixed = this.eval_libc_i32("MAP_FIXED");
44+
45+
// This is a horrible hack, but since the guard page mechanism calls mmap and expects a particular return value, we just give it that value.
46+
if this.frame_in_std() && this.tcx.sess.target.os == "macos" && (flags & map_fixed) != 0 {
47+
return Ok(Scalar::from_maybe_pointer(addr, this));
48+
}
49+
50+
let prot_read = this.eval_libc_i32("PROT_READ");
51+
let prot_write = this.eval_libc_i32("PROT_WRITE");
52+
53+
// First, we do some basic argument validation as required by mmap
54+
if (flags & (map_private | map_shared)).count_ones() != 1 {
55+
this.set_last_error(Scalar::from_i32(this.eval_libc_i32("EINVAL")))?;
56+
return Ok(Scalar::from_maybe_pointer(Pointer::null(), this));
57+
}
58+
if length == 0 {
59+
this.set_last_error(Scalar::from_i32(this.eval_libc_i32("EINVAL")))?;
60+
return Ok(Scalar::from_maybe_pointer(Pointer::null(), this));
61+
}
62+
63+
// If a user tries to map a file, we want to loudly inform them that this is not going
64+
// to work. It is possible that POSIX gives us enough leeway to return an error, but the
65+
// outcome for the user (I need to add cfg(miri)) is the same, just more frustrating.
66+
if fd != -1 {
67+
throw_unsup_format!("Miri does not support file-backed memory mappings");
68+
}
69+
70+
// POSIX says:
71+
// [ENOTSUP]
72+
// * MAP_FIXED or MAP_PRIVATE was specified in the flags argument and the implementation
73+
// does not support this functionality.
74+
// * The implementation does not support the combination of accesses requested in the
75+
// prot argument.
76+
//
77+
// Miri doesn't support MAP_FIXED or any any protections other than PROT_READ|PROT_WRITE.
78+
if flags & map_fixed != 0 || prot != prot_read | prot_write {
79+
this.set_last_error(Scalar::from_i32(this.eval_libc_i32("ENOTSUP")))?;
80+
return Ok(Scalar::from_maybe_pointer(Pointer::null(), this));
81+
}
82+
83+
// Miri does not support shared mappings, or any of the other extensions that for example
84+
// Linux has added to the flags arguments.
85+
if flags != map_private | map_anonymous {
86+
throw_unsup_format!(
87+
"Miri only supports calls to mmap which set the flags argument to MAP_PRIVATE|MAP_ANONYMOUS"
88+
);
89+
}
90+
91+
// This is only used for file mappings, which we don't support anyway.
92+
if offset != 0 {
93+
throw_unsup_format!("Miri does not support non-zero offsets to mmap");
94+
}
95+
96+
let align = Align::from_bytes(this.machine.page_size).unwrap();
97+
let map_length = this.machine.round_up_to_multiple_of_page_size(length).unwrap_or(u64::MAX);
98+
99+
let ptr =
100+
this.allocate_ptr(Size::from_bytes(map_length), align, MiriMemoryKind::Mmap.into())?;
101+
// We just allocated this, the access is definitely in-bounds and fits into our address space.
102+
// mmap guarantees new mappings are zero-init.
103+
this.write_bytes_ptr(
104+
ptr.into(),
105+
std::iter::repeat(0u8).take(usize::try_from(map_length).unwrap()),
106+
)
107+
.unwrap();
108+
// Memory mappings are always exposed
109+
let (prov, _) = ptr.into_parts();
110+
let Provenance::Concrete { alloc_id, tag } = prov else { unreachable!() };
111+
intptrcast::GlobalStateInner::expose_ptr(this, alloc_id, tag)?;
112+
113+
Ok(Scalar::from_pointer(ptr, this))
114+
}
115+
116+
fn mremap(
117+
&mut self,
118+
old_address: &OpTy<'tcx, Provenance>,
119+
old_size: &OpTy<'tcx, Provenance>,
120+
new_size: &OpTy<'tcx, Provenance>,
121+
flags: &OpTy<'tcx, Provenance>,
122+
) -> InterpResult<'tcx, Scalar<Provenance>> {
123+
let this = self.eval_context_mut();
124+
125+
let old_address = this.read_pointer(old_address)?;
126+
let old_size = this.read_scalar(old_size)?.to_machine_usize(this)?;
127+
let new_size = this.read_scalar(new_size)?.to_machine_usize(this)?;
128+
let flags = this.read_scalar(flags)?.to_i32()?;
129+
130+
// old_address must be a multiple of the page size
131+
#[allow(clippy::integer_arithmetic)] // PAGE_SIZE is nonzero
132+
if old_address.addr().bytes() % this.machine.page_size != 0 || new_size == 0 {
133+
this.set_last_error(Scalar::from_i32(this.eval_libc_i32("EINVAL")))?;
134+
return Ok(this.eval_libc("MAP_FAILED"));
135+
}
136+
137+
if flags & this.eval_libc_i32("MREMAP_FIXED") != 0 {
138+
throw_unsup_format!("Miri does not support mremap wth MREMAP_FIXED");
139+
}
140+
141+
if flags & this.eval_libc_i32("MREMAP_DONTUNMAP") != 0 {
142+
throw_unsup_format!("Miri does not support mremap wth MREMAP_DONTUNMAP");
143+
}
144+
145+
if flags & this.eval_libc_i32("MREMAP_MAYMOVE") == 0 {
146+
// We only support MREMAP_MAYMOVE, so not passing the flag is just a failure
147+
this.set_last_error(Scalar::from_i32(this.eval_libc_i32("EINVAL")))?;
148+
return Ok(Scalar::from_maybe_pointer(Pointer::null(), this));
149+
}
150+
151+
let align = this.machine.page_align();
152+
let ptr = this.reallocate_ptr(
153+
old_address,
154+
Some((Size::from_bytes(old_size), align)),
155+
Size::from_bytes(new_size),
156+
align,
157+
MiriMemoryKind::Mmap.into(),
158+
)?;
159+
if let Some(increase) = new_size.checked_sub(old_size) {
160+
// We just allocated this, the access is definitely in-bounds and fits into our address space.
161+
// mmap guarantees new mappings are zero-init.
162+
this.write_bytes_ptr(
163+
ptr.offset(Size::from_bytes(old_size), this).unwrap().into(),
164+
std::iter::repeat(0u8).take(usize::try_from(increase).unwrap()),
165+
)
166+
.unwrap();
167+
}
168+
// Memory mappings are always exposed
169+
let (prov, _) = ptr.into_parts();
170+
let Provenance::Concrete { alloc_id, tag } = prov else { unreachable!() };
171+
intptrcast::GlobalStateInner::expose_ptr(this, alloc_id, tag)?;
172+
173+
Ok(Scalar::from_pointer(ptr, this))
174+
}
175+
176+
fn munmap(
177+
&mut self,
178+
addr: &OpTy<'tcx, Provenance>,
179+
length: &OpTy<'tcx, Provenance>,
180+
) -> InterpResult<'tcx, Scalar<Provenance>> {
181+
let this = self.eval_context_mut();
182+
183+
let addr = this.read_pointer(addr)?;
184+
let length = this.read_scalar(length)?.to_machine_usize(this)?;
185+
186+
// addr must be a multiple of the page size
187+
#[allow(clippy::integer_arithmetic)] // PAGE_SIZE is nonzero
188+
if addr.addr().bytes() % this.machine.page_size != 0 {
189+
this.set_last_error(Scalar::from_i32(this.eval_libc_i32("EINVAL")))?;
190+
// FIXME: The man page says this returns MAP_FAILED but that is type void*, and this
191+
// function returns int.
192+
return Ok(Scalar::from_i32(-1));
193+
}
194+
195+
// All pages containing a part of the indicated range are unmapped.
196+
let length = this.machine.round_up_to_multiple_of_page_size(length).unwrap_or(u64::MAX);
197+
198+
// FIXME: POSIX says that it is unspecified whether the address range needs to be mapped,
199+
// Linux is clear that it is not an error to munmap a region which is not mapped.
200+
this.deallocate_ptr(
201+
addr,
202+
Some((Size::from_bytes(length), this.machine.page_align())),
203+
MiriMemoryKind::Mmap.into(),
204+
)?;
205+
Ok(Scalar::from_i32(0))
206+
}
207+
}

src/shims/unix/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ pub mod dlsym;
22
pub mod foreign_items;
33

44
mod fs;
5+
mod mem;
56
mod sync;
67
mod thread;
78

tests/fail/mmap_invalid_dealloc.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
//@compile-flags: -Zmiri-disable-isolation
2+
//@ignore-target-windows: No libc on Windows
3+
4+
#![feature(rustc_private)]
5+
6+
fn main() {
7+
unsafe {
8+
let ptr = libc::mmap(
9+
std::ptr::null_mut(),
10+
4096,
11+
libc::PROT_READ | libc::PROT_WRITE,
12+
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
13+
-1,
14+
0,
15+
);
16+
libc::free(ptr); //~ ERROR: which is mmap memory, using C heap deallocation operation
17+
}
18+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
error: Undefined Behavior: deallocating ALLOC, which is mmap memory, using C heap deallocation operation
2+
--> $DIR/mmap_invalid_dealloc.rs:LL:CC
3+
|
4+
LL | libc::free(ptr);
5+
| ^^^^^^^^^^^^^^^ deallocating ALLOC, which is mmap memory, using C heap deallocation operation
6+
|
7+
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
8+
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
9+
= note: BACKTRACE:
10+
= note: inside `main` at $DIR/mmap_invalid_dealloc.rs:LL:CC
11+
12+
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
13+
14+
error: aborting due to previous error
15+

0 commit comments

Comments
 (0)