From 71aa7ed78b0ce22e592022ff09a1cf582ca3b5cc Mon Sep 17 00:00:00 2001 From: playX18 <158266309+playX18@users.noreply.github.com> Date: Wed, 24 Jul 2024 05:53:12 +0000 Subject: [PATCH] feat(fuzzer): Fuzzer randomization (#4019) - Made fuzzer configuration more random - Increased number of programs generated - Increased success/failure rate compared to the fuzzer before this commit --- utils/runtime-fuzzer/src/generator.rs | 6 +- .../src/generator/upload_program.rs | 57 ++----- utils/runtime-fuzzer/src/runtime/account.rs | 18 +- utils/wasm-gen/src/config.rs | 159 +++++++++++++++++- .../wasm-gen/src/config/syscalls/injection.rs | 27 +++ utils/wasm-gen/src/tests.rs | 41 +++++ 6 files changed, 260 insertions(+), 48 deletions(-) diff --git a/utils/runtime-fuzzer/src/generator.rs b/utils/runtime-fuzzer/src/generator.rs index 7039c9799d1..ae12cb077bf 100644 --- a/utils/runtime-fuzzer/src/generator.rs +++ b/utils/runtime-fuzzer/src/generator.rs @@ -251,7 +251,11 @@ fn arbitrary_limited_bytes(u: &mut Unstructured, limit: usize) -> Result fn arbitrary_value(u: &mut Unstructured, current_balance: u128) -> Result { let (lower, upper) = match u.int_in_range(0..=99)? { - 5..=19 => (0, 0), + 5..=10 => (0, 0), + 11..=30 => ( + EXISTENTIAL_DEPOSIT, + (current_balance / 4).max(EXISTENTIAL_DEPOSIT), + ), 0..=2 => (0, EXISTENTIAL_DEPOSIT), _ => (EXISTENTIAL_DEPOSIT, current_balance), }; diff --git a/utils/runtime-fuzzer/src/generator/upload_program.rs b/utils/runtime-fuzzer/src/generator/upload_program.rs index 684d3e44585..22321055be7 100644 --- a/utils/runtime-fuzzer/src/generator/upload_program.rs +++ b/utils/runtime-fuzzer/src/generator/upload_program.rs @@ -25,8 +25,8 @@ use gear_core::ids::{prelude::*, CodeId, ProgramId}; use gear_utils::NonEmpty; use gear_wasm_gen::{ wasm_gen_arbitrary::{Result, Unstructured}, - ActorKind, EntryPointsSet, InvocableSyscall, PtrParamAllowedValues, RegularParamType, - StandardGearWasmConfigsBundle, SyscallName, SyscallsInjectionTypes, SyscallsParamsConfig, + ActorKind, PtrParamAllowedValues, RandomizedGearWasmConfigBundle, RegularParamType, + SyscallsParamsConfig, }; use runtime_primitives::Balance; use vara_runtime::EXISTENTIAL_DEPOSIT; @@ -61,16 +61,14 @@ pub(crate) fn generate( ) -> Result { log::trace!("New gear-wasm generation"); log::trace!("Random data before wasm gen {}", unstructured.len()); - - let code = gear_wasm_gen::generate_gear_program_code( + let config = config( unstructured, - config( - programs, - codes, - Some(format!("Generated program from corpus - {corpus_id}")), - current_balance, - ), - )?; + programs, + codes, + Some(format!("Generated program from corpus - {corpus_id}")), + current_balance, + ); + let code = gear_wasm_gen::generate_gear_program_code(unstructured, config)?; log::trace!("Random data after wasm gen {}", unstructured.len()); log::trace!("Code length {:?}", code.len()); @@ -88,6 +86,7 @@ pub(crate) fn generate( let value = super::arbitrary_value(unstructured, current_balance)?; log::trace!("Random data after value generation {}", unstructured.len()); log::trace!("Sending value (upload_program) - {value}"); + log::trace!("Current balance (upload_program - {current_balance}"); let program_id = ProgramId::generate_from_user(CodeId::generate(&code), &salt); log::trace!("Generated code for program id - {program_id}"); @@ -100,34 +99,13 @@ fn arbitrary_salt(u: &mut Unstructured) -> Result> { } fn config( + unstructured: &mut Unstructured, programs: Option<&NonEmpty>, codes: Option<&NonEmpty>, log_info: Option, current_balance: Balance, -) -> StandardGearWasmConfigsBundle { +) -> RandomizedGearWasmConfigBundle { let initial_pages = 2; - let mut injection_types = SyscallsInjectionTypes::all_with_range(1..=3); - injection_types.set_multiple( - [ - (SyscallName::SendInit, 3..=5), - (SyscallName::ReserveGas, 3..=5), - (SyscallName::Debug, 0..=1), - (SyscallName::Wait, 0..=1), - (SyscallName::WaitFor, 0..=1), - (SyscallName::WaitUpTo, 0..=1), - (SyscallName::Wake, 0..=1), - (SyscallName::Leave, 0..=0), - (SyscallName::Panic, 0..=0), - (SyscallName::OomPanic, 0..=0), - (SyscallName::EnvVars, 0..=0), - (SyscallName::Send, 10..=15), - (SyscallName::Exit, 0..=1), - (SyscallName::Alloc, 3..=6), - (SyscallName::Free, 3..=6), - ] - .map(|(syscall, range)| (InvocableSyscall::Loose(syscall), range)) - .into_iter(), - ); let max_value = { let d = current_balance @@ -137,6 +115,7 @@ fn config( current_balance.saturating_div(d) }; + let mut params_config = SyscallsParamsConfig::new() .with_default_regular_config() .with_rule(RegularParamType::Alloc, (10..=20).into()) @@ -179,13 +158,5 @@ fn config( range: EXISTENTIAL_DEPOSIT..=max_value, }); } - - StandardGearWasmConfigsBundle { - entry_points_set: EntryPointsSet::InitHandleHandleReply, - injection_types, - log_info, - params_config, - initial_pages: initial_pages as u32, - ..Default::default() - } + RandomizedGearWasmConfigBundle::new_arbitrary(unstructured, log_info, params_config) } diff --git a/utils/runtime-fuzzer/src/runtime/account.rs b/utils/runtime-fuzzer/src/runtime/account.rs index 1174f3e88a4..291f293aaa4 100644 --- a/utils/runtime-fuzzer/src/runtime/account.rs +++ b/utils/runtime-fuzzer/src/runtime/account.rs @@ -102,10 +102,22 @@ impl<'a> BalanceManager<'a> { pub(crate) fn update_balance(&mut self) -> Result { let max_balance = runtime::gas_to_value(runtime::acc_max_balance_gas()); - let new_balance = self - .unstructured - .int_in_range(EXISTENTIAL_DEPOSIT..=max_balance)?; + // In 3/4 cases we're going to get max_balance account which helps us to run code to completion. + // + // Note that before there was another branch here that also did more calculation on `max_balance` to get into the sweet spot + // but it turns out to slightly move the balance of success/failure rate to 50/50 which is not good. With only these two branches + // we get around 80/20 success/failure rate. Note that this also depends on number of instructions in the program. + let mut new_balance = if self.unstructured.ratio(2, 4)? { + max_balance + } else { + self.unstructured + .int_in_range(EXISTENTIAL_DEPOSIT..=max_balance)? + }; + + if new_balance < EXISTENTIAL_DEPOSIT { + new_balance = EXISTENTIAL_DEPOSIT; + } runtime::set_balance(self.sender.clone(), new_balance) .unwrap_or_else(|e| unreachable!("Balance update failed: {e:?}")); assert_eq!( diff --git a/utils/wasm-gen/src/config.rs b/utils/wasm-gen/src/config.rs index a4ed9e43b3a..20fe4f76225 100644 --- a/utils/wasm-gen/src/config.rs +++ b/utils/wasm-gen/src/config.rs @@ -93,16 +93,20 @@ //! There's a pre-defined one - [`StandardGearWasmConfigsBundle`], usage of which will result //! in generation of valid (always) gear-wasm module. -use std::num::NonZeroU32; +use std::num::{NonZeroU32, NonZeroUsize}; mod generator; mod module; mod syscalls; +use arbitrary::Unstructured; +use gear_wasm_instrument::SyscallName; pub use generator::*; pub use module::*; pub use syscalls::*; +use crate::InvocableSyscall; + /// Trait which describes a type that stores and manages data for generating /// [`GearWasmGeneratorConfig`] and [`SelectableParams`], which are both used /// by [`crate::generate_gear_program_code`] and [`crate::generate_gear_program_module`] @@ -217,3 +221,156 @@ impl ConfigsBundle for StandardGearWasmConfigsBundle { (gear_wasm_generator_config, selectable_params) } } + +#[derive(Debug, Clone)] +pub struct RandomizedGearWasmConfigBundle { + pub max_instructions: usize, + pub no_control: bool, + pub min_funcs: usize, + pub max_funcs: usize, + pub standard_gear_wasm_config_bundle: StandardGearWasmConfigsBundle, +} + +impl RandomizedGearWasmConfigBundle { + pub fn new_arbitrary( + unstructured: &mut Unstructured, + log_info: Option, + params_config: SyscallsParamsConfig, + ) -> Self { + // Observation: with balance increased and no control instructions we get *a lot* of programs executed even if we're + // running fuzzer for just 5 minutes. Without balance increase disabling control results in gas limit exceeding quite often. + let no_control = unstructured.ratio(1, 4).unwrap(); + // Carefully adjusted to run fine with `account.rs` balance calculation. + // 1) When max_balance or max_balance/4 is used we're almost always guaranteed to finish execution of a program so + // do not try to get too much instructions in case of control insns enabled to not get infinite loops or recursion. + // 2) Otherwise we can run quite a lot of insns + // with no control as we're guaranteed to terminate execution. + let max_instructions = if no_control { + // when no control insns are enabled we generate a small amount of instructions + // as it should be more than enough to test the program and exhaust the gas + unstructured.int_in_range(80..=120).unwrap() + } else { + unstructured.int_in_range(500..=800).unwrap() + }; + + let (min_funcs, max_funcs) = if no_control { (1, 1) } else { (2, 3) }; + + let initial_pages = 2; + // pump up injection rates of syscalls when there's control instructions and lower it when there's no control + // instructions (no control => all syscalls should be executed, control => some won't be executed due to if's or loops) + let mut injection_types = + SyscallsInjectionTypes::all_with_range(if no_control { 1..=2 } else { 1..=10 }); + + injection_types.set_multiple( + [ + ( + SyscallName::SendInit, + if no_control { 1..=3 } else { 3..=5 }, + ), + ( + SyscallName::ReserveGas, + if no_control { 1..=2 } else { 3..=5 }, + ), + (SyscallName::Debug, 0..=1), + (SyscallName::Wait, if no_control { 0..=0 } else { 0..=1 }), + (SyscallName::WaitFor, if no_control { 0..=0 } else { 0..=1 }), + ( + SyscallName::WaitUpTo, + if no_control { 0..=0 } else { 0..=1 }, + ), + (SyscallName::Wake, if no_control { 0..=0 } else { 0..=1 }), + (SyscallName::Leave, 0..=0), + (SyscallName::Panic, 0..=0), + (SyscallName::OomPanic, 0..=0), + (SyscallName::EnvVars, 0..=0), + ( + SyscallName::Send, + // lower the amount of sends in no_control case because + // we do not want to exhaust the gas. + if no_control { 1..=2 } else { 10..=15 }, + ), + (SyscallName::Exit, 0..=1), + (SyscallName::Alloc, if no_control { 0..=1 } else { 3..=6 }), + (SyscallName::Free, if no_control { 0..=1 } else { 3..=6 }), + ] + .map(|(syscall, range)| (InvocableSyscall::Loose(syscall), range)) + .into_iter(), + ); + + RandomizedGearWasmConfigBundle { + standard_gear_wasm_config_bundle: StandardGearWasmConfigsBundle { + entry_points_set: EntryPointsSet::InitHandleHandleReply, + injection_types, + log_info, + params_config, + initial_pages: initial_pages as u32, + waiting_probability: NonZeroU32::new(unstructured.int_in_range(1..=4).unwrap()), + ..Default::default() + }, + max_funcs, + min_funcs, + max_instructions, + no_control, + } + } +} + +impl ConfigsBundle for RandomizedGearWasmConfigBundle { + fn into_parts(self) -> (GearWasmGeneratorConfig, SelectableParams) { + let RandomizedGearWasmConfigBundle { + max_funcs, + max_instructions, + min_funcs, + no_control, + standard_gear_wasm_config_bundle: + StandardGearWasmConfigsBundle { + critical_gas_limit, + entry_points_set, + initial_pages, + injection_types, + log_info: _, + params_config, + remove_recursion, + stack_end_page, + waiting_probability, + }, + } = self; + + let mut selectable_params = SelectableParams { + max_instructions, + max_funcs: NonZeroUsize::new(max_funcs).unwrap(), + min_funcs: NonZeroUsize::new(min_funcs).unwrap(), + ..Default::default() + }; + + if no_control { + let index = selectable_params + .allowed_instructions + .iter() + .position(|x| x == &InstructionKind::Control) + .unwrap(); + selectable_params.allowed_instructions.remove(index); + } + + let mut syscalls_config_builder = SyscallsConfigBuilder::new(injection_types) + .with_log_info("RandomizedGearWasmConfigBuilder".to_owned()) + .with_waiting_probability(waiting_probability.unwrap()); + + syscalls_config_builder = syscalls_config_builder.with_params_config(params_config); + + let memory_pages_config = MemoryPagesConfig { + initial_size: initial_pages, + stack_end_page, + upper_limit: None, + }; + let gear_wasm_generator_config = GearWasmGeneratorConfigBuilder::new() + .with_critical_gas_limit(critical_gas_limit) + .with_recursions_removed(remove_recursion) + .with_syscalls_config(syscalls_config_builder.build()) + .with_entry_points_config(entry_points_set) + .with_memory_config(memory_pages_config) + .build(); + + (gear_wasm_generator_config, selectable_params) + } +} diff --git a/utils/wasm-gen/src/config/syscalls/injection.rs b/utils/wasm-gen/src/config/syscalls/injection.rs index 6d07e01eeab..aec1e290686 100644 --- a/utils/wasm-gen/src/config/syscalls/injection.rs +++ b/utils/wasm-gen/src/config/syscalls/injection.rs @@ -24,6 +24,7 @@ use crate::InvocableSyscall; +use arbitrary::Unstructured; use gear_wasm_instrument::syscalls::SyscallName; use indexmap::IndexSet; use std::{collections::HashMap, ops::RangeInclusive}; @@ -75,6 +76,32 @@ impl SyscallsInjectionTypes { Self::new_with_injection_type(SyscallInjectionType::Function(range)) } + pub fn all_from_unstructured(unstructured: &mut Unstructured) -> Self { + Self { + inner: SyscallName::instrumentable() + .map(|name| { + let range = unstructured.int_in_range(1..=3).unwrap() + ..=unstructured.int_in_range(3..=20).unwrap(); + let injection_type = SyscallInjectionType::Function(range); + (InvocableSyscall::Loose(name), injection_type) + }) + .chain( + SyscallName::instrumentable() + .filter(|&name| InvocableSyscall::has_precise_variant(name)) + .map(|name| { + let injection_type = SyscallInjectionType::Function( + /*unstructured.int_in_range(1..=3).unwrap() + ..=unstructured.int_in_range(3..=20).unwrap(),*/ + 1..=3, + ); + (InvocableSyscall::Precise(name), injection_type.clone()) + }), + ) + .collect(), + order: IndexSet::new(), + } + } + /// Instantiate a syscalls map with given injection type. fn new_with_injection_type(injection_type: SyscallInjectionType) -> Self { Self { diff --git a/utils/wasm-gen/src/tests.rs b/utils/wasm-gen/src/tests.rs index be50a360d0c..ec265941515 100644 --- a/utils/wasm-gen/src/tests.rs +++ b/utils/wasm-gen/src/tests.rs @@ -80,6 +80,47 @@ proptest! { assert_eq!(first, second); } + + #[test] + fn test_randomized_config(buf in prop::collection::vec(any::(), UNSTRUCTURED_SIZE)) { + let mut u = Unstructured::new(&buf); + + let configs_bundle: RandomizedGearWasmConfigBundle = RandomizedGearWasmConfigBundle::new_arbitrary( + &mut u, + Default::default(), + Default::default() + ); + + let original_code = generate_gear_program_code(&mut u, configs_bundle) + .expect("failed generating wasm"); + + let code_res = Code::try_new(original_code, 1, |_| CustomConstantCostRules::default(), None, None, None); + assert!(code_res.is_ok()); + } + + #[test] + fn test_randomized_config_reproducible(buf in prop::collection::vec(any::(), UNSTRUCTURED_SIZE)) { + let mut u = Unstructured::new(&buf); + let mut u2 = Unstructured::new(&buf); + let configs_bundle1: RandomizedGearWasmConfigBundle = RandomizedGearWasmConfigBundle::new_arbitrary( + &mut u, + Default::default(), + Default::default() + ); + + let configs_bundle2: RandomizedGearWasmConfigBundle = RandomizedGearWasmConfigBundle::new_arbitrary( + &mut u2, + Default::default(), + Default::default() + ); + + let first = generate_gear_program_code(&mut u, configs_bundle1) + .expect("failed generating wasm"); + let second = generate_gear_program_code(&mut u2, configs_bundle2) + .expect("failed generating wasm"); + + assert_eq!(first, second); + } } #[test]