Skip to content

Commit fe263ae

Browse files
authored
Add a new oom fuzz target that runs Wasm and checks it handles OOM (#13421)
* Add a new `oom` fuzz target that runs Wasm and checks it handles OOM * don't compile error when not cfg(arc_try_new)
1 parent aed0abc commit fe263ae

3 files changed

Lines changed: 154 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

fuzz/Cargo.toml

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ pulley-interpreter-fuzz = { workspace = true }
3030
smallvec = { workspace = true }
3131
wasmparser = { workspace = true }
3232
wasmtime = { workspace = true, features = ["winch"] }
33+
wasmtime-core = { workspace = true }
3334
wasmtime-fuzzing = { workspace = true }
3435
wasmtime-test-util = { workspace = true }
3536
log = { workspace = true }
@@ -126,3 +127,9 @@ path = "fuzz_targets/gc_ops.rs"
126127
test = false
127128
doc = false
128129
bench = false
130+
131+
[[bin]]
132+
name = "oom"
133+
path = "fuzz_targets/oom.rs"
134+
test = false
135+
doc = false

fuzz/fuzz_targets/oom.rs

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
#![no_main]
2+
3+
use libfuzzer_sys::arbitrary::{Arbitrary, Result, Unstructured};
4+
use wasmtime::{Engine, Module, Store, Trap, Val, error::OutOfMemory};
5+
use wasmtime_core::alloc::TryVec;
6+
use wasmtime_fuzzing::generators::Config;
7+
use wasmtime_fuzzing::oom::{OomTest, OomTestAllocator};
8+
use wasmtime_fuzzing::oracles::dummy;
9+
use wasmtime_fuzzing::single_module_fuzzer::KnownValid;
10+
11+
const OOM_TEST_ITERS: u32 = 10;
12+
const OOM_TEST_FUEL: u64 = 1000;
13+
14+
#[global_allocator]
15+
static GLOBAL_ALLOCATOR: OomTestAllocator = OomTestAllocator::new();
16+
17+
wasmtime_fuzzing::single_module_fuzzer!(execute gen_module);
18+
19+
#[derive(Debug)]
20+
struct OomInput {
21+
config: Config,
22+
seed: u64,
23+
}
24+
25+
impl<'a> Arbitrary<'a> for OomInput {
26+
fn arbitrary(u: &mut Unstructured<'a>) -> Result<Self> {
27+
let mut config: Config = u.arbitrary()?;
28+
config.module_config.config.exceptions_enabled = false;
29+
config.module_config.config.gc_enabled = false;
30+
config.module_config.config.reference_types_enabled = false;
31+
config.module_config.function_references_enabled = false;
32+
config.module_config.config.export_everything = true;
33+
config.wasmtime.strategy =
34+
wasmtime_fuzzing::generators::InstanceAllocationStrategy::OnDemand;
35+
let seed = u.arbitrary()?;
36+
Ok(OomInput { config, seed })
37+
}
38+
}
39+
40+
fn compile(config: &Config, wasm: &[u8]) -> wasmtime::Result<Vec<u8>> {
41+
let mut wasmtime_config = config.to_wasmtime();
42+
wasmtime_config.concurrency_support(false);
43+
wasmtime_config.consume_fuel(true);
44+
let engine = Engine::new(&wasmtime_config)?;
45+
let module = Module::new(&engine, wasm)?;
46+
module.serialize()
47+
}
48+
49+
fn execute(
50+
module: &[u8],
51+
_known_valid: KnownValid,
52+
input: OomInput,
53+
_u: &mut Unstructured<'_>,
54+
) -> Result<()> {
55+
if cfg!(not(arc_try_new)) {
56+
panic!(
57+
"The OOM fuzzer is disabled because `cfg(arc_try_new)` was not enabled. Build with \
58+
`RUSTFLAGS=--cfg=arc_try_new` to enable."
59+
);
60+
}
61+
62+
let module_bytes = match compile(&input.config, module) {
63+
Ok(bytes) => bytes,
64+
Err(_) => return Ok(()),
65+
};
66+
67+
let mut oom_config = input.config.to_wasmtime();
68+
oom_config.enable_compiler(false);
69+
oom_config.concurrency_support(false);
70+
oom_config.consume_fuel(true);
71+
72+
// Prevent real process-level OOM: fuzzer-generated configs can set
73+
// `memory_reservation(0)`, forcing `mmap` to commit real pages that bypass
74+
// `OomTestAllocator`. Use large virtual reservations instead.
75+
oom_config.memory_reservation(1 << 32);
76+
oom_config.memory_guard_size(1 << 31);
77+
78+
let oom_engine = match Engine::new(&oom_config) {
79+
Ok(e) => e,
80+
Err(_) => return Ok(()),
81+
};
82+
83+
let _ = OomTest::new()
84+
.seed(input.seed)
85+
.max_iters(OOM_TEST_ITERS)
86+
.allow_alloc_after_oom(true)
87+
.alloc_succeeds_after_oom(true)
88+
.allow_missed_oom_errors(true)
89+
.fuzz(|| {
90+
let module = unsafe { Module::deserialize(&oom_engine, &module_bytes)? };
91+
92+
let mut store = Store::try_new(&oom_engine, ())?;
93+
store.set_fuel(OOM_TEST_FUEL).unwrap();
94+
95+
let linker = dummy::dummy_linker(&mut store, &module)?;
96+
let instance = linker.instantiate(&mut store, &module)?;
97+
98+
'export_loop: for export in module.exports() {
99+
let extern_ty = export.ty();
100+
let Some(func_ty) = extern_ty.func() else {
101+
continue;
102+
};
103+
let func = instance.get_func(&mut store, export.name()).unwrap();
104+
105+
// Build default params; skip if any param type has no default.
106+
let mut params: TryVec<Val> = TryVec::with_capacity(func_ty.params().len())?;
107+
for p in func_ty.params() {
108+
match p.default_value() {
109+
Some(v) => params.push(v)?,
110+
None => {
111+
continue 'export_loop;
112+
}
113+
}
114+
}
115+
116+
let mut results: TryVec<Val> = TryVec::with_capacity(func_ty.results().len())?;
117+
for _ in 0..func_ty.results().len() {
118+
results.push(Val::I32(0))?;
119+
}
120+
121+
match func.call(&mut store, &params, &mut results) {
122+
// OOM; return from this OOM test iteration.
123+
Err(e) if e.is::<OutOfMemory>() => return Err(e),
124+
125+
// Out of fuel; stop calling exports.
126+
Err(e)
127+
if e.downcast_ref::<Trap>()
128+
.is_some_and(|trap| *trap == Trap::OutOfFuel) =>
129+
{
130+
break;
131+
}
132+
133+
Err(_) | Ok(_) => {}
134+
}
135+
}
136+
137+
Ok(())
138+
});
139+
140+
Ok(())
141+
}
142+
143+
fn gen_module(input: &mut OomInput, u: &mut Unstructured<'_>) -> Result<(Vec<u8>, KnownValid)> {
144+
let module = input.config.generate(u, Some(1000))?;
145+
Ok((module.to_bytes(), KnownValid::Yes))
146+
}

0 commit comments

Comments
 (0)