1 //! Utilities for testing and fuzzing out-of-memory handling. 2 //! 3 //! Inspired by SpiderMonkey's `oomTest()` helper: 4 //! https://firefox-source-docs.mozilla.org/js/hacking_tips.html#how-to-debug-oomtest-failures 5 6 use backtrace::Backtrace; 7 use std::{ 8 alloc::GlobalAlloc, 9 cell::Cell, 10 mem, 11 pin::Pin, 12 ptr, 13 task::{Context, Poll}, 14 time, 15 }; 16 use wasmtime_core::error::{OutOfMemory, Result, bail}; 17 18 /// An allocator for use with `OomTest`. 19 #[non_exhaustive] 20 pub struct OomTestAllocator; 21 22 impl OomTestAllocator { 23 /// Create a new OOM test allocator. 24 pub const fn new() -> Self { 25 OomTestAllocator 26 } 27 } 28 29 #[derive(Clone, Debug, Default, PartialEq, Eq)] 30 enum OomState { 31 /// We are in code that is not part of an OOM test. 32 #[default] 33 OutsideOomTest, 34 35 /// We are inside an OOM test and should inject an OOM when the counter 36 /// reaches zero. 37 OomOnAlloc { 38 counter: u32, 39 allow_alloc_after: bool, 40 }, 41 42 /// We are inside an OOM test and we already injected an OOM. 43 DidOom { allow_alloc: bool }, 44 } 45 46 thread_local! { 47 static OOM_STATE: Cell<OomState> = const { Cell::new(OomState::OutsideOomTest) }; 48 } 49 50 /// Set the new OOM state, returning the old state. 51 fn set_oom_state(state: OomState) -> OomState { 52 OOM_STATE.with(|s| s.replace(state)) 53 } 54 55 /// RAII helper to set the OOM state within a block of code and reset it upon 56 /// exiting that block (even if exiting via panic unwinding). 57 struct ScopedOomState<'a> { 58 state: &'a mut OomState, 59 } 60 61 impl<'a> ScopedOomState<'a> { 62 /// The OOM state will be initialized to `*state` on construction, and then 63 /// `state` will be updated to be the old OOM state that existed when the 64 /// original state is replaced on this type's drop. 65 fn new(state: &'a mut OomState) -> Self { 66 *state = set_oom_state(mem::take(state)); 67 ScopedOomState { state } 68 } 69 } 70 71 impl Drop for ScopedOomState<'_> { 72 fn drop(&mut self) { 73 *self.state = set_oom_state(mem::take(self.state)); 74 } 75 } 76 77 unsafe impl GlobalAlloc for OomTestAllocator { 78 unsafe fn alloc(&self, layout: std::alloc::Layout) -> *mut u8 { 79 let old_state = set_oom_state(OomState::OutsideOomTest); 80 81 let new_state; 82 let ptr; 83 84 'outside_oom_test: { 85 // NB: It's okay to log/backtrace/etc... in this block because the 86 // current state is `OutsideOomTest`, so any re-entrant allocations 87 // will be passed through to the system allocator. 88 89 // Don't panic on allocation-after-OOM attempts if we 90 // are already in the middle of panicking. That will 91 // cause an abort and we won't get as good of an error 92 // message for the original panic, which is most likely 93 // some kind of test failure. 94 if old_state == OomState::OutsideOomTest || std::thread::panicking() { 95 new_state = old_state; 96 ptr = unsafe { std::alloc::System.alloc(layout) }; 97 break 'outside_oom_test; 98 } 99 100 let bt = Backtrace::new(); 101 let bt = format!("{bt:?}"); 102 103 // XXX: `env_logger` internally buffers writes in a `Vec` which 104 // means our OOM tests might sporadically fail when you enable 105 // logging to debug stuff, so simply let the allocation through if 106 // `env_logger` is on the stack. 107 if bt.contains("env_logger") { 108 new_state = old_state; 109 ptr = unsafe { std::alloc::System.alloc(layout) }; 110 break 'outside_oom_test; 111 } 112 113 match old_state { 114 OomState::OutsideOomTest => unreachable!("handled above"), 115 116 OomState::OomOnAlloc { 117 counter: 0, 118 allow_alloc_after, 119 } => { 120 log::trace!( 121 "injecting OOM for allocation: {layout:?}\nAllocation backtrace:\n{bt}" 122 ); 123 new_state = OomState::DidOom { 124 allow_alloc: allow_alloc_after, 125 }; 126 ptr = ptr::null_mut(); 127 } 128 129 OomState::OomOnAlloc { 130 counter: c, 131 allow_alloc_after, 132 } => { 133 new_state = OomState::OomOnAlloc { 134 counter: c - 1, 135 allow_alloc_after, 136 }; 137 ptr = unsafe { std::alloc::System.alloc(layout) }; 138 } 139 140 OomState::DidOom { allow_alloc } => { 141 log::trace!("Attempt to allocate {layout:?} after OOM:\n{bt}"); 142 if allow_alloc { 143 new_state = OomState::DidOom { allow_alloc: true }; 144 ptr = ptr::null_mut(); 145 } else { 146 panic!( 147 "OOM test attempted to allocate after OOM: {layout:?}\n\ 148 \n\ 149 Hint: if this is acceptable, configure the OOM test to allow allocation \ 150 after OOM with `OomTest::allow_alloc_after_oom`" 151 ) 152 } 153 } 154 } 155 } 156 157 set_oom_state(new_state); 158 ptr 159 } 160 161 unsafe fn dealloc(&self, ptr: *mut u8, layout: std::alloc::Layout) { 162 unsafe { 163 std::alloc::System.dealloc(ptr, layout); 164 } 165 } 166 } 167 168 /// A test helper that checks that some code handles OOM correctly. 169 /// 170 /// `OomTest` will only work correctly when `OomTestAllocator` is configured as 171 /// the global allocator. 172 /// 173 /// `OomTest` does not support reentrancy, so you cannot run an `OomTest` within 174 /// an `OomTest`. 175 /// 176 /// # Example 177 /// 178 /// ```no_run 179 /// use std::time::Duration; 180 /// use wasmtime::Result; 181 /// use wasmtime_fuzzing::oom::{OomTest, OomTestAllocator}; 182 /// 183 /// #[global_allocator] 184 /// static GLOBAL_ALLOCATOR: OomTestAllocator = OomTestAllocator::new(); 185 /// 186 /// #[test] 187 /// fn my_oom_test() -> Result<()> { 188 /// OomTest::new() 189 /// .max_iters(1_000_000) 190 /// .max_duration(Duration::from_secs(5)) 191 /// .allow_alloc_after_oom(true) 192 /// .test(|| { 193 /// todo!("insert code here that should handle OOM here...") 194 /// }) 195 /// } 196 /// ``` 197 pub struct OomTest { 198 max_iters: Option<u32>, 199 max_duration: Option<time::Duration>, 200 allow_alloc_after_oom: bool, 201 } 202 203 impl OomTest { 204 /// Create a new OOM test. 205 /// 206 /// By default there is no iteration or time limit, tests will be executed 207 /// until the pass (or fail). 208 pub fn new() -> Self { 209 let _ = env_logger::try_init(); 210 211 // NB: `std::backtrace::Backtrace` doesn't have ways to handle 212 // OOM. Ideally we would just disable the `"backtrace"` cargo feature, 213 // but workspace feature resolution doesn't play nice with that. 214 wasmtime_core::error::disable_backtrace(); 215 216 OomTest { 217 max_iters: None, 218 max_duration: None, 219 allow_alloc_after_oom: false, 220 } 221 } 222 223 /// Configure the maximum number of times to run an OOM test. 224 pub fn max_iters(&mut self, max_iters: u32) -> &mut Self { 225 self.max_iters = Some(max_iters); 226 self 227 } 228 229 /// Configure the maximum duration of time to run an OOM text. 230 pub fn max_duration(&mut self, max_duration: time::Duration) -> &mut Self { 231 self.max_duration = Some(max_duration); 232 self 233 } 234 235 /// Configure whether to allow allocation attempts after an OOM has already 236 /// been injected. 237 /// 238 /// The default is `false`. 239 pub fn allow_alloc_after_oom(&mut self, allow: bool) -> &mut Self { 240 self.allow_alloc_after_oom = allow; 241 self 242 } 243 244 /// Repeatedly run the given test function, injecting OOMs at different 245 /// times and checking that it correctly handles them. 246 /// 247 /// The test function should not use threads, or else allocations may not be 248 /// tracked correctly and OOM injection may be incorrect. 249 /// 250 /// The test function should return an `Err(_)` if and only if it encounters 251 /// an OOM. 252 /// 253 /// Returns early once the test function returns `Ok(())` before an OOM has 254 /// been injected. 255 pub fn test(&self, test_func: impl Fn() -> Result<()>) -> Result<()> { 256 let future = self.test_async(|| async { test_func() }); 257 crate::block_on(future) 258 } 259 260 /// The same as `test` but `async`. 261 pub async fn test_async(&self, test_func: impl AsyncFn() -> Result<()>) -> Result<()> { 262 let start = time::Instant::now(); 263 264 for i in 0.. { 265 if self.max_iters.is_some_and(|n| i >= n) 266 || self.max_duration.is_some_and(|d| start.elapsed() >= d) 267 { 268 break; 269 } 270 271 log::trace!("=== Injecting OOM after {i} allocations ==="); 272 273 let future = std::pin::pin!(test_func()); 274 let (result, oom_state) = OomTestFuture::new( 275 future, 276 OomState::OomOnAlloc { 277 counter: i, 278 allow_alloc_after: self.allow_alloc_after_oom, 279 }, 280 ) 281 .await; 282 283 match (result, oom_state) { 284 (_, OomState::OutsideOomTest) => unreachable!(), 285 286 // The test function completed successfully before we ran out of 287 // allocation fuel, so we're done. 288 (Ok(()), OomState::OomOnAlloc { .. }) => break, 289 290 // We injected an OOM and the test function handled it 291 // correctly; continue to the next iteration. 292 (Err(e), OomState::DidOom { .. }) if e.is::<OutOfMemory>() => {} 293 294 // Missed OOMs. 295 (Ok(()), OomState::DidOom { .. }) => { 296 bail!("OOM test function missed an OOM: returned Ok(())"); 297 } 298 (Err(e), OomState::DidOom { .. }) => { 299 return Err( 300 e.context("OOM test function missed an OOM: returned non-OOM error") 301 ); 302 } 303 304 // Unexpected error. 305 (Err(e), OomState::OomOnAlloc { .. }) => { 306 return Err( 307 e.context("OOM test function returned an error when there was no OOM") 308 ); 309 } 310 } 311 } 312 313 Ok(()) 314 } 315 } 316 317 struct OomTestFuture<'a, F> { 318 inner: Pin<&'a mut F>, 319 state: OomState, 320 } 321 322 impl<'a, F> OomTestFuture<'a, F> { 323 fn new(inner: Pin<&'a mut F>, state: OomState) -> Self { 324 OomTestFuture { inner, state } 325 } 326 } 327 328 impl<F> Future for OomTestFuture<'_, F> 329 where 330 F: Future<Output = Result<()>>, 331 { 332 type Output = (Result<()>, OomState); 333 334 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { 335 log::trace!("OomTestFuture::poll: Entered ---"); 336 337 let result = { 338 let OomTestFuture { inner, state } = &mut *self; 339 340 let guard = ScopedOomState::new(state); 341 assert_eq!(*guard.state, OomState::OutsideOomTest); 342 343 inner.as_mut().poll(cx) 344 }; 345 346 log::trace!("OomTestFuture::poll: inner result: {result:?}"); 347 log::trace!("OomTestFuture::poll: old state: {:?}", self.state); 348 349 match result { 350 Poll::Pending => { 351 set_oom_state(OomState::OutsideOomTest); 352 Poll::Pending 353 } 354 Poll::Ready(result) => Poll::Ready((result, mem::take(&mut self.state))), 355 } 356 } 357 } 358