Skip to content

Commit

Permalink
Add support for mremap(2) on Linux.
Browse files Browse the repository at this point in the history
Closes #81
  • Loading branch information
Phantomical authored May 29, 2023
1 parent 10ca70a commit 0c9b7f7
Show file tree
Hide file tree
Showing 2 changed files with 234 additions and 0 deletions.
186 changes: 186 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,28 @@ impl Mmap {
pub fn unlock(&self) -> Result<()> {
self.inner.unlock()
}

/// Adjust the size of the memory mapping.
///
/// This will try to resize the memory mapping in place. If
/// [`RemapOptions::may_move`] is specified it will move the mapping if it
/// could not resize in place, otherwise it will error.
///
/// Only supported on Linux.
///
/// See the [`mremap(2)`] man page.
///
/// # Safety
///
/// Resizing the memory mapping beyond the end of the mapped file will
/// result in UB should you happen to access memory beyond the end of the
/// file.
///
/// [`mremap(2)`]: https://man7.org/linux/man-pages/man2/mremap.2.html
#[cfg(target_os = "linux")]
pub unsafe fn remap(&mut self, new_len: usize, options: RemapOptions) -> Result<()> {
self.inner.remap(new_len, options)
}
}

#[cfg(feature = "stable_deref_trait")]
Expand Down Expand Up @@ -863,6 +885,28 @@ impl MmapRaw {
pub fn unlock(&self) -> Result<()> {
self.inner.unlock()
}

/// Adjust the size of the memory mapping.
///
/// This will try to resize the memory mapping in place. If
/// [`RemapOptions::may_move`] is specified it will move the mapping if it
/// could not resize in place, otherwise it will error.
///
/// Only supported on Linux.
///
/// See the [`mremap(2)`] man page.
///
/// # Safety
///
/// Resizing the memory mapping beyond the end of the mapped file will
/// result in UB should you happen to access memory beyond the end of the
/// file.
///
/// [`mremap(2)`]: https://man7.org/linux/man-pages/man2/mremap.2.html
#[cfg(target_os = "linux")]
pub unsafe fn remap(&mut self, new_len: usize, options: RemapOptions) -> Result<()> {
self.inner.remap(new_len, options)
}
}

impl fmt::Debug for MmapRaw {
Expand Down Expand Up @@ -1131,6 +1175,28 @@ impl MmapMut {
pub fn unlock(&self) -> Result<()> {
self.inner.unlock()
}

/// Adjust the size of the memory mapping.
///
/// This will try to resize the memory mapping in place. If
/// [`RemapOptions::may_move`] is specified it will move the mapping if it
/// could not resize in place, otherwise it will error.
///
/// Only supported on Linux.
///
/// See the [`mremap(2)`] man page.
///
/// # Safety
///
/// Resizing the memory mapping beyond the end of the mapped file will
/// result in UB should you happen to access memory beyond the end of the
/// file.
///
/// [`mremap(2)`]: https://man7.org/linux/man-pages/man2/mremap.2.html
#[cfg(target_os = "linux")]
pub unsafe fn remap(&mut self, new_len: usize, options: RemapOptions) -> Result<()> {
self.inner.remap(new_len, options)
}
}

#[cfg(feature = "stable_deref_trait")]
Expand Down Expand Up @@ -1175,6 +1241,51 @@ impl fmt::Debug for MmapMut {
}
}

/// Options for [`Mmap::remap`] and [`MmapMut::remap`].
#[derive(Copy, Clone, Default, Debug)]
#[cfg(target_os = "linux")]
pub struct RemapOptions {
may_move: bool,
}

#[cfg(target_os = "linux")]
impl RemapOptions {
/// Creates a mew set of options for resizing a memory map.
pub fn new() -> Self {
Self::default()
}

/// Controls whether the memory map can be moved if it is not possible to
/// resize it in place.
///
/// If false then the memory map is guaranteed to remain at the same
/// address when being resized but attempting to resize will return an
/// error if the new memory map would overlap with something else in the
/// current process' memory.
///
/// By default this is false.
///
/// # `may_move` and `StableDeref`
/// If the `stable_deref_trait` feature is enabled then [`Mmap`] and
/// [`MmapMut`] implement `StableDeref`. `StableDeref` promises that the
/// memory map dereferences to a fixed address, however, calling `remap`
/// with `may_move` set may result in the backing memory of the mapping
/// being moved to a new address. This may cause UB in other code
/// depending on the `StableDeref` guarantees.
pub fn may_move(mut self, may_move: bool) -> Self {
self.may_move = may_move;
self
}

pub(crate) fn into_flags(self) -> libc::c_int {
if self.may_move {
libc::MREMAP_MAYMOVE
} else {
0
}
}
}

