Skip to content

Commit 755979d

Browse files
authored
Add a knob to OomTest to allow/disallow allocation after OOM injection (#12554)
* Add a knob to `OomTest` to allow/disallow allocation after OOM injection * Fix leaks in OOM tests
1 parent efb98a9 commit 755979d

2 files changed

Lines changed: 121 additions & 17 deletions

File tree

crates/fuzzing/src/oom.rs

Lines changed: 53 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,13 @@ enum OomState {
2626

2727
/// We are inside an OOM test and should inject an OOM when the counter
2828
/// reaches zero.
29-
OomOnAlloc(u32),
29+
OomOnAlloc {
30+
counter: u32,
31+
allow_alloc_after: bool,
32+
},
3033

3134
/// We are inside an OOM test and we already injected an OOM.
32-
DidOom,
35+
DidOom { allow_alloc: bool },
3336
}
3437

3538
thread_local! {
@@ -107,22 +110,43 @@ unsafe impl GlobalAlloc for OomTestAllocator {
107110
match old_state {
108111
OomState::OutsideOomTest => unreachable!("handled above"),
109112

110-
OomState::OomOnAlloc(0) => {
113+
OomState::OomOnAlloc {
114+
counter: 0,
115+
allow_alloc_after,
116+
} => {
111117
log::trace!(
112118
"injecting OOM for allocation: {layout:?}\nAllocation backtrace:\n{bt}"
113119
);
114-
new_state = OomState::DidOom;
120+
new_state = OomState::DidOom {
121+
allow_alloc: allow_alloc_after,
122+
};
115123
ptr = ptr::null_mut();
116124
}
117125

118-
OomState::OomOnAlloc(c) => {
119-
new_state = OomState::OomOnAlloc(c - 1);
126+
OomState::OomOnAlloc {
127+
counter: c,
128+
allow_alloc_after,
129+
} => {
130+
new_state = OomState::OomOnAlloc {
131+
counter: c - 1,
132+
allow_alloc_after,
133+
};
120134
ptr = unsafe { std::alloc::System.alloc(layout) };
121135
}
122136

123-
OomState::DidOom => {
137+
OomState::DidOom { allow_alloc } => {
124138
log::trace!("Attempt to allocate {layout:?} after OOM:\n{bt}");
125-
panic!("OOM test attempted to allocate after OOM: {layout:?}")
139+
if allow_alloc {
140+
new_state = OomState::DidOom { allow_alloc: true };
141+
ptr = ptr::null_mut();
142+
} else {
143+
panic!(
144+
"OOM test attempted to allocate after OOM: {layout:?}\n\
145+
\n\
146+
Hint: if this is acceptable, configure the OOM test to allow allocation \
147+
after OOM with `OomTest::allow_alloc_after_oom`"
148+
)
149+
}
126150
}
127151
}
128152
}
@@ -161,6 +185,7 @@ unsafe impl GlobalAlloc for OomTestAllocator {
161185
/// OomTest::new()
162186
/// .max_iters(1_000_000)
163187
/// .max_duration(Duration::from_secs(5))
188+
/// .allow_alloc_after_oom(true)
164189
/// .test(|| {
165190
/// todo!("insert code here that should handle OOM here...")
166191
/// })
@@ -169,6 +194,7 @@ unsafe impl GlobalAlloc for OomTestAllocator {
169194
pub struct OomTest {
170195
max_iters: Option<u32>,
171196
max_duration: Option<time::Duration>,
197+
allow_alloc_after_oom: bool,
172198
}
173199

174200
impl OomTest {
@@ -187,6 +213,7 @@ impl OomTest {
187213
OomTest {
188214
max_iters: None,
189215
max_duration: None,
216+
allow_alloc_after_oom: false,
190217
}
191218
}
192219

@@ -202,6 +229,15 @@ impl OomTest {
202229
self
203230
}
204231

232+
/// Configure whether to allow allocation attempts after an OOM has already
233+
/// been injected.
234+
///
235+
/// The default is `false`.
236+
pub fn allow_alloc_after_oom(&mut self, allow: bool) -> &mut Self {
237+
self.allow_alloc_after_oom = allow;
238+
self
239+
}
240+
205241
/// Repeatedly run the given test function, injecting OOMs at different
206242
/// times and checking that it correctly handles them.
207243
///
@@ -225,7 +261,10 @@ impl OomTest {
225261

226262
log::trace!("=== Injecting OOM after {i} allocations ===");
227263
let (result, old_state) = {
228-
let guard = ScopedOomState::new(OomState::OomOnAlloc(i));
264+
let guard = ScopedOomState::new(OomState::OomOnAlloc {
265+
counter: i,
266+
allow_alloc_after: self.allow_alloc_after_oom,
267+
});
229268
assert_eq!(guard.prev_state, OomState::OutsideOomTest);
230269

231270
let result = test_func();
@@ -238,24 +277,24 @@ impl OomTest {
238277

239278
// The test function completed successfully before we ran out of
240279
// allocation fuel, so we're done.
241-
(Ok(()), OomState::OomOnAlloc(_)) => break,
280+
(Ok(()), OomState::OomOnAlloc { .. }) => break,
242281

243282
// We injected an OOM and the test function handled it
244283
// correctly; continue to the next iteration.
245-
(Err(e), OomState::DidOom) if self.is_oom_error(&e) => {}
284+
(Err(e), OomState::DidOom { .. }) if self.is_oom_error(&e) => {}
246285

247286
// Missed OOMs.
248-
(Ok(()), OomState::DidOom) => {
287+
(Ok(()), OomState::DidOom { .. }) => {
249288
bail!("OOM test function missed an OOM: returned Ok(())");
250289
}
251-
(Err(e), OomState::DidOom) => {
290+
(Err(e), OomState::DidOom { .. }) => {
252291
return Err(
253292
e.context("OOM test function missed an OOM: returned non-OOM error")
254293
);
255294
}
256295

257296
// Unexpected error.
258-
(Err(e), OomState::OomOnAlloc(_)) => {
297+
(Err(e), OomState::OomOnAlloc { .. }) => {
259298
return Err(
260299
e.context("OOM test function returned an error when there was no OOM")
261300
);

crates/fuzzing/tests/oom.rs

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
use cranelift_bitset::CompoundBitSet;
22
use std::{
3-
alloc::{Layout, alloc},
3+
alloc::{Layout, alloc, dealloc},
44
fmt::{self, Write},
55
iter,
6+
ops::Deref,
67
sync::atomic::{AtomicU32, Ordering::SeqCst},
78
};
89
use wasmtime::{error::OutOfMemory, *};
@@ -13,6 +14,38 @@ use wasmtime_fuzzing::oom::{OomTest, OomTestAllocator};
1314
#[global_allocator]
1415
static GLOBAL_ALOCATOR: OomTestAllocator = OomTestAllocator::new();
1516

17+
/// RAII wrapper around a raw allocation to deallocate it on drop.
18+
struct Alloc {
19+
layout: Layout,
20+
ptr: *mut u8,
21+
}
22+
23+
impl Drop for Alloc {
24+
fn drop(&mut self) {
25+
if !self.ptr.is_null() {
26+
unsafe {
27+
dealloc(self.ptr, self.layout);
28+
}
29+
}
30+
}
31+
}
32+
33+
impl Deref for Alloc {
34+
type Target = *mut u8;
35+
36+
fn deref(&self) -> &Self::Target {
37+
&self.ptr
38+
}
39+
}
40+
41+
impl Alloc {
42+
/// Safety: same as `std::alloc::alloc`.
43+
unsafe fn new(layout: Layout) -> Self {
44+
let ptr = unsafe { alloc(layout) };
45+
Alloc { layout, ptr }
46+
}
47+
}
48+
1649
#[test]
1750
fn smoke_test_ok() -> Result<()> {
1851
OomTest::new().test(|| Ok(()))
@@ -21,8 +54,8 @@ fn smoke_test_ok() -> Result<()> {
2154
#[test]
2255
fn smoke_test_missed_oom() -> Result<()> {
2356
let err = OomTest::new()
24-
.test(|| {
25-
let _ = unsafe { alloc(Layout::new::<u64>()) };
57+
.test(|| unsafe {
58+
let _ = Alloc::new(Layout::new::<u64>());
2659
Ok(())
2760
})
2861
.unwrap_err();
@@ -34,6 +67,38 @@ fn smoke_test_missed_oom() -> Result<()> {
3467
Ok(())
3568
}
3669

70+
#[test]
71+
fn smoke_test_disallow_alloc_after_oom() -> Result<()> {
72+
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
73+
let _ = OomTest::new().test(|| unsafe {
74+
let layout = Layout::new::<u64>();
75+
let p = Alloc::new(layout);
76+
let _q = Alloc::new(layout);
77+
if p.is_null() {
78+
Err(OutOfMemory::new(layout.size()).into())
79+
} else {
80+
Ok(())
81+
}
82+
});
83+
}));
84+
assert!(result.is_err());
85+
Ok(())
86+
}
87+
88+
#[test]
89+
fn smoke_test_allow_alloc_after_oom() -> Result<()> {
90+
OomTest::new().allow_alloc_after_oom(true).test(|| unsafe {
91+
let layout = Layout::new::<u64>();
92+
let p = Alloc::new(layout);
93+
let q = Alloc::new(layout);
94+
if p.is_null() || q.is_null() {
95+
Err(OutOfMemory::new(layout.size()).into())
96+
} else {
97+
Ok(())
98+
}
99+
})
100+
}
101+
37102
#[test]
38103
#[cfg(arc_try_new)]
39104
fn try_new_arc() -> Result<()> {

0 commit comments

Comments
 (0)