Skip to content

Commit 1a9d4b2

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 1a9d4b2

1 file changed

Lines changed: 138 additions & 18 deletions

File tree

src/mmap/unix.rs

Lines changed: 138 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,13 @@ 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) -> Self {
112+
self.alignment = min_align;
113+
self
114+
}
115+
97116
/// Create the `MmapRegion` object with the specified `file_offset`.
98117
pub fn with_file_offset(mut self, file_offset: FileOffset) -> Self {
99118
self.file_offset = Some(file_offset);
@@ -119,6 +138,11 @@ impl<B: Bitmap> MmapRegionBuilder<B> {
119138

120139
/// Build the `MmapRegion` object.
121140
pub fn build(self) -> Result<MmapRegion<B>> {
141+
let page_size = system_page_size();
142+
if !self.alignment.is_power_of_two() || self.alignment < page_size {
143+
return Err(Error::InvalidAlignment);
144+
}
145+
122146
if self.raw_ptr.is_some() {
123147
return self.build_raw();
124148
}
@@ -136,18 +160,74 @@ impl<B: Bitmap> MmapRegionBuilder<B> {
136160
};
137161

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

153233
#[cfg(not(miri))]
@@ -163,12 +243,15 @@ impl<B: Bitmap> MmapRegionBuilder<B> {
163243
// Miri does not support the mmap syscall, so we use rust's allocator for miri tests
164244
#[cfg(miri)]
165245
let addr = unsafe {
166-
std::alloc::alloc_zeroed(std::alloc::Layout::from_size_align(self.size, 8).unwrap())
246+
std::alloc::alloc_zeroed(
247+
std::alloc::Layout::from_size_align(self.size, self.alignment).unwrap(),
248+
)
167249
};
168250

169251
Ok(MmapRegion {
170252
addr: addr as *mut u8,
171253
size: self.size,
254+
alignment: self.alignment,
172255
bitmap: self.bitmap,
173256
file_offset: self.file_offset,
174257
prot: self.prot,
@@ -179,19 +262,18 @@ impl<B: Bitmap> MmapRegionBuilder<B> {
179262
}
180263

181264
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;
185265
let addr = self.raw_ptr.unwrap();
186266

187267
// Check that the pointer to the mapping is page-aligned.
188-
if (addr as usize) & (page_size - 1) != 0 {
268+
// `self.alignment` already validated by caller (`fn build`).
269+
if (addr as usize) & (self.alignment - 1) != 0 {
189270
return Err(Error::InvalidPointer);
190271
}
191272

192273
Ok(MmapRegion {
193274
addr,
194275
size: self.size,
276+
alignment: self.alignment,
195277
bitmap: self.bitmap,
196278
file_offset: self.file_offset,
197279
prot: self.prot,
@@ -215,6 +297,7 @@ impl<B: Bitmap> MmapRegionBuilder<B> {
215297
pub struct MmapRegion<B = ()> {
216298
addr: *mut u8,
217299
size: usize,
300+
alignment: usize,
218301
bitmap: B,
219302
file_offset: Option<FileOffset>,
220303
prot: i32,
@@ -323,6 +406,11 @@ impl<B: Bitmap> MmapRegion<B> {
323406
self.size
324407
}
325408

409+
/// Returns the minimum alignment this region is guaranteed to have.
410+
pub fn minimum_alignment(&self) -> usize {
411+
self.alignment
412+
}
413+
326414
/// Returns information regarding the offset into the file backing this region (if any).
327415
pub fn file_offset(&self) -> Option<&FileOffset> {
328416
self.file_offset.as_ref()
@@ -425,7 +513,7 @@ impl<B> Drop for MmapRegion<B> {
425513
#[cfg(miri)]
426514
std::alloc::dealloc(
427515
self.addr,
428-
std::alloc::Layout::from_size_align(self.size, 8).unwrap(),
516+
std::alloc::Layout::from_size_align(self.size, self.alignment).unwrap(),
429517
);
430518
}
431519
}
@@ -617,6 +705,38 @@ mod tests {
617705
assert!(!r.owned());
618706
}
619707

708+
#[test]
709+
fn test_mmap_alignment() {
710+
let page_size = system_page_size();
711+
let region_size = page_size - 1;
712+
713+
let builder = MmapRegionBuilder::new_with_bitmap(region_size, ())
714+
.with_minimum_alignment(page_size + 1);
715+
matches!(builder.build(), Err(Error::InvalidAlignment));
716+
717+
let builder = MmapRegionBuilder::new_with_bitmap(region_size, ())
718+
.with_minimum_alignment(page_size - 1);
719+
matches!(builder.build(), Err(Error::InvalidAlignment));
720+
721+
let builder = MmapRegionBuilder::new_with_bitmap(region_size, ())
722+
.with_minimum_alignment(page_size * 3); // not a power of two
723+
matches!(builder.build(), Err(Error::InvalidAlignment));
724+
725+
let align_1gb = 1 * 1024 * 1024 * 1024; // 1 GiB
726+
let builder =
727+
MmapRegionBuilder::new_with_bitmap(region_size, ()).with_minimum_alignment(align_1gb);
728+
let mapping = builder.build().unwrap();
729+
assert_eq!(mapping.minimum_alignment(), align_1gb);
730+
assert_eq!((mapping.as_ptr() as usize) % align_1gb, 0);
731+
732+
let builder = MmapRegionBuilder::new_with_bitmap(region_size, ());
733+
let mapping = builder.build().unwrap();
734+
735+
assert_eq!(mapping.minimum_alignment(), page_size);
736+
// Technically we can get lucky and have the alignment naturally fall on a 1 GiB boundary,
737+
// so we can't assert that we DON'T here.
738+
}
739+
620740
#[test]
621741
#[cfg(not(miri))] // Miri cannot mmap files
622742
fn test_mmap_region_fds_overlap() {

0 commit comments

Comments
 (0)