Add the instance allocation strategy to generated fuzzing configs. (#3780)

* Add the instance allocation strategy to generated fuzzing configs.

This commit adds support for generating configs with arbitrary instance
allocation strategies.

With this, the pooling allocator will be fuzzed as part of the existing fuzz
targets.

* Refine maximum constants for arbitrary module limits.

* Add an `instantiate-many` fuzz target.

This commit adds a new `instantiate-many` fuzz target that will attempt to
instantiate and terminate modules in an arbitrary order.

It generates up to 5 modules, from which a random sequence of instances will be
created.

The primary benefactor of this fuzz target is the pooling instance allocator.

* Allow no aliasing in generated modules when using the pooling allocator.

This commit prevents aliases in the generated modules as they might count
against the configured import limits of the pooling allocator.

As the existing module linking proposal implementation will eventually be
deprecated in favor of the component model proposal, it isn't very important
that we test aliases in generated modules with the pooling allocator.

* Improve distribution of memory config in fuzzing.

The previous commit attempted to provide a 32-bit upper bound to 64-bit
arbitrary values, which skewed the distribution heavily in favor of the upper
bound.

This commit removes the constraint and instead uses arbitrary 32-bit values
that are converted to 64-bit values in the `Arbitrary` implementation.
This commit is contained in:
Peter Huene
2022-02-10 11:55:44 -08:00
committed by GitHub
parent 027dea549a
commit 41eb225765
5 changed files with 406 additions and 30 deletions

View File

@@ -36,13 +36,156 @@ impl OptLevel {
} }
} }
/// Configuration for `wasmtime::PoolingAllocationStrategy`.
#[derive(Arbitrary, Clone, Debug, PartialEq, Eq, Hash)]
pub enum PoolingAllocationStrategy {
/// Use next available instance slot.
NextAvailable,
/// Use random instance slot.
Random,
/// Use an affinity-based strategy.
ReuseAffinity,
}
impl PoolingAllocationStrategy {
fn to_wasmtime(&self) -> wasmtime::PoolingAllocationStrategy {
match self {
PoolingAllocationStrategy::NextAvailable => {
wasmtime::PoolingAllocationStrategy::NextAvailable
}
PoolingAllocationStrategy::Random => wasmtime::PoolingAllocationStrategy::Random,
PoolingAllocationStrategy::ReuseAffinity => {
wasmtime::PoolingAllocationStrategy::ReuseAffinity
}
}
}
}
/// Configuration for `wasmtime::ModuleLimits`.
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct ModuleLimits {
imported_functions: u32,
imported_tables: u32,
imported_memories: u32,
imported_globals: u32,
types: u32,
functions: u32,
tables: u32,
memories: u32,
/// The maximum number of globals that can be defined in a module.
pub globals: u32,
table_elements: u32,
memory_pages: u64,
}
impl ModuleLimits {
fn to_wasmtime(&self) -> wasmtime::ModuleLimits {
wasmtime::ModuleLimits {
imported_functions: self.imported_functions,
imported_tables: self.imported_tables,
imported_memories: self.imported_memories,
imported_globals: self.imported_globals,
types: self.types,
functions: self.functions,
tables: self.tables,
memories: self.memories,
globals: self.globals,
table_elements: self.table_elements,
memory_pages: self.memory_pages,
}
}
}
impl<'a> Arbitrary<'a> for ModuleLimits {
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
const MAX_IMPORTS: u32 = 1000;
const MAX_TYPES: u32 = 1000;
const MAX_FUNCTIONS: u32 = 1000;
const MAX_TABLES: u32 = 10;
const MAX_MEMORIES: u32 = 10;
const MAX_GLOBALS: u32 = 1000;
const MAX_ELEMENTS: u32 = 1000;
const MAX_MEMORY_PAGES: u64 = 0x10000;
Ok(Self {
imported_functions: u.int_in_range(0..=MAX_IMPORTS)?,
imported_tables: u.int_in_range(0..=MAX_IMPORTS)?,
imported_memories: u.int_in_range(0..=MAX_IMPORTS)?,
imported_globals: u.int_in_range(0..=MAX_IMPORTS)?,
types: u.int_in_range(0..=MAX_TYPES)?,
functions: u.int_in_range(0..=MAX_FUNCTIONS)?,
tables: u.int_in_range(0..=MAX_TABLES)?,
memories: u.int_in_range(0..=MAX_MEMORIES)?,
globals: u.int_in_range(0..=MAX_GLOBALS)?,
table_elements: u.int_in_range(0..=MAX_ELEMENTS)?,
memory_pages: u.int_in_range(0..=MAX_MEMORY_PAGES)?,
})
}
}
/// Configuration for `wasmtime::PoolingAllocationStrategy`.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct InstanceLimits {
/// The maximum number of instances that can be instantiated in the pool at a time.
pub count: u32,
}
impl InstanceLimits {
fn to_wasmtime(&self) -> wasmtime::InstanceLimits {
wasmtime::InstanceLimits { count: self.count }
}
}
impl<'a> Arbitrary<'a> for InstanceLimits {
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
const MAX_COUNT: u32 = 100;
Ok(Self {
count: u.int_in_range(1..=MAX_COUNT)?,
})
}
}
/// Configuration for `wasmtime::InstanceAllocationStrategy`.
#[derive(Arbitrary, Clone, Debug, Eq, PartialEq, Hash)]
pub enum InstanceAllocationStrategy {
/// Use the on-demand instance allocation strategy.
OnDemand,
/// Use the pooling instance allocation strategy.
Pooling {
/// The pooling strategy to use.
strategy: PoolingAllocationStrategy,
/// The module limits.
module_limits: ModuleLimits,
/// The instance limits.
instance_limits: InstanceLimits,
},
}
impl InstanceAllocationStrategy {
fn to_wasmtime(&self) -> wasmtime::InstanceAllocationStrategy {
match self {
InstanceAllocationStrategy::OnDemand => wasmtime::InstanceAllocationStrategy::OnDemand,
InstanceAllocationStrategy::Pooling {
strategy,
module_limits,
instance_limits,
} => wasmtime::InstanceAllocationStrategy::Pooling {
strategy: strategy.to_wasmtime(),
module_limits: module_limits.to_wasmtime(),
instance_limits: instance_limits.to_wasmtime(),
},
}
}
}
/// Configuration for `wasmtime::Config` and generated modules for a session of /// Configuration for `wasmtime::Config` and generated modules for a session of
/// fuzzing. /// fuzzing.
/// ///
/// This configuration guides what modules are generated, how wasmtime /// This configuration guides what modules are generated, how wasmtime
/// configuration is generated, and is typically itself generated through a call /// configuration is generated, and is typically itself generated through a call
/// to `Arbitrary` which allows for a form of "swarm testing". /// to `Arbitrary` which allows for a form of "swarm testing".
#[derive(Arbitrary, Debug)] #[derive(Debug)]
pub struct Config { pub struct Config {
/// Configuration related to the `wasmtime::Config`. /// Configuration related to the `wasmtime::Config`.
pub wasmtime: WasmtimeConfig, pub wasmtime: WasmtimeConfig,
@@ -50,6 +193,49 @@ pub struct Config {
pub module_config: ModuleConfig, pub module_config: ModuleConfig,
} }
impl<'a> Arbitrary<'a> for Config {
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
let mut config = Self {
wasmtime: u.arbitrary()?,
module_config: u.arbitrary()?,
};
// If using the pooling allocator, constrain the memory and module configurations
// to the module limits.
if let InstanceAllocationStrategy::Pooling {
module_limits: limits,
..
} = &config.wasmtime.strategy
{
// Force the use of a normal memory config when using the pooling allocator and
// limit the static memory maximum to be the same as the pooling allocator's memory
// page limit.
let mut memory_config: NormalMemoryConfig = u.arbitrary()?;
memory_config.static_memory_maximum_size = Some(limits.memory_pages * 0x10000);
config.wasmtime.memory_config = MemoryConfig::Normal(memory_config);
let cfg = &mut config.module_config.config;
cfg.max_imports = limits.imported_functions.min(
limits
.imported_globals
.min(limits.imported_memories.min(limits.imported_tables)),
) as usize;
cfg.max_types = limits.types as usize;
cfg.max_funcs = limits.functions as usize;
cfg.max_globals = limits.globals as usize;
cfg.max_memories = limits.memories as usize;
cfg.max_tables = limits.tables as usize;
cfg.max_memory_pages = limits.memory_pages;
// Force no aliases in any generated modules as they might count against the
// import limits above.
cfg.max_aliases = 0;
}
Ok(config)
}
}
/// Configuration related to `wasmtime::Config` and the various settings which /// Configuration related to `wasmtime::Config` and the various settings which
/// can be tweaked from within. /// can be tweaked from within.
#[derive(Arbitrary, Clone, Debug, Eq, Hash, PartialEq)] #[derive(Arbitrary, Clone, Debug, Eq, Hash, PartialEq)]
@@ -63,6 +249,8 @@ pub struct WasmtimeConfig {
force_jump_veneers: bool, force_jump_veneers: bool,
memfd: bool, memfd: bool,
use_precompiled_cwasm: bool, use_precompiled_cwasm: bool,
/// Configuration for the instance allocation strategy to use.
pub strategy: InstanceAllocationStrategy,
} }
#[derive(Arbitrary, Clone, Debug, Eq, Hash, PartialEq)] #[derive(Arbitrary, Clone, Debug, Eq, Hash, PartialEq)]
@@ -70,15 +258,7 @@ enum MemoryConfig {
/// Configuration for linear memories which correspond to normal /// Configuration for linear memories which correspond to normal
/// configuration settings in `wasmtime` itself. This will tweak various /// configuration settings in `wasmtime` itself. This will tweak various
/// parameters about static/dynamic memories. /// parameters about static/dynamic memories.
/// Normal(NormalMemoryConfig),
/// Note that we use 32-bit values here to avoid blowing the 64-bit address
/// space by requesting ungodly-large sizes/guards.
Normal {
static_memory_maximum_size: Option<u32>,
static_memory_guard_size: Option<u32>,
dynamic_memory_guard_size: Option<u32>,
guard_before_linear_memory: bool,
},
/// Configuration to force use of a linear memory that's unaligned at its /// Configuration to force use of a linear memory that's unaligned at its
/// base address to force all wasm addresses to be unaligned at the hardware /// base address to force all wasm addresses to be unaligned at the hardware
@@ -86,6 +266,27 @@ enum MemoryConfig {
CustomUnaligned, CustomUnaligned,
} }
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
struct NormalMemoryConfig {
static_memory_maximum_size: Option<u64>,
static_memory_guard_size: Option<u64>,
dynamic_memory_guard_size: Option<u64>,
guard_before_linear_memory: bool,
}
impl<'a> Arbitrary<'a> for NormalMemoryConfig {
fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
// This attempts to limit memory and guard sizes to 32-bit ranges so
// we don't exhaust a 64-bit address space easily.
Ok(Self {
static_memory_maximum_size: <Option<u32> as Arbitrary>::arbitrary(u)?.map(Into::into),
static_memory_guard_size: <Option<u32> as Arbitrary>::arbitrary(u)?.map(Into::into),
dynamic_memory_guard_size: <Option<u32> as Arbitrary>::arbitrary(u)?.map(Into::into),
guard_before_linear_memory: u.arbitrary()?,
})
}
}
impl Config { impl Config {
/// Converts this to a `wasmtime::Config` object /// Converts this to a `wasmtime::Config` object
pub fn to_wasmtime(&self) -> wasmtime::Config { pub fn to_wasmtime(&self) -> wasmtime::Config {
@@ -102,7 +303,8 @@ impl Config {
.cranelift_opt_level(self.wasmtime.opt_level.to_wasmtime()) .cranelift_opt_level(self.wasmtime.opt_level.to_wasmtime())
.interruptable(self.wasmtime.interruptable) .interruptable(self.wasmtime.interruptable)
.consume_fuel(self.wasmtime.consume_fuel) .consume_fuel(self.wasmtime.consume_fuel)
.memfd(self.wasmtime.memfd); .memfd(self.wasmtime.memfd)
.allocation_strategy(self.wasmtime.strategy.to_wasmtime());
// If the wasm-smith-generated module use nan canonicalization then we // If the wasm-smith-generated module use nan canonicalization then we
// don't need to enable it, but if it doesn't enable it already then we // don't need to enable it, but if it doesn't enable it already then we
@@ -126,16 +328,13 @@ impl Config {
} }
match &self.wasmtime.memory_config { match &self.wasmtime.memory_config {
MemoryConfig::Normal { MemoryConfig::Normal(memory_config) => {
static_memory_maximum_size, cfg.static_memory_maximum_size(
static_memory_guard_size, memory_config.static_memory_maximum_size.unwrap_or(0),
dynamic_memory_guard_size, )
guard_before_linear_memory, .static_memory_guard_size(memory_config.static_memory_guard_size.unwrap_or(0))
} => { .dynamic_memory_guard_size(memory_config.dynamic_memory_guard_size.unwrap_or(0))
cfg.static_memory_maximum_size(static_memory_maximum_size.unwrap_or(0).into()) .guard_before_linear_memory(memory_config.guard_before_linear_memory);
.static_memory_guard_size(static_memory_guard_size.unwrap_or(0).into())
.dynamic_memory_guard_size(dynamic_memory_guard_size.unwrap_or(0).into())
.guard_before_linear_memory(*guard_before_linear_memory);
} }
MemoryConfig::CustomUnaligned => { MemoryConfig::CustomUnaligned => {
cfg.with_host_memory(Arc::new(UnalignedMemoryCreator)) cfg.with_host_memory(Arc::new(UnalignedMemoryCreator))
@@ -145,6 +344,7 @@ impl Config {
.guard_before_linear_memory(false); .guard_before_linear_memory(false);
} }
} }
return cfg; return cfg;
} }
@@ -153,11 +353,16 @@ impl Config {
pub fn to_store(&self) -> Store<StoreLimits> { pub fn to_store(&self) -> Store<StoreLimits> {
let engine = Engine::new(&self.to_wasmtime()).unwrap(); let engine = Engine::new(&self.to_wasmtime()).unwrap();
let mut store = Store::new(&engine, StoreLimits::new()); let mut store = Store::new(&engine, StoreLimits::new());
self.configure_store(&mut store);
store
}
/// Configures a store based on this configuration.
pub fn configure_store(&self, store: &mut Store<StoreLimits>) {
store.limiter(|s| s as &mut dyn wasmtime::ResourceLimiter); store.limiter(|s| s as &mut dyn wasmtime::ResourceLimiter);
if self.wasmtime.consume_fuel { if self.wasmtime.consume_fuel {
store.add_fuel(u64::max_value()).unwrap(); store.add_fuel(u64::max_value()).unwrap();
} }
return store;
} }
/// Generates an arbitrary method of timing out an instance, ensuring that /// Generates an arbitrary method of timing out an instance, ensuring that

View File

@@ -14,6 +14,7 @@ pub mod dummy;
use crate::generators; use crate::generators;
use anyhow::Context; use anyhow::Context;
use arbitrary::Arbitrary;
use log::{debug, warn}; use log::{debug, warn};
use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; use std::sync::atomic::{AtomicUsize, Ordering::SeqCst};
use std::sync::{Arc, Condvar, Mutex}; use std::sync::{Arc, Condvar, Mutex};
@@ -142,14 +143,107 @@ pub fn instantiate(wasm: &[u8], known_valid: bool, config: &generators::Config,
Timeout::None => {} Timeout::None => {}
} }
log_wasm(wasm); if let Some(module) = compile_module(store.engine(), wasm, known_valid, config) {
let module = match config.compile(store.engine(), wasm) {
Ok(module) => module,
Err(_) if !known_valid => return,
Err(e) => panic!("failed to compile module: {:?}", e),
};
instantiate_with_dummy(&mut store, &module); instantiate_with_dummy(&mut store, &module);
}
}
/// Represents supported commands to the `instantiate_many` function.
#[derive(Arbitrary, Debug)]
pub enum Command {
/// Instantiates a module.
///
/// The value is the index of the module to instantiate.
///
/// The module instantiated will be this value modulo the number of modules provided to `instantiate_many`.
Instantiate(usize),
/// Terminates a "running" instance.
///
/// The value is the index of the instance to terminate.
///
/// The instance terminated will be this value modulo the number of currently running
/// instances.
///
/// If no instances are running, the command will be ignored.
Terminate(usize),
}
/// Instantiates many instances from the given modules.
///
/// The engine will be configured using the provided config.
///
/// The modules are expected to *not* have start functions as no timeouts are configured.
pub fn instantiate_many(
modules: &[Vec<u8>],
known_valid: bool,
config: &generators::Config,
commands: &[Command],
) {
assert!(!config.module_config.config.allow_start_export);
let engine = Engine::new(&config.to_wasmtime()).unwrap();
let modules = modules
.iter()
.filter_map(|bytes| compile_module(&engine, bytes, known_valid, config))
.collect::<Vec<_>>();
// If no modules were valid, we're done
if modules.is_empty() {
return;
}
// This stores every `Store` where a successful instantiation takes place
let mut stores = Vec::new();
for command in commands {
match command {
Command::Instantiate(index) => {
let module = &modules[*index % modules.len()];
let mut store = Store::new(&engine, StoreLimits::new());
config.configure_store(&mut store);
if instantiate_with_dummy(&mut store, module).is_some() {
stores.push(Some(store));
}
}
Command::Terminate(index) => {
if stores.is_empty() {
continue;
}
stores.swap_remove(*index % stores.len());
}
}
}
}
fn compile_module(
engine: &Engine,
bytes: &[u8],
known_valid: bool,
config: &generators::Config,
) -> Option<Module> {
log_wasm(bytes);
match config.compile(engine, bytes) {
Ok(module) => Some(module),
Err(_) if !known_valid => None,
Err(e) => {
if let generators::InstanceAllocationStrategy::Pooling { .. } =
&config.wasmtime.strategy
{
// When using the pooling allocator, accept failures to compile when arbitrary
// table element limits have been exceeded as there is currently no way
// to constrain the generated module table types.
let string = e.to_string();
if string.contains("minimum element size") {
return None;
}
}
panic!("failed to compile module: {:?}", e);
}
}
} }
fn instantiate_with_dummy(store: &mut Store<StoreLimits>, module: &Module) -> Option<Instance> { fn instantiate_with_dummy(store: &mut Store<StoreLimits>, module: &Module) -> Option<Instance> {
@@ -188,8 +282,13 @@ fn instantiate_with_dummy(store: &mut Store<StoreLimits>, module: &Module) -> Op
return None; return None;
} }
// Also allow failures to instantiate as a result of hitting instance limits
if string.contains("concurrent instances has been reached") {
return None;
}
// Everything else should be a bug in the fuzzer or a bug in wasmtime // Everything else should be a bug in the fuzzer or a bug in wasmtime
panic!("failed to instantiate {:?}", e); panic!("failed to instantiate: {:?}", e);
} }
/// Instantiate the given Wasm module with each `Config` and call all of its /// Instantiate the given Wasm module with each `Config` and call all of its

