use anyhow::Result;
use rand::Rng;
use rand::RngCore;
use rand::SeedableRng;
use wirm::ir::function::FunctionBuilder;
use wirm::ir::id::{FunctionID, GlobalID, LocalID, MemoryID};
use wirm::ir::module::module_functions::FuncKind;
use wirm::ir::types::{InitExpr, Instructions, Value};
use wirm::module_builder::AddLocal;
use wirm::opcode::Inject;
use wirm::wasmparser::{ExternalKind, MemArg, Operator, Validator};
use wirm::{DataType, InitInstr, Module, Opcode};
use crate::constants::{
AFL_COVERAGE_MAP_SIZE, API_VERSION_IC0, COVERAGE_FN_EXPORT_NAME,
INSTRUCTION_COUNT_FN_EXPORT_NAME,
};
use std::collections::HashSet;
pub struct InstrumentationArgs {
pub wasm_bytes: Vec<u8>,
pub history_size: usize,
pub seed: Seed,
pub instrument_instruction_count: bool,
}
#[derive(Copy, Clone, Debug)]
pub enum Seed {
Random,
Static(u32),
}
pub static mut COVERAGE_MAP: &mut [u8] = &mut [0; AFL_COVERAGE_MAP_SIZE as usize];
pub fn instrument_wasm_for_fuzzing(instrumentation_args: InstrumentationArgs) -> Vec<u8> {
assert!(
matches!(instrumentation_args.history_size, 1 | 2 | 4 | 8),
"History size must be 1, 2, 4, or 8"
);
let mut module = Module::parse(&instrumentation_args.wasm_bytes, false, false)
.expect("Failed to parse module with wirm");
instrument_for_afl(&mut module, &instrumentation_args)
.expect("Unable to instrument wasm module for AFL");
let buf = vec![0u8; AFL_COVERAGE_MAP_SIZE as usize * instrumentation_args.history_size]
.into_boxed_slice();
let buf: &'static mut [u8] = Box::leak(buf);
unsafe {
COVERAGE_MAP = buf;
}
let instrumented_wasm = module.encode();
validate_wasm(&instrumented_wasm).expect("Wasm is not valid");
instrumented_wasm
}
fn instrument_for_afl(
module: &mut Module<'_>,
instrumentation_args: &InstrumentationArgs,
) -> Result<()> {
let is_memory64 = is_memory64(module);
let inst_count = instrumentation_args.instrument_instruction_count;
let (msg_reply_data_append_idx, msg_reply_idx, perf_counter_idx) =
ensure_ic0_imports(module, is_memory64, inst_count)?;
let (afl_prev_loc_indices, afl_mem_ptr_idx, instruction_count_globals) = inject_globals(
module,
instrumentation_args.history_size,
is_memory64,
inst_count,
);
println!(
" -> Injected globals: prev_locs @ indices {afl_prev_loc_indices:?}, mem_ptr @ index {afl_mem_ptr_idx:?}"
);
inject_afl_coverage_export(
module,
instrumentation_args.history_size,
afl_mem_ptr_idx,
msg_reply_data_append_idx,
msg_reply_idx,
is_memory64,
)?;
println!(" -> Injected `canister_update __export_coverage_for_afl` function.");
let mut skip_function_ids = HashSet::new();
let call_count_global = if let Some((ic_global, call_count_global)) = instruction_count_globals
{
let perf_counter_idx = perf_counter_idx.unwrap();
println!(
" -> Injected instruction count globals: ic @ {ic_global:?}, call_count @ {call_count_global:?}"
);
println!(" -> Ensured ic0.performance_counter import @ {perf_counter_idx:?}");
let cost_per_afl_call = compute_cost_per_afl_call(instrumentation_args.history_size);
println!(" -> Computed AFL instrumentation cost per call: {cost_per_afl_call}");
let wrapper_ids = inject_method_wrappers(
module,
call_count_global,
ic_global,
perf_counter_idx,
cost_per_afl_call,
);
for id in &wrapper_ids {
skip_function_ids.insert(*id);
}
println!(
" -> Injected {} method wrapper(s) for instruction counting.",
wrapper_ids.len()
);
let export_fn_id = inject_instruction_count_export(
module,
instrumentation_args.history_size,
afl_mem_ptr_idx,
ic_global,
msg_reply_data_append_idx,
msg_reply_idx,
is_memory64,
)?;
skip_function_ids.insert(export_fn_id);
println!(" -> Injected `canister_query {INSTRUCTION_COUNT_FN_EXPORT_NAME}` function.");
Some(call_count_global)
} else {
None
};
instrument_branches(
module,
&afl_prev_loc_indices,
afl_mem_ptr_idx,
instrumentation_args.seed,
is_memory64,
&skip_function_ids,
call_count_global,
);
println!(" -> Instrumented branch instructions in all functions.");
Ok(())
}
fn inject_globals(
module: &mut Module<'_>,
history_size: usize,
is_memory64: bool,
instrument_instruction_count: bool,
) -> (Vec<GlobalID>, GlobalID, Option<(GlobalID, GlobalID)>) {
let mut afl_prev_loc_indices = Vec::with_capacity(history_size);
let (ptr_type, init_val) = if is_memory64 {
(DataType::I64, Value::I64(0))
} else {
(DataType::I32, Value::I32(0))
};
for _ in 0..history_size {
let global_id = module.add_global(
InitExpr::new(vec![InitInstr::Value(init_val)]),
ptr_type,
true,
false,
);
afl_prev_loc_indices.push(global_id);
}
let afl_mem_ptr_idx = module.add_global(
InitExpr::new(vec![InitInstr::Value(init_val)]),
ptr_type,
false,
false,
);
let instruction_count_globals = if instrument_instruction_count {
let instruction_count_global = module.add_global(
InitExpr::new(vec![InitInstr::Value(Value::I64(0))]),
DataType::I64,
true,
false,
);
let call_count_global = module.add_global(
InitExpr::new(vec![InitInstr::Value(Value::I64(0))]),
DataType::I64,
true,
false,
);
Some((instruction_count_global, call_count_global))
} else {
None
};
(
afl_prev_loc_indices,
afl_mem_ptr_idx,
instruction_count_globals,
)
}
fn inject_afl_coverage_export<'a>(
module: &mut Module<'a>,
history_size: usize,
afl_mem_ptr_idx: GlobalID,
msg_reply_data_append_idx: FunctionID,
msg_reply_idx: FunctionID,
is_memory64: bool,
) -> Result<()> {
let mut func_builder = FunctionBuilder::new(&[], &[]);
if is_memory64 {
func_builder
.global_get(afl_mem_ptr_idx)
.i64_const(AFL_COVERAGE_MAP_SIZE as i64 * history_size as i64)
.call(msg_reply_data_append_idx)
.call(msg_reply_idx)
.global_get(afl_mem_ptr_idx)
.i32_const(0)
.i64_const(AFL_COVERAGE_MAP_SIZE as i64 * history_size as i64)
.memory_fill(0);
} else {
func_builder
.global_get(afl_mem_ptr_idx)
.i32_const(AFL_COVERAGE_MAP_SIZE * history_size as i32)
.call(msg_reply_data_append_idx)
.call(msg_reply_idx)
.global_get(afl_mem_ptr_idx)
.i32_const(0)
.i32_const(AFL_COVERAGE_MAP_SIZE * history_size as i32)
.memory_fill(0);
}
let coverage_function_id = func_builder.finish_module(module);
let export_name = format!("canister_update {COVERAGE_FN_EXPORT_NAME}");
module
.exports
.add_export_func(export_name, coverage_function_id.0);
Ok(())
}
fn instrument_branches(
module: &mut Module<'_>,
afl_prev_loc_indices: &[GlobalID],
afl_mem_ptr_idx: GlobalID,
seed: Seed,
is_memory64: bool,
skip_function_ids: &HashSet<FunctionID>,
call_count_global: Option<GlobalID>,
) {
let instrumentation_function = afl_instrumentation_slice(
module,
afl_prev_loc_indices,
afl_mem_ptr_idx,
is_memory64,
call_count_global,
);
let seed = match seed {
Seed::Random => rand::rng().next_u32(),
Seed::Static(s) => s,
};
println!("The seed used for instrumentation is {seed}");
let mut rng = rand::rngs::StdRng::seed_from_u64(seed as u64);
let mut create_instrumentation_ops = |ops: &mut Vec<Operator>| {
let curr_location =
rng.random_range(0..AFL_COVERAGE_MAP_SIZE * afl_prev_loc_indices.len() as i32);
if is_memory64 {
ops.push(Operator::I64Const {
value: curr_location as i64,
});
} else {
ops.push(Operator::I32Const {
value: curr_location as i32,
});
}
ops.push(Operator::Call {
function_index: instrumentation_function.0,
});
};
for (function_index, function) in module.functions.iter_mut().enumerate() {
let func_id = FunctionID(function_index as u32);
if matches!(function.kind(), FuncKind::Local(_))
&& func_id != instrumentation_function
&& !skip_function_ids.contains(&func_id)
{
let local_function = function.unwrap_local_mut();
let mut new_instructions = Vec::with_capacity(local_function.body.num_instructions * 2);
create_instrumentation_ops(&mut new_instructions);
for instruction in local_function.body.instructions.get_ops() {
match instruction {
Operator::Block { .. }
| Operator::Loop { .. }
| Operator::If { .. }
| Operator::Else => {
new_instructions.push(instruction.clone());
create_instrumentation_ops(&mut new_instructions);
}
Operator::Br { .. }
| Operator::BrIf { .. }
| Operator::BrTable { .. }
| Operator::Return => {
create_instrumentation_ops(&mut new_instructions);
new_instructions.push(instruction.clone());
}
_ => new_instructions.push(instruction.clone()),
}
}
local_function.body.instructions = Instructions::new(
new_instructions.iter().map(|i| (i.clone(), 0)).collect(),
0,
false,
);
}
}
}
fn afl_instrumentation_slice(
module: &mut Module<'_>,
afl_prev_loc_indices: &[GlobalID],
afl_mem_ptr_idx: GlobalID,
is_memory64: bool,
call_count_global: Option<GlobalID>,
) -> FunctionID {
if is_memory64 {
let mut func_builder = FunctionBuilder::new(&[DataType::I64], &[]);
let curr_location = LocalID(0);
let afl_local_idx = func_builder.add_local(DataType::I64);
func_builder.local_get(curr_location);
for &prev_loc_idx in afl_prev_loc_indices {
func_builder.global_get(prev_loc_idx).i64_xor();
}
func_builder
.global_get(afl_mem_ptr_idx)
.i64_add()
.local_tee(afl_local_idx)
.local_get(afl_local_idx)
.i64_load8_u(MemArg {
offset: 0,
align: 0,
memory: 0,
max_align: 0,
})
.i64_const(1)
.i64_add();
func_builder.inject(Operator::I64Store8 {
memarg: MemArg {
offset: 0,
align: 0,
memory: 0,
max_align: 0,
},
});
for i in (1..afl_prev_loc_indices.len()).rev() {
func_builder
.global_get(afl_prev_loc_indices[i - 1])
.i64_const(1)
.i64_shr_unsigned()
.global_set(afl_prev_loc_indices[i]);
}
func_builder
.local_get(curr_location)
.i64_const(1)
.i64_shr_unsigned()
.global_set(afl_prev_loc_indices[0]);
if let Some(call_count_idx) = call_count_global {
func_builder
.global_get(call_count_idx)
.i64_const(1)
.i64_add()
.global_set(call_count_idx);
}
func_builder.finish_module(module)
} else {
let mut func_builder = FunctionBuilder::new(&[DataType::I32], &[]);
let curr_location = LocalID(0);
let afl_local_idx = func_builder.add_local(DataType::I32);
func_builder.local_get(curr_location);
for &prev_loc_idx in afl_prev_loc_indices {
func_builder.global_get(prev_loc_idx).i32_xor();
}
func_builder
.global_get(afl_mem_ptr_idx)
.i32_add()
.local_tee(afl_local_idx)
.local_get(afl_local_idx)
.i32_load8_u(MemArg {
offset: 0,
align: 0,
memory: 0,
max_align: 0,
})
.i32_const(1)
.i32_add()
.i32_store8(MemArg {
offset: 0,
align: 0,
memory: 0,
max_align: 0,
});
for i in (1..afl_prev_loc_indices.len()).rev() {
func_builder
.global_get(afl_prev_loc_indices[i - 1])
.i32_const(1)
.i32_shr_unsigned()
.global_set(afl_prev_loc_indices[i]);
}
func_builder
.local_get(curr_location)
.i32_const(1)
.i32_shr_unsigned()
.global_set(afl_prev_loc_indices[0]);
if let Some(call_count_idx) = call_count_global {
func_builder
.global_get(call_count_idx)
.i64_const(1)
.i64_add()
.global_set(call_count_idx);
}
func_builder.finish_module(module)
}
}
fn compute_cost_per_afl_call(history_size: usize) -> i64 {
let n = history_size as i64;
23 + 6 * n
}
const WRAPPER_OVERHEAD_COST: i64 = 213;
fn inject_method_wrappers(
module: &mut Module<'_>,
call_count_global: GlobalID,
instruction_count_global: GlobalID,
perf_counter_idx: FunctionID,
cost_per_afl_call: i64,
) -> Vec<FunctionID> {
let exports_to_wrap: Vec<(usize, FunctionID)> = module
.exports
.iter()
.enumerate()
.filter(|(_, exp)| {
matches!(exp.kind, ExternalKind::Func)
&& exp.name.starts_with("canister_update ")
&& !exp.name.contains(COVERAGE_FN_EXPORT_NAME)
&& !exp.name.contains(INSTRUCTION_COUNT_FN_EXPORT_NAME)
})
.map(|(idx, exp)| (idx, FunctionID(exp.index)))
.collect();
let mut wrapper_ids = Vec::new();
for (export_idx, original_func_id) in exports_to_wrap {
let mut func_builder = FunctionBuilder::new(&[], &[]);
func_builder.i64_const(0).global_set(call_count_global);
func_builder.call(original_func_id);
func_builder.i32_const(1).call(perf_counter_idx);
func_builder
.global_get(call_count_global)
.i64_const(cost_per_afl_call)
.i64_mul()
.i64_sub();
func_builder.i64_const(WRAPPER_OVERHEAD_COST).i64_sub();
func_builder.global_set(instruction_count_global);
let wrapper_id = func_builder.finish_module(module);
wrapper_ids.push(wrapper_id);
for (idx, exp) in module.exports.iter_mut().enumerate() {
if idx == export_idx {
exp.index = *wrapper_id;
break;
}
}
}
wrapper_ids
}
fn inject_instruction_count_export<'a>(
module: &mut Module<'a>,
history_size: usize,
afl_mem_ptr_idx: GlobalID,
instruction_count_global: GlobalID,
msg_reply_data_append_idx: FunctionID,
msg_reply_idx: FunctionID,
is_memory64: bool,
) -> Result<FunctionID> {
let scratch_offset = AFL_COVERAGE_MAP_SIZE as i64 * history_size as i64;
let mut func_builder = FunctionBuilder::new(&[], &[]);
if is_memory64 {
func_builder
.global_get(afl_mem_ptr_idx)
.i64_const(scratch_offset)
.i64_add();
func_builder
.global_get(instruction_count_global)
.i64_store(MemArg {
offset: 0,
align: 3, memory: 0,
max_align: 0,
});
func_builder
.global_get(afl_mem_ptr_idx)
.i64_const(scratch_offset)
.i64_add()
.i64_const(8)
.call(msg_reply_data_append_idx)
.call(msg_reply_idx);
} else {
func_builder
.global_get(afl_mem_ptr_idx)
.i32_const(scratch_offset as i32)
.i32_add();
func_builder
.global_get(instruction_count_global)
.i64_store(MemArg {
offset: 0,
align: 3,
memory: 0,
max_align: 0,
});
func_builder
.global_get(afl_mem_ptr_idx)
.i32_const(scratch_offset as i32)
.i32_add()
.i32_const(8)
.call(msg_reply_data_append_idx)
.call(msg_reply_idx);
}
let function_id = func_builder.finish_module(module);
let export_name = format!("canister_query {INSTRUCTION_COUNT_FN_EXPORT_NAME}");
module.exports.add_export_func(export_name, function_id.0);
Ok(function_id)
}
fn ensure_ic0_imports(
module: &mut Module<'_>,
is_memory64: bool,
instrument_instruction_count: bool,
) -> Result<(FunctionID, FunctionID, Option<FunctionID>)> {
let mut data_append_idx = module.imports.get_func(
API_VERSION_IC0.to_string(),
"msg_reply_data_append".to_string(),
);
let mut reply_idx = module
.imports
.get_func(API_VERSION_IC0.to_string(), "msg_reply".to_string());
if data_append_idx.is_none() {
let ptr_type = if is_memory64 {
DataType::I64
} else {
DataType::I32
};
let type_id = module.types.add_func_type(&[ptr_type, ptr_type], &[]);
let (func_index, _) = module.add_import_func(
API_VERSION_IC0.to_string(),
"msg_reply_data_append".to_string(),
type_id,
);
data_append_idx = Some(func_index);
}
if reply_idx.is_none() {
let type_id = module.types.add_func_type(&[], &[]);
let (func_index, _) = module.add_import_func(
API_VERSION_IC0.to_string(),
"msg_reply".to_string(),
type_id,
);
reply_idx = Some(func_index);
}
let perf_counter_idx = if instrument_instruction_count {
let existing = module.imports.get_func(
API_VERSION_IC0.to_string(),
"performance_counter".to_string(),
);
if let Some(idx) = existing {
Some(idx)
} else {
let type_id = module
.types
.add_func_type(&[DataType::I32], &[DataType::I64]);
let (func_index, _) = module.add_import_func(
API_VERSION_IC0.to_string(),
"performance_counter".to_string(),
type_id,
);
Some(func_index)
}
} else {
None
};
Ok((
data_append_idx.unwrap(),
reply_idx.unwrap(),
perf_counter_idx,
))
}
fn is_memory64(module: &Module<'_>) -> bool {
let memory_64 = module
.memories
.get_mem_by_id(MemoryID(0))
.map(|m| m.ty.memory64)
.unwrap_or(false);
if !memory_64 {
return false;
}
let data_append_idx = module.imports.get_func(
API_VERSION_IC0.to_string(),
"msg_reply_data_append".to_string(),
);
if data_append_idx.is_none() {
return true;
}
let type_id = module
.functions
.get_fn_by_id(data_append_idx.unwrap())
.unwrap()
.get_type_id();
let ty = module.types.get(type_id).unwrap();
ty.params().iter().all(|v| matches!(v, DataType::I64))
}
fn validate_wasm(wasm_bytes: &[u8]) -> Result<()> {
let mut validator = Validator::new();
validator.validate_all(wasm_bytes)?;
println!("Validation of instrumented Wasm successful.");
Ok(())
}
#[cfg(test)]
mod tests {
use wirm::{ir::module::module_globals::GlobalKind, wasmparser::ValType};
use super::*;
#[test]
fn inject_globals_empty_module() {
let wat = wat::parse_str(
r#"
(module)
"#,
)
.unwrap();
let history_range: [usize; 4] = [1, 2, 4, 8];
for history_size in history_range {
let mut module = Module::parse(&wat, false, false).unwrap();
let (afl_prev_loc_indices, afl_mem_ptr_idx, _) =
inject_globals(&mut module, history_size, false, false);
assert_eq!(afl_prev_loc_indices.len(), history_size);
(0..history_size)
.for_each(|index| assert_eq!(afl_prev_loc_indices[index], GlobalID(index as u32)));
assert_eq!(afl_mem_ptr_idx, GlobalID(history_size as u32));
}
}
#[test]
fn inject_globals_two_globals() {
let wat = wat::parse_str(
r#"
(module
(global (;0;) (mut i32) i32.const 0)
(global (;1;) (mut i32) i32.const 0)
)
"#,
)
.unwrap();
let offset: u32 = 2;
let history_range: [usize; 4] = [1, 2, 4, 8];
for history_size in history_range {
let mut module = Module::parse(&wat, false, false).unwrap();
let (afl_prev_loc_indices, afl_mem_ptr_idx, _) =
inject_globals(&mut module, history_size, false, false);
assert_eq!(afl_prev_loc_indices.len(), history_size);
(0..history_size).for_each(|index| {
assert_eq!(afl_prev_loc_indices[index], GlobalID(index as u32 + offset))
});
assert_eq!(afl_mem_ptr_idx, GlobalID(history_size as u32 + offset));
}
}
#[test]
fn inject_globals_check_global_values() {
let wat = wat::parse_str(
r#"
(module
(global (;0;) (mut i32) i32.const 0)
(global (;1;) (mut i32) i32.const 0)
)
"#,
)
.unwrap();
let validate_global = |global: GlobalKind, mutable: bool| {
assert_matches::assert_matches!(global, GlobalKind::Local(_));
if let GlobalKind::Local(local) = global {
assert_eq!(local.ty.content_type, ValType::I32);
assert_eq!(local.ty.mutable, mutable);
assert!(!local.ty.shared);
assert_eq!(local.init_expr.instructions().len(), 1);
assert_matches::assert_matches!(
local.init_expr.instructions()[0],
InitInstr::Value(Value::I32(0))
);
}
};
let history_range: [usize; 4] = [1, 2, 4, 8];
for history_size in history_range {
let mut module = Module::parse(&wat, false, false).unwrap();
let (afl_prev_loc_indices, afl_mem_ptr_idx, _) =
inject_globals(&mut module, history_size, false, false);
for g in afl_prev_loc_indices.iter() {
let global = module.globals.get_kind(*g);
validate_global(global.clone(), true);
}
let global = module.globals.get_kind(afl_mem_ptr_idx);
validate_global(global.clone(), false);
}
}
#[test]
fn inject_ic0_imports_empty_module() {
let wat = wat::parse_str(
r#"
(module)
"#,
)
.unwrap();
let mut module = Module::parse(&wat, false, false).unwrap();
let (data_append_idx, reply_idx, _) =
ensure_ic0_imports(&mut module, false, false).unwrap();
assert_eq!(data_append_idx, FunctionID(0));
assert_eq!(reply_idx, FunctionID(1));
}
#[test]
fn inject_ic0_imports_one_import() {
let wat = wat::parse_str(
r#"
(module
(type (;0;) (func (param i32)))
(import "ic0" "dummy" (func (;0;) (type 0)))
)
"#,
)
.unwrap();
let mut module = Module::parse(&wat, false, false).unwrap();
let (data_append_idx, reply_idx, _) =
ensure_ic0_imports(&mut module, false, false).unwrap();
assert_eq!(data_append_idx, FunctionID(1));
assert_eq!(reply_idx, FunctionID(2));
}
#[test]
fn inject_ic0_imports_data_append_exists() {
let wat = wat::parse_str(
r#"
(module
(type (;0;) (func (param i32 i32)))
(import "ic0" "msg_reply_data_append" (func (;0;) (type 0)))
)
"#,
)
.unwrap();
let mut module = Module::parse(&wat, false, false).unwrap();
let (data_append_idx, reply_idx, _) =
ensure_ic0_imports(&mut module, false, false).unwrap();
assert_eq!(data_append_idx, FunctionID(0));
assert_eq!(reply_idx, FunctionID(1));
}
#[test]
fn inject_ic0_imports_reply_exists() {
let wat = wat::parse_str(
r#"
(module
(type (;0;) (func))
(import "ic0" "msg_reply" (func (;0;) (type 0)))
)
"#,
)
.unwrap();
let mut module = Module::parse(&wat, false, false).unwrap();
let (data_append_idx, reply_idx, _) =
ensure_ic0_imports(&mut module, false, false).unwrap();
assert_eq!(data_append_idx, FunctionID(1));
assert_eq!(reply_idx, FunctionID(0));
}
#[test]
fn inject_ic0_imports_both_exists() {
let wat = wat::parse_str(
r#"
(module
(type (;0;) (func (param i32 i32)))
(type (;1;) (func))
(import "ic0" "msg_reply_data_append" (func (;0;) (type 0)))
(import "ic0" "msg_reply" (func (;1;) (type 1)))
)
"#,
)
.unwrap();
let mut module = Module::parse(&wat, false, false).unwrap();
let (data_append_idx, reply_idx, _) =
ensure_ic0_imports(&mut module, false, false).unwrap();
assert_eq!(data_append_idx, FunctionID(0));
assert_eq!(reply_idx, FunctionID(1));
}
fn wasm_equality(generated: Vec<u8>, expected: Vec<u8>) {
if generated != expected {
let generated_text = wasmprinter::print_bytes(&generated).unwrap();
let expected_text = wasmprinter::print_bytes(&expected).unwrap();
difference::assert_diff!(generated_text.as_str(), expected_text.as_str(), "\n", 0)
}
}
#[test]
fn inject_instrumentation_function_history_1() {
let wat = wat::parse_str(
r#"
(module
(memory (;0;) 1)
)
"#,
)
.unwrap();
let history_size = 1;
let mut module = Module::parse(&wat, false, false).unwrap();
let (afl_prev_loc_indices, afl_mem_ptr_idx, _) =
inject_globals(&mut module, history_size, false, false);
let instrumentation_function = afl_instrumentation_slice(
&mut module,
&afl_prev_loc_indices,
afl_mem_ptr_idx,
false,
None,
);
assert_eq!(instrumentation_function, FunctionID(0));
let expected_wasm = wat::parse_str(
r#"(module
(type (;0;) (func (param i32)))
(memory (;0;) 1)
(global (;0;) (mut i32) i32.const 0)
(global (;1;) i32 i32.const 0)
(func (;0;) (type 0) (param i32)
(local i32)
local.get 0
global.get 0
i32.xor
global.get 1
i32.add
local.tee 1
local.get 1
i32.load8_u
i32.const 1
i32.add
i32.store8
local.get 0
i32.const 1
i32.shr_u
global.set 0
)
)"#,
)
.unwrap();
let mut expected_module = Module::parse(&expected_wasm, false, false).unwrap();
wasm_equality(module.encode(), expected_module.encode());
}
#[test]
fn inject_instrumentation_function_history_2() {
let wat = wat::parse_str(
r#"
(module
(memory (;0;) 1)
)
"#,
)
.unwrap();
let history_size = 2;
let mut module = Module::parse(&wat, false, false).unwrap();
let (afl_prev_loc_indices, afl_mem_ptr_idx, _) =
inject_globals(&mut module, history_size, false, false);
let instrumentation_function = afl_instrumentation_slice(
&mut module,
&afl_prev_loc_indices,
afl_mem_ptr_idx,
false,
None,
);
assert_eq!(instrumentation_function, FunctionID(0));
let expected_wasm = wat::parse_str(
r#"(module
(type (;0;) (func (param i32)))
(memory (;0;) 1)
(global (;0;) (mut i32) i32.const 0)
(global (;1;) (mut i32) i32.const 0)
(global (;2;) i32 i32.const 0)
(func (;0;) (type 0) (param i32)
(local i32)
local.get 0
global.get 0
i32.xor
global.get 1
i32.xor
global.get 2
i32.add
local.tee 1
local.get 1
i32.load8_u
i32.const 1
i32.add
i32.store8
global.get 0
i32.const 1
i32.shr_u
global.set 1
local.get 0
i32.const 1
i32.shr_u
global.set 0
)
)"#,
)
.unwrap();
let mut expected_module = Module::parse(&expected_wasm, false, false).unwrap();
wasm_equality(module.encode(), expected_module.encode());
}
#[test]
fn inject_afl_coverage_export_history_1() {
let wat = wat::parse_str(
r#"
(module
(memory (;0;) 1)
)
"#,
)
.unwrap();
let history_size = 1;
let mut module = Module::parse(&wat, false, false).unwrap();
let (msg_reply_data_append_idx, msg_reply_idx, _) =
ensure_ic0_imports(&mut module, false, false).unwrap();
let (_, afl_mem_ptr_idx, _) = inject_globals(&mut module, history_size, false, false);
let coverage_function = inject_afl_coverage_export(
&mut module,
history_size,
afl_mem_ptr_idx,
msg_reply_data_append_idx,
msg_reply_idx,
false,
);
assert!(coverage_function.is_ok());
let expected_wasm = wat::parse_str(
r#"(module
(type (;0;) (func (param i32 i32)))
(type (;1;) (func))
(import "ic0" "msg_reply_data_append" (func (;0;) (type 0)))
(import "ic0" "msg_reply" (func (;1;) (type 1)))
(memory (;0;) 1)
(global (;0;) (mut i32) i32.const 0)
(global (;1;) i32 i32.const 0)
(export "canister_update __export_coverage_for_afl" (func 2))
(func (;2;) (type 1)
global.get 1
i32.const 65536
call 0
call 1
global.get 1
i32.const 0
i32.const 65536
memory.fill
)
)"#,
)
.unwrap();
let mut expected_module = Module::parse(&expected_wasm, false, false).unwrap();
wasm_equality(module.encode(), expected_module.encode());
}
#[test]
fn inject_afl_coverage_export_history_2() {
let wat = wat::parse_str(
r#"
(module
(memory (;0;) 1)
)
"#,
)
.unwrap();
let history_size = 2;
let mut module = Module::parse(&wat, false, false).unwrap();
let (msg_reply_data_append_idx, msg_reply_idx, _) =
ensure_ic0_imports(&mut module, false, false).unwrap();
let (_, afl_mem_ptr_idx, _) = inject_globals(&mut module, history_size, false, false);
let coverage_function = inject_afl_coverage_export(
&mut module,
history_size,
afl_mem_ptr_idx,
msg_reply_data_append_idx,
msg_reply_idx,
false,
);
assert!(coverage_function.is_ok());
let expected_wasm = wat::parse_str(
r#"(module
(type (;0;) (func (param i32 i32)))
(type (;1;) (func))
(import "ic0" "msg_reply_data_append" (func (;0;) (type 0)))
(import "ic0" "msg_reply" (func (;1;) (type 1)))
(memory (;0;) 1)
(global (;0;) (mut i32) i32.const 0)
(global (;1;) (mut i32) i32.const 0)
(global (;2;) i32 i32.const 0)
(export "canister_update __export_coverage_for_afl" (func 2))
(func (;2;) (type 1)
global.get 2
i32.const 131072
call 0
call 1
global.get 2
i32.const 0
i32.const 131072
memory.fill
)
)"#,
)
.unwrap();
let mut expected_module = Module::parse(&expected_wasm, false, false).unwrap();
wasm_equality(module.encode(), expected_module.encode());
}
fn instrument_branches_helper(module: &str, expected: &[Operator]) {
let wat = wat::parse_str(module).unwrap();
let history_size = 2;
let mut module = Module::parse(&wat, false, false).unwrap();
let (afl_prev_loc_indices, afl_mem_ptr_idx, _) =
inject_globals(&mut module, history_size, false, false);
instrument_branches(
&mut module,
&afl_prev_loc_indices,
afl_mem_ptr_idx,
Seed::Static(42),
false,
&HashSet::new(),
None,
);
let instructions = module
.functions
.get_fn_by_id(FunctionID(0))
.unwrap()
.unwrap_local()
.body
.instructions
.get_ops();
assert_eq!(instructions, expected);
}
#[test]
fn inject_branch_instrumentation_func() {
instrument_branches_helper(
r#"
(module
(type (;0;) (func))
(memory (;0;) 1)
(func (;0;) (type 0))
)
"#,
&[
Operator::I32Const { value: 17486 },
Operator::Call { function_index: 1 },
Operator::End,
],
);
}
#[test]
fn inject_branch_instrumentation_block() {
instrument_branches_helper(
r#"
(module
(memory (;0;) 1)
(func
block
nop
end
)
)
"#,
&[
Operator::I32Const { value: 17486 },
Operator::Call { function_index: 1 },
Operator::Block {
blockty: wirm::wasmparser::BlockType::Empty,
},
Operator::I32Const { value: 69016 },
Operator::Call { function_index: 1 },
Operator::Nop,
Operator::End,
Operator::End,
],
);
}
#[test]
fn inject_branch_instrumentation_loop() {
instrument_branches_helper(
r#"
(module
(memory (;0;) 1)
(func
loop
nop
end
)
)
"#,
&[
Operator::I32Const { value: 17486 },
Operator::Call { function_index: 1 },
Operator::Loop {
blockty: wirm::wasmparser::BlockType::Empty,
},
Operator::I32Const { value: 69016 },
Operator::Call { function_index: 1 },
Operator::Nop,
Operator::End,
Operator::End,
],
);
}
#[test]
fn inject_branch_instrumentation_if() {
instrument_branches_helper(
r#"
(module
(memory (;0;) 1)
(func
i32.const 0 ;; Condition
if
nop
end
)
)
"#,
&[
Operator::I32Const { value: 17486 },
Operator::Call { function_index: 1 },
Operator::I32Const { value: 0 },
Operator::If {
blockty: wirm::wasmparser::BlockType::Empty,
},
Operator::I32Const { value: 69016 },
Operator::Call { function_index: 1 },
Operator::Nop,
Operator::End,
Operator::End,
],
);
}
#[test]
fn inject_branch_instrumentation_if_else() {
instrument_branches_helper(
r#"
(module
(memory (;0;) 1)
(func
i32.const 0
if
nop
else
nop
end
)
)
"#,
&[
Operator::I32Const { value: 17486 },
Operator::Call { function_index: 1 },
Operator::I32Const { value: 0 },
Operator::If {
blockty: wirm::wasmparser::BlockType::Empty,
},
Operator::I32Const { value: 69016 },
Operator::Call { function_index: 1 },
Operator::Nop,
Operator::Else,
Operator::I32Const { value: 32602 },
Operator::Call { function_index: 1 },
Operator::Nop,
Operator::End,
Operator::End,
],
);
}
#[test]
fn inject_branch_instrumentation_br() {
instrument_branches_helper(
r#"
(module
(memory (;0;) 1)
(func
block
br 0
end
)
)
"#,
&[
Operator::I32Const { value: 17486 },
Operator::Call { function_index: 1 },
Operator::Block {
blockty: wirm::wasmparser::BlockType::Empty,
},
Operator::I32Const { value: 69016 },
Operator::Call { function_index: 1 },
Operator::I32Const { value: 32602 },
Operator::Call { function_index: 1 },
Operator::Br { relative_depth: 0 },
Operator::End,
Operator::End,
],
);
}
#[test]
fn inject_branch_instrumentation_brif() {
instrument_branches_helper(
r#"
(module
(memory (;0;) 1)
(func
block
i32.const 0
br_if 0
end
)
)
"#,
&[
Operator::I32Const { value: 17486 },
Operator::Call { function_index: 1 },
Operator::Block {
blockty: wirm::wasmparser::BlockType::Empty,
},
Operator::I32Const { value: 69016 },
Operator::Call { function_index: 1 },
Operator::I32Const { value: 0 },
Operator::I32Const { value: 32602 },
Operator::Call { function_index: 1 },
Operator::BrIf { relative_depth: 0 },
Operator::End,
Operator::End,
],
);
}
#[test]
fn inject_branch_instrumentation_return() {
instrument_branches_helper(
r#"
(module
(memory (;0;) 1)
(func
return
)
)
"#,
&[
Operator::I32Const { value: 17486 },
Operator::Call { function_index: 1 },
Operator::I32Const { value: 69016 },
Operator::Call { function_index: 1 },
Operator::Return,
Operator::End,
],
);
}
#[test]
fn inject_branch_instrumentation_brtable() {
let wat = wat::parse_str(
r#"
(module
(memory (;0;) 1)
(func
block
i32.const 0
br_table 0 0
end
)
)
"#,
)
.unwrap();
let history_size = 2;
let mut module = Module::parse(&wat, false, false).unwrap();
let (afl_prev_loc_indices, afl_mem_ptr_idx, _) =
inject_globals(&mut module, history_size, false, false);
instrument_branches(
&mut module,
&afl_prev_loc_indices,
afl_mem_ptr_idx,
Seed::Static(42),
false,
&HashSet::new(),
None,
);
let instructions = module
.functions
.get_fn_by_id(FunctionID(0))
.unwrap()
.unwrap_local()
.body
.instructions
.get_ops();
let exptected = vec![
Operator::I32Const { value: 17486 },
Operator::Call { function_index: 1 },
Operator::Block {
blockty: wirm::wasmparser::BlockType::Empty,
},
Operator::I32Const { value: 69016 },
Operator::Call { function_index: 1 },
Operator::I32Const { value: 0 },
Operator::I32Const { value: 32602 },
Operator::Call { function_index: 1 },
Operator::End,
Operator::End,
];
assert_eq!(
instructions
.iter()
.filter(|o| !matches!(o, Operator::BrTable { targets: _ }))
.cloned()
.collect::<Vec<_>>(),
exptected
);
}
#[should_panic]
#[test]
fn instrumentation_panic_history_size_3() {
let wat = wat::parse_str(
r#"
(module)
"#,
)
.unwrap();
let history_size: usize = 3;
let _ = instrument_wasm_for_fuzzing(InstrumentationArgs {
wasm_bytes: wat,
history_size,
seed: Seed::Random,
instrument_instruction_count: false,
});
}
#[test]
fn instrumentation_round_trip() {
let wat = wat::parse_str(
r#"
(module
(type (;0;) (func (param i32)))
(memory (;0;) 1)
(export "memory" (memory 0))
(export "check_even" (func 0))
(func (;0;) (type 0) (param $num i32)
local.get $num
i32.const 2
i32.rem_u
i32.eqz
if ;; label = @1
i32.const 0
i32.const 1
i32.store
else
i32.const 0
i32.const 0
i32.store
end
)
)
"#,
)
.unwrap();
let history_size: usize = 2;
let expected = wat::parse_str(
r#"
(module
(type (;0;) (func (param i32)))
(type (;1;) (func (param i32 i32)))
(type (;2;) (func))
(import "ic0" "msg_reply_data_append" (func (;0;) (type 1)))
(import "ic0" "msg_reply" (func (;1;) (type 2)))
(memory (;0;) 1)
(global (;0;) (mut i32) i32.const 0)
(global (;1;) (mut i32) i32.const 0)
(global (;2;) i32 i32.const 0)
(export "memory" (memory 0))
(export "check_even" (func 2))
(export "canister_update __export_coverage_for_afl" (func 3))
(func (;2;) (type 0) (param i32)
i32.const 17486
call 4
local.get 0
i32.const 2
i32.rem_u
i32.eqz
if ;; label = @1
i32.const 69016
call 4
i32.const 0
i32.const 1
i32.store
else
i32.const 32602
call 4
i32.const 0
i32.const 0
i32.store
end
)
(func (;3;) (type 2)
i32.const 71136
call 4
global.get 2
i32.const 131072
call 0
call 1
global.get 2
i32.const 0
i32.const 131072
memory.fill
)
(func (;4;) (type 0) (param i32)
(local i32)
local.get 0
global.get 0
i32.xor
global.get 1
i32.xor
global.get 2
i32.add
local.tee 1
local.get 1
i32.load8_u
i32.const 1
i32.add
i32.store8
global.get 0
i32.const 1
i32.shr_u
global.set 1
local.get 0
i32.const 1
i32.shr_u
global.set 0
)
)
"#,
)
.unwrap();
let generated = instrument_wasm_for_fuzzing(InstrumentationArgs {
wasm_bytes: wat,
history_size,
seed: Seed::Static(42),
instrument_instruction_count: false,
});
wasm_equality(generated, expected);
}
#[test]
fn instrumentation_round_trip_wasm64() {
let wat = wat::parse_str(
r#"
(module
(type (;0;) (func (param i64)))
(memory (;0;) i64 1)
(export "memory" (memory 0))
(export "check_even" (func 0))
(func (;0;) (type 0) (param $num i64)
local.get $num
i64.const 2
i64.rem_u
i64.eqz
if ;; label = @1
i64.const 0
i64.const 1
i64.store
else
i64.const 0
i64.const 0
i64.store
end
)
)
"#,
)
.unwrap();
let history_size: usize = 2;
let expected = wat::parse_str(
r#"
(module
(type (;0;) (func (param i64)))
(type (;1;) (func (param i64 i64)))
(type (;2;) (func))
(import "ic0" "msg_reply_data_append" (func (;0;) (type 1)))
(import "ic0" "msg_reply" (func (;1;) (type 2)))
(memory (;0;) i64 1)
(global (;0;) (mut i64) i64.const 0)
(global (;1;) (mut i64) i64.const 0)
(global (;2;) i64 i64.const 0)
(export "memory" (memory 0))
(export "check_even" (func 2))
(export "canister_update __export_coverage_for_afl" (func 3))
(func (;2;) (type 0) (param i64)
i64.const 17486
call 4
local.get 0
i64.const 2
i64.rem_u
i64.eqz
if ;; label = @1
i64.const 69016
call 4
i64.const 0
i64.const 1
i64.store
else
i64.const 32602
call 4
i64.const 0
i64.const 0
i64.store
end
)
(func (;3;) (type 2)
i64.const 71136
call 4
global.get 2
i64.const 131072
call 0
call 1
global.get 2
i32.const 0
i64.const 131072
memory.fill
)
(func (;4;) (type 0) (param i64)
(local i64)
local.get 0
global.get 0
i64.xor
global.get 1
i64.xor
global.get 2
i64.add
local.tee 1
local.get 1
i64.load8_u
i64.const 1
i64.add
i64.store8
global.get 0
i64.const 1
i64.shr_u
global.set 1
local.get 0
i64.const 1
i64.shr_u
global.set 0
)
)
"#,
)
.unwrap();
let generated = instrument_wasm_for_fuzzing(InstrumentationArgs {
wasm_bytes: wat,
history_size,
seed: Seed::Static(42),
instrument_instruction_count: false,
});
wasm_equality(generated, expected);
}
#[test]
fn test_is_memory64_wasm32() {
let wat = wat::parse_str(r#"(module (memory 1))"#).unwrap();
let module = Module::parse(&wat, false, false).unwrap();
assert!(!is_memory64(&module));
}
#[test]
fn test_is_memory64_wasm64_no_import() {
let wat = wat::parse_str(r#"(module (memory i64 1))"#).unwrap();
let module = Module::parse(&wat, false, false).unwrap();
assert!(is_memory64(&module));
}
#[test]
fn test_is_memory64_wasm64_import_i64() {
let wat = wat::parse_str(
r#"
(module
(import "ic0" "msg_reply_data_append" (func (param i64 i64)))
(memory i64 1)
)
"#,
)
.unwrap();
let module = Module::parse(&wat, false, false).unwrap();
assert!(is_memory64(&module));
}
#[test]
fn test_is_memory64_wasm64_import_i32() {
let wat = wat::parse_str(
r#"
(module
(import "ic0" "msg_reply_data_append" (func (param i32 i32)))
(memory i64 1)
)
"#,
)
.unwrap();
let module = Module::parse(&wat, false, false).unwrap();
assert!(!is_memory64(&module));
}
#[test]
fn instruction_count_instrumentation_wasm32() {
let wat = wat::parse_str(
r#"
(module
(type (;0;) (func))
(import "ic0" "msg_reply" (func (;0;) (type 0)))
(memory (;0;) 1)
(export "memory" (memory 0))
(export "canister_update my_method" (func 1))
(func (;1;) (type 0)
call 0
)
)
"#,
)
.unwrap();
let generated = instrument_wasm_for_fuzzing(InstrumentationArgs {
wasm_bytes: wat,
history_size: 1,
seed: Seed::Static(42),
instrument_instruction_count: true,
});
validate_wasm(&generated).unwrap();
let module = Module::parse(&generated, false, false).unwrap();
let has_coverage_export = module
.exports
.get_by_name(format!("canister_update {COVERAGE_FN_EXPORT_NAME}"))
.is_some();
let has_instruction_export = module
.exports
.get_by_name(format!("canister_query {INSTRUCTION_COUNT_FN_EXPORT_NAME}"))
.is_some();
let has_original_export = module
.exports
.get_by_name("canister_update my_method".to_string())
.is_some();
assert!(has_coverage_export, "Missing coverage export");
assert!(has_instruction_export, "Missing instruction count export");
assert!(has_original_export, "Missing original method export");
let original_func_id = module
.exports
.get_func_by_name("canister_update my_method".to_string())
.unwrap();
assert_ne!(
original_func_id,
FunctionID(1),
"Export should point to wrapper, not original function"
);
let perf_counter = module
.imports
.get_func("ic0".to_string(), "performance_counter".to_string());
assert!(perf_counter.is_some(), "Missing performance_counter import");
}
#[test]
fn instruction_count_no_canister_exports() {
let wat = wat::parse_str(
r#"
(module
(memory (;0;) 1)
(export "memory" (memory 0))
(func (;0;)
nop
)
)
"#,
)
.unwrap();
let generated = instrument_wasm_for_fuzzing(InstrumentationArgs {
wasm_bytes: wat,
history_size: 1,
seed: Seed::Static(42),
instrument_instruction_count: true,
});
validate_wasm(&generated).unwrap();
let module = Module::parse(&generated, false, false).unwrap();
let has_instruction_export = module
.exports
.get_by_name(format!("canister_query {INSTRUCTION_COUNT_FN_EXPORT_NAME}"))
.is_some();
assert!(has_instruction_export, "Missing instruction count export");
}
#[test]
fn compute_cost_per_afl_call_values() {
assert_eq!(compute_cost_per_afl_call(1), 29);
assert_eq!(compute_cost_per_afl_call(2), 35);
assert_eq!(compute_cost_per_afl_call(4), 47);
assert_eq!(compute_cost_per_afl_call(8), 71);
}
}