Skip to content

Commit 4b90db3

Browse files
committed
Auto merge of #2520 - saethlin:mmap-shim, r=RalfJung
mmap/munmap/mremamp shims This adds basic support for `mmap`/`mremap`/`munmap`, with the specific goal of testing allocators targeting Linux under Miri. This supports `mmap` with `MAP_PRIVATE|MAP_ANONYMOUS`, and `PROT_READ|PROT_WRITE`, and explicitly does not support `MAP_SHARED` (because that's asking for MMIO) as well as any kind of file mapping (because it seems like nobody does `MAP_PRIVATE` on files even though that would be very sensible). And (officially) we don't support `MAP_FIXED`, so we always ignore the `addr` argument. This supports `mremap` only when the implementation is allowed to move the mapping (so no `MREMAP_FIXED`, no `MREMAP_DONTUNMAP`, and required `MREMAP_MAYMOVE`), and also when the entirety of a region previously mapped by `mmap` is being remapped. This supports `munmap` but only when the entirety of a region previously mapped by `mmap` is unmapped.
2 parents a9df7d7 + 4916a77 commit 4b90db3

18 files changed

+521
-12
lines changed

src/concurrency/data_race.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -714,7 +714,8 @@ impl VClockAlloc {
714714
MiriMemoryKind::Rust
715715
| MiriMemoryKind::Miri
716716
| MiriMemoryKind::C
717-
| MiriMemoryKind::WinHeap,
717+
| MiriMemoryKind::WinHeap
718+
| MiriMemoryKind::Mmap,
718719
)
719720
| MemoryKind::Stack => {
720721
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
@@ -112,6 +112,8 @@ pub enum MiriMemoryKind {
112112
/// Memory for thread-local statics.
113113
/// This memory may leak.
114114
Tls,
115+
/// Memory mapped directly by the program
116+
Mmap,
115117
}
116118

117119
impl From<MiriMemoryKind> for MemoryKind<MiriMemoryKind> {
@@ -127,7 +129,7 @@ impl MayLeak for MiriMemoryKind {
127129
use self::MiriMemoryKind::*;
128130
match self {
129131
Rust | Miri | C | WinHeap | Runtime => false,
130-
Machine | Global | ExternStatic | Tls => true,
132+
Machine | Global | ExternStatic | Tls | Mmap => true,
131133
}
132134
}
133135
}
@@ -145,6 +147,7 @@ impl fmt::Display for MiriMemoryKind {
145147
Global => write!(f, "global (static or const)"),
146148
ExternStatic => write!(f, "extern static"),
147149
Tls => write!(f, "thread-local static"),
150+
Mmap => write!(f, "mmap"),
148151
}
149152
}
150153
}
@@ -726,6 +729,15 @@ impl<'mir, 'tcx> MiriMachine<'mir, 'tcx> {
726729
// will panic when given the file.
727730
drop(self.profiler.take());
728731
}
732+
733+
pub(crate) fn round_up_to_multiple_of_page_size(&self, length: u64) -> Option<u64> {
734+
#[allow(clippy::arithmetic_side_effects)] // page size is nonzero
735+
(length.checked_add(self.page_size - 1)? / self.page_size).checked_mul(self.page_size)
736+
}
737+
738+
pub(crate) fn page_align(&self) -> Align {
739+
Align::from_bytes(self.page_size).unwrap()
740+
}
729741
}
730742

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

src/shims/unix/foreign_items.rs