View File

@@ -91,3 +91,9 @@ name = "cranelift-fuzzgen-verify"
path = "fuzz_targets/cranelift-fuzzgen-verify.rs" path = "fuzz_targets/cranelift-fuzzgen-verify.rs"
test = false test = false
doc = false doc = false
[[bin]]
name = "instantiate-many"
path = "fuzz_targets/instantiate-many.rs"
test = false
doc = false

View File

@@ -0,0 +1,54 @@
//! This fuzz target is used to test multiple concurrent instantiations from
//! multiple modules.
#![no_main]
use libfuzzer_sys::arbitrary::{Result, Unstructured};
use libfuzzer_sys::fuzz_target;
use wasmtime_fuzzing::{generators, oracles};
const MAX_MODULES: usize = 5;
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.
drop(run(data));
});
fn run(data: &[u8]) -> Result<()> {
let mut u = Unstructured::new(data);
let mut config: generators::Config = u.arbitrary()?;
// Don't generate start functions
// No wasm code execution is necessary for this fuzz target and thus we don't
// use timeouts or ensure that the generated wasm code will terminate.
config.module_config.config.allow_start_export = false;
// Create the modules to instantiate
let modules = (0..u.int_in_range(1..=MAX_MODULES)?)
.map(|_| Ok(config.module_config.generate(&mut u)?.to_bytes()))
.collect::<Result<Vec<_>>>()?;
let max_instances = match &config.wasmtime.strategy {
generators::InstanceAllocationStrategy::OnDemand => u.int_in_range(1..=100)?,
generators::InstanceAllocationStrategy::Pooling {
instance_limits, ..
} => instance_limits.count,
};
// Front-load with instantiation commands
let mut commands: Vec<oracles::Command> = (0..u.int_in_range(1..=max_instances)?)
.map(|_| Ok(oracles::Command::Instantiate(u.arbitrary()?)))
.collect::<Result<_>>()?;
// Then add some more arbitrary commands
commands.extend(
(0..u.int_in_range(0..=2 * max_instances)?)
.map(|_| u.arbitrary())
.collect::<Result<Vec<_>>>()?,
);
oracles::instantiate_many(&modules, true, &config, &commands);
Ok(())
}

