From 853b986b229d694dda0f584d8264a5eaa77f4d7c Mon Sep 17 00:00:00 2001 From: Alex Crichton Date: Fri, 17 Sep 2021 15:47:47 -0500 Subject: [PATCH] Move `custom_limiter_detect_os_oom_failure` to its own test (#3366) This test uses `rlimit` which can't be executed in parallel with other tests. Previously this used `libc::fork` but the call afterwards to `libc::wait` was racing all other child subprocesses since it would wait for any child instead of the specific child we were interested in. There was also difficulty getting the output of the child on failure coming to the parent, so this commit simplifies the situation by moving the test to its own executable where it's the only test. --- tests/all/limits.rs | 88 ------------------------------------- tests/rlimited-memory.rs | 94 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 88 deletions(-) create mode 100644 tests/rlimited-memory.rs diff --git a/tests/all/limits.rs b/tests/all/limits.rs index 14c0704113..c6490c05a8 100644 --- a/tests/all/limits.rs +++ b/tests/all/limits.rs @@ -464,91 +464,3 @@ fn custom_limiter_detect_grow_failure() -> Result<()> { Ok(()) } - -// This test only works on Linux. It may be portable to MacOS as well, -// but the original author did not have a machine available to test it. -#[cfg(target_os = "linux")] -#[test] -fn custom_limiter_detect_os_oom_failure() -> Result<()> { - if std::env::var("WASMTIME_TEST_NO_HOG_MEMORY").is_ok() { - return Ok(()); - } - - let pid = unsafe { libc::fork() }; - if pid == 0 { - // Child process - let r = std::panic::catch_unwind(|| { - // Ask Linux to limit this process to 128MiB of memory - let process_max_memory: usize = 128 * 1024 * 1024; - unsafe { - // limit process to 128MiB memory - let rlimit = libc::rlimit { - rlim_cur: 0, - rlim_max: process_max_memory as u64, - }; - let res = libc::setrlimit(libc::RLIMIT_DATA, &rlimit); - assert_eq!(res, 0, "setrlimit failed: {}", res); - }; - - // Default behavior of on-demand memory allocation so that a - // memory grow will hit Linux for a larger mmap. - let engine = Engine::default(); - let linker = Linker::new(&engine); - let module = Module::new(&engine, r#"(module (memory (export "m") 0))"#).unwrap(); - - let context = MemoryGrowFailureDetector::default(); - - let mut store = Store::new(&engine, context); - store.limiter(|s| s as &mut dyn ResourceLimiter); - let instance = linker.instantiate(&mut store, &module).unwrap(); - let memory = instance.get_memory(&mut store, "m").unwrap(); - - // Small (640KiB) grow should succeed - memory.grow(&mut store, 10).unwrap(); - assert!(store.data().error.is_none()); - assert_eq!(store.data().current, 0); - assert_eq!(store.data().desired, 10 * 64 * 1024); - - // Try to grow past the process's memory limit. - // This should fail. - let pages_exceeding_limit = process_max_memory / (64 * 1024); - let err_msg = memory - .grow(&mut store, pages_exceeding_limit as u64) - .unwrap_err() - .to_string(); - assert!( - err_msg.starts_with("failed to grow memory"), - "unexpected error: {}", - err_msg - ); - - assert_eq!(store.data().current, 10 * 64 * 1024); - assert_eq!( - store.data().desired, - (pages_exceeding_limit + 10) * 64 * 1024 - ); - // The memory_grow_failed hook should show Linux gave OOM: - let err_msg = store.data().error.as_ref().unwrap(); - assert!( - err_msg.starts_with("System call failed: Cannot allocate memory"), - "unexpected error: {}", - err_msg - ); - }); - // on assertion failure, exit 1 so parent process can fail test. - std::process::exit(if r.is_err() { 1 } else { 0 }); - } else { - // Parent process - let mut wstatus: libc::c_int = 0; - unsafe { libc::wait(&mut wstatus) }; - if libc::WIFEXITED(wstatus) { - if libc::WEXITSTATUS(wstatus) == 0 { - Ok(()) - } else { - anyhow::bail!("child exited with failure"); - } - } else { - anyhow::bail!("child didnt exit??") - } - } -} diff --git a/tests/rlimited-memory.rs b/tests/rlimited-memory.rs new file mode 100644 index 0000000000..2d64a6edef --- /dev/null +++ b/tests/rlimited-memory.rs @@ -0,0 +1,94 @@ +// This test only works on Linux. It may be portable to MacOS as well, +// but the original author did not have a machine available to test it. +#![cfg(target_os = "linux")] + +use anyhow::Result; +use wasmtime::*; + +#[derive(Default)] +struct MemoryGrowFailureDetector { + current: usize, + desired: usize, + error: Option, +} + +impl ResourceLimiter for MemoryGrowFailureDetector { + fn memory_growing(&mut self, current: usize, desired: usize, _maximum: Option) -> bool { + self.current = current; + self.desired = desired; + true + } + + fn memory_grow_failed(&mut self, err: &anyhow::Error) { + self.error = Some(err.to_string()); + } + + fn table_growing(&mut self, _current: u32, _desired: u32, _maximum: Option) -> bool { + true + } +} + +#[test] +fn custom_limiter_detect_os_oom_failure() -> Result<()> { + if std::env::var("WASMTIME_TEST_NO_HOG_MEMORY").is_ok() { + return Ok(()); + } + + // Default behavior of on-demand memory allocation so that a + // memory grow will hit Linux for a larger mmap. + let engine = Engine::default(); + let linker = Linker::new(&engine); + let module = Module::new(&engine, r#"(module (memory (export "m") 0))"#).unwrap(); + + // Ask Linux to limit this process to 256MiB of memory + let process_max_memory: usize = 256 * 1024 * 1024; + unsafe { + // limit process to 256MiB memory + let rlimit = libc::rlimit { + rlim_cur: 0, + rlim_max: process_max_memory as u64, + }; + let res = libc::setrlimit(libc::RLIMIT_DATA, &rlimit); + assert_eq!(res, 0, "setrlimit failed: {}", res); + }; + + let context = MemoryGrowFailureDetector::default(); + + let mut store = Store::new(&engine, context); + store.limiter(|s| s as &mut dyn ResourceLimiter); + let instance = linker.instantiate(&mut store, &module).unwrap(); + let memory = instance.get_memory(&mut store, "m").unwrap(); + + // Small (640KiB) grow should succeed + memory.grow(&mut store, 10).unwrap(); + assert!(store.data().error.is_none()); + assert_eq!(store.data().current, 0); + assert_eq!(store.data().desired, 10 * 64 * 1024); + + // Try to grow past the process's memory limit. + // This should fail. + let pages_exceeding_limit = process_max_memory / (64 * 1024); + let err_msg = memory + .grow(&mut store, pages_exceeding_limit as u64) + .unwrap_err() + .to_string(); + assert!( + err_msg.starts_with("failed to grow memory"), + "unexpected error: {}", + err_msg + ); + + assert_eq!(store.data().current, 10 * 64 * 1024); + assert_eq!( + store.data().desired, + (pages_exceeding_limit + 10) * 64 * 1024 + ); + // The memory_grow_failed hook should show Linux gave OOM: + let err_msg = store.data().error.as_ref().unwrap(); + assert!( + err_msg.starts_with("System call failed: Cannot allocate memory"), + "unexpected error: {}", + err_msg + ); + Ok(()) +}