Lines changed: 12 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,17 @@ 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+
"munmap" => {
223+
let [addr, length] = this.check_shim(abi, Abi::C {unwind: false}, link_name, args)?;
224+
let result = this.munmap(addr, length)?;
225+
this.write_scalar(result, dest)?;
226+
}
227+
216228
// Dynamic symbol loading
217229
"dlsym" => {
218230
let [handle, symbol] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;

src/shims/unix/linux/foreign_items.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use crate::*;
77
use shims::foreign_items::EmulateByNameResult;
88
use shims::unix::fs::EvalContextExt as _;
99
use shims::unix::linux::fd::EvalContextExt as _;
10+
use shims::unix::linux::mem::EvalContextExt as _;
1011
use shims::unix::linux::sync::futex;
1112
use shims::unix::sync::EvalContextExt as _;
1213
use shims::unix::thread::EvalContextExt as _;
@@ -68,6 +69,12 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
6869
let result = this.eventfd(val, flag)?;
6970
this.write_scalar(result, dest)?;
7071
}
72+
"mremap" => {
73+
let [old_address, old_size, new_size, flags] =
74+
this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
75+
let ptr = this.mremap(old_address, old_size, new_size, flags)?;
76+
this.write_scalar(ptr, dest)?;
77+
}
7178
"socketpair" => {
7279
let [domain, type_, protocol, sv] =
7380
this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;

src/shims/unix/linux/mem.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//! This follows the pattern in src/shims/unix/mem.rs: We only support uses of mremap that would
2+
//! correspond to valid uses of realloc.
3+
4+
use crate::*;
5+
use rustc_target::abi::Size;
6+
7+
impl<'mir, 'tcx: 'mir> EvalContextExt<'mir, 'tcx> for crate::MiriInterpCx<'mir, 'tcx> {}
8+
pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
9+
fn mremap(
10+
&mut self,
11+
old_address: &OpTy<'tcx, Provenance>,
12+
old_size: &OpTy<'tcx, Provenance>,
13+
new_size: &OpTy<'tcx, Provenance>,
14+
flags: &OpTy<'tcx, Provenance>,
15+
) -> InterpResult<'tcx, Scalar<Provenance>> {
16+
let this = self.eval_context_mut();
17+
18+
let old_address = this.read_target_usize(old_address)?;
19+
let old_size = this.read_target_usize(old_size)?;
20+
let new_size = this.read_target_usize(new_size)?;
21+
let flags = this.read_scalar(flags)?.to_i32()?;
22+
23+
// old_address must be a multiple of the page size
24+
#[allow(clippy::arithmetic_side_effects)] // PAGE_SIZE is nonzero
25+
if old_address % this.machine.page_size != 0 || new_size == 0 {
26+
this.set_last_error(Scalar::from_i32(this.eval_libc_i32("EINVAL")))?;
27+
return Ok(this.eval_libc("MAP_FAILED"));
28+
}
29+
30+
if flags & this.eval_libc_i32("MREMAP_FIXED") != 0 {
31+
throw_unsup_format!("Miri does not support mremap wth MREMAP_FIXED");
32+
}
33+
34+
if flags & this.eval_libc_i32("MREMAP_DONTUNMAP") != 0 {
35+
throw_unsup_format!("Miri does not support mremap wth MREMAP_DONTUNMAP");
36+
}
37+
38+
if flags & this.eval_libc_i32("MREMAP_MAYMOVE") == 0 {
39+
// We only support MREMAP_MAYMOVE, so not passing the flag is just a failure
40+
this.set_last_error(Scalar::from_i32(this.eval_libc_i32("EINVAL")))?;
41+
return Ok(Scalar::from_maybe_pointer(Pointer::null(), this));
42+
}
43+
44+
let old_address = Machine::ptr_from_addr_cast(this, old_address)?;
45+
let align = this.machine.page_align();
46+
let ptr = this.reallocate_ptr(
47+
old_address,
48+
Some((Size::from_bytes(old_size), align)),
49+
Size::from_bytes(new_size),
50+
align,
51+
MiriMemoryKind::Mmap.into(),
52+
)?;
53+
if let Some(increase) = new_size.checked_sub(old_size) {
54+
// We just allocated this, the access is definitely in-bounds and fits into our address space.
55+
// mmap guarantees new mappings are zero-init.
56+
this.write_bytes_ptr(
57+
ptr.offset(Size::from_bytes(old_size), this).unwrap().into(),
58+
std::iter::repeat(0u8).take(usize::try_from(increase).unwrap()),
59+
)
60+
.unwrap();
61+
}
62+
// Memory mappings are always exposed
63+
Machine::expose_ptr(this, ptr)?;
64+
65+
Ok(Scalar::from_pointer(ptr, this))
66+
}
67+
}