View File

@@ -2,6 +2,7 @@
use libfuzzer_sys::arbitrary::{Result, Unstructured}; use libfuzzer_sys::arbitrary::{Result, Unstructured};
use libfuzzer_sys::fuzz_target; use libfuzzer_sys::fuzz_target;
use wasmtime_fuzzing::generators::InstanceAllocationStrategy;
use wasmtime_fuzzing::oracles::Timeout; use wasmtime_fuzzing::oracles::Timeout;
use wasmtime_fuzzing::{generators, oracles}; use wasmtime_fuzzing::{generators, oracles};
@@ -27,6 +28,17 @@ fn run(data: &[u8]) -> Result<()> {
// Enable module linking for this fuzz target specifically // Enable module linking for this fuzz target specifically
config.module_config.config.module_linking_enabled = u.arbitrary()?; config.module_config.config.module_linking_enabled = u.arbitrary()?;
// When using the pooling allocator without a timeout, we must
// allow at least 1 more global because the `ensure_termination` call below
// will define one.
if let Timeout::None = timeout {
if let InstanceAllocationStrategy::Pooling { module_limits, .. } =
&mut config.wasmtime.strategy
{
module_limits.globals += 1;
}
}
let mut module = config.module_config.generate(&mut u)?; let mut module = config.module_config.generate(&mut u)?;
if let Timeout::None = timeout { if let Timeout::None = timeout {
module.ensure_termination(1000); module.ensure_termination(1000);