|
| 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, ¶ms, &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