Skip to content

Commit

Permalink
feat(fuzzer): Fuzzer randomization (#4019)
Browse files Browse the repository at this point in the history
- Made fuzzer configuration more random
- Increased number of programs generated 
- Increased success/failure rate compared to the fuzzer before this commit
  • Loading branch information
playX18 authored Jul 24, 2024
1 parent 10de1f1 commit 71aa7ed
Show file tree
Hide file tree
Showing 6 changed files with 260 additions and 48 deletions.
6 changes: 5 additions & 1 deletion utils/runtime-fuzzer/src/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,7 +251,11 @@ fn arbitrary_limited_bytes(u: &mut Unstructured, limit: usize) -> Result<Vec<u8>

fn arbitrary_value(u: &mut Unstructured, current_balance: u128) -> Result<u128> {
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),
};
Expand Down
57 changes: 14 additions & 43 deletions utils/runtime-fuzzer/src/generator/upload_program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -61,16 +61,14 @@ pub(crate) fn generate(
) -> Result<GearCall> {
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());

Expand All @@ -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}");
Expand All @@ -100,34 +99,13 @@ fn arbitrary_salt(u: &mut Unstructured) -> Result<Vec<u8>> {
}

fn config(
unstructured: &mut Unstructured,
programs: Option<&NonEmpty<ProgramId>>,
codes: Option<&NonEmpty<CodeId>>,
log_info: Option<String>,
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
Expand All @@ -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())
Expand Down Expand Up @@ -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)
}
18 changes: 15 additions & 3 deletions utils/runtime-fuzzer/src/runtime/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,10 +102,22 @@ impl<'a> BalanceManager<'a> {

pub(crate) fn update_balance(&mut self) -> Result<BalanceState> {
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!(
Expand Down
159 changes: 158 additions & 1 deletion utils/wasm-gen/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`]
Expand Down Expand Up @@ -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<String>,
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)
}
}
27 changes: 27 additions & 0 deletions utils/wasm-gen/src/config/syscalls/injection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit 71aa7ed

Please sign in to comment.