Browse Source
* Add a fuzzer for async wasm This commit revives a very old branch of mine to add a fuzzer for Wasmtime in async mode. This work was originally blocked on llvm/llvm-project#53891 and while that's still an issue it now contains a workaround for that issue. Support for async fuzzing required a good deal of refactorings and changes, and the highlights are: * The main part is that new intrinsics, `__sanitizer_{start,finish}_fiber_switch` are now invoked around the stack-switching routines of fibers. This only works on Unix and is set to only compile when ASAN is enabled (otherwise everything is a noop). This required refactoring of things to get it all in just the right way for ASAN since it appears that these functions not only need to be called but more-or-less need to be adjacent to each other in the code. My guess is that while we're switching ASAN is in a "weird state" and it's not ready to run arbitrary code. * Stacks are a problem. The above issue in LLVM outlines how stacks cannot be deallocated at this time because if the deallocated virtual memory is later used for the heap then ASAN will have a false positive about stack overflow. To handle this stacks are specially handled in asan mode by using a special allocation path that never deallocates stacks. This logic additionally applies to the pooling allocator which uses a different stack allocation strategy with ASAN. With all of the above a new fuzzer is added. This fuzzer generates an arbitrary module, selects an arbitrary means of async (e.g. epochs/fuel), and then tries to execute the exports of the module with various values. In general the fuzzer is looking for crashes/panics as opposed to correct answers as there's no oracle here. This is also intended to stress the code used to switch on and off stacks. * Fix non-async build * Remove unused import * Review comments * Fix compile on MIRI * Fix Windows buildpull/8450/head
Alex Crichton
7 months ago
committed by
GitHub
17 changed files with 825 additions and 225 deletions
@ -0,0 +1,44 @@ |
|||
use arbitrary::{Arbitrary, Unstructured}; |
|||
use std::time::Duration; |
|||
|
|||
/// Configuration for async support within a store.
|
|||
///
|
|||
/// Note that the `Arbitrary` implementation for this type always returns
|
|||
/// `Disabled` because this is something that is statically chosen if the fuzzer
|
|||
/// has support for async.
|
|||
#[derive(Clone, Debug, Eq, Hash, PartialEq)] |
|||
pub enum AsyncConfig { |
|||
/// No async support enabled.
|
|||
Disabled, |
|||
/// Async support is enabled and cooperative yielding is done with fuel.
|
|||
YieldWithFuel(u64), |
|||
/// Async support is enabled and cooperative yielding is done with epochs.
|
|||
YieldWithEpochs { |
|||
/// Duration between epoch ticks.
|
|||
dur: Duration, |
|||
/// Number of ticks between yields.
|
|||
ticks: u64, |
|||
}, |
|||
} |
|||
|
|||
impl AsyncConfig { |
|||
/// Applies this async configuration to the `wasmtime::Config` provided to
|
|||
/// ensure it's ready to execute with the resulting modules.
|
|||
pub fn configure(&self, config: &mut wasmtime::Config) { |
|||
match self { |
|||
AsyncConfig::Disabled => {} |
|||
AsyncConfig::YieldWithFuel(_) => { |
|||
config.async_support(true).consume_fuel(true); |
|||
} |
|||
AsyncConfig::YieldWithEpochs { .. } => { |
|||
config.async_support(true).epoch_interruption(true); |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
impl<'a> Arbitrary<'a> for AsyncConfig { |
|||
fn arbitrary(_: &mut Unstructured<'a>) -> arbitrary::Result<AsyncConfig> { |
|||
Ok(AsyncConfig::Disabled) |
|||
} |
|||
} |
@ -0,0 +1,66 @@ |
|||
#![cfg_attr(not(asan), allow(dead_code))] |
|||
|
|||
use crate::PoolingInstanceAllocatorConfig; |
|||
use anyhow::{bail, Result}; |
|||
use std::sync::atomic::{AtomicU64, Ordering}; |
|||
|
|||
/// A generic implementation of a stack pool.
|
|||
///
|
|||
/// This implementation technically doesn't actually pool anything at this time.
|
|||
/// Originally this was the implementation for non-Unix (e.g. Windows and
|
|||
/// MIRI), but nowadays this is also used for fuzzing. For more documentation
|
|||
/// for why this is used on fuzzing see the `asan` module in the
|
|||
/// `wasmtime-fiber` crate.
|
|||
///
|
|||
/// Currently the only purpose of `StackPool` is to limit the total number of
|
|||
/// concurrent stacks while otherwise leveraging `wasmtime_fiber::FiberStack`
|
|||
/// natively.
|
|||
#[derive(Debug)] |
|||
pub struct StackPool { |
|||
stack_size: usize, |
|||
live_stacks: AtomicU64, |
|||
stack_limit: u64, |
|||
} |
|||
|
|||
impl StackPool { |
|||
pub fn new(config: &PoolingInstanceAllocatorConfig) -> Result<Self> { |
|||
Ok(StackPool { |
|||
stack_size: config.stack_size, |
|||
live_stacks: AtomicU64::new(0), |
|||
stack_limit: config.limits.total_stacks.into(), |
|||
}) |
|||
} |
|||
|
|||
pub fn is_empty(&self) -> bool { |
|||
self.live_stacks.load(Ordering::Acquire) == 0 |
|||
} |
|||
|
|||
pub fn allocate(&self) -> Result<wasmtime_fiber::FiberStack> { |
|||
if self.stack_size == 0 { |
|||
bail!("fiber stack allocation not supported") |
|||
} |
|||
|
|||
let old_count = self.live_stacks.fetch_add(1, Ordering::AcqRel); |
|||
if old_count >= self.stack_limit { |
|||
self.live_stacks.fetch_sub(1, Ordering::AcqRel); |
|||
bail!( |
|||
"maximum concurrent fiber limit of {} reached", |
|||
self.stack_limit |
|||
); |
|||
} |
|||
|
|||
match wasmtime_fiber::FiberStack::new(self.stack_size) { |
|||
Ok(stack) => Ok(stack), |
|||
Err(e) => { |
|||
self.live_stacks.fetch_sub(1, Ordering::AcqRel); |
|||
Err(anyhow::Error::from(e)) |
|||
} |
|||
} |
|||
} |
|||
|
|||
pub unsafe fn deallocate(&self, stack: &wasmtime_fiber::FiberStack) { |
|||
self.live_stacks.fetch_sub(1, Ordering::AcqRel); |
|||
// A no-op as we don't own the fiber stack on Windows.
|
|||
let _ = stack; |
|||
} |
|||
} |
@ -1,3 +1,5 @@ |
|||
#![cfg_attr(asan, allow(dead_code))] |
|||
|
|||
use super::{ |
|||
index_allocator::{SimpleIndexAllocator, SlotId}, |
|||
round_up_to_pow2, |
@ -0,0 +1,39 @@ |
|||
#![no_main] |
|||
|
|||
use libfuzzer_sys::arbitrary::{Result, Unstructured}; |
|||
use libfuzzer_sys::fuzz_target; |
|||
use wasmtime_fuzzing::{generators, oracles}; |
|||
|
|||
fuzz_target!(|data: &[u8]| { |
|||
// errors in `run` have to do with not enough input in `data`, which we
|
|||
// ignore here since it doesn't affect how we'd like to fuzz.
|
|||
let _ = run_one(data); |
|||
}); |
|||
|
|||
fn run_one(data: &[u8]) -> Result<()> { |
|||
let mut u = Unstructured::new(data); |
|||
let mut config: generators::Config = u.arbitrary()?; |
|||
|
|||
// Try to ensure imports/exports/etc are generated by adding one to the
|
|||
// minimums/maximums.
|
|||
config.module_config.config.min_types = 1; |
|||
config.module_config.config.max_types += 1; |
|||
config.module_config.config.min_imports = 1; |
|||
config.module_config.config.max_imports += 1; |
|||
config.module_config.config.min_funcs = 1; |
|||
config.module_config.config.max_funcs += 1; |
|||
config.module_config.config.min_exports = 1; |
|||
config.module_config.config.max_exports += 1; |
|||
|
|||
// Use the fuzz input to select an async strategy.
|
|||
config.enable_async(&mut u)?; |
|||
|
|||
let mut poll_amts = Vec::with_capacity(u.arbitrary_len::<u32>()?); |
|||
for _ in 0..poll_amts.capacity() { |
|||
poll_amts.push(u.int_in_range(0..=10_000)?); |
|||
} |
|||
let module = config.module_config.generate(&mut u, None)?; |
|||
oracles::call_async(&module.to_bytes(), &config, &poll_amts); |
|||
|
|||
Ok(()) |
|||
} |
Loading…
Reference in new issue