Skip to main content

sparse_mmap/
unix.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Linux implementation for memory mapping abstractions.
5
6#![cfg(unix)]
7
8use pal::unix::SyscallResult;
9use std::ffi::c_void;
10use std::fs::File;
11use std::io;
12use std::io::Error;
13use std::os::unix::prelude::*;
14use std::ptr::null_mut;
15use std::sync::atomic::AtomicUsize;
16use std::sync::atomic::Ordering;
17
18pub(crate) fn page_size() -> usize {
19    static PAGE_SIZE: AtomicUsize = AtomicUsize::new(0);
20    let s = PAGE_SIZE.load(Ordering::Relaxed);
21    if s != 0 {
22        s
23    } else {
24        let s = unsafe { libc::sysconf(libc::_SC_PAGESIZE) as usize };
25        PAGE_SIZE.store(s, Ordering::Relaxed);
26        s
27    }
28}
29
30/// A reserved virtual address range that may be partially populated with memory
31/// mappings.
32#[derive(Debug)]
33pub struct SparseMapping {
34    address: *mut c_void,
35    len: usize,
36}
37
38/// An owned handle to an OS object that can be mapped into a [`SparseMapping`].
39///
40/// On Windows, this is a section handle. On Linux, it is a file descriptor.
41pub type Mappable = OwnedFd;
42
43/// An object that can be mapped into a `SparseMapping`.
44///
45/// On Windows, this is a section handle. On Linux, it is a file descriptor.
46pub use std::os::unix::io::AsFd as AsMappableRef;
47
48/// A reference to an object that can be mapped into a [`SparseMapping`].
49///
50/// On Windows, this is a section handle. On Linux, it is a file descriptor.
51pub type MappableRef<'a> = BorrowedFd<'a>;
52
53/// Creates a new mappable from a file.
54///
55/// N.B. `writable` and `executable` have no effect on Linux.
56pub fn new_mappable_from_file(
57    file: &File,
58    _writable: bool,
59    _executable: bool,
60) -> io::Result<Mappable> {
61    file.as_fd().try_clone_to_owned()
62}
63
64// SAFETY: SparseMapping's internal pointer represents an owned virtual address
65// range. There is no safety issue accessing this pointer across threads.
66unsafe impl Send for SparseMapping {}
67// SAFETY: See above comment
68unsafe impl Sync for SparseMapping {}
69
70unsafe fn mmap(
71    addr: *mut c_void,
72    len: usize,
73    prot: i32,
74    flags: i32,
75    fd: i32,
76    offset: i64,
77) -> Result<*mut c_void, Error> {
78    let address = unsafe { libc::mmap(addr, len, prot, flags, fd, offset) };
79    if address == libc::MAP_FAILED {
80        return Err(Error::last_os_error());
81    }
82    Ok(address)
83}
84
85unsafe fn munmap(addr: *mut c_void, len: usize) -> Result<(), Error> {
86    if unsafe { libc::munmap(addr, len) } < 0 {
87        return Err(Error::last_os_error());
88    }
89    Ok(())
90}
91
92impl SparseMapping {
93    /// Reserves a sparse mapping range with the given size.
94    ///
95    /// The range will be aligned to the largest system page size that's smaller
96    /// or equal to `len`.
97    pub fn new(len: usize) -> Result<Self, Error> {
98        Self::new_with_minimum_alignment(len, 1)
99    }
100
101    /// Reserves a sparse mapping range with at least the requested alignment.
102    pub fn new_with_minimum_alignment(len: usize, minimum_alignment: usize) -> Result<Self, Error> {
103        trycopy::initialize_try_copy();
104
105        // Length of 0 return an OS error, so we need to handle it explicitly.
106        if len == 0 {
107            return Err(Error::new(
108                io::ErrorKind::InvalidInput,
109                "length must be greater than 0",
110            ));
111        }
112
113        let page_size = page_size();
114        let alignment = crate::reservation_alignment(len, minimum_alignment)?;
115
116        let len = len
117            .checked_add(alignment - 1)
118            .map(|temp| temp & !(alignment - 1))
119            .ok_or_else(|| {
120                Error::new(
121                    io::ErrorKind::InvalidInput,
122                    "length and alignment combination causes overflow",
123                )
124            })?;
125
126        let alloc_len = len
127            .checked_add(alignment)
128            .map(|temp| temp - page_size)
129            .ok_or_else(|| {
130                Error::new(
131                    io::ErrorKind::InvalidInput,
132                    "length and alignment combination causes overflow",
133                )
134            })?;
135
136        // SAFETY: calling mmap to allocate a new range.
137        let address = unsafe {
138            mmap(
139                null_mut(),
140                alloc_len,
141                libc::PROT_NONE,
142                libc::MAP_PRIVATE | libc::MAP_ANONYMOUS,
143                -1,
144                0,
145            )? as usize
146        };
147        let aligned_address = (address + alignment - 1) & !(alignment - 1);
148        let end = address + alloc_len;
149        let aligned_end = aligned_address + len;
150        assert!(aligned_end <= end);
151
152        if address != aligned_address {
153            // SAFETY: freeing VA just allocated above.
154            unsafe { munmap(address as *mut _, aligned_address - address).unwrap() };
155        }
156        if aligned_end != end {
157            // SAFETY: freeing VA just allocated above.
158            unsafe { munmap(aligned_end as *mut _, end - aligned_end).unwrap() };
159        }
160        Ok(Self {
161            address: aligned_address as *mut _,
162            len,
163        })
164    }
165
166    /// Returns true if the mapping is local to the current process.
167    pub fn is_local(&self) -> bool {
168        true
169    }
170
171    /// Returns the pointer to the beginning of the sparse mapping.
172    pub fn as_ptr(&self) -> *mut c_void {
173        self.address
174    }
175
176    /// Returns the length of the mapping, in bytes.
177    pub fn len(&self) -> usize {
178        self.len
179    }
180
181    fn validate_offset_len(&self, offset: usize, len: usize) -> io::Result<usize> {
182        let end = offset.checked_add(len).ok_or(io::ErrorKind::InvalidInput)?;
183        let page_size = page_size();
184        if !offset.is_multiple_of(page_size) || !end.is_multiple_of(page_size) || end > self.len {
185            return Err(io::ErrorKind::InvalidInput.into());
186        }
187        Ok(end)
188    }
189
190    /// Allocates private, writable memory at the given offset within the mapping.
191    pub fn alloc(&self, offset: usize, len: usize) -> Result<(), Error> {
192        // SAFETY: The flags passed in are guaranteed to be valid
193        unsafe {
194            self.mmap_anonymous(
195                offset,
196                len,
197                libc::PROT_READ | libc::PROT_WRITE,
198                libc::MAP_PRIVATE,
199            )
200        }
201    }
202
203    /// Maps read-only zero pages at the given offset within the mapping.
204    pub fn map_zero(&self, offset: usize, len: usize) -> Result<(), Error> {
205        // SAFETY: The flags passed in are guaranteed to be valid
206        unsafe { self.mmap_anonymous(offset, len, libc::PROT_READ, libc::MAP_PRIVATE) }
207    }
208
209    /// Updates the protection flags of the mapping at the given offset and length
210    /// to allow or disallow writes.
211    pub fn set_writable(&self, offset: usize, len: usize, allow_writes: bool) -> Result<(), Error> {
212        let prot = if allow_writes {
213            libc::PROT_READ | libc::PROT_WRITE
214        } else {
215            libc::PROT_READ
216        };
217        self.mprotect(offset, len, prot)
218    }
219
220    /// Calls `mprotect` on the mapping at the given offset and length, changing
221    /// the protection flags to `prot`.
222    fn mprotect(&self, offset: usize, len: usize, prot: i32) -> Result<(), Error> {
223        self.validate_offset_len(offset, len)?;
224        if prot & !(libc::PROT_READ | libc::PROT_WRITE) != 0 {
225            return Err(Error::new(
226                io::ErrorKind::InvalidInput,
227                "unsupported protection flags",
228            ));
229        }
230        // SAFETY: The flags and address passed in are guaranteed to be valid.
231        unsafe {
232            if libc::mprotect(self.address.add(offset), len, prot) < 0 {
233                return Err(Error::last_os_error());
234            }
235        }
236        Ok(())
237    }
238
239    /// Maps a portion of a file mapping at `offset`.
240    pub fn map_file(
241        &self,
242        offset: usize,
243        len: usize,
244        file_mapping: impl AsFd,
245        file_offset: u64,
246        writable: bool,
247    ) -> Result<(), Error> {
248        let prot = if writable {
249            libc::PROT_READ | libc::PROT_WRITE
250        } else {
251            libc::PROT_READ
252        };
253
254        // SAFETY: The flags passed in are guaranteed to be valid. MAP_SHARED is required.
255        unsafe {
256            self.mmap(
257                offset,
258                len,
259                prot,
260                libc::MAP_SHARED,
261                file_mapping.as_fd(),
262                file_offset as i64,
263            )
264        }
265    }
266
267    /// Calls `mbind(MPOL_BIND)` on a range within this mapping, binding
268    /// pages to a specific host NUMA node.
269    ///
270    /// The range at `offset..offset+len` must already be mapped (via
271    /// `alloc`, `map_file`, etc.) before calling this.
272    #[cfg(target_os = "linux")]
273    pub fn mbind_at(&self, offset: usize, len: usize, numa_node: u32) -> Result<(), Error> {
274        let _ = self.validate_offset_len(offset, len)?;
275        // SAFETY: validate_offset_len confirmed offset+len is within the
276        // mapping, so `self.address + offset` is valid for `len` bytes.
277        unsafe { mbind_range(self.address.add(offset), len, numa_node) }
278    }
279
280    /// Maps memory into the mapping, passing parameters through to the mmap
281    /// syscall.
282    ///
283    /// # Safety
284    ///
285    /// This routine is safe to use as long as the caller ensures `map_flags` excludes
286    /// any flags that render the memory region non-unmappable (e.g., `MAP_LOCKED`).
287    /// Misuse may lead to system resource issues, such as falsely perceived out-of-memory
288    /// conditions.
289    pub unsafe fn mmap(
290        &self,
291        offset: usize,
292        len: usize,
293        prot: i32,
294        map_flags: i32,
295        fd: impl AsFd,
296        file_offset: i64,
297    ) -> Result<(), Error> {
298        let _ = self.validate_offset_len(offset, len)?;
299
300        // SAFETY: guaranteed by caller and offset + len checks above
301        unsafe {
302            let address = self.address.add(offset);
303            let mapped_address = mmap(
304                address,
305                len,
306                prot,
307                map_flags | libc::MAP_FIXED,
308                fd.as_fd().as_raw_fd(),
309                file_offset,
310            )?;
311            assert_eq!(mapped_address, address);
312        }
313        Ok(())
314    }
315
316    /// Maps anonymous memory into the mapping, with parameters for the mmap syscall.
317    ///
318    /// # Safety
319    ///
320    /// This routine is safe to use as long as the caller ensures `map_flags` excludes
321    /// any flags that render the memory region non-unmappable (e.g., `MAP_LOCKED`).
322    /// Misuse may lead to system resource issues, such as falsely perceived out-of-memory
323    /// conditions.
324    pub unsafe fn mmap_anonymous(
325        &self,
326        offset: usize,
327        len: usize,
328        prot: i32,
329        map_flags: i32,
330    ) -> io::Result<()> {
331        let _ = self.validate_offset_len(offset, len)?;
332
333        // SAFETY: guaranteed by caller and offset + len checks above
334        unsafe {
335            let address = self.address.add(offset);
336            let mapped_address = mmap(
337                address,
338                len,
339                prot,
340                map_flags | libc::MAP_ANONYMOUS | libc::MAP_FIXED,
341                -1,
342                0,
343            )?;
344            assert_eq!(mapped_address, address);
345        }
346        Ok(())
347    }
348
349    /// Decommits a range of memory, releasing physical pages back to the host.
350    ///
351    /// The virtual address range remains accessible; the next access will get
352    /// fresh zero pages from the kernel.
353    pub fn decommit(&self, offset: usize, len: usize) -> Result<(), Error> {
354        let _ = self.validate_offset_len(offset, len)?;
355        if len == 0 {
356            return Ok(());
357        }
358        // SAFETY: the address and length have been validated above.
359        unsafe {
360            let addr = self.address.add(offset);
361            if libc::madvise(addr, len, libc::MADV_DONTNEED) < 0 {
362                return Err(Error::last_os_error());
363            }
364        }
365        Ok(())
366    }
367
368    /// Marks a range as eligible for Transparent Huge Pages.
369    ///
370    /// This calls `madvise(MADV_HUGEPAGE)` so that khugepaged can collapse
371    /// small pages into huge pages. It applies to anonymous mappings and to
372    /// file-backed mappings whose filesystem supports THP, such as shmem/tmpfs
373    /// mappings when enabled by the kernel's shmem THP policy. Success records
374    /// the advice but does not guarantee that huge pages will be allocated.
375    #[cfg(target_os = "linux")]
376    pub fn madvise_hugepage(&self, offset: usize, len: usize) -> Result<(), Error> {
377        let _ = self.validate_offset_len(offset, len)?;
378        if len == 0 {
379            return Ok(());
380        }
381        // SAFETY: the address and length have been validated above.
382        unsafe {
383            let addr = self.address.add(offset);
384            if libc::madvise(addr, len, libc::MADV_HUGEPAGE) < 0 {
385                return Err(Error::last_os_error());
386            }
387        }
388        Ok(())
389    }
390
391    /// Names an anonymous mapping range so it appears as `[anon:name]` in
392    /// `/proc/{pid}/smaps` and related tools.
393    ///
394    /// Uses `prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME)`. If the prctl fails
395    /// (e.g. on older kernels), the error is silently ignored. No-op on
396    /// non-Linux platforms.
397    #[cfg(target_os = "linux")]
398    pub fn set_name(&self, offset: usize, len: usize, name: &str) {
399        if len == 0 {
400            return;
401        }
402        if self.validate_offset_len(offset, len).is_err() {
403            return;
404        }
405        let Ok(name) = std::ffi::CString::new(name) else {
406            return;
407        };
408        // SAFETY: address and length are validated, name is a valid CString.
409        unsafe {
410            libc::prctl(
411                libc::PR_SET_VMA,
412                libc::PR_SET_VMA_ANON_NAME,
413                self.address.add(offset),
414                len,
415                name.as_ptr(),
416            );
417        }
418    }
419
420    /// Names a mapping range for debugging. No-op on non-Linux Unix platforms.
421    #[cfg(not(target_os = "linux"))]
422    pub fn set_name(&self, _offset: usize, _len: usize, _name: &str) {}
423
424    /// Commits a range of memory, making it accessible.
425    ///
426    /// On Linux, this is a no-op because the kernel handles page faults
427    /// transparently for anonymous memory.
428    pub fn commit(&self, offset: usize, len: usize) -> Result<(), Error> {
429        let _ = self.validate_offset_len(offset, len)?;
430        Ok(())
431    }
432
433    /// Unmaps memory from the mapping.
434    pub fn unmap(&self, offset: usize, len: usize) -> io::Result<()> {
435        let _ = self.validate_offset_len(offset, len)?;
436
437        // Skipping this check would result in the "expect" below
438        if len == 0 {
439            return Err(io::ErrorKind::InvalidInput.into());
440        }
441
442        // Remap to PROT_NONE to preserve the reservation.
443        // SAFETY: guaranteed by caller and offset + len checks above
444        unsafe {
445            let address = self.address.add(offset);
446            let mapped_address = mmap(
447                address,
448                len,
449                libc::PROT_NONE,
450                libc::MAP_PRIVATE | libc::MAP_ANONYMOUS | libc::MAP_FIXED,
451                -1,
452                0,
453            )
454            .expect("remap to PROT_NONE should not fail (except for low resources)");
455            assert_eq!(mapped_address, address);
456        }
457        Ok(())
458    }
459}
460
461impl Drop for SparseMapping {
462    fn drop(&mut self) {
463        unsafe {
464            libc::munmap(self.address, self.len)
465                .syscall_result()
466                .expect("unmap should not fail");
467        }
468    }
469}
470#[cfg(target_os = "linux")]
471fn new_memfd(name: &str, flags: libc::c_uint) -> io::Result<File> {
472    let name =
473        std::ffi::CString::new(name).map_err(|e| Error::new(io::ErrorKind::InvalidInput, e))?;
474    // SAFETY: creating and truncating a new file descriptor according to
475    // the documented contract.
476    unsafe {
477        let fd = libc::memfd_create(name.as_ptr(), flags).syscall_result()?;
478        Ok(File::from_raw_fd(fd))
479    }
480}
481
482#[cfg(not(target_os = "linux"))]
483fn new_memfd(_name: &str) -> io::Result<File> {
484    // Use a random name because shm_open creates objects in a global namespace.
485    // A predictable name would allow other processes to collide with or squat
486    // on the name. There is not enough room to include the user-provided name.
487    let mut rand = [0; 16];
488    getrandom::fill(&mut rand).unwrap();
489    let mut name = format!("{:x}", u128::from_ne_bytes(rand));
490    // macOS limits the name length to 31 bytes, which is sufficient to ensure uniqueness.
491    name.truncate(31);
492    let name = std::ffi::CString::new(name).unwrap();
493    unsafe {
494        // Create a new shared memory object.
495        let fd = libc::shm_open(name.as_ptr(), libc::O_RDWR | libc::O_EXCL | libc::O_CREAT)
496            .syscall_result()?;
497        // Unlink it to make it anonymous.
498        let _ = libc::shm_unlink(name.as_ptr());
499        Ok(File::from_raw_fd(fd))
500    }
501}
502
503/// Allocates a mappable shared memory object of `size` bytes.
504///
505/// `name` labels the memfd so it appears as `/memfd:<name>` in
506/// `/proc/{pid}/smaps` on Linux.
507pub fn alloc_shared_memory(size: usize, name: &str) -> io::Result<OwnedFd> {
508    #[cfg(target_os = "linux")]
509    let fd = new_memfd(name, libc::MFD_CLOEXEC)?;
510    #[cfg(not(target_os = "linux"))]
511    let fd = new_memfd(name)?;
512    fd.set_len(size as u64)?;
513    Ok(fd.into())
514}
515
516/// Allocates a hugetlb mappable shared memory object of `size` bytes.
517///
518/// If `hugepage_size` is specified, it is encoded in the memfd flags using
519/// the Linux `MFD_HUGE_*` convention.
520#[cfg(target_os = "linux")]
521pub fn alloc_shared_memory_hugetlb(
522    size: usize,
523    name: &str,
524    hugepage_size: Option<usize>,
525    _numa_node: Option<u32>,
526) -> io::Result<OwnedFd> {
527    const MFD_HUGE_SHIFT: libc::c_uint = 26;
528
529    let mut flags = libc::MFD_CLOEXEC | libc::MFD_HUGETLB;
530    if let Some(hugepage_size) = hugepage_size {
531        if !hugepage_size.is_power_of_two() {
532            return Err(Error::new(
533                io::ErrorKind::InvalidInput,
534                "hugepage size must be a power of two",
535            ));
536        }
537        flags |= (hugepage_size.trailing_zeros() as libc::c_uint) << MFD_HUGE_SHIFT;
538    }
539
540    let fd = new_memfd(name, flags)?;
541    let size = libc::off_t::try_from(size).map_err(|_| {
542        Error::new(
543            io::ErrorKind::InvalidInput,
544            "hugetlb allocation size is too large",
545        )
546    })?;
547
548    // Unlike ftruncate, fallocate forces hugetlb page reservation now, so
549    // insufficient hugepage pools fail during guest RAM allocation instead of
550    // later when the lazy VA mapper first mmaps the memfd.
551    unsafe { libc::fallocate(fd.as_raw_fd(), 0, 0, size).syscall_result()? };
552    Ok(fd.into())
553}
554
555/// Allocates a hugetlb mappable shared memory object of `size` bytes.
556#[cfg(not(target_os = "linux"))]
557pub fn alloc_shared_memory_hugetlb(
558    _size: usize,
559    _name: &str,
560    _hugepage_size: Option<usize>,
561    _numa_node: Option<u32>,
562) -> io::Result<OwnedFd> {
563    Err(Error::new(
564        io::ErrorKind::Unsupported,
565        "hugetlb shared memory is only supported on Linux",
566    ))
567}
568
569/// Calls `mbind(MPOL_BIND)` on an already-mapped virtual address range,
570/// binding it to a specific host NUMA node.
571///
572/// # Safety
573///
574/// `addr` must point to a valid mapped region of at least `len` bytes.
575#[cfg(target_os = "linux")]
576unsafe fn mbind_range(addr: *mut c_void, len: usize, numa_node: u32) -> io::Result<()> {
577    // Cap the node ID to prevent accidental large allocations for the
578    // nodemask bitmask below.
579    if numa_node > 0xffff {
580        return Err(Error::new(
581            io::ErrorKind::InvalidInput,
582            "NUMA node exceeds maximum supported value",
583        ));
584    }
585
586    // Build nodemask bitmask. The kernel expects an array of unsigned long with
587    // bit `numa_node` set.
588    //
589    // maxnode should be the number of bits in the nodemask, but the kernel's
590    // get_nodes() has an off-by-one: it decrements maxnode before use, so we
591    // must pass numa_node + 2 instead of numa_node + 1. This is a known kernel
592    // bug since 2004 that will not be fixed (ABI). See
593    // <https://lore.kernel.org/linux-mm/20240720173543.897972-1-jglisse@google.com/>
594    let maxnode = numa_node as usize + 2;
595    let word_bits = libc::c_ulong::BITS as usize;
596    let num_words = maxnode.div_ceil(word_bits);
597    let mut nodemask = vec![0 as libc::c_ulong; num_words];
598    nodemask[numa_node as usize / word_bits] = 1 << (numa_node as usize % word_bits);
599
600    // Use flags = 0: just set the NUMA policy for future page faults.
601    // The memory was just mapped, so there are no resident pages to move.
602    let result = unsafe {
603        libc::syscall(
604            libc::SYS_mbind,
605            addr,
606            len,
607            libc::MPOL_BIND,
608            nodemask.as_ptr(),
609            maxnode,
610            0,
611        )
612    };
613
614    if result == -1 {
615        return Err(Error::last_os_error());
616    }
617
618    Ok(())
619}