mmtk/util/
memory.rs

1use crate::util::alloc::AllocationError;
2use crate::util::opaque_pointer::*;
3use crate::util::Address;
4use crate::vm::{Collection, VMBinding};
5use bytemuck::NoUninit;
6use libc::{PROT_EXEC, PROT_NONE, PROT_READ, PROT_WRITE};
7use std::io::{Error, Result};
8use sysinfo::MemoryRefreshKind;
9use sysinfo::{RefreshKind, System};
10
11#[cfg(target_os = "linux")]
12// MAP_FIXED_NOREPLACE returns EEXIST if already mapped
13const MMAP_FLAGS: libc::c_int = libc::MAP_ANON | libc::MAP_PRIVATE | libc::MAP_FIXED_NOREPLACE;
14#[cfg(target_os = "macos")]
15// MAP_FIXED is used instead of MAP_FIXED_NOREPLACE (which is not available on macOS). We are at the risk of overwriting pre-existing mappings.
16const MMAP_FLAGS: libc::c_int = libc::MAP_ANON | libc::MAP_PRIVATE | libc::MAP_FIXED;
17
18/// Strategy for performing mmap
19#[derive(Debug, Copy, Clone)]
20pub struct MmapStrategy {
21    /// Do we support huge pages?
22    pub huge_page: HugePageSupport,
23    /// The protection flags for mmap
24    pub prot: MmapProtection,
25}
26
27impl MmapStrategy {
28    /// Create a new strategy
29    pub fn new(transparent_hugepages: bool, prot: MmapProtection) -> Self {
30        Self {
31            huge_page: if transparent_hugepages {
32                HugePageSupport::TransparentHugePages
33            } else {
34                HugePageSupport::No
35            },
36            prot,
37        }
38    }
39
40    /// The strategy for MMTk's own internal memory
41    pub const INTERNAL_MEMORY: Self = Self {
42        huge_page: HugePageSupport::No,
43        prot: MmapProtection::ReadWrite,
44    };
45
46    /// The strategy for MMTk side metadata
47    pub const SIDE_METADATA: Self = Self::INTERNAL_MEMORY;
48
49    /// The strategy for MMTk's test memory
50    #[cfg(test)]
51    pub const TEST: Self = Self::INTERNAL_MEMORY;
52}
53
54/// The protection flags for Mmap
55#[repr(i32)]
56#[derive(Debug, Copy, Clone)]
57pub enum MmapProtection {
58    /// Allow read + write
59    ReadWrite,
60    /// Allow read + write + code execution
61    ReadWriteExec,
62    /// Do not allow any access
63    NoAccess,
64}
65
66impl MmapProtection {
67    /// Turn the protection enum into the native flags
68    pub fn into_native_flags(self) -> libc::c_int {
69        match self {
70            Self::ReadWrite => PROT_READ | PROT_WRITE,
71            Self::ReadWriteExec => PROT_READ | PROT_WRITE | PROT_EXEC,
72            Self::NoAccess => PROT_NONE,
73        }
74    }
75}
76
77/// Support for huge pages
78#[repr(u8)]
79#[derive(Debug, Copy, Clone, NoUninit)]
80pub enum HugePageSupport {
81    /// No support for huge page
82    No,
83    /// Enable transparent huge pages for the pages that are mapped. This option is only for linux.
84    TransparentHugePages,
85}
86
87/// Annotation for an mmap entry.
88///
89/// Invocations of `mmap_fixed` and other functions that may transitively call `mmap_fixed`
90/// require an annotation that indicates the purpose of the memory mapping.
91///
92/// This is for debugging.  On Linux, mmtk-core will use `prctl` with `PR_SET_VMA` to set the
93/// human-readable name for the given mmap region.  The annotation is ignored on other platforms.
94///
95/// Note that when using `Map32` (even when running on 64-bit architectures), the discontiguous
96/// memory range is shared between different spaces. Spaces may use `mmap` to map new chunks, but
97/// the same chunk may later be reused by other spaces. The annotation only applies when `mmap` is
98/// called for a chunk for the first time, which reflects which space first attempted the mmap, not
99/// which space is currently using the chunk.  Use `crate::policy::space::print_vm_map` to print a
100/// more accurate mapping between address ranges and spaces.
101///
102/// On 32-bit architecture, side metadata are allocated in a chunked fasion.  One single `mmap`
103/// region will contain many different metadata.  In that case, we simply annotate the whole region
104/// with a `MmapAnnotation::SideMeta` where `meta` is `"all"`.
105pub enum MmapAnnotation<'a> {
106    /// The mmap is for a space.
107    Space {
108        /// The name of the space.
109        name: &'a str,
110    },
111    /// The mmap is for a side metadata.
112    SideMeta {
113        /// The name of the space.
114        space: &'a str,
115        /// The name of the side metadata.
116        meta: &'a str,
117    },
118    /// The mmap is for a test case.  Usually constructed using the [`mmap_anno_test!`] macro.
119    Test {
120        /// The source file.
121        file: &'a str,
122        /// The line number.
123        line: u32,
124    },
125    /// For all other use cases.
126    Misc {
127        /// A human-readable descriptive name.
128        name: &'a str,
129    },
130}
131
132/// Construct an `MmapAnnotation::Test` with the current file name and line number.
133#[macro_export]
134macro_rules! mmap_anno_test {
135    () => {
136        &$crate::util::memory::MmapAnnotation::Test {
137            file: file!(),
138            line: line!(),
139        }
140    };
141}
142
143// Export this to external crates
144pub use mmap_anno_test;
145
146impl std::fmt::Display for MmapAnnotation<'_> {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        match self {
149            MmapAnnotation::Space { name } => write!(f, "mmtk:space:{name}"),
150            MmapAnnotation::SideMeta { space, meta } => write!(f, "mmtk:sidemeta:{space}:{meta}"),
151            MmapAnnotation::Test { file, line } => write!(f, "mmtk:test:{file}:{line}"),
152            MmapAnnotation::Misc { name } => write!(f, "mmtk:misc:{name}"),
153        }
154    }
155}
156
157/// Check the result from an mmap function in this module.
158/// Return true if the mmap has failed due to an existing conflicting mapping.
159pub(crate) fn result_is_mapped(result: Result<()>) -> bool {
160    match result {
161        Ok(_) => false,
162        Err(err) => err.raw_os_error().unwrap() == libc::EEXIST,
163    }
164}
165
166/// Set a range of memory to 0.
167pub fn zero(start: Address, len: usize) {
168    set(start, 0, len);
169}
170
171/// Set a range of memory to the given value. Similar to memset.
172pub fn set(start: Address, val: u8, len: usize) {
173    unsafe {
174        std::ptr::write_bytes::<u8>(start.to_mut_ptr(), val, len);
175    }
176}
177
178/// Demand-zero mmap:
179/// This function mmaps the memory and guarantees to zero all mapped memory.
180/// This function WILL overwrite existing memory mapping. The user of this function
181/// needs to be aware of this, and use it cautiously.
182///
183/// # Safety
184/// This function WILL overwrite existing memory mapping if there is any. So only use this function if you know
185/// the memory has been reserved by mmtk (e.g. after the use of mmap_noreserve()). Otherwise using this function
186/// may corrupt others' data.
187#[allow(clippy::let_and_return)] // Zeroing is not neceesary for some OS/s
188pub unsafe fn dzmmap(
189    start: Address,
190    size: usize,
191    strategy: MmapStrategy,
192    anno: &MmapAnnotation,
193) -> Result<()> {
194    let flags = libc::MAP_ANON | libc::MAP_PRIVATE | libc::MAP_FIXED;
195    let ret = mmap_fixed(start, size, flags, strategy, anno);
196    // We do not need to explicitly zero for Linux (memory is guaranteed to be zeroed)
197    #[cfg(not(target_os = "linux"))]
198    if ret.is_ok() {
199        zero(start, size)
200    }
201    ret
202}
203/// Demand-zero mmap (no replace):
204/// This function mmaps the memory and guarantees to zero all mapped memory.
205/// This function will not overwrite existing memory mapping, and it will result Err if there is an existing mapping.
206#[allow(clippy::let_and_return)] // Zeroing is not neceesary for some OS/s
207pub fn dzmmap_noreplace(
208    start: Address,
209    size: usize,
210    strategy: MmapStrategy,
211    anno: &MmapAnnotation,
212) -> Result<()> {
213    let flags = MMAP_FLAGS;
214    let ret = mmap_fixed(start, size, flags, strategy, anno);
215    // We do not need to explicitly zero for Linux (memory is guaranteed to be zeroed)
216    #[cfg(not(target_os = "linux"))]
217    if ret.is_ok() {
218        zero(start, size)
219    }
220    ret
221}
222
223/// mmap with no swap space reserve:
224/// This function does not reserve swap space for this mapping, which means there is no guarantee that writes to the
225/// mapping can always be successful. In case of out of physical memory, one may get a segfault for writing to the mapping.
226/// We can use this to reserve the address range, and then later overwrites the mapping with dzmmap().
227pub fn mmap_noreserve(
228    start: Address,
229    size: usize,
230    mut strategy: MmapStrategy,
231    anno: &MmapAnnotation,
232) -> Result<()> {
233    strategy.prot = MmapProtection::NoAccess;
234    let flags = MMAP_FLAGS | libc::MAP_NORESERVE;
235    mmap_fixed(start, size, flags, strategy, anno)
236}
237
238fn mmap_fixed(
239    start: Address,
240    size: usize,
241    flags: libc::c_int,
242    strategy: MmapStrategy,
243    _anno: &MmapAnnotation,
244) -> Result<()> {
245    let ptr = start.to_mut_ptr();
246    let prot = strategy.prot.into_native_flags();
247    wrap_libc_call(
248        &|| unsafe { libc::mmap(start.to_mut_ptr(), size, prot, flags, -1, 0) },
249        ptr,
250    )?;
251
252    #[cfg(all(
253        any(target_os = "linux", target_os = "android"),
254        not(feature = "no_mmap_annotation")
255    ))]
256    {
257        // `PR_SET_VMA` is new in Linux 5.17.  We compile against a version of the `libc` crate that
258        // has the `PR_SET_VMA_ANON_NAME` constant.  When runnning on an older kernel, it will not
259        // recognize this attribute and will return `EINVAL`.  However, `prctl` may return `EINVAL`
260        // for other reasons, too.  That includes `start` being an invalid address, and the
261        // formatted `anno_cstr` being longer than 80 bytes including the trailing `'\0'`.  But
262        // since this prctl is used for debugging, we log the error instead of panicking.
263        let anno_str = _anno.to_string();
264        let anno_cstr = std::ffi::CString::new(anno_str).unwrap();
265        let result = wrap_libc_call(
266            &|| unsafe {
267                libc::prctl(
268                    libc::PR_SET_VMA,
269                    libc::PR_SET_VMA_ANON_NAME,
270                    start.to_ptr::<libc::c_void>(),
271                    size,
272                    anno_cstr.as_ptr(),
273                )
274            },
275            0,
276        );
277        if let Err(e) = result {
278            debug!("Error while calling prctl: {e}");
279        }
280    }
281
282    match strategy.huge_page {
283        HugePageSupport::No => Ok(()),
284        HugePageSupport::TransparentHugePages => {
285            #[cfg(target_os = "linux")]
286            {
287                wrap_libc_call(
288                    &|| unsafe { libc::madvise(start.to_mut_ptr(), size, libc::MADV_HUGEPAGE) },
289                    0,
290                )
291            }
292            // Setting the transparent hugepage option to true will not pass
293            // the validation on non-Linux OSes
294            #[cfg(not(target_os = "linux"))]
295            unreachable!()
296        }
297    }
298}
299
300/// Unmap the given memory (in page granularity). This wraps the unsafe libc munmap call.
301pub fn munmap(start: Address, size: usize) -> Result<()> {
302    wrap_libc_call(&|| unsafe { libc::munmap(start.to_mut_ptr(), size) }, 0)
303}
304
305/// Properly handle errors from a mmap Result, including invoking the binding code in the case of
306/// an OOM error.
307pub fn handle_mmap_error<VM: VMBinding>(
308    error: Error,
309    tls: VMThread,
310    addr: Address,
311    bytes: usize,
312) -> ! {
313    use std::io::ErrorKind;
314
315    eprintln!("Failed to mmap {}, size {}", addr, bytes);
316    eprintln!("{}", get_process_memory_maps());
317
318    match error.kind() {
319        // From Rust nightly 2021-05-12, we started to see Rust added this ErrorKind.
320        ErrorKind::OutOfMemory => {
321            // Signal `MmapOutOfMemory`. Expect the VM to abort immediately.
322            trace!("Signal MmapOutOfMemory!");
323            VM::VMCollection::out_of_memory(tls, AllocationError::MmapOutOfMemory);
324            unreachable!()
325        }
326        // Before Rust had ErrorKind::OutOfMemory, this is how we capture OOM from OS calls.
327        // TODO: We may be able to remove this now.
328        ErrorKind::Other => {
329            // further check the error
330            if let Some(os_errno) = error.raw_os_error() {
331                // If it is OOM, we invoke out_of_memory() through the VM interface.
332                if os_errno == libc::ENOMEM {
333                    // Signal `MmapOutOfMemory`. Expect the VM to abort immediately.
334                    trace!("Signal MmapOutOfMemory!");
335                    VM::VMCollection::out_of_memory(tls, AllocationError::MmapOutOfMemory);
336                    unreachable!()
337                }
338            }
339        }
340        ErrorKind::AlreadyExists => {
341            panic!("Failed to mmap, the address is already mapped. Should MMTk quarantine the address range first?");
342        }
343        _ => {}
344    }
345    panic!("Unexpected mmap failure: {:?}", error)
346}
347
348/// Checks if the memory has already been mapped. If not, we panic.
349///
350/// Note that the checking has a side effect that it will map the memory if it was unmapped. So we panic if it was unmapped.
351/// Be very careful about using this function.
352///
353/// This function is currently left empty for non-linux, and should be implemented in the future.
354/// As the function is only used for assertions, MMTk will still run even if we never panic.
355pub(crate) fn panic_if_unmapped(_start: Address, _size: usize, _anno: &MmapAnnotation) {
356    #[cfg(target_os = "linux")]
357    {
358        let flags = MMAP_FLAGS;
359        match mmap_fixed(
360            _start,
361            _size,
362            flags,
363            MmapStrategy {
364                huge_page: HugePageSupport::No,
365                prot: MmapProtection::ReadWrite,
366            },
367            _anno,
368        ) {
369            Ok(_) => panic!("{} of size {} is not mapped", _start, _size),
370            Err(e) => {
371                assert!(
372                    e.kind() == std::io::ErrorKind::AlreadyExists,
373                    "Failed to check mapped: {:?}",
374                    e
375                );
376            }
377        }
378    }
379}
380
381/// Unprotect the given memory (in page granularity) to allow access (PROT_READ/WRITE/EXEC).
382pub fn munprotect(start: Address, size: usize, prot: MmapProtection) -> Result<()> {
383    let prot = prot.into_native_flags();
384    wrap_libc_call(
385        &|| unsafe { libc::mprotect(start.to_mut_ptr(), size, prot) },
386        0,
387    )
388}
389
390/// Protect the given memory (in page granularity) to forbid any access (PROT_NONE).
391pub fn mprotect(start: Address, size: usize) -> Result<()> {
392    wrap_libc_call(
393        &|| unsafe { libc::mprotect(start.to_mut_ptr(), size, PROT_NONE) },
394        0,
395    )
396}
397
398fn wrap_libc_call<T: PartialEq>(f: &dyn Fn() -> T, expect: T) -> Result<()> {
399    let ret = f();
400    if ret == expect {
401        Ok(())
402    } else {
403        Err(std::io::Error::last_os_error())
404    }
405}
406
407/// Get the memory maps for the process. The returned string is a multi-line string.
408/// This is only meant to be used for debugging. For example, log process memory maps after detecting a clash.
409#[cfg(any(target_os = "linux", target_os = "android"))]
410pub fn get_process_memory_maps() -> String {
411    // print map
412    use std::fs::File;
413    use std::io::Read;
414    let mut data = String::new();
415    let mut f = File::open("/proc/self/maps").unwrap();
416    f.read_to_string(&mut data).unwrap();
417    data
418}
419
420/// Get the memory maps for the process. The returned string is a multi-line string.
421/// This is only meant to be used for debugging. For example, log process memory maps after detecting a clash.
422#[cfg(target_os = "macos")]
423pub fn get_process_memory_maps() -> String {
424    // Get the current process ID (replace this with a specific PID if needed)
425    let pid = std::process::id();
426
427    // Execute the vmmap command
428    let output = std::process::Command::new("vmmap")
429        .arg(pid.to_string()) // Pass the PID as an argument
430        .output() // Capture the output
431        .expect("Failed to execute vmmap command");
432
433    // Check if the command was successful
434    if output.status.success() {
435        // Convert the command output to a string
436        let output_str =
437            std::str::from_utf8(&output.stdout).expect("Failed to convert output to string");
438        output_str.to_string()
439    } else {
440        // Handle the error case
441        let error_message =
442            std::str::from_utf8(&output.stderr).expect("Failed to convert error message to string");
443        panic!("Failed to get process memory map: {}", error_message)
444    }
445}
446
447/// Get the memory maps for the process. The returned string is a multi-line string.
448/// This is only meant to be used for debugging. For example, log process memory maps after detecting a clash.
449#[cfg(not(any(target_os = "linux", target_os = "android", target_os = "macos")))]
450pub fn get_process_memory_maps() -> String {
451    "(process map unavailable)".to_string()
452}
453
454/// Returns the total physical memory for the system in bytes.
455pub(crate) fn get_system_total_memory() -> u64 {
456    // TODO: Note that if we want to get system info somewhere else in the future, we should
457    // refactor this instance into some global struct. sysinfo recommends sharing one instance of
458    // `System` instead of making multiple instances.
459    // See https://docs.rs/sysinfo/0.29.0/sysinfo/index.html#usage for more info
460    //
461    // If we refactor the `System` instance to use it for other purposes, please make sure start-up
462    // time is not affected.  It takes a long time to load all components in sysinfo (e.g. by using
463    // `System::new_all()`).  Some applications, especially short-running scripts, are sensitive to
464    // start-up time.  During start-up, MMTk core only needs the total memory to initialize the
465    // `Options`.  If we only load memory-related components on start-up, it should only take <1ms
466    // to initialize the `System` instance.
467    let sys = System::new_with_specifics(
468        RefreshKind::nothing().with_memory(MemoryRefreshKind::nothing().with_ram()),
469    );
470    sys.total_memory()
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476    use crate::util::constants::BYTES_IN_PAGE;
477    use crate::util::test_util::MEMORY_TEST_REGION;
478    use crate::util::test_util::{serial_test, with_cleanup};
479
480    // In the tests, we will mmap this address. This address should not be in our heap (in case we mess up with other tests)
481    const START: Address = MEMORY_TEST_REGION.start;
482
483    #[test]
484    fn test_mmap() {
485        serial_test(|| {
486            with_cleanup(
487                || {
488                    let res = unsafe {
489                        dzmmap(START, BYTES_IN_PAGE, MmapStrategy::TEST, mmap_anno_test!())
490                    };
491                    assert!(res.is_ok());
492                    // We can overwrite with dzmmap
493                    let res = unsafe {
494                        dzmmap(START, BYTES_IN_PAGE, MmapStrategy::TEST, mmap_anno_test!())
495                    };
496                    assert!(res.is_ok());
497                },
498                || {
499                    assert!(munmap(START, BYTES_IN_PAGE).is_ok());
500                },
501            );
502        });
503    }
504
505    #[test]
506    fn test_munmap() {
507        serial_test(|| {
508            with_cleanup(
509                || {
510                    let res = dzmmap_noreplace(
511                        START,
512                        BYTES_IN_PAGE,
513                        MmapStrategy::TEST,
514                        mmap_anno_test!(),
515                    );
516                    assert!(res.is_ok());
517                    let res = munmap(START, BYTES_IN_PAGE);
518                    assert!(res.is_ok());
519                },
520                || {
521                    assert!(munmap(START, BYTES_IN_PAGE).is_ok());
522                },
523            )
524        })
525    }
526
527    #[cfg(target_os = "linux")]
528    #[test]
529    fn test_mmap_noreplace() {
530        serial_test(|| {
531            with_cleanup(
532                || {
533                    // Make sure we mmapped the memory
534                    let res = unsafe {
535                        dzmmap(START, BYTES_IN_PAGE, MmapStrategy::TEST, mmap_anno_test!())
536                    };
537                    assert!(res.is_ok());
538                    // Use dzmmap_noreplace will fail
539                    let res = dzmmap_noreplace(
540                        START,
541                        BYTES_IN_PAGE,
542                        MmapStrategy::TEST,
543                        mmap_anno_test!(),
544                    );
545                    assert!(res.is_err());
546                },
547                || {
548                    assert!(munmap(START, BYTES_IN_PAGE).is_ok());
549                },
550            )
551        });
552    }
553
554    #[test]
555    fn test_mmap_noreserve() {
556        serial_test(|| {
557            with_cleanup(
558                || {
559                    let res =
560                        mmap_noreserve(START, BYTES_IN_PAGE, MmapStrategy::TEST, mmap_anno_test!());
561                    assert!(res.is_ok());
562                    // Try reserve it
563                    let res = unsafe {
564                        dzmmap(START, BYTES_IN_PAGE, MmapStrategy::TEST, mmap_anno_test!())
565                    };
566                    assert!(res.is_ok());
567                },
568                || {
569                    assert!(munmap(START, BYTES_IN_PAGE).is_ok());
570                },
571            )
572        })
573    }
574
575    #[cfg(target_os = "linux")]
576    #[test]
577    #[should_panic]
578    fn test_check_is_mmapped_for_unmapped() {
579        serial_test(|| {
580            with_cleanup(
581                || {
582                    // We expect this call to panic
583                    panic_if_unmapped(START, BYTES_IN_PAGE, mmap_anno_test!());
584                },
585                || {
586                    assert!(munmap(START, BYTES_IN_PAGE).is_ok());
587                },
588            )
589        })
590    }
591
592    #[test]
593    fn test_check_is_mmapped_for_mapped() {
594        serial_test(|| {
595            with_cleanup(
596                || {
597                    assert!(dzmmap_noreplace(
598                        START,
599                        BYTES_IN_PAGE,
600                        MmapStrategy::TEST,
601                        mmap_anno_test!()
602                    )
603                    .is_ok());
604                    panic_if_unmapped(START, BYTES_IN_PAGE, mmap_anno_test!());
605                },
606                || {
607                    assert!(munmap(START, BYTES_IN_PAGE).is_ok());
608                },
609            )
610        })
611    }
612
613    #[cfg(target_os = "linux")]
614    #[test]
615    #[should_panic]
616    fn test_check_is_mmapped_for_unmapped_next_to_mapped() {
617        serial_test(|| {
618            with_cleanup(
619                || {
620                    // map 1 page from START
621                    assert!(dzmmap_noreplace(
622                        START,
623                        BYTES_IN_PAGE,
624                        MmapStrategy::TEST,
625                        mmap_anno_test!(),
626                    )
627                    .is_ok());
628
629                    // check if the next page is mapped - which should panic
630                    panic_if_unmapped(START + BYTES_IN_PAGE, BYTES_IN_PAGE, mmap_anno_test!());
631                },
632                || {
633                    assert!(munmap(START, BYTES_IN_PAGE * 2).is_ok());
634                },
635            )
636        })
637    }
638
639    #[test]
640    #[should_panic]
641    // This is a bug we need to fix. We need to figure out a way to properly check if a piece of memory is mapped or not.
642    // Alternatively, we should remove the code that calls the function.
643    #[ignore]
644    fn test_check_is_mmapped_for_partial_mapped() {
645        serial_test(|| {
646            with_cleanup(
647                || {
648                    // map 1 page from START
649                    assert!(dzmmap_noreplace(
650                        START,
651                        BYTES_IN_PAGE,
652                        MmapStrategy::TEST,
653                        mmap_anno_test!()
654                    )
655                    .is_ok());
656
657                    // check if the 2 pages from START are mapped. The second page is unmapped, so it should panic.
658                    panic_if_unmapped(START, BYTES_IN_PAGE * 2, mmap_anno_test!());
659                },
660                || {
661                    assert!(munmap(START, BYTES_IN_PAGE * 2).is_ok());
662                },
663            )
664        })
665    }
666
667    #[test]
668    fn test_get_system_total_memory() {
669        let total = get_system_total_memory();
670        println!("Total memory: {:?}", total);
671    }
672}