Skip to content

Commit c830f0b

Browse files
author
Jared White
committed
mmap/unix.rs: Add with_minimum_alignment
Add an option to specify a minimal alignment for the mmap'd region. This is useful to ensure VM memory backings get huge EPT entries. Signed-off-by: Jared White <git@jaredwhite.dev>
1 parent 6de1c4b commit c830f0b

1 file changed

Lines changed: 150 additions & 18 deletions

File tree

src/mmap/unix.rs

Lines changed: 150 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,25 @@ pub enum Error {
4040
/// The `mmap` call returned an error.
4141
#[error("{0}")]
4242
Mmap(io::Error),
43+
/// Invalid aligment specified.
44+
#[error("The minimum alignment is not a power of two and at least the system page size")]
45+
InvalidAlignment,
4346
}
4447

4548
pub type Result<T> = result::Result<T, Error>;
4649

50+
/// Retrieve the system page size from sysconf
51+
fn system_page_size() -> usize {
52+
// SAFETY: Safe because this call just returns the page size and doesn't have any side
53+
// effects.
54+
unsafe { libc::sysconf(libc::_SC_PAGESIZE) as usize }
55+
}
56+
4757
/// A factory struct to build `MmapRegion` objects.
4858
#[derive(Debug)]
4959
pub struct MmapRegionBuilder<B = ()> {
5060
size: usize,
61+
alignment: usize,
5162
prot: i32,
5263
flags: i32,
5364
file_offset: Option<FileOffset>,
@@ -73,6 +84,7 @@ impl<B: Bitmap> MmapRegionBuilder<B> {
7384
pub fn new_with_bitmap(size: usize, bitmap: B) -> Self {
7485
MmapRegionBuilder {
7586
size,
87+
alignment: system_page_size(),
7688
prot: 0,
7789
flags: libc::MAP_ANONYMOUS | libc::MAP_PRIVATE,
7890
file_offset: None,
@@ -94,6 +106,17 @@ impl<B: Bitmap> MmapRegionBuilder<B> {
94106
self
95107
}
96108

109+
/// Create the `MmapRegion` object with the specified minimum alignment.
110+
/// `min_align` must be a power of two and at least the system page size.
111+
pub fn with_minimum_alignment(mut self, min_align: usize) -> Result<Self> {
112+
if !self.alignment.is_power_of_two() || self.alignment < system_page_size() {
113+
return Err(Error::InvalidAlignment);
114+
}
115+
116+
self.alignment = min_align;
117+
Ok(self)
118+
}
119+
97120
/// Create the `MmapRegion` object with the specified `file_offset`.
98121
pub fn with_file_offset(mut self, file_offset: FileOffset) -> Self {
99122
self.file_offset = Some(file_offset);
@@ -136,18 +159,74 @@ impl<B: Bitmap> MmapRegionBuilder<B> {
136159
};
137160

138161
#[cfg(not(miri))]
139-
// SAFETY: This is safe because we're not allowing MAP_FIXED, and invalid parameters
140-
// cannot break Rust safety guarantees (things may change if we're mapping /dev/mem or
141-
// some wacky file).
142-
let addr = unsafe {
143-
libc::mmap(
144-
null_mut(),
145-
self.size,
146-
self.prot,
147-
self.flags,
148-
fd,
149-
offset as libc::off_t,
150-
)
162+
let addr = {
163+
// To support alignment, first reserve a sufficiently large region of address space
164+
// using `MAP_NORESERVE`, and then find and place the real mapping at the aligned
165+
// sub-region contained within via `MAP_FIXED`. Then free the excess.
166+
let page_aligned_size = self.size.next_multiple_of(system_page_size());
167+
let va_reserved_size = page_aligned_size + self.alignment;
168+
169+
// SAFETY: Calling mmap with correct arguments to reserve virtual address space.
170+
let va_reserved_addr = unsafe {
171+
libc::mmap(
172+
null_mut(),
173+
va_reserved_size,
174+
libc::PROT_NONE,
175+
libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | libc::MAP_NORESERVE,
176+
-1, // fd for MAP_ANONYMOUS
177+
0, // no offset
178+
)
179+
};
180+
181+
if va_reserved_addr == libc::MAP_FAILED {
182+
return Err(Error::Mmap(io::Error::last_os_error()));
183+
}
184+
185+
let va_reserved_addr = va_reserved_addr as usize;
186+
let aligned_addr = va_reserved_addr.next_multiple_of(self.alignment);
187+
188+
// Need to free the extra address space later - calculate the ranges
189+
let va_prefix_size = aligned_addr - va_reserved_addr;
190+
let va_suffix_addr = (aligned_addr + page_aligned_size) as *mut libc::c_void;
191+
let va_suffix_size = va_reserved_size - va_prefix_size - page_aligned_size;
192+
let va_reserved_addr = va_reserved_addr as *mut libc::c_void;
193+
194+
// Place the actual mapping within the reserved region.
195+
// SAFETY: This is safe because we're not allowing MAP_FIXED, and invalid parameters
196+
// cannot break Rust safety guarantees (things may change if we're mapping /dev/mem or
197+
// some wacky file).
198+
let addr = unsafe {
199+
libc::mmap(
200+
aligned_addr as *mut libc::c_void,
201+
// This is safe even if `self.size != page_aligned_size`. The mapping
202+
// happens at the page-granularity, so it is still replaced.
203+
self.size,
204+
self.prot,
205+
self.flags | libc::MAP_FIXED, // Legal since we already reserved VA space
206+
fd,
207+
offset as libc::off_t,
208+
)
209+
};
210+
211+
// Don't leak the VA.
212+
if addr == libc::MAP_FAILED {
213+
let e = io::Error::last_os_error();
214+
// SAFETY: Calling as documented.
215+
unsafe { libc::munmap(va_reserved_addr, va_reserved_size) };
216+
return Err(Error::Mmap(e));
217+
}
218+
219+
if va_prefix_size > 0 {
220+
// SAFETY: Calling as documented.
221+
unsafe { libc::munmap(va_reserved_addr, va_prefix_size) };
222+
}
223+
224+
if va_suffix_size > 0 {
225+
// SAFETY: Calling as documented.
226+
unsafe { libc::munmap(va_suffix_addr, va_suffix_size) };
227+
}
228+
229+
addr
151230
};
152231

153232
#[cfg(not(miri))]
@@ -163,12 +242,15 @@ impl<B: Bitmap> MmapRegionBuilder<B> {
163242
// Miri does not support the mmap syscall, so we use rust's allocator for miri tests
164243
#[cfg(miri)]
165244
let addr = unsafe {
166-
std::alloc::alloc_zeroed(std::alloc::Layout::from_size_align(self.size, 8).unwrap())
245+
std::alloc::alloc_zeroed(
246+
std::alloc::Layout::from_size_align(self.size, self.alignment).unwrap(),
247+
)
167248
};
168249

169250
Ok(MmapRegion {
170251
addr: addr as *mut u8,
171252
size: self.size,
253+
alignment: self.alignment,
172254
bitmap: self.bitmap,
173255
file_offset: self.file_offset,
174256
prot: self.prot,
@@ -179,19 +261,17 @@ impl<B: Bitmap> MmapRegionBuilder<B> {
179261
}
180262

181263
fn build_raw(self) -> Result<MmapRegion<B>> {
182-
// SAFETY: Safe because this call just returns the page size and doesn't have any side
183-
// effects.
184-
let page_size = unsafe { libc::sysconf(libc::_SC_PAGESIZE) } as usize;
185264
let addr = self.raw_ptr.unwrap();
186265

187266
// Check that the pointer to the mapping is page-aligned.
188-
if (addr as usize) & (page_size - 1) != 0 {
267+
if (addr as usize) & (self.alignment - 1) != 0 {
189268
return Err(Error::InvalidPointer);
190269
}
191270

192271
Ok(MmapRegion {
193272
addr,
194273
size: self.size,
274+
alignment: self.alignment,
195275
bitmap: self.bitmap,
196276
file_offset: self.file_offset,
197277
prot: self.prot,
@@ -215,6 +295,7 @@ impl<B: Bitmap> MmapRegionBuilder<B> {
215295
pub struct MmapRegion<B = ()> {
216296
addr: *mut u8,
217297
size: usize,
298+
alignment: usize,
218299
bitmap: B,
219300
file_offset: Option<FileOffset>,
220301
prot: i32,
@@ -323,6 +404,11 @@ impl<B: Bitmap> MmapRegion<B> {
323404
self.size
324405
}
325406

407+
/// Returns the minimum alignment this region is guaranteed to have.
408+
pub fn minimum_alignment(&self) -> usize {
409+
self.alignment
410+
}
411+
326412
/// Returns information regarding the offset into the file backing this region (if any).
327413
pub fn file_offset(&self) -> Option<&FileOffset> {
328414
self.file_offset.as_ref()
@@ -425,7 +511,7 @@ impl<B> Drop for MmapRegion<B> {
425511
#[cfg(miri)]
426512
std::alloc::dealloc(
427513
self.addr,
428-
std::alloc::Layout::from_size_align(self.size, 8).unwrap(),
514+
std::alloc::Layout::from_size_align(self.size, self.alignment).unwrap(),
429515
);
430516
}
431517
}
@@ -617,6 +703,52 @@ mod tests {
617703
assert!(!r.owned());
618704
}
619705

706+
#[test]
707+
fn test_mmap_alignment() {
708+
let page_size = system_page_size();
709+
let region_size = page_size - 1;
710+
711+
let builder = MmapRegionBuilder::new_with_bitmap(region_size, ());
712+
matches!(
713+
builder.with_minimum_alignment(page_size + 1),
714+
Err(Error::InvalidAlignment)
715+
);
716+
717+
let builder = MmapRegionBuilder::new_with_bitmap(region_size, ());
718+
matches!(
719+
builder.with_minimum_alignment(page_size - 1),
720+
Err(Error::InvalidAlignment)
721+
);
722+
723+
let builder = MmapRegionBuilder::new_with_bitmap(region_size, ());
724+
matches!(
725+
builder.with_minimum_alignment(page_size * 3),
726+
Err(Error::InvalidAlignment)
727+
);
728+
729+
let builder = MmapRegionBuilder::new_with_bitmap(region_size, ());
730+
let mapping = builder.build().unwrap();
731+
732+
assert_eq!(mapping.minimum_alignment(), page_size);
733+
// Technically we can get lucky and have the alignment naturally fall on a 1 GiB boundary,
734+
// so we can't assert that we DON'T here.
735+
}
736+
737+
#[test]
738+
#[cfg(not(miri))] // 1 GiB alignment is too big for Miri
739+
fn test_mmap_alignment_1gib() {
740+
let page_size = system_page_size();
741+
let region_size = page_size - 1;
742+
743+
let align_1gb = 1024 * 1024 * 1024; // 1 GiB
744+
let builder = MmapRegionBuilder::new_with_bitmap(region_size, ())
745+
.with_minimum_alignment(align_1gb)
746+
.unwrap();
747+
let mapping = builder.build().unwrap();
748+
assert_eq!(mapping.minimum_alignment(), align_1gb);
749+
assert_eq!((mapping.as_ptr() as usize) % align_1gb, 0);
750+
}
751+
620752
#[test]
621753
#[cfg(not(miri))] // Miri cannot mmap files
622754
fn test_mmap_region_fds_overlap() {

0 commit comments

Comments
 (0)