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}