Skip to content

Commit

Permalink
mmap/munmap/mremamp shims
Browse files Browse the repository at this point in the history
  • Loading branch information
saethlin committed Jan 16, 2023
1 parent 73452b3 commit c6f389f
Show file tree
Hide file tree
Showing 9 changed files with 252 additions and 12 deletions.
3 changes: 2 additions & 1 deletion src/concurrency/data_race.rs
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,8 @@ impl VClockAlloc {
MiriMemoryKind::Rust
| MiriMemoryKind::Miri
| MiriMemoryKind::C
| MiriMemoryKind::WinHeap,
| MiriMemoryKind::WinHeap
| MiriMemoryKind::Mmap,
)
| MemoryKind::Stack => {
let (alloc_index, clocks) = global.current_thread_state(thread_mgr);
Expand Down
14 changes: 13 additions & 1 deletion src/machine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ pub enum MiriMemoryKind {
/// Memory for thread-local statics.
/// This memory may leak.
Tls,
/// Memory mapped directly by the program
Mmap,
}

impl From<MiriMemoryKind> for MemoryKind<MiriMemoryKind> {
Expand All @@ -119,7 +121,7 @@ impl MayLeak for MiriMemoryKind {
use self::MiriMemoryKind::*;
match self {
Rust | Miri | C | WinHeap | Runtime => false,
Machine | Global | ExternStatic | Tls => true,
Machine | Global | ExternStatic | Tls | Mmap => true,
}
}
}
Expand All @@ -137,6 +139,7 @@ impl fmt::Display for MiriMemoryKind {
Global => write!(f, "global (static or const)"),
ExternStatic => write!(f, "extern static"),
Tls => write!(f, "thread-local static"),
Mmap => write!(f, "mmap"),
}
}
}
Expand Down Expand Up @@ -674,6 +677,15 @@ impl<'mir, 'tcx> MiriMachine<'mir, 'tcx> {
let def_id = frame.instance.def_id();
def_id.is_local() || self.local_crates.contains(&def_id.krate)
}

pub(crate) fn round_up_to_multiple_of_page_size(&self, length: u64) -> Option<u64> {
#[allow(clippy::integer_arithmetic)] // page size is nonzero
(length.checked_add(self.page_size - 1)? / self.page_size).checked_mul(self.page_size)
}

pub(crate) fn page_align(&self) -> Align {
Align::from_bytes(self.page_size).unwrap()
}
}

impl VisitTags for MiriMachine<'_, '_> {
Expand Down
17 changes: 17 additions & 0 deletions src/shims/unix/foreign_items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use rustc_target::spec::abi::Abi;
use crate::*;
use shims::foreign_items::EmulateByNameResult;
use shims::unix::fs::EvalContextExt as _;
use shims::unix::mem::EvalContextExt as _;
use shims::unix::sync::EvalContextExt as _;
use shims::unix::thread::EvalContextExt as _;

Expand Down Expand Up @@ -213,6 +214,22 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
}
}

"mmap" => {
let [addr, length, prot, flags, fd, offset] = this.check_shim(abi, Abi::C {unwind: false}, link_name, args)?;
let ptr = this.mmap(addr, length, prot, flags, fd, offset)?;
this.write_pointer(ptr, dest)?;
}
"mremap" => {
let [old_address, old_size, new_size, flags] = this.check_shim(abi, Abi::C {unwind: false}, link_name, args)?;
let result = this.mremap(old_address, old_size, new_size, flags)?;
this.write_pointer(result, dest)?;
}
"munmap" => {
let [addr, length] = this.check_shim(abi, Abi::C {unwind: false}, link_name, args)?;
let result = this.munmap(addr, length)?;
this.write_scalar(Scalar::from_i32(result), dest)?;
}

// Dynamic symbol loading
"dlsym" => {
let [handle, symbol] = this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
Expand Down
10 changes: 0 additions & 10 deletions src/shims/unix/macos/foreign_items.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,16 +197,6 @@ pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
this.write_scalar(res, dest)?;
}

// Incomplete shims that we "stub out" just to get pre-main initialization code to work.
// These shims are enabled only when the caller is in the standard library.
"mmap" if this.frame_in_std() => {
// 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.
let [addr, _, _, _, _, _] =
this.check_shim(abi, Abi::C { unwind: false }, link_name, args)?;
let addr = this.read_scalar(addr)?;
this.write_scalar(addr, dest)?;
}

_ => return Ok(EmulateByNameResult::NotSupported),
};

Expand Down
157 changes: 157 additions & 0 deletions src/shims/unix/mem.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
//! This is an incomplete implementation of mmap/mremap/munmap which is restricted in order to be
//! implementable on top of the existing memory system. The point of these function as-written is
//! to allow memory allocators written entirely in Rust to be executed by Miri. This implementation
//! does not support other uses of mmap such as file mappings.
//!
//! mmap/mremap/munmap behave a lot like alloc/realloc/dealloc, and for simple use they are exactly
//! equivalent. But the memory-mapping API provides more control. For example:
//!
//! * It is possible to munmap a single page in the middle of a mapped region. We do not have a way
//! to express non-contiguous allocations.
//!
//! * With MAP_FIXED it is possible to call mmap multiple times, but create a single contiguous
//! range of mapped virtual addresses. A memory allocator can then choose to carve this up into
//! allocations in arbitrary ways.
use crate::*;
use rustc_target::abi::{Align, Size};

impl<'mir, 'tcx: 'mir> EvalContextExt<'mir, 'tcx> for crate::MiriInterpCx<'mir, 'tcx> {}
pub trait EvalContextExt<'mir, 'tcx: 'mir>: crate::MiriInterpCxExt<'mir, 'tcx> {
fn mmap(
&mut self,
addr: &OpTy<'tcx, Provenance>,
length: &OpTy<'tcx, Provenance>,
prot: &OpTy<'tcx, Provenance>,
flags: &OpTy<'tcx, Provenance>,
fd: &OpTy<'tcx, Provenance>,
offset: &OpTy<'tcx, Provenance>,
) -> InterpResult<'tcx, Pointer<Option<Provenance>>> {
let this = self.eval_context_mut();

// We do not support MAP_FIXED, so the addr argument is always ignored
let _addr = this.read_pointer(addr)?.into_parts().1;
let length = this.read_scalar(length)?.to_machine_usize(this)?;
let prot = this.read_scalar(prot)?.to_i32()?;
let flags = this.read_scalar(flags)?.to_i32()?;
let fd = this.read_scalar(fd)?.to_i32()?;
let offset = this.read_scalar(offset)?.to_machine_usize(this)?;

let map_private = this.eval_libc_i32("MAP_PRIVATE");
let map_anonymous = this.eval_libc_i32("MAP_ANONYMOUS");

let prot_read = this.eval_libc_i32("PROT_READ");
let prot_write = this.eval_libc_i32("PROT_WRITE");

if flags != map_private | map_anonymous {
throw_unsup_format!(
"Miri only supports calls to mmap which set flags to MAP_PRIVATE|MAP_ANONYMOUS"
);
}

if prot != prot_read | prot_write {
throw_unsup_format!(
"Miri only supports calls to mmap which set protection to PROT_READ|PROT_WRITE"
);
}

if offset != 0 {
throw_unsup_format!("Miri does not support non-zero offsets to mmap");
}

if fd != -1 {
throw_unsup_format!("Miri does not support file-backed memory mappings");
}

if length == 0 {
this.set_last_error(Scalar::from_i32(this.eval_libc_i32("EINVAL")))?;
return Ok(Pointer::null());
}

let align = Align::from_bytes(this.machine.page_size).unwrap();
let map_length = this.machine.round_up_to_multiple_of_page_size(length).unwrap_or(u64::MAX);

let ptr =
this.allocate_ptr(Size::from_bytes(map_length), align, MiriMemoryKind::Mmap.into())?;

// We just allocated this, the access is definitely in-bounds and fits into our address space.
// mmap guarantees new mappings are zero-init.
this.write_bytes_ptr(
ptr.into(),
std::iter::repeat(0u8).take(usize::try_from(map_length).unwrap()),
)
.unwrap();

Ok(ptr.into())
}

fn mremap(
&mut self,
old_address: &OpTy<'tcx, Provenance>,
old_size: &OpTy<'tcx, Provenance>,
new_size: &OpTy<'tcx, Provenance>,
flags: &OpTy<'tcx, Provenance>,
) -> InterpResult<'tcx, Pointer<Option<Provenance>>> {
let this = self.eval_context_mut();

// This is actually a pointer but we really do not care about its provenance
let old_address = this.read_pointer(old_address)?;

let old_size = this.read_scalar(old_size)?.to_machine_usize(this)?;
let new_size = this.read_scalar(new_size)?.to_machine_usize(this)?;
let flags = this.read_scalar(flags)?.to_i32()?;

if flags & this.eval_libc_i32("MREMAP_FIXED") > 0 {
throw_unsup_format!("Miri does not support mremap wth MREMAP_FIXED");
}

if flags & this.eval_libc_i32("MREMAP_DONTUNMAP") > 0 {
throw_unsup_format!("Miri does not support mremap wth MREMAP_DONTUNMAP");
}

if flags & this.eval_libc_i32("MREMAP_MAYMOVE") == 0 {
// We only support MREMAP_MAYMOVE, so not passing the flag is just a failure
this.set_last_error(Scalar::from_i32(this.eval_libc_i32("EINVAL")))?;
return Ok(Pointer::null());
}

let align = this.machine.page_align();
this.reallocate_ptr(
old_address,
Some((Size::from_bytes(old_size), align)),
Size::from_bytes(new_size),
align,
MiriMemoryKind::Mmap.into(),
)
.map(|p| p.into())
}

fn munmap(
&mut self,
addr: &OpTy<'tcx, Provenance>,
length: &OpTy<'tcx, Provenance>,
) -> InterpResult<'tcx, i32> {
let this = self.eval_context_mut();

let length = this.read_scalar(length)?.to_machine_usize(this)?;

// The address addr must be a multiple of the page size (but length need not be).
#[allow(clippy::integer_arithmetic)] // PAGE_SIZE is nonzero
if this.read_scalar(addr)?.to_machine_usize(this)? % this.machine.page_size != 0 {
this.set_last_error(Scalar::from_i32(this.eval_libc_i32("EINVAL")))?;
return Ok(-1);
}

let addr = this.read_pointer(addr)?;

// All pages containing a part of the indicated range are unmapped.
let length = this.machine.round_up_to_multiple_of_page_size(length).unwrap_or(u64::MAX);

this.deallocate_ptr(
addr,
Some((Size::from_bytes(length), this.machine.page_align())),
MiriMemoryKind::Mmap.into(),
)?;
Ok(0)
}
}
1 change: 1 addition & 0 deletions src/shims/unix/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ pub mod dlsym;
pub mod foreign_items;

mod fs;
mod mem;
mod sync;
mod thread;

Expand Down
18 changes: 18 additions & 0 deletions tests/fail/mmap_invalid_dealloc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
//@compile-flags: -Zmiri-disable-isolation
//@ignore-target-windows: No libc on Windows

#![feature(rustc_private)]

fn main() {
unsafe {
let ptr = libc::mmap(
std::ptr::null_mut(),
4096,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
-1,
0,
);
libc::free(ptr); //~ ERROR: which is mmap memory, using C heap deallocation operation
}
}
15 changes: 15 additions & 0 deletions tests/fail/mmap_invalid_dealloc.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
error: Undefined Behavior: deallocating ALLOC, which is mmap memory, using C heap deallocation operation
--> $DIR/mmap_invalid_dealloc.rs:LL:CC
|
LL | libc::free(ptr);
| ^^^^^^^^^^^^^^^ deallocating ALLOC, which is mmap memory, using C heap deallocation operation
|
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
= note: BACKTRACE:
= note: inside `main` at $DIR/mmap_invalid_dealloc.rs:LL:CC

note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace

error: aborting due to previous error

29 changes: 29 additions & 0 deletions tests/pass-dep/shims/mmap.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//@ignore-target-windows: No libc on Windows
//@compile-flags: -Zmiri-disable-isolation
#![feature(strict_provenance)]

use std::slice;

fn test_mmap() {
// Standard mmap to allocate memory
let ptr = unsafe {
libc::mmap(
std::ptr::null_mut(),
4096,
libc::PROT_READ | libc::PROT_WRITE,
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
-1,
0,
)
};
assert!(!ptr.is_null());
let slice = unsafe { slice::from_raw_parts(ptr as *const u8, 4096) };
assert_eq!(&[0u8; 4096], slice);

let res = unsafe { libc::munmap(ptr, 4096) };
assert_eq!(res, 0);
}

fn main() {
test_mmap();
}

0 comments on commit c6f389f

Please sign in to comment.