#[cfg(test)]
mod test {
extern crate tempfile;
Expand Down Expand Up @@ -1785,4 +1896,79 @@ mod test {
#[cfg(target_os = "linux")]
assert!(!is_locked());
}

#[test]
#[cfg(target_os = "linux")]
fn remap_grow() {
use crate::RemapOptions;

let initial_len = 128;
let final_len = 2000;

let zeros = vec![0u8; final_len];
let incr: Vec<u8> = (0..final_len).map(|v| v as u8).collect();

let file = tempfile::tempfile().unwrap();
file.set_len(final_len as u64).unwrap();

let mut mmap = unsafe { MmapOptions::new().len(initial_len).map_mut(&file).unwrap() };
assert_eq!(mmap.len(), initial_len);
assert_eq!(&mmap[..], &zeros[..initial_len]);

unsafe {
mmap.remap(final_len, RemapOptions::new().may_move(true))
.unwrap()
};

// The size should have been updated
assert_eq!(mmap.len(), final_len);

// Should still be all zeros
assert_eq!(&mmap[..], &zeros);

// Write out to the whole expanded slice.
mmap.copy_from_slice(&incr);
}

#[test]
#[cfg(target_os = "linux")]
fn remap_shrink() {
use crate::RemapOptions;

let initial_len = 20000;
let final_len = 400;

let incr: Vec<u8> = (0..final_len).map(|v| v as u8).collect();

let file = tempfile::tempfile().unwrap();
file.set_len(initial_len as u64).unwrap();

let mut mmap = unsafe { MmapMut::map_mut(&file).unwrap() };
assert_eq!(mmap.len(), initial_len);

unsafe { mmap.remap(final_len, RemapOptions::new()).unwrap() };
assert_eq!(mmap.len(), final_len);

// Check that the mmap is still writable along the slice length
mmap.copy_from_slice(&incr);
}

#[test]
#[cfg(target_os = "linux")]
#[cfg(target_pointer_width = "32")]
fn remap_len_overflow() {
use crate::RemapOptions;

let file = tempfile::tempfile().unwrap();
file.set_len(1024).unwrap();
let mut mmap = unsafe { MmapOptions::new().len(1024).map(&file).unwrap() };

let res = unsafe { mmap.remap(0x80000000, RemapOptions::new().may_move(true)) };
assert_eq!(
res.unwrap_err().to_string(),
"memory map length overflows isize"
);

assert_eq!(mmap.len(), 1024);
}
}
48 changes: 48 additions & 0 deletions src/unix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,54 @@ impl MmapInner {
}
}

#[cfg(target_os = "linux")]
pub fn remap(&mut self, new_len: usize, options: crate::RemapOptions) -> io::Result<()> {
use std::isize;

// Rust's slice cannot be larger than isize::MAX.
// See https://doc.rust-lang.org/std/slice/fn.from_raw_parts.html
//
// This is not a problem on 64-bit targets, but on 32-bit one
// having a file or an anonymous mapping larger than 2GB is quite normal
// and we have to prevent it.
//
// The code below is essentially the same as in Rust's std:
// https://github.com/rust-lang/rust/blob/db78ab70a88a0a5e89031d7ee4eccec835dcdbde/library/alloc/src/raw_vec.rs#L495
if std::mem::size_of::<usize>() < 8 && new_len > isize::MAX as usize {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"memory map length overflows isize",
));
}

// Undo the pointer adjustments done as part of MmapInner::new to recover
// the pointer and length passed to mmap.
//
// See the comments in MmapInner::new for relevant details.
let alignment = self.ptr() as usize % page_size();
let old_len = self.len() + alignment;
let old_len = old_len.max(1);

let old_ptr = unsafe { self.ptr.offset(-(alignment as isize)) };

// Adjust the new length to reflect that the start of the memory map is
// actually at the start of the nearest page.
let aligned_new_len = new_len + alignment;
let aligned_new_len = aligned_new_len.max(1);

unsafe {
let new_ptr = libc::mremap(old_ptr, old_len, aligned_new_len, options.into_flags());

if new_ptr == libc::MAP_FAILED {
Err(io::Error::last_os_error())
} else {
self.ptr = new_ptr.offset(alignment as isize);
self.len = new_len;
Ok(())
}
}
}

pub fn lock(&self) -> io::Result<()> {
unsafe {
if libc::mlock(self.ptr, self.len) != 0 {
Expand Down

0 comments on commit 0c9b7f7

Please sign in to comment.