src/shims/unix/linux/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
pub mod dlsym;
22
pub mod fd;
33
pub mod foreign_items;
4+
pub mod mem;
45
pub mod sync;

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: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
//! This is an incomplete implementation of mmap/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/munmap behave a lot like alloc/dealloc, and for simple use they are exactly
7+
//! equivalent. That is the only part we support: no MAP_FIXED or MAP_SHARED or anything
8+
//! else that goes beyond a basic allocation API.
9+
10+
use crate::*;
11+
use rustc_target::abi::Size;
12+
13+
impl<'mir, 'tcx: 'mir> EvalContextExt<'mir, 'tcx> for crate::MiriInterpCx<'mir, 'tcx> {}
14+
pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
15+
fn mmap(
16+
&mut self,
17+
addr: &OpTy<'tcx, Provenance>,
18+
length: &OpTy<'tcx, Provenance>,
19+
prot: &OpTy<'tcx, Provenance>,
20+
flags: &OpTy<'tcx, Provenance>,
21+
fd: &OpTy<'tcx, Provenance>,
22+
offset: &OpTy<'tcx, Provenance>,
23+
) -> InterpResult<'tcx, Scalar<Provenance>> {
24+
let this = self.eval_context_mut();
25+
26+
// We do not support MAP_FIXED, so the addr argument is always ignored (except for the MacOS hack)
27+
let addr = this.read_target_usize(addr)?;
28+
let length = this.read_target_usize(length)?;
29+
let prot = this.read_scalar(prot)?.to_i32()?;
30+
let flags = this.read_scalar(flags)?.to_i32()?;
31+
let fd = this.read_scalar(fd)?.to_i32()?;
32+
let offset = this.read_target_usize(offset)?;
33+
34+
let map_private = this.eval_libc_i32("MAP_PRIVATE");
35+
let map_anonymous = this.eval_libc_i32("MAP_ANONYMOUS");
36+
let map_shared = this.eval_libc_i32("MAP_SHARED");
37+
let map_fixed = this.eval_libc_i32("MAP_FIXED");
38+
39+
// This is a horrible hack, but on MacOS the guard page mechanism uses mmap
40+
// in a way we do not support. We just give it the return value it expects.
41+
if this.frame_in_std() && this.tcx.sess.target.os == "macos" && (flags & map_fixed) != 0 {
42+
return Ok(Scalar::from_maybe_pointer(Pointer::from_addr_invalid(addr), this));
43+
}
44+
45+
let prot_read = this.eval_libc_i32("PROT_READ");
46+
let prot_write = this.eval_libc_i32("PROT_WRITE");
47+
48+
// First, we do some basic argument validation as required by mmap
49+
if (flags & (map_private | map_shared)).count_ones() != 1 {
50+
this.set_last_error(Scalar::from_i32(this.eval_libc_i32("EINVAL")))?;
51+
return Ok(Scalar::from_maybe_pointer(Pointer::null(), this));
52+
}
53+
if length == 0 {
54+
this.set_last_error(Scalar::from_i32(this.eval_libc_i32("EINVAL")))?;
55+
return Ok(Scalar::from_maybe_pointer(Pointer::null(), this));
56+
}
57+
58+
// If a user tries to map a file, we want to loudly inform them that this is not going
59+
// to work. It is possible that POSIX gives us enough leeway to return an error, but the
60+
// outcome for the user (I need to add cfg(miri)) is the same, just more frustrating.
61+
if fd != -1 {
62+
throw_unsup_format!("Miri does not support file-backed memory mappings");
63+
}
64+
65+
// POSIX says:
66+
// [ENOTSUP]
67+
// * MAP_FIXED or MAP_PRIVATE was specified in the flags argument and the implementation
68+
// does not support this functionality.
69+
// * The implementation does not support the combination of accesses requested in the
70+
// prot argument.
71+
//
72+
// Miri doesn't support MAP_FIXED or any any protections other than PROT_READ|PROT_WRITE.
73+
if flags & map_fixed != 0 || prot != prot_read | prot_write {
74+
this.set_last_error(Scalar::from_i32(this.eval_libc_i32("ENOTSUP")))?;
75+
return Ok(Scalar::from_maybe_pointer(Pointer::null(), this));
76+
}
77+
78+
// Miri does not support shared mappings, or any of the other extensions that for example
79+
// Linux has added to the flags arguments.
80+
if flags != map_private | map_anonymous {
81+
throw_unsup_format!(
82+
"Miri only supports calls to mmap which set the flags argument to MAP_PRIVATE|MAP_ANONYMOUS"
83+
);
84+
}
85+
86+
// This is only used for file mappings, which we don't support anyway.
87+
if offset != 0 {
88+
throw_unsup_format!("Miri does not support non-zero offsets to mmap");
89+
}
90+
91+
let align = this.machine.page_align();
92+
let map_length = this.machine.round_up_to_multiple_of_page_size(length).unwrap_or(u64::MAX);
93+
94+
let ptr =
95+
this.allocate_ptr(Size::from_bytes(map_length), align, MiriMemoryKind::Mmap.into())?;
96+
// We just allocated this, the access is definitely in-bounds and fits into our address space.
97+
// mmap guarantees new mappings are zero-init.
98+
this.write_bytes_ptr(
99+
ptr.into(),
100+
std::iter::repeat(0u8).take(usize::try_from(map_length).unwrap()),
101+
)
102+
.unwrap();
103+
// Memory mappings don't use provenance, and are always exposed.
104+
Machine::expose_ptr(this, ptr)?;
105+
106+
Ok(Scalar::from_pointer(ptr, this))
107+
}
108+
109+
fn munmap(
110+
&mut self,
111+
addr: &OpTy<'tcx, Provenance>,
112+
length: &OpTy<'tcx, Provenance>,
113+
) -> InterpResult<'tcx, Scalar<Provenance>> {
114+
let this = self.eval_context_mut();
115+
116+
let addr = this.read_target_usize(addr)?;
117+
let length = this.read_target_usize(length)?;
118+
119+
// addr must be a multiple of the page size
120+
#[allow(clippy::arithmetic_side_effects)] // PAGE_SIZE is nonzero
121+
if addr % this.machine.page_size != 0 {
122+
this.set_last_error(Scalar::from_i32(this.eval_libc_i32("EINVAL")))?;
123+
return Ok(Scalar::from_i32(-1));
124+
}
125+
126+
let length = this.machine.round_up_to_multiple_of_page_size(length).unwrap_or(u64::MAX);
127+
128+
let ptr = Machine::ptr_from_addr_cast(this, addr)?;
129+
130+
let Ok(ptr) = ptr.into_pointer_or_addr() else {
131+
throw_unsup_format!("Miri only supports munmap on memory allocated directly by mmap");
132+
};
133+
let Some((alloc_id, offset, _prov)) = Machine::ptr_get_alloc(this, ptr) else {
134+
throw_unsup_format!("Miri only supports munmap on memory allocated directly by mmap");
135+
};
136+
137+
// Elsewhere in this function we are careful to check what we can and throw an unsupported
138+
// error instead of Undefined Behavior when use of this function falls outside of the
139+
// narrow scope we support. We deliberately do not check the MemoryKind of this allocation,
140+
// because we want to report UB on attempting to unmap memory that Rust "understands", such
141+
// the stack, heap, or statics.
142+
let (_kind, alloc) = this.memory.alloc_map().get(alloc_id).unwrap();
143+
if offset != Size::ZERO || alloc.len() as u64 != length {
144+
throw_unsup_format!(
145+
"Miri only supports munmap calls that exactly unmap a region previously returned by mmap"
146+
);
147+
}
148+
149+
let len = Size::from_bytes(alloc.len() as u64);
150+
this.deallocate_ptr(
151+
ptr.into(),
152+
Some((len, this.machine.page_align())),
153+
MemoryKind::Machine(MiriMemoryKind::Mmap),
154+
)?;
155+
156+
Ok(Scalar::from_i32(0))
157+
}
158+
}

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

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)