1use backtrace::Backtrace;
7use rand::{RngExt, SeedableRng, rngs::SmallRng};
8use std::{
9 alloc::GlobalAlloc,
10 cell::Cell,
11 mem,
12 pin::Pin,
13 ptr,
14 task::{Context, Poll},
15 time,
16};
17use wasmtime_core::error::{Context as _, OutOfMemory, Result, bail, ensure};
18
19#[non_exhaustive]
21pub struct OomTestAllocator;
22
23impl OomTestAllocator {
24 pub const fn new() -> Self {
26 OomTestAllocator
27 }
28}
29
30#[derive(Clone, Debug, Default, PartialEq, Eq)]
31enum OomState {
32 #[default]
34 OutsideOomTest,
35
36 Counting { count: u32 },
38
39 OomOnAlloc {
42 counter: u32,
43 allow_alloc_after: bool,
44 alloc_succeeds_after: bool,
45 },
46
47 DidOom {
49 allow_alloc: bool,
50 alloc_succeeds_after: bool,
51 },
52}
53
54thread_local! {
55 static OOM_STATE: Cell<OomState> = const { Cell::new(OomState::OutsideOomTest) };
56}
57
58fn set_oom_state(state: OomState) -> OomState {
60 OOM_STATE.with(|s| s.replace(state))
61}
62
63struct ScopedOomState<'a> {
66 state: &'a mut OomState,
67}
68
69impl<'a> ScopedOomState<'a> {
70 fn new(state: &'a mut OomState) -> Self {
74 *state = set_oom_state(mem::take(state));
75 ScopedOomState { state }
76 }
77}
78
79impl Drop for ScopedOomState<'_> {
80 fn drop(&mut self) {
81 *self.state = set_oom_state(mem::take(self.state));
82 }
83}
84
85unsafe impl GlobalAlloc for OomTestAllocator {
86 unsafe fn alloc(&self, layout: std::alloc::Layout) -> *mut u8 {
87 let old_state = set_oom_state(OomState::OutsideOomTest);
88
89 let new_state;
90 let ptr;
91
92 'outside_oom_test: {
93 if old_state == OomState::OutsideOomTest || std::thread::panicking() {
103 new_state = old_state;
104 ptr = unsafe { std::alloc::System.alloc(layout) };
105 break 'outside_oom_test;
106 }
107
108 let bt = Backtrace::new();
109 let bt = format!("{bt:?}");
110
111 if bt.contains("env_logger") {
116 new_state = old_state;
117 ptr = unsafe { std::alloc::System.alloc(layout) };
118 break 'outside_oom_test;
119 }
120
121 match old_state {
122 OomState::OutsideOomTest => unreachable!("handled above"),
123
124 OomState::Counting { count } => {
125 new_state = OomState::Counting { count: count + 1 };
126 ptr = unsafe { std::alloc::System.alloc(layout) };
127 }
128
129 OomState::OomOnAlloc {
130 counter: 0,
131 allow_alloc_after,
132 alloc_succeeds_after,
133 } => {
134 log::debug!(
135 "injecting OOM for allocation: {layout:?}\nAllocation backtrace:\n{bt}"
136 );
137 new_state = OomState::DidOom {
138 allow_alloc: allow_alloc_after,
139 alloc_succeeds_after,
140 };
141 ptr = ptr::null_mut();
142 }
143
144 OomState::OomOnAlloc {
145 counter: c,
146 allow_alloc_after,
147 alloc_succeeds_after,
148 } => {
149 new_state = OomState::OomOnAlloc {
150 counter: c - 1,
151 allow_alloc_after,
152 alloc_succeeds_after,
153 };
154 ptr = unsafe { std::alloc::System.alloc(layout) };
155 }
156
157 OomState::DidOom {
158 allow_alloc,
159 alloc_succeeds_after,
160 } => {
161 log::trace!("Attempt to allocate {layout:?} after OOM:\n{bt}");
162 if allow_alloc {
163 new_state = OomState::DidOom {
164 allow_alloc: true,
165 alloc_succeeds_after,
166 };
167 ptr = if alloc_succeeds_after {
168 unsafe { std::alloc::System.alloc(layout) }
169 } else {
170 ptr::null_mut()
171 };
172 } else {
173 panic!(
174 "OOM test attempted to allocate after OOM: {layout:?}\n\
175 \n\
176 Hint: if this is acceptable, configure the OOM test to allow allocation \
177 after OOM with `OomTest::allow_alloc_after_oom`"
178 )
179 }
180 }
181 }
182 }
183
184 set_oom_state(new_state);
185 ptr
186 }
187
188 unsafe fn dealloc(&self, ptr: *mut u8, layout: std::alloc::Layout) {
189 unsafe {
190 std::alloc::System.dealloc(ptr, layout);
191 }
192 }
193}
194
195pub struct OomTest {
243 max_iters: Option<u32>,
244 max_duration: Option<time::Duration>,
245 allow_alloc_after_oom: bool,
246 alloc_succeeds_after_oom: bool,
247 allow_missed_oom_errors: bool,
248 seed: u64,
249}
250
251impl OomTest {
252 pub fn new() -> Self {
257 let _ = env_logger::try_init();
258
259 wasmtime_core::error::disable_backtrace();
263
264 OomTest {
265 max_iters: None,
266 max_duration: None,
267 allow_alloc_after_oom: false,
268 alloc_succeeds_after_oom: false,
269 allow_missed_oom_errors: false,
270 seed: 0,
271 }
272 }
273
274 pub fn max_iters(&mut self, max_iters: u32) -> &mut Self {
276 self.max_iters = Some(max_iters);
277 self
278 }
279
280 pub fn max_duration(&mut self, max_duration: time::Duration) -> &mut Self {
282 self.max_duration = Some(max_duration);
283 self
284 }
285
286 pub fn allow_alloc_after_oom(&mut self, allow: bool) -> &mut Self {
291 self.allow_alloc_after_oom = allow;
292 self
293 }
294
295 pub fn alloc_succeeds_after_oom(&mut self, succeeds: bool) -> &mut Self {
300 self.alloc_succeeds_after_oom = succeeds;
301 self
302 }
303
304 pub fn allow_missed_oom_errors(&mut self, allow: bool) -> &mut Self {
310 self.allow_missed_oom_errors = allow;
311 self
312 }
313
314 pub fn seed(&mut self, seed: u64) -> &mut Self {
318 self.seed = seed;
319 self
320 }
321
322 pub fn test(&self, test_func: impl Fn() -> Result<()>) -> Result<()> {
334 let future = self.test_async(|| async { test_func() });
335 crate::block_on(future)
336 }
337
338 pub async fn test_async(&self, test_func: impl AsyncFn() -> Result<()>) -> Result<()> {
340 self.test_with_reuse_async(
341 async || Ok(()),
342 async |_: &mut ()| test_func().await,
343 async |_: ()| Ok(()),
344 )
345 .await
346 }
347
348 pub fn test_with_reuse<T>(
370 &self,
371 make_context: impl Fn() -> Result<T>,
372 test_func: impl Fn(&mut T) -> Result<()>,
373 post_oom_func: impl Fn(T) -> Result<()>,
374 ) -> Result<()> {
375 let future = self.test_with_reuse_async(
376 async || make_context(),
377 async |context: &mut T| test_func(context),
378 async |context: T| post_oom_func(context),
379 );
380 crate::block_on(future)
381 }
382
383 pub async fn test_with_reuse_async<T>(
385 &self,
386 make_context: impl AsyncFn() -> Result<T>,
387 test_func: impl AsyncFn(&mut T) -> Result<()>,
388 post_oom_func: impl AsyncFn(T) -> Result<()>,
389 ) -> Result<()> {
390 let total_allocs =
391 Self::count_allocations(&make_context, &test_func, &post_oom_func).await?;
392
393 let start = time::Instant::now();
394
395 for i in 0..total_allocs {
396 if self.max_iters.is_some_and(|n| i >= n)
397 || self.max_duration.is_some_and(|d| start.elapsed() >= d)
398 {
399 break;
400 }
401
402 log::trace!("=== Injecting OOM after {i} allocations ===");
403 self.run_one_oom_injection(&make_context, &test_func, &post_oom_func, i)
404 .await?;
405 }
406
407 Ok(())
408 }
409
410 pub fn fuzz(&self, test_func: impl Fn() -> Result<()>) -> Result<()> {
415 let future = self.fuzz_async(|| async { test_func() });
416 crate::block_on(future)
417 }
418
419 pub async fn fuzz_async(&self, test_func: impl AsyncFn() -> Result<()>) -> Result<()> {
421 let max_iters = match self.max_iters {
422 Some(n) => n,
423 None => bail!("OomTest::fuzz requires max_iters to be set"),
424 };
425
426 let make_context = async || Ok(());
427 let test_func = async |_: &mut ()| test_func().await;
428 let post_oom_func = async |_: ()| Ok(());
429
430 let total_allocs =
431 Self::count_allocations(&make_context, &test_func, &post_oom_func).await?;
432
433 if total_allocs == 0 {
434 return Ok(());
435 }
436
437 let mut rng = SmallRng::seed_from_u64(self.seed);
438
439 for _ in 0..max_iters {
440 let i = rng.random_range(0..total_allocs);
441 log::trace!("=== Injecting OOM after {i} allocations (fuzz) ===");
442 self.run_one_oom_injection(&make_context, &test_func, &post_oom_func, i)
443 .await?;
444 }
445
446 Ok(())
447 }
448
449 async fn run_one_oom_injection<T>(
452 &self,
453 make_context: &impl AsyncFn() -> Result<T>,
454 test_func: &impl AsyncFn(&mut T) -> Result<()>,
455 post_oom_func: &impl AsyncFn(T) -> Result<()>,
456 i: u32,
457 ) -> Result<()> {
458 let mut context = make_context()
459 .await
460 .context("OOM test failed to create its reused context")?;
461
462 let (result, oom_state) = {
463 let future = std::pin::pin!(test_func(&mut context));
464 OomTestFuture::new(
465 future,
466 OomState::OomOnAlloc {
467 counter: i,
468 allow_alloc_after: self.allow_alloc_after_oom,
469 alloc_succeeds_after: self.alloc_succeeds_after_oom,
470 },
471 )
472 .await
473 };
474
475 match (result, oom_state) {
476 (_, OomState::OutsideOomTest | OomState::Counting { .. }) => unreachable!(),
477
478 (Ok(()), OomState::OomOnAlloc { .. }) => {}
482
483 (Err(e), OomState::DidOom { .. }) if e.is::<OutOfMemory>() => {}
485
486 (Ok(()), OomState::DidOom { .. }) => {
488 ensure!(
489 self.allow_missed_oom_errors,
490 "OOM test function missed an OOM: returned Ok(())"
491 );
492 }
493 (Err(e), OomState::DidOom { .. }) => {
494 if !self.allow_missed_oom_errors {
495 return Err(
496 e.context("OOM test function missed an OOM: returned non-OOM error")
497 );
498 }
499 }
500
501 (Err(e), OomState::OomOnAlloc { .. }) => {
503 if !self.allow_missed_oom_errors {
504 return Err(
505 e.context("OOM test function returned an error when there was no OOM")
506 );
507 }
508 }
509 }
510
511 post_oom_func(context)
512 .await
513 .context("OOM test's post-OOM function failed")
514 }
515
516 async fn count_allocations<T>(
522 make_context: &impl AsyncFn() -> Result<T>,
523 test_func: &impl AsyncFn(&mut T) -> Result<()>,
524 post_oom_func: &impl AsyncFn(T) -> Result<()>,
525 ) -> Result<u32> {
526 const MAX_DRY_RUN_ITERS: usize = 10;
527
528 let mut total_allocs = u32::MAX;
529 for i in 0..MAX_DRY_RUN_ITERS {
530 log::trace!("=== Counting allocations (dry run {i}) ===");
531
532 let mut context = make_context()
533 .await
534 .context("OOM test failed to create its reused context")?;
535
536 let (result, oom_state) = {
537 let future = std::pin::pin!(test_func(&mut context));
538 OomTestFuture::new(future, OomState::Counting { count: 0 }).await
539 };
540 result?;
541
542 post_oom_func(context)
543 .await
544 .context("OOM test's post-OOM function failed")?;
545
546 let count = match oom_state {
547 OomState::Counting { count } => count,
548 _ => unreachable!(),
549 };
550 log::trace!("=== Counted {count} allocations ===");
551 if count == total_allocs {
552 return Ok(total_allocs);
553 }
554 total_allocs = count;
555 }
556
557 bail!(
558 "allocation count did not stabilize after {MAX_DRY_RUN_ITERS} \
559 dry-run iterations (last count: {total_allocs})"
560 )
561 }
562}
563
564struct OomTestFuture<'a, F> {
565 inner: Pin<&'a mut F>,
566 state: OomState,
567}
568
569impl<'a, F> OomTestFuture<'a, F> {
570 fn new(inner: Pin<&'a mut F>, state: OomState) -> Self {
571 OomTestFuture { inner, state }
572 }
573}
574
575impl<F> Future for OomTestFuture<'_, F>
576where
577 F: Future<Output = Result<()>>,
578{
579 type Output = (Result<()>, OomState);
580
581 fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
582 log::trace!("OomTestFuture::poll: Entered ---");
583
584 let result = {
585 let OomTestFuture { inner, state } = &mut *self;
586
587 let guard = ScopedOomState::new(state);
588 assert_eq!(*guard.state, OomState::OutsideOomTest);
589
590 inner.as_mut().poll(cx)
591 };
592
593 log::trace!("OomTestFuture::poll: inner result: {result:?}");
594 log::trace!("OomTestFuture::poll: old state: {:?}", self.state);
595
596 match result {
597 Poll::Pending => {
598 set_oom_state(OomState::OutsideOomTest);
599 Poll::Pending
600 }
601 Poll::Ready(result) => Poll::Ready((result, mem::take(&mut self.state))),
602 }
603 }
604}