Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bytecodealliance
GitHub Repository: bytecodealliance/wasmtime
Path: blob/main/crates/fuzzing/src/oom.rs
3061 views
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::{alloc::GlobalAlloc, cell::Cell, mem, ptr, time};
8
use wasmtime_core::error::{Error, OutOfMemory, Result, bail};
9
10
/// An allocator for use with `OomTest`.
11
#[non_exhaustive]
12
pub struct OomTestAllocator;
13
14
impl OomTestAllocator {
15
/// Create a new OOM test allocator.
16
pub const fn new() -> Self {
17
OomTestAllocator
18
}
19
}
20
21
#[derive(Clone, Debug, Default, PartialEq, Eq)]
22
enum OomState {
23
/// We are in code that is not part of an OOM test.
24
#[default]
25
OutsideOomTest,
26
27
/// We are inside an OOM test and should inject an OOM when the counter
28
/// reaches zero.
29
OomOnAlloc(u32),
30
31
/// We are inside an OOM test and we already injected an OOM.
32
DidOom,
33
}
34
35
thread_local! {
36
static OOM_STATE: Cell<OomState> = const { Cell::new(OomState::OutsideOomTest) };
37
}
38
39
/// Set the new OOM state, returning the old state.
40
fn set_oom_state(state: OomState) -> OomState {
41
OOM_STATE.with(|s| s.replace(state))
42
}
43
44
/// RAII helper to set the OOM state within a block of code and reset it upon
45
/// exiting that block (even if exiting via panic unwinding).
46
struct ScopedOomState {
47
prev_state: OomState,
48
}
49
50
impl ScopedOomState {
51
fn new(state: OomState) -> Self {
52
ScopedOomState {
53
prev_state: set_oom_state(state),
54
}
55
}
56
57
/// Finish this OOM state scope early, resetting the OOM state to what it
58
/// was before this scope was created, and returning the previous state that
59
/// was just overwritten by the reset.
60
fn finish(&self) -> OomState {
61
set_oom_state(self.prev_state.clone())
62
}
63
}
64
65
impl Drop for ScopedOomState {
66
fn drop(&mut self) {
67
set_oom_state(mem::take(&mut self.prev_state));
68
}
69
}
70
71
unsafe impl GlobalAlloc for OomTestAllocator {
72
unsafe fn alloc(&self, layout: std::alloc::Layout) -> *mut u8 {
73
let old_state = set_oom_state(OomState::OutsideOomTest);
74
75
let new_state;
76
let ptr;
77
78
'outside_oom_test: {
79
// NB: It's okay to log/backtrace/etc... in this block because the
80
// current state is `OutsideOomTest`, so any re-entrant allocations
81
// will be passed through to the system allocator.
82
83
// Don't panic on allocation-after-OOM attempts if we
84
// are already in the middle of panicking. That will
85
// cause an abort and we won't get as good of an error
86
// message for the original panic, which is most likely
87
// some kind of test failure.
88
if old_state == OomState::OutsideOomTest || std::thread::panicking() {
89
new_state = old_state;
90
ptr = unsafe { std::alloc::System.alloc(layout) };
91
break 'outside_oom_test;
92
}
93
94
let bt = Backtrace::new();
95
let bt = format!("{bt:?}");
96
97
// XXX: `env_logger` internally buffers writes in a `Vec` which
98
// means our OOM tests might sporadically fail when you enable
99
// logging to debug stuff, so simply let the allocation through if
100
// `env_logger` is on the stack.
101
if bt.contains("env_logger") {
102
new_state = old_state;
103
ptr = unsafe { std::alloc::System.alloc(layout) };
104
break 'outside_oom_test;
105
}
106
107
match old_state {
108
OomState::OutsideOomTest => unreachable!("handled above"),
109
110
OomState::OomOnAlloc(0) => {
111
log::trace!(
112
"injecting OOM for allocation: {layout:?}\nAllocation backtrace:\n{bt}"
113
);
114
new_state = OomState::DidOom;
115
ptr = ptr::null_mut();
116
}
117
118
OomState::OomOnAlloc(c) => {
119
new_state = OomState::OomOnAlloc(c - 1);
120
ptr = unsafe { std::alloc::System.alloc(layout) };
121
}
122
123
OomState::DidOom => {
124
log::trace!("Attempt to allocate {layout:?} after OOM:\n{bt}");
125
panic!("OOM test attempted to allocate after OOM: {layout:?}")
126
}
127
}
128
}
129
130
set_oom_state(new_state);
131
ptr
132
}
133
134
unsafe fn dealloc(&self, ptr: *mut u8, layout: std::alloc::Layout) {
135
unsafe {
136
std::alloc::System.dealloc(ptr, layout);
137
}
138
}
139
}
140
141
/// A test helper that checks that some code handles OOM correctly.
142
///
143
/// `OomTest` will only work correctly when `OomTestAllocator` is configured as
144
/// the global allocator.
145
///
146
/// `OomTest` does not support reentrancy, so you cannot run an `OomTest` within
147
/// an `OomTest`.
148
///
149
/// # Example
150
///
151
/// ```no_run
152
/// use std::time::Duration;
153
/// use wasmtime::Result;
154
/// use wasmtime_fuzzing::oom::{OomTest, OomTestAllocator};
155
///
156
/// #[global_allocator]
157
/// static GLOBAL_ALOCATOR: OomTestAllocator = OomTestAllocator::new();
158
///
159
/// #[test]
160
/// fn my_oom_test() -> Result<()> {
161
/// OomTest::new()
162
/// .max_iters(1_000_000)
163
/// .max_duration(Duration::from_secs(5))
164
/// .test(|| {
165
/// todo!("insert code here that should handle OOM here...")
166
/// })
167
/// }
168
/// ```
169
pub struct OomTest {
170
max_iters: Option<u32>,
171
max_duration: Option<time::Duration>,
172
}
173
174
impl OomTest {
175
/// Create a new OOM test.
176
///
177
/// By default there is no iteration or time limit, tests will be executed
178
/// until the pass (or fail).
179
pub fn new() -> Self {
180
let _ = env_logger::try_init();
181
182
// NB: `std::backtrace::Backtrace` doesn't have ways to handle
183
// OOM. Ideally we would just disable the `"backtrace"` cargo feature,
184
// but workspace feature resolution doesn't play nice with that.
185
wasmtime_core::error::disable_backtrace();
186
187
OomTest {
188
max_iters: None,
189
max_duration: None,
190
}
191
}
192
193
/// Configure the maximum number of times to run an OOM test.
194
pub fn max_iters(&mut self, max_iters: u32) -> &mut Self {
195
self.max_iters = Some(max_iters);
196
self
197
}
198
199
/// Configure the maximum duration of time to run an OOM text.
200
pub fn max_duration(&mut self, max_duration: time::Duration) -> &mut Self {
201
self.max_duration = Some(max_duration);
202
self
203
}
204
205
/// Repeatedly run the given test function, injecting OOMs at different
206
/// times and checking that it correctly handles them.
207
///
208
/// The test function should not use threads, or else allocations may not be
209
/// tracked correctly and OOM injection may be incorrect.
210
///
211
/// The test function should return an `Err(_)` if and only if it encounters
212
/// an OOM.
213
///
214
/// Returns early once the test function returns `Ok(())` before an OOM has
215
/// been injected.
216
pub fn test(&self, test_func: impl Fn() -> Result<()>) -> Result<()> {
217
let start = time::Instant::now();
218
219
for i in 0.. {
220
if self.max_iters.is_some_and(|n| i >= n)
221
|| self.max_duration.is_some_and(|d| start.elapsed() >= d)
222
{
223
break;
224
}
225
226
log::trace!("=== Injecting OOM after {i} allocations ===");
227
let (result, old_state) = {
228
let guard = ScopedOomState::new(OomState::OomOnAlloc(i));
229
assert_eq!(guard.prev_state, OomState::OutsideOomTest);
230
231
let result = test_func();
232
233
(result, guard.finish())
234
};
235
236
match (result, old_state) {
237
(_, OomState::OutsideOomTest) => unreachable!(),
238
239
// The test function completed successfully before we ran out of
240
// allocation fuel, so we're done.
241
(Ok(()), OomState::OomOnAlloc(_)) => break,
242
243
// We injected an OOM and the test function handled it
244
// correctly; continue to the next iteration.
245
(Err(e), OomState::DidOom) if self.is_oom_error(&e) => {}
246
247
// Missed OOMs.
248
(Ok(()), OomState::DidOom) => {
249
bail!("OOM test function missed an OOM: returned Ok(())");
250
}
251
(Err(e), OomState::DidOom) => {
252
return Err(
253
e.context("OOM test function missed an OOM: returned non-OOM error")
254
);
255
}
256
257
// Unexpected error.
258
(Err(e), OomState::OomOnAlloc(_)) => {
259
return Err(
260
e.context("OOM test function returned an error when there was no OOM")
261
);
262
}
263
}
264
}
265
266
Ok(())
267
}
268
269
fn is_oom_error(&self, e: &Error) -> bool {
270
e.is::<OutOfMemory>()
271
}
272
}
273
274