diff --git a/crates/movy-fuzz/src/executor.rs b/crates/movy-fuzz/src/executor.rs index 0625450..b9a1c6f 100644 --- a/crates/movy-fuzz/src/executor.rs +++ b/crates/movy-fuzz/src/executor.rs @@ -9,6 +9,7 @@ use libafl::{ use libafl_bolts::tuples::{Handle, MatchNameRef, RefIndexable}; use movy_replay::{ db::{ObjectStoreInfo, ObjectStoreMintObject}, + event::{ModuleProvider, NotifierTracer}, exec::{ExecutionTracedResults, SuiExecutor}, tracer::{concolic::ConcolicState, fuzz::SuiFuzzTracer, op::Log, oracle::SuiGeneralOracle}, }; @@ -17,6 +18,8 @@ use movy_types::{ input::{FunctionIdent, MoveAddress}, oracle::{Event, OracleFinding}, }; +use move_core_types::account_address::AccountAddress; +use movy_types::error::MovyError; use serde::{Deserialize, Serialize}; use sui_types::{ effects::TransactionEffectsAPI, @@ -33,6 +36,51 @@ use crate::{ pub const CODE_OBSERVER_NAME: &str = "code_observer"; +pub struct FuzzModuleProvider<'a, E> { + env: &'a E, +} + +impl<'a, E> FuzzModuleProvider<'a, E> +where + E: ObjectStore, +{ + pub fn new(env: &'a E) -> Self { + Self { env } + } +} + +impl<'a, E> ModuleProvider for FuzzModuleProvider<'a, E> +where + E: ObjectStore, +{ + fn get_module( + &mut self, + address: AccountAddress, + name: &str, + ) -> Result, MovyError> { + use sui_types::base_types::ObjectID; + + let db = self.env; + let package_id = ObjectID::from(address); + + let package_obj = match db.get_object(&package_id) { + Some(obj) => obj, + None => return Ok(None), + }; + + if let Some(pkg) = package_obj.data.try_as_package() { + for (module_name, bytes) in pkg.serialized_module_map() { + if module_name.as_str() == name { + let module = move_binary_format::CompiledModule::deserialize_with_defaults(bytes)?; + return Ok(Some(module)); + } + } + } + + Ok(None) + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct ExecutionExtraOutcome { pub logs: BTreeMap>, @@ -98,6 +146,7 @@ where + ObjectStoreInfo + Clone + 'static, + E: ObjectStore, OT: ObserversTuple, RT: for<'a> SuiGeneralOracle, S>, I: MoveInput, @@ -133,8 +182,16 @@ where trace!("Executing input: {}", input.sequence()); state.executions_mut().add_assign(1); let gas_id = state.fuzz_state().gas_id; - let tracer = SuiFuzzTracer::new(&mut self.ob, state, &mut self.oracles, CODE_OBSERVER_NAME); - + let provider = FuzzModuleProvider::new(&self.executor.db); + let tracer = NotifierTracer::with_provider( + SuiFuzzTracer::new( + &mut self.ob, + state, + &mut self.oracles, + CODE_OBSERVER_NAME, + ), + provider, + ); let result = self.executor.run_ptb_with_gas( input.sequence().to_ptb()?, epoch, @@ -152,6 +209,7 @@ where let mut trace_outcome = tracer .expect("tracer should be present when tracing is enabled") + .into_inner() .outcome(); trace!("Execution finished with status: {:?}", effects.status()); diff --git a/crates/movy-fuzz/src/oracles/sui/bool_judgement.rs b/crates/movy-fuzz/src/oracles/sui/bool_judgement.rs index 88f37d3..aa5806e 100644 --- a/crates/movy-fuzz/src/oracles/sui/bool_judgement.rs +++ b/crates/movy-fuzz/src/oracles/sui/bool_judgement.rs @@ -1,6 +1,5 @@ use move_binary_format::file_format::Bytecode; use move_trace_format::format::TraceEvent; -use move_vm_stack::Stack; use serde_json::json; use sui_types::effects::TransactionEffects; use z3::{ @@ -11,6 +10,7 @@ use z3::{ use movy_replay::tracer::{ concolic::{ConcolicState, SymbolValue}, oracle::SuiGeneralOracle, + trace::TraceState, }; use movy_types::{ error::MovyError, @@ -36,13 +36,13 @@ impl SuiGeneralOracle for BoolJudgementOracle { fn event( &mut self, event: &TraceEvent, - _stack: Option<&Stack>, + _trace_state: &TraceState, symbol_stack: &ConcolicState, current_function: Option<&movy_types::input::FunctionIdent>, _state: &mut S, ) -> Result, MovyError> { match event { - TraceEvent::BeforeInstruction { + TraceEvent::Instruction { pc, instruction, .. } => { let stack_syms = &symbol_stack.stack; diff --git a/crates/movy-fuzz/src/oracles/sui/infinite_loop.rs b/crates/movy-fuzz/src/oracles/sui/infinite_loop.rs index d8f0a33..21e7946 100644 --- a/crates/movy-fuzz/src/oracles/sui/infinite_loop.rs +++ b/crates/movy-fuzz/src/oracles/sui/infinite_loop.rs @@ -2,12 +2,12 @@ use std::collections::BTreeMap; use move_binary_format::file_format::Bytecode; use move_trace_format::format::TraceEvent; -use move_vm_stack::Stack; use serde_json::json; use movy_replay::tracer::{ concolic::{ConcolicState, SymbolValue}, oracle::SuiGeneralOracle, + trace::TraceState, }; use movy_types::{ error::MovyError, @@ -38,7 +38,7 @@ impl SuiGeneralOracle for InfiniteLoopOracle { fn event( &mut self, event: &TraceEvent, - _stack: Option<&Stack>, + _trace_state: &TraceState, symbol_stack: &ConcolicState, current_function: Option<&movy_types::input::FunctionIdent>, _state: &mut S, @@ -49,7 +49,7 @@ impl SuiGeneralOracle for InfiniteLoopOracle { let key = hash_to_u64(&key); self.branch_counts.remove(&key); } - TraceEvent::BeforeInstruction { + TraceEvent::Instruction { pc, instruction, .. } => { match instruction { diff --git a/crates/movy-fuzz/src/oracles/sui/overflow.rs b/crates/movy-fuzz/src/oracles/sui/overflow.rs index f692e50..e990821 100644 --- a/crates/movy-fuzz/src/oracles/sui/overflow.rs +++ b/crates/movy-fuzz/src/oracles/sui/overflow.rs @@ -1,12 +1,12 @@ use move_binary_format::file_format::Bytecode; use move_core_types::u256::U256; -use move_trace_format::format::TraceEvent; -use move_vm_stack::Stack; +use move_trace_format::format::{TraceEvent, TraceValue}; use serde_json::json; use movy_replay::tracer::{ concolic::{ConcolicState, value_bitwidth, value_to_u256}, oracle::SuiGeneralOracle, + trace::TraceState, }; use movy_types::{ error::MovyError, @@ -19,7 +19,7 @@ use sui_types::effects::TransactionEffects; pub struct OverflowOracle; /// Count the number of significant bits in the concrete value (0 => 0 bits). -fn value_sig_bits(v: &move_vm_types::values::Value) -> u32 { +fn value_sig_bits(v: &TraceValue) -> u32 { let as_u256 = value_to_u256(v); if as_u256 == U256::zero() { 0 @@ -41,27 +41,24 @@ impl SuiGeneralOracle for OverflowOracle { fn event( &mut self, event: &TraceEvent, - stack: Option<&Stack>, + trace_state: &TraceState, _symbol_stack: &ConcolicState, current_function: Option<&movy_types::input::FunctionIdent>, _state: &mut S, ) -> Result, MovyError> { match event { - TraceEvent::BeforeInstruction { + TraceEvent::Instruction { pc, instruction, .. } => { if !matches!(instruction, Bytecode::Shl) { return Ok(vec![]); } - let stack = match stack { - Some(s) => s, - None => return Ok(vec![]), - }; - let Ok(vals_iter) = stack.last_n(2) else { + let stack = &trace_state.operand_stack; + if stack.len() < 2 { return Ok(vec![]); - }; - let vals: Vec<_> = vals_iter.collect(); - let (lhs, rhs) = (vals[0], vals[1]); + } + let lhs = &stack[stack.len() - 2]; + let rhs = &stack[stack.len() - 1]; let lhs_width_bits = value_bitwidth(lhs); // type width (u8/u16/...) let lhs_sig_bits = value_sig_bits(lhs); // actual significant bits of the value let rhs_bits = value_to_u256(rhs); diff --git a/crates/movy-fuzz/src/oracles/sui/precision_loss.rs b/crates/movy-fuzz/src/oracles/sui/precision_loss.rs index 9f50ccc..15fa85c 100644 --- a/crates/movy-fuzz/src/oracles/sui/precision_loss.rs +++ b/crates/movy-fuzz/src/oracles/sui/precision_loss.rs @@ -1,10 +1,10 @@ use move_trace_format::format::TraceEvent; -use move_vm_stack::Stack; use serde_json::json; use movy_replay::tracer::{ concolic::{ConcolicState, SymbolValue}, oracle::SuiGeneralOracle, + trace::TraceState, }; use movy_types::{error::MovyError, input::MoveSequence, oracle::OracleFinding}; use sui_types::effects::TransactionEffects; @@ -26,13 +26,13 @@ impl SuiGeneralOracle for PrecisionLossOracle { fn event( &mut self, event: &TraceEvent, - _stack: Option<&Stack>, + _trace_state: &TraceState, symbol_stack: &ConcolicState, current_function: Option<&movy_types::input::FunctionIdent>, _state: &mut S, ) -> Result, MovyError> { match event { - TraceEvent::BeforeInstruction { + TraceEvent::Instruction { pc, instruction, .. } => { let loss = match instruction { diff --git a/crates/movy-fuzz/src/oracles/sui/proceeds.rs b/crates/movy-fuzz/src/oracles/sui/proceeds.rs index dcbacc3..13e902a 100644 --- a/crates/movy-fuzz/src/oracles/sui/proceeds.rs +++ b/crates/movy-fuzz/src/oracles/sui/proceeds.rs @@ -4,10 +4,9 @@ use std::{ }; use move_trace_format::format::TraceEvent; -use move_vm_stack::Stack; use tracing::debug; -use movy_replay::tracer::{concolic::ConcolicState, oracle::SuiGeneralOracle}; +use movy_replay::tracer::{concolic::ConcolicState, oracle::SuiGeneralOracle, trace::TraceState}; use movy_types::{ error::MovyError, input::{InputArgument, MoveSequence, SuiObjectInputArgument}, @@ -216,7 +215,7 @@ where fn event( &mut self, _event: &TraceEvent, - _stack: Option<&Stack>, + _trace_state: &TraceState, _symbol_stack: &ConcolicState, _current_function: Option<&movy_types::input::FunctionIdent>, _state: &mut S, diff --git a/crates/movy-fuzz/src/oracles/sui/type_conversion.rs b/crates/movy-fuzz/src/oracles/sui/type_conversion.rs index ba0247c..507fac8 100644 --- a/crates/movy-fuzz/src/oracles/sui/type_conversion.rs +++ b/crates/movy-fuzz/src/oracles/sui/type_conversion.rs @@ -1,12 +1,11 @@ use move_binary_format::file_format::Bytecode; use move_trace_format::format::TraceEvent; -use move_vm_stack::Stack; use movy_types::input::MoveSequence; use movy_types::oracle::OracleFinding; use serde_json::json; use movy_replay::tracer::concolic::value_bitwidth; -use movy_replay::tracer::{concolic::ConcolicState, oracle::SuiGeneralOracle}; +use movy_replay::tracer::{concolic::ConcolicState, oracle::SuiGeneralOracle, trace::TraceState}; use movy_types::error::MovyError; use sui_types::effects::TransactionEffects; @@ -26,24 +25,20 @@ impl SuiGeneralOracle for TypeConversionOracle { fn event( &mut self, event: &TraceEvent, - stack: Option<&Stack>, + trace_state: &TraceState, _symbol_stack: &ConcolicState, current_function: Option<&movy_types::input::FunctionIdent>, _state: &mut S, ) -> Result, MovyError> { match event { - TraceEvent::BeforeInstruction { + TraceEvent::Instruction { pc, instruction, .. } => { - let stack = match stack { - Some(s) => s, - None => return Ok(vec![]), - }; - let Ok(vals_iter) = stack.last_n(1) else { + let stack = &trace_state.operand_stack; + if stack.len() < 1 { return Ok(vec![]); - }; - let vals: Vec<_> = vals_iter.collect(); - let val = vals.first().unwrap(); + } + let val = &stack[stack.len() - 1]; let unnecessary = match instruction { Bytecode::CastU8 => value_bitwidth(val) == 8, Bytecode::CastU16 => value_bitwidth(val) == 16, diff --git a/crates/movy-fuzz/src/oracles/sui/typed_bug.rs b/crates/movy-fuzz/src/oracles/sui/typed_bug.rs index 77b50a5..ef50aca 100644 --- a/crates/movy-fuzz/src/oracles/sui/typed_bug.rs +++ b/crates/movy-fuzz/src/oracles/sui/typed_bug.rs @@ -1,8 +1,7 @@ use move_trace_format::format::TraceEvent; -use move_vm_stack::Stack; use tracing::{debug, trace}; -use movy_replay::tracer::{concolic::ConcolicState, oracle::SuiGeneralOracle}; +use movy_replay::tracer::{concolic::ConcolicState, oracle::SuiGeneralOracle, trace::TraceState}; use movy_types::{error::MovyError, input::MoveSequence, oracle::OracleFinding}; use serde_json::json; use sui_types::{ @@ -46,7 +45,7 @@ where fn event( &mut self, _event: &TraceEvent, - _stack: Option<&Stack>, + _trace_state: &TraceState, _symbol_stack: &ConcolicState, _current_function: Option<&movy_types::input::FunctionIdent>, _state: &mut S, diff --git a/crates/movy-replay/src/event.rs b/crates/movy-replay/src/event.rs new file mode 100644 index 0000000..a82d539 --- /dev/null +++ b/crates/movy-replay/src/event.rs @@ -0,0 +1,297 @@ +use std::{collections::BTreeMap, marker::PhantomData}; + +use move_binary_format::CompiledModule; +use move_core_types::account_address::AccountAddress; +use move_trace_format::{ + format::TraceEvent, + interface::{Tracer, Writer}, +}; +use movy_types::error::MovyError; + +pub trait ModuleProvider { + fn get_module( + &mut self, + address: AccountAddress, + name: &str, + ) -> Result, MovyError>; +} + +pub struct NoModuleProvider; + +impl ModuleProvider for NoModuleProvider { + fn get_module( + &mut self, + _address: AccountAddress, + _name: &str, + ) -> Result, MovyError> { + Ok(None) + } +} + +pub trait TraceNotifier: Tracer { + fn notify_event(&mut self, event: &TraceEvent) -> Result<(), MovyError>; + fn handle_before_instruction( + &mut self, + _ctx: &TraceEvent, + _extra: Option<&InstructionExtraInformation>, + ) -> Result<(), MovyError> { + Ok(()) + } +} + +pub struct NotifierTracer +where + N: TraceNotifier, + P: ModuleProvider, +{ + pub notifier: N, + module_provider: P, + module_stack: Vec<(AccountAddress, String)>, + module_cache: BTreeMap<(AccountAddress, String), CompiledModule>, + _phantom: PhantomData

, +} + +impl NotifierTracer +where + N: TraceNotifier, +{ + pub fn new(notifier: N) -> Self { + Self { + notifier, + module_provider: NoModuleProvider, + module_stack: Vec::new(), + module_cache: BTreeMap::new(), + _phantom: PhantomData, + } + } +} + +impl NotifierTracer +where + N: TraceNotifier, + P: ModuleProvider, +{ + pub fn with_provider(notifier: N, provider: P) -> Self { + Self { + notifier, + module_provider: provider, + module_stack: Vec::new(), + module_cache: BTreeMap::new(), + _phantom: PhantomData, + } + } + + pub fn notifier_mut(&mut self) -> &mut N { + &mut self.notifier + } + + pub fn notifier(&self) -> &N { + &self.notifier + } + + pub fn into_inner(self) -> N { + self.notifier + } + fn current_module(&self) -> Option<(AccountAddress, String)> { + self.module_stack.last().cloned() + } + fn get_or_load_module( + &mut self, + address: AccountAddress, + name: &str, + ) -> Option<&CompiledModule> { + let key = (address, name.to_string()); + + if !self.module_cache.contains_key(&key) { + if let Ok(Some(module)) = self.module_provider.get_module(address, name) { + self.module_cache.insert(key.clone(), module); + } + } + + self.module_cache.get(&key) + } +} + +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum InstructionExtraInformation { + Pack(usize), + PackGeneric(usize), + PackVariant(usize), + PackVariantGeneric(usize), + Unpack(usize), + UnpackVariant(usize), + UnpackGeneric(usize), + UnpackVariantGeneric(usize), +} + +fn create_instruction_extra( + tracer: &mut NotifierTracer, + event: &TraceEvent, +) -> Option +where + N: TraceNotifier, + P: ModuleProvider, +{ + match event { + TraceEvent::Instruction { instruction, .. } => { + use move_binary_format::file_format::Bytecode as B; + use move_binary_format::file_format::StructFieldInformation; + let mut extra = None; + + if let Some((address, name)) = tracer.current_module() { + if let Some(module) = tracer.get_or_load_module(address, &name) { + match instruction { + B::Unpack(sidx) => { + let struct_def = module.struct_def_at(*sidx); + let field_count = match &struct_def.field_information { + StructFieldInformation::Native => 0, + StructFieldInformation::Declared(fields) => fields.len(), + }; + extra = Some(InstructionExtraInformation::Unpack(field_count as usize)); + } + B::UnpackVariant(vidx) + | B::UnpackVariantImmRef(vidx) + | B::UnpackVariantMutRef(vidx) => { + let variant_handle = module.variant_handle_at(*vidx); + let variant_def = module + .variant_def_at(variant_handle.enum_def, variant_handle.variant); + let field_count = variant_def.fields.len(); + extra = Some(InstructionExtraInformation::UnpackVariant( + field_count as usize, + )); + } + B::UnpackGeneric(sidx) => { + let struct_inst = module.struct_instantiation_at(*sidx); + let struct_def = module.struct_def_at(struct_inst.def); + let field_count = match &struct_def.field_information { + StructFieldInformation::Native => 0, + StructFieldInformation::Declared(fields) => fields.len(), + }; + extra = Some(InstructionExtraInformation::UnpackGeneric( + field_count as usize, + )); + } + B::UnpackVariantGeneric(vidx) + | B::UnpackVariantGenericImmRef(vidx) + | B::UnpackVariantGenericMutRef(vidx) => { + let variant_inst_handle = module.variant_instantiation_handle_at(*vidx); + let enum_inst = + module.enum_instantiation_at(variant_inst_handle.enum_def); + let variant_def = + module.variant_def_at(enum_inst.def, variant_inst_handle.variant); + let field_count = variant_def.fields.len(); + extra = Some(InstructionExtraInformation::UnpackVariantGeneric( + field_count as usize, + )); + } + B::Pack(sidx) => { + let struct_def = module.struct_def_at(*sidx); + let field_count = match &struct_def.field_information { + StructFieldInformation::Native => 0, + StructFieldInformation::Declared(fields) => fields.len(), + }; + extra = Some(InstructionExtraInformation::Pack(field_count as usize)); + } + B::PackGeneric(sidx) => { + let struct_inst = module.struct_instantiation_at(*sidx); + let struct_def = module.struct_def_at(struct_inst.def); + let field_count = match &struct_def.field_information { + StructFieldInformation::Native => 0, + StructFieldInformation::Declared(fields) => fields.len(), + }; + extra = Some(InstructionExtraInformation::PackGeneric( + field_count as usize, + )); + } + B::PackVariant(vidx) => { + let variant_handle = module.variant_handle_at(*vidx); + let variant_def = module + .variant_def_at(variant_handle.enum_def, variant_handle.variant); + let field_count = variant_def.fields.len(); + extra = Some(InstructionExtraInformation::PackVariant( + field_count as usize, + )); + } + B::PackVariantGeneric(vidx) => { + let variant_inst_handle = module.variant_instantiation_handle_at(*vidx); + let enum_inst = + module.enum_instantiation_at(variant_inst_handle.enum_def); + let variant_def = + module.variant_def_at(enum_inst.def, variant_inst_handle.variant); + let field_count = variant_def.fields.len(); + extra = Some(InstructionExtraInformation::PackVariantGeneric( + field_count as usize, + )); + } + _ => {} + } + } else { + } + } + + extra + } + _ => None, + } +} + +impl Tracer for NotifierTracer +where + N: TraceNotifier, + P: ModuleProvider, +{ + fn notify( + &mut self, + event: &TraceEvent, + _writer: &mut Writer<'_>, + _stack: Option<&move_vm_stack::Stack>, + ) { + self.notifier.notify(event, _writer, _stack); + + match event { + TraceEvent::OpenFrame { frame, gas_left: _ } => { + let address = *frame.module.address(); + let name = frame.module.name().to_string(); + // self.module_stack = Some((address, name)); + self.module_stack.push((address, name)); + if let Err(e) = self.notifier.notify_event(event) { + tracing::error!("NotifierTracer: failed to notify OpenFrame: {:?}", e); + } + } + + TraceEvent::CloseFrame { + frame_id: _, + return_: _, + gas_left: _, + } => { + // self.module_stack = None; + self.module_stack.pop(); + if let Err(e) = self.notifier.notify_event(event) { + tracing::error!("NotifierTracer: failed to notify CloseFrame: {:?}", e); + } + } + + TraceEvent::Instruction { .. } => { + let extra = create_instruction_extra(self, event); + if let Err(e) = self + .notifier + .handle_before_instruction(event, extra.as_ref()) + { + tracing::error!( + "NotifierTracer: failed to handle before instruction: {:?}", + e + ); + } + if let Err(e) = self.notifier.notify_event(event) { + tracing::error!("NotifierTracer: failed to notify Instruction: {:?}", e); + } + } + + _ => { + if let Err(e) = self.notifier.notify_event(event) { + tracing::error!("NotifierTracer: failed to notify event: {:?}", e); + } + } + } + } +} diff --git a/crates/movy-replay/src/exec.rs b/crates/movy-replay/src/exec.rs index 5025d9f..3e6eba7 100644 --- a/crates/movy-replay/src/exec.rs +++ b/crates/movy-replay/src/exec.rs @@ -307,8 +307,24 @@ where // } if package_id == ObjectID::ZERO { - // derive id - let id = ObjectID::derive_id(digest, creation_num); + // Derive the package id from the digest of the publish transaction. + let mut module_bytes = vec![]; + for module in &modules { + let mut buf = vec![]; + module.serialize_with_version(module.version, &mut buf)?; + module_bytes.push(buf); + } + let preview_ptb = ProgrammableTransaction { + inputs: vec![CallArg::Pure(bcs::to_bytes(&admin)?)], + commands: vec![ + Command::Publish(module_bytes, dependencies.clone()), + Command::TransferObjects(vec![Argument::Result(0)], Argument::Input(0)), + ], + }; + let gas_ref = self.db.get_move_object_info(gas.into())?.sui_reference(); + let tx_kind = TransactionKind::ProgrammableTransaction(preview_ptb); + let tx_data = TransactionData::new(tx_kind, admin, gas_ref, 1_000_000_000, 1); + let id = ObjectID::derive_id(tx_data.digest(), 0); substitute_package_id(&mut modules, id)?; } else { // ensure the modules has the expected id diff --git a/crates/movy-replay/src/lib.rs b/crates/movy-replay/src/lib.rs index 73685fb..96374fe 100644 --- a/crates/movy-replay/src/lib.rs +++ b/crates/movy-replay/src/lib.rs @@ -1,5 +1,6 @@ pub mod db; pub mod env; +pub mod event; pub mod exec; pub mod meta; pub mod tracer; diff --git a/crates/movy-replay/src/tracer/concolic.rs b/crates/movy-replay/src/tracer/concolic.rs index f01af8f..69d7396 100644 --- a/crates/movy-replay/src/tracer/concolic.rs +++ b/crates/movy-replay/src/tracer/concolic.rs @@ -1,10 +1,13 @@ use std::{cmp::Ordering, collections::BTreeMap, str::FromStr}; +use crate::event::InstructionExtraInformation; +use crate::tracer::trace::TraceState; use move_binary_format::file_format::Bytecode; use move_core_types::{language_storage::TypeTag, u256::U256}; -use move_trace_format::format::{Effect, ExtraInstructionInformation, TraceEvent, TypeTagWithRefs}; -use move_vm_stack::Stack; -use move_vm_types::values::{Reference, VMValueCast, Value}; +use move_trace_format::{ + format::{Effect, TraceEvent, TraceValue, TypeTagWithRefs}, + value::SerializableMoveValue, +}; use tracing::{trace, warn}; use z3::ast::{Ast, Bool, Int}; @@ -19,13 +22,6 @@ enum PrimitiveValue { U256(U256), } -fn try_value_as(v: &Value) -> Option -where - Value: VMValueCast, -{ - v.copy_value().ok()?.value_as::().ok() -} - #[derive(Clone, Debug, PartialEq, Eq)] pub enum SymbolValue { Value(Int), @@ -37,6 +33,7 @@ pub struct ConcolicState { pub stack: Vec, pub locals: Vec>, pub args: Vec>, + pub pending_instruction_extra: Option, pub disable: bool, } @@ -72,37 +69,17 @@ impl PrimitiveValue { } } -fn extract_primitive_value(v: &Value) -> PrimitiveValue { - if let Some(reference) = try_value_as::(v) { - let inner = reference - .read_ref() - .expect("failed to read reference for comparison"); - return extract_primitive_value(&inner); - } - - if let Some(b) = try_value_as::(v) { - return PrimitiveValue::Bool(b); - } - if let Some(u) = try_value_as::(v) { - return PrimitiveValue::U8(u); - } - if let Some(u) = try_value_as::(v) { - return PrimitiveValue::U16(u); +fn extract_primitive_value(v: &TraceValue) -> PrimitiveValue { + match v.snapshot() { + SerializableMoveValue::Bool(b) => PrimitiveValue::Bool(*b), + SerializableMoveValue::U8(u) => PrimitiveValue::U8(*u), + SerializableMoveValue::U16(u) => PrimitiveValue::U16(*u), + SerializableMoveValue::U32(u) => PrimitiveValue::U32(*u), + SerializableMoveValue::U64(u) => PrimitiveValue::U64(*u), + SerializableMoveValue::U128(u) => PrimitiveValue::U128(*u), + SerializableMoveValue::U256(u) => PrimitiveValue::U256(*u), + v => panic!("Unsupported value type {:?} for comparison", v), } - if let Some(u) = try_value_as::(v) { - return PrimitiveValue::U32(u); - } - if let Some(u) = try_value_as::(v) { - return PrimitiveValue::U64(u); - } - if let Some(u) = try_value_as::(v) { - return PrimitiveValue::U128(u); - } - if let Some(u) = try_value_as::(v) { - return PrimitiveValue::U256(u); - } - - panic!("Unsupported value type {:?} for comparison", v); } fn compare_value_impl(v1: &PrimitiveValue, v2: &PrimitiveValue) -> Ordering { @@ -121,20 +98,29 @@ fn compare_value_impl(v1: &PrimitiveValue, v2: &PrimitiveValue) -> Ordering { } } -pub fn compare_value(v1: &Value, v2: &Value) -> Ordering { +pub fn compare_value(v1: &TraceValue, v2: &TraceValue) -> Ordering { let p1 = extract_primitive_value(v1); let p2 = extract_primitive_value(v2); compare_value_impl(&p1, &p2) } -pub fn value_to_u256(v: &Value) -> U256 { +pub fn value_to_u256(v: &TraceValue) -> U256 { extract_primitive_value(v).as_u256() } -pub fn value_bitwidth(v: &Value) -> u32 { +pub fn value_bitwidth(v: &TraceValue) -> u32 { extract_primitive_value(v).bitwidth() } +fn stack_top2(stack: &[TraceValue]) -> (&TraceValue, &TraceValue) { + let len = stack.len(); + (&stack[len - 2], &stack[len - 1]) +} + +fn stack_top1(stack: &[TraceValue]) -> &TraceValue { + &stack[stack.len() - 1] +} + fn int_two_pow(bits: u32) -> Int { let v = U256::one() << bits; Int::from_str(&v.to_string()).unwrap() @@ -242,11 +228,25 @@ pub fn int_bvxor_const(x: &Int, mask: U256, bits: u32) -> Int { } impl ConcolicState { + pub fn handle_before_instruction( + &mut self, + ctx: &TraceEvent, + extra: Option<&InstructionExtraInformation>, + trace_state: &TraceState, + ) -> Option { + if !matches!(ctx, TraceEvent::Instruction { .. }) { + return None; + } + self.pending_instruction_extra = extra.cloned(); + self.notify_event(ctx, trace_state) + } + pub fn new() -> Self { Self { stack: Vec::new(), locals: Vec::new(), args: Vec::new(), + pending_instruction_extra: None, disable: false, } } @@ -287,7 +287,7 @@ impl ConcolicState { } } - fn resolve_value(value: &Value) -> Int { + fn resolve_value(value: &TraceValue) -> Int { match extract_primitive_value(value) { PrimitiveValue::Bool(b) => { let int_val = if b { 1 } else { 0 }; @@ -301,53 +301,41 @@ impl ConcolicState { PrimitiveValue::U256(u) => Int::from_str(&u.to_string()).unwrap(), } } - - pub fn notify_event(&mut self, event: &TraceEvent, stack: Option<&Stack>) -> Option { - if self.disable { - return None; - } - if let Some(s) = stack { - if self.stack.len() != s.value.len() && s.value.is_empty() { - self.stack.clear(); + #[inline] + fn process_binary_op(&mut self, stack: &[TraceValue]) -> Option<(Int, Int)> { + let (rhs, lhs) = (self.stack.pop().unwrap(), self.stack.pop().unwrap()); + let stack_len = stack.len(); + let true_lhs = &stack[stack_len - 2]; + let true_rhs = &stack[stack_len - 1]; + let (new_l, new_r) = match (lhs, rhs) { + (SymbolValue::Value(l), SymbolValue::Value(r)) => (l, r), + (SymbolValue::Value(l), SymbolValue::Unknown) => { + let new_r = Self::resolve_value(true_rhs); + (l, new_r) } - if let TraceEvent::Effect(v) = event - && let Effect::ExecutionError(_) = v.as_ref() - { - self.stack.pop(); + (SymbolValue::Unknown, SymbolValue::Value(r)) => { + let new_l = Self::resolve_value(true_lhs); + (new_l, r) } - if self.stack.len() != s.value.len() { - warn!( - "stack: {:?}, stack from trace: {:?}, event: {:?}, disabling concolic execution", - self.stack, s.value, event - ); - self.disable = true; + (SymbolValue::Unknown, SymbolValue::Unknown) => { return None; } - } else { - trace!("No stack available for event: {:?}", event); - } - - let mut process_binary_op = || { - let (rhs, lhs) = (self.stack.pop().unwrap(), self.stack.pop().unwrap()); - let mut stack_iter = stack.unwrap().last_n(2).unwrap(); - let true_lhs = stack_iter.next().unwrap(); - let true_rhs = stack_iter.next().unwrap(); - let (new_l, new_r) = match (lhs, rhs) { - (SymbolValue::Value(l), SymbolValue::Value(r)) => (l, r), - (SymbolValue::Value(l), SymbolValue::Unknown) => { - let new_r = Self::resolve_value(true_rhs); - (l, new_r) - } - (SymbolValue::Unknown, SymbolValue::Value(r)) => { - let new_l = Self::resolve_value(true_lhs); - (new_l, r) - } - (SymbolValue::Unknown, SymbolValue::Unknown) => { - return None; - } - }; - Some((new_l, new_r)) }; + Some((new_l, new_r)) + } + pub fn notify_event(&mut self, event: &TraceEvent, trace_state: &TraceState) -> Option { + if self.disable { + return None; + } + let stack = &trace_state.operand_stack; + // if self.stack.len() != stack.len() && stack.is_empty() { + // self.stack.clear(); + // } + if let TraceEvent::Effect(v) = event + && let Effect::ExecutionError(_) = v.as_ref() + { + self.stack.pop(); + } match event { TraceEvent::External(v) => { @@ -359,7 +347,7 @@ impl ConcolicState { } TraceEvent::OpenFrame { frame, gas_left: _ } => { trace!("Open frame: {:?}", frame); - trace!("Current stack: {:?}", stack.map(|s| &s.value)); + trace!("Current stack: {:?}", &trace_state.operand_stack); let param_count = frame.parameters.len(); if self.locals.is_empty() { let mut locals = if frame.locals_types.is_empty() { @@ -413,22 +401,30 @@ impl ConcolicState { return_: _, gas_left: _, } => { - trace!("Close frame. Current stack: {:?}", stack.map(|s| &s.value)); + trace!( + "Close frame. Current stack: {:?}", + &trace_state.operand_stack + ); self.locals.pop(); } - TraceEvent::BeforeInstruction { + TraceEvent::Instruction { type_parameters: _, pc, gas_left: _, instruction, - extra, } => { + let extra = self.pending_instruction_extra.take(); + if self.stack.len() != stack.len() { + warn!( + "stack: {:?}, stack from trace: {:?}, event: {:?}, disabling concolic execution", + self.stack, stack, event + ); + self.disable = true; + return None; + } trace!( "Before instruction at pc {}: {:?}, extra: {:?}. Current stack: {:?}", - pc, - instruction, - extra, - stack.map(|s| &s.value) + pc, instruction, extra, &trace_state.operand_stack ); match instruction { Bytecode::Pop @@ -500,7 +496,7 @@ impl ConcolicState { } } Bytecode::Add => { - if let Some((l, r)) = process_binary_op() { + if let Some((l, r)) = self.process_binary_op(stack) { // overflow check not implemented yet let sum = l + r; self.stack.push(SymbolValue::Value(sum)); @@ -509,7 +505,7 @@ impl ConcolicState { } } Bytecode::Sub => { - if let Some((l, r)) = process_binary_op() { + if let Some((l, r)) = self.process_binary_op(stack) { // overflow check not implemented yet let diff = l - r; self.stack.push(SymbolValue::Value(diff)); @@ -518,7 +514,7 @@ impl ConcolicState { } } Bytecode::Mul => { - if let Some((l, r)) = process_binary_op() { + if let Some((l, r)) = self.process_binary_op(stack) { // overflow check not implemented yet let prod = l * r; self.stack.push(SymbolValue::Value(prod)); @@ -527,7 +523,7 @@ impl ConcolicState { } } Bytecode::Div => { - if let Some((l, r)) = process_binary_op() { + if let Some((l, r)) = self.process_binary_op(stack) { // overflow check not implemented yet let quot = l / r; self.stack.push(SymbolValue::Value(quot)); @@ -536,7 +532,7 @@ impl ConcolicState { } } Bytecode::Mod => { - if let Some((l, r)) = process_binary_op() { + if let Some((l, r)) = self.process_binary_op(stack) { // overflow check not implemented yet let rem = l % r; self.stack.push(SymbolValue::Value(rem)); @@ -546,9 +542,7 @@ impl ConcolicState { } Bytecode::And | Bytecode::BitAnd => { let (rhs, lhs) = (self.stack.pop().unwrap(), self.stack.pop().unwrap()); - let mut stack_iter = stack.unwrap().last_n(2).unwrap(); - let true_lhs = stack_iter.next().unwrap(); - let true_rhs = stack_iter.next().unwrap(); + let (true_lhs, true_rhs) = stack_top2(stack); let bit_width = value_bitwidth(true_lhs); let (true_l, true_r) = (value_to_u256(true_lhs), value_to_u256(true_rhs)); @@ -571,9 +565,7 @@ impl ConcolicState { } Bytecode::Or | Bytecode::BitOr => { let (rhs, lhs) = (self.stack.pop().unwrap(), self.stack.pop().unwrap()); - let mut stack_iter = stack.unwrap().last_n(2).unwrap(); - let true_lhs = stack_iter.next().unwrap(); - let true_rhs = stack_iter.next().unwrap(); + let (true_lhs, true_rhs) = stack_top2(stack); let bit_width = value_bitwidth(true_lhs); let (true_l, true_r) = (value_to_u256(true_lhs), value_to_u256(true_rhs)); @@ -596,9 +588,7 @@ impl ConcolicState { } Bytecode::Xor => { let (rhs, lhs) = (self.stack.pop().unwrap(), self.stack.pop().unwrap()); - let mut stack_iter = stack.unwrap().last_n(2).unwrap(); - let true_lhs = stack_iter.next().unwrap(); - let true_rhs = stack_iter.next().unwrap(); + let (true_lhs, true_rhs) = stack_top2(stack); let bit_width = value_bitwidth(true_lhs); let (true_l, true_r) = (value_to_u256(true_lhs), value_to_u256(true_rhs)); @@ -621,9 +611,7 @@ impl ConcolicState { } Bytecode::Shl => { let (rhs, lhs) = (self.stack.pop().unwrap(), self.stack.pop().unwrap()); - let mut stack_iter = stack.unwrap().last_n(2).unwrap(); - let true_lhs = stack_iter.next().unwrap(); - let true_rhs = stack_iter.next().unwrap(); + let (true_lhs, true_rhs) = stack_top2(stack); let bit_width = value_bitwidth(true_lhs); let true_r = value_to_u256(true_rhs).unchecked_as_u32(); let threshold = Self::max_u_bits(bit_width); @@ -647,9 +635,7 @@ impl ConcolicState { } Bytecode::Shr => { let (rhs, lhs) = (self.stack.pop().unwrap(), self.stack.pop().unwrap()); - let mut stack_iter = stack.unwrap().last_n(2).unwrap(); - let _true_lhs = stack_iter.next().unwrap(); - let true_rhs = stack_iter.next().unwrap(); + let (_true_lhs, true_rhs) = stack_top2(stack); let true_r = value_to_u256(true_rhs).unchecked_as_u32(); match (lhs, rhs) { @@ -672,9 +658,7 @@ impl ConcolicState { if let Some(v) = self.stack.pop() { match v { SymbolValue::Value(n) => { - let bit_width = value_bitwidth( - stack.unwrap().last_n(1).unwrap().next().unwrap(), - ); + let bit_width = value_bitwidth(stack_top1(stack)); let not_n = int_bvnot(&n, bit_width); self.stack.push(SymbolValue::Value(not_n)); } @@ -739,9 +723,7 @@ impl ConcolicState { } Bytecode::Eq => { let (rhs, lhs) = (self.stack.pop().unwrap(), self.stack.pop().unwrap()); - let mut stack_iter = stack.unwrap().last_n(2).unwrap(); - let true_lhs = stack_iter.next().unwrap(); - let true_rhs = stack_iter.next().unwrap(); + let (true_lhs, true_rhs) = stack_top2(stack); let (new_l, new_r) = match (lhs, rhs) { (SymbolValue::Value(l), SymbolValue::Value(r)) => (l, r), (SymbolValue::Value(l), SymbolValue::Unknown) => { @@ -772,9 +754,7 @@ impl ConcolicState { } Bytecode::Neq => { let (rhs, lhs) = (self.stack.pop().unwrap(), self.stack.pop().unwrap()); - let mut stack_iter = stack.unwrap().last_n(2).unwrap(); - let true_lhs = stack_iter.next().unwrap(); - let true_rhs = stack_iter.next().unwrap(); + let (true_lhs, true_rhs) = stack_top2(stack); let (new_l, new_r) = match (lhs, rhs) { (SymbolValue::Value(l), SymbolValue::Value(r)) => (l, r), (SymbolValue::Value(l), SymbolValue::Unknown) => { @@ -805,9 +785,7 @@ impl ConcolicState { } Bytecode::Lt => { let (rhs, lhs) = (self.stack.pop().unwrap(), self.stack.pop().unwrap()); - let mut stack_iter = stack.unwrap().last_n(2).unwrap(); - let true_lhs = stack_iter.next().unwrap(); - let true_rhs = stack_iter.next().unwrap(); + let (true_lhs, true_rhs) = stack_top2(stack); let (new_l, new_r) = match (lhs, rhs) { (SymbolValue::Value(l), SymbolValue::Value(r)) => (l, r), (SymbolValue::Value(l), SymbolValue::Unknown) => { @@ -838,9 +816,7 @@ impl ConcolicState { } Bytecode::Le => { let (rhs, lhs) = (self.stack.pop().unwrap(), self.stack.pop().unwrap()); - let mut stack_iter = stack.unwrap().last_n(2).unwrap(); - let true_lhs = stack_iter.next().unwrap(); - let true_rhs = stack_iter.next().unwrap(); + let (true_lhs, true_rhs) = stack_top2(stack); let (new_l, new_r) = match (lhs, rhs) { (SymbolValue::Value(l), SymbolValue::Value(r)) => (l, r), (SymbolValue::Value(l), SymbolValue::Unknown) => { @@ -871,9 +847,7 @@ impl ConcolicState { } Bytecode::Gt => { let (rhs, lhs) = (self.stack.pop().unwrap(), self.stack.pop().unwrap()); - let mut stack_iter = stack.unwrap().last_n(2).unwrap(); - let true_lhs = stack_iter.next().unwrap(); - let true_rhs = stack_iter.next().unwrap(); + let (true_lhs, true_rhs) = stack_top2(stack); let (new_l, new_r) = match (lhs, rhs) { (SymbolValue::Value(l), SymbolValue::Value(r)) => (l, r), (SymbolValue::Value(l), SymbolValue::Unknown) => { @@ -904,9 +878,7 @@ impl ConcolicState { } Bytecode::Ge => { let (rhs, lhs) = (self.stack.pop().unwrap(), self.stack.pop().unwrap()); - let mut stack_iter = stack.unwrap().last_n(2).unwrap(); - let true_lhs = stack_iter.next().unwrap(); - let true_rhs = stack_iter.next().unwrap(); + let (true_lhs, true_rhs) = stack_top2(stack); let (new_l, new_r) = match (lhs, rhs) { (SymbolValue::Value(l), SymbolValue::Value(r)) => (l, r), (SymbolValue::Value(l), SymbolValue::Unknown) => { @@ -954,8 +926,8 @@ impl ConcolicState { } Bytecode::Pack(_) | Bytecode::PackGeneric(_) => { match extra.as_ref().unwrap() { - ExtraInstructionInformation::Pack(count) - | ExtraInstructionInformation::PackGeneric(count) => { + InstructionExtraInformation::Pack(count) + | InstructionExtraInformation::PackGeneric(count) => { for _ in 0..*count { self.stack.pop(); } @@ -967,8 +939,8 @@ impl ConcolicState { Bytecode::Unpack(_) | Bytecode::UnpackGeneric(_) => { self.stack.pop(); match extra.as_ref().unwrap() { - ExtraInstructionInformation::Unpack(count) - | ExtraInstructionInformation::UnpackGeneric(count) => { + InstructionExtraInformation::Unpack(count) + | InstructionExtraInformation::UnpackGeneric(count) => { for _ in 0..*count { self.stack.push(SymbolValue::Unknown); // represent each field as unknown } @@ -978,8 +950,8 @@ impl ConcolicState { } Bytecode::PackVariant(_) | Bytecode::PackVariantGeneric(_) => { match extra.as_ref().unwrap() { - ExtraInstructionInformation::PackVariant(count) - | ExtraInstructionInformation::PackVariantGeneric(count) => { + InstructionExtraInformation::PackVariant(count) + | InstructionExtraInformation::PackVariantGeneric(count) => { for _ in 0..*count { self.stack.pop(); } @@ -1001,8 +973,8 @@ impl ConcolicState { return None; } match extra.as_ref().unwrap() { - ExtraInstructionInformation::UnpackVariant(count) - | ExtraInstructionInformation::UnpackVariantGeneric(count) => { + InstructionExtraInformation::UnpackVariant(count) + | InstructionExtraInformation::UnpackVariantGeneric(count) => { for _ in 0..*count { self.stack.push(SymbolValue::Unknown); // represent each field as unknown } diff --git a/crates/movy-replay/src/tracer/fuzz.rs b/crates/movy-replay/src/tracer/fuzz.rs index ec8c31a..ac32e13 100644 --- a/crates/movy-replay/src/tracer/fuzz.rs +++ b/crates/movy-replay/src/tracer/fuzz.rs @@ -5,18 +5,20 @@ use libafl::{executors::ExitKind, observers::StdMapObserver}; use libafl_bolts::tuples::{Handle, MatchName, MatchNameRef}; use move_binary_format::file_format::Bytecode; use move_trace_format::{ - format::{Effect, TraceEvent}, - interface::Tracer, + format::{Effect, TraceEvent, TraceValue}, + interface::{Tracer, Writer}, }; -use move_vm_stack::Stack; -use move_vm_types::values::IntegerValue; use movy_types::{error::MovyError, input::FunctionIdent, oracle::OracleFinding}; use tracing::{trace, warn}; -use crate::tracer::{ - concolic::ConcolicState, - op::{CastLog, CmpLog, CmpOp, Log, Magic, ShlLog}, - oracle::SuiGeneralOracle, +use crate::{ + event::InstructionExtraInformation, + tracer::{ + concolic::ConcolicState, + op::{CastLog, CmpLog, CmpOp, Log, Magic, ShlLog}, + oracle::SuiGeneralOracle, + trace::TraceState, + }, }; #[derive(Debug)] @@ -127,6 +129,7 @@ where { current_functions: Vec, coverage: CoverageTracer<'a, OT>, + trace_state: TraceState, state: &'s mut S, outcome: TraceOutcome, oracles: &'s mut O, @@ -147,6 +150,7 @@ where Self { current_functions: vec![], coverage: CoverageTracer::new(ob, ob_name), + trace_state: TraceState::new(), state, outcome: TraceOutcome::new(), oracles, @@ -158,37 +162,19 @@ where self.outcome } - fn bin_ops(stack: Option<&Stack>) -> Result<(Magic, Magic), MovyError> { - if let Some(stack) = stack { - let stack_len = stack.value.len(); - let rhs = stack - .value - .get(stack_len - 1) - .ok_or_else(|| eyre!("stack less than 2?!"))? - .copy_value()? - .value_as::()? - .into(); - let lhs = stack - .value - .get(stack_len - 2) - .ok_or_else(|| eyre!("stack less than 2?!"))? - .copy_value()? - .value_as::()? - .into(); - Ok((lhs, rhs)) - } else { - Err(eyre!("we need two values on top of stack but get none...").into()) + fn bin_ops(stack: &[TraceValue]) -> Result<(Magic, Magic), MovyError> { + if stack.len() < 2 { + return Err(eyre!("stack less than 2?!").into()); } + let lhs = Magic::try_from(&stack[stack.len() - 2])?; + let rhs = Magic::try_from(&stack[stack.len() - 1])?; + Ok((lhs, rhs)) } - pub fn notify_event( - &mut self, - event: &TraceEvent, - stack: Option<&move_vm_stack::Stack>, - ) -> Result<(), MovyError> { + fn apply_oracle_findings(&mut self, event: &TraceEvent) -> Result<(), MovyError> { let oracle_vulns = self.oracles.event( event, - stack, + &self.trace_state, &self.outcome.concolic, self.current_functions.last(), self.state, @@ -199,7 +185,118 @@ where for info in oracle_vulns { self.outcome.findings.push(info); } - let constraint = self.outcome.concolic.notify_event(event, stack); + Ok(()) + } + + pub fn handle_before_instruction( + &mut self, + ctx: &TraceEvent, + extra: Option<&InstructionExtraInformation>, + ) -> Result<(), MovyError> { + let TraceEvent::Instruction { + pc, instruction, .. + } = ctx + else { + return Ok(()); + }; + + self.apply_oracle_findings(ctx)?; + let constraint = + self.outcome + .concolic + .handle_before_instruction(ctx, extra, &self.trace_state); + + self.coverage.may_do_coverage(*pc); + match instruction { + Bytecode::BrFalse(_) + | Bytecode::BrTrue(_) + | Bytecode::Branch(_) + | Bytecode::VariantSwitch(_) => { + self.coverage.will_branch(); + } + Bytecode::Lt + | Bytecode::Le + | Bytecode::Ge + | Bytecode::Gt + | Bytecode::Neq + | Bytecode::Eq => match Self::bin_ops(&self.trace_state.operand_stack) { + Ok((lhs, rhs)) => { + if let Some(current_function) = self.current_functions.first() { + let op = CmpOp::try_from(instruction)?; + self.outcome + .logs + .entry(current_function.clone()) + .or_default() + .push(Log::CmpLog(CmpLog { + lhs, + rhs, + op, + constraint, + })); + } else { + warn!("Fail to track cmplog because of no current function") + } + } + Err(e) => { + if !matches!(instruction, Bytecode::Eq) && !matches!(instruction, Bytecode::Neq) + { + warn!("Can not track cmplog due to {}", e); + } + } + }, + Bytecode::Shl => match Self::bin_ops(&self.trace_state.operand_stack) { + Ok((lhs, rhs)) => { + if let Some(current_function) = self.current_functions.first() { + self.outcome + .logs + .entry(current_function.clone()) + .or_default() + .push(Log::ShlLog(ShlLog { + lhs, + rhs, + constraint, + })); + } else { + warn!("Fail to track cmplog because of no current function") + } + } + Err(e) => { + if !matches!(instruction, Bytecode::Eq) && !matches!(instruction, Bytecode::Neq) + { + warn!("Can not track cmplog due to {}", e); + } + } + }, + Bytecode::CastU8 + | Bytecode::CastU16 + | Bytecode::CastU32 + | Bytecode::CastU64 + | Bytecode::CastU128 => { + if let Some(lhs) = self.trace_state.operand_stack.last() { + let lhs: Magic = Magic::try_from(lhs)?; + if let Some(current_function) = self.current_functions.first() { + self.outcome + .logs + .entry(current_function.clone()) + .or_default() + .push(Log::CastLog(CastLog { lhs, constraint })); + } else { + warn!("Fail to track castlog because of no current function") + } + } else { + warn!("Can not track castlog due to stack empty"); + } + } + _ => {} + } + Ok(()) + } + + pub fn notify_event(&mut self, event: &TraceEvent) -> Result<(), MovyError> { + if !matches!(event, TraceEvent::Instruction { .. }) { + self.apply_oracle_findings(event)?; + let _ = self.outcome.concolic.notify_event(event, &self.trace_state); + } trace!("Tracing event: {:?}", event); match event { TraceEvent::OpenFrame { frame, gas_left: _ } => { @@ -225,107 +322,6 @@ where self.coverage.call_end_package(); self.current_functions.pop(); } - TraceEvent::BeforeInstruction { - type_parameters: _, - pc, - gas_left: _, - instruction, - extra: _, - } => { - // if let Some(metrics) = self.state.eval_metrics_mut() { - // if let Some(current) = self.current_functions.last() { - // metrics.on_pc(¤t.0, ¤t.1, *pc); - // } else { - // warn!("no current function when before instruction at {}", pc); - // } - // } - self.coverage.may_do_coverage(*pc); - match instruction { - Bytecode::BrFalse(_) - | Bytecode::BrTrue(_) - | Bytecode::Branch(_) - | Bytecode::VariantSwitch(_) => { - self.coverage.will_branch(); - } - Bytecode::Lt - | Bytecode::Le - | Bytecode::Ge - | Bytecode::Gt - | Bytecode::Neq - | Bytecode::Eq => match Self::bin_ops(stack) { - Ok((lhs, rhs)) => { - if let Some(current_function) = self.current_functions.first() { - let op = CmpOp::try_from(instruction)?; - self.outcome - .logs - .entry(current_function.clone()) - .or_default() - .push(Log::CmpLog(CmpLog { - lhs, - rhs, - op, - constraint, - })); - } else { - warn!("Fail to track cmplog because of no current function") - } - } - Err(e) => { - if !matches!(instruction, Bytecode::Eq) - && !matches!(instruction, Bytecode::Neq) - { - warn!("Can not track cmplog due to {}", e); - } - } - }, - Bytecode::Shl => match Self::bin_ops(stack) { - Ok((lhs, rhs)) => { - if let Some(current_function) = self.current_functions.first() { - self.outcome - .logs - .entry(current_function.clone()) - .or_default() - .push(Log::ShlLog(ShlLog { - lhs, - rhs, - constraint, - })); - } else { - warn!("Fail to track cmplog because of no current function") - } - } - Err(e) => { - if !matches!(instruction, Bytecode::Eq) - && !matches!(instruction, Bytecode::Neq) - { - warn!("Can not track cmplog due to {}", e); - } - } - }, - Bytecode::CastU8 - | Bytecode::CastU16 - | Bytecode::CastU32 - | Bytecode::CastU64 - | Bytecode::CastU128 => { - if let Some(Some(lhs)) = stack.map(|s| s.value.last()) { - let lhs: Magic = lhs.copy_value()?.value_as::()?.into(); - if let Some(current_function) = self.current_functions.first() { - self.outcome - .logs - .entry(current_function.clone()) - .or_default() - .push(Log::CastLog(CastLog { lhs, constraint })); - } else { - warn!("Fail to track castlog because of no current function") - } - } else { - warn!("Can not track castlog due to stack empty"); - } - } - - _ => {} - } - } TraceEvent::Effect(e) => { if let Effect::ExecutionError(e) = e.as_ref() && e.contains("!! TRACING ERROR !!") @@ -348,11 +344,27 @@ where fn notify( &mut self, event: &TraceEvent, - _writer: &mut move_trace_format::interface::Writer<'_>, - stack: Option<&move_vm_stack::Stack>, + writer: &mut Writer<'_>, + _stack: Option<&move_vm_stack::Stack>, ) { - if let Err(e) = self.notify_event(event, stack) { - warn!("Error during tracing: {}", e); - } + self.trace_state.notify(event, writer, None); + } +} + +impl<'a, 's, S, O, T, OT> crate::event::TraceNotifier for SuiFuzzTracer<'a, 's, S, O, T, OT> +where + O: SuiGeneralOracle, + OT: MatchNameRef + MatchName, +{ + fn notify_event(&mut self, event: &TraceEvent) -> Result<(), MovyError> { + SuiFuzzTracer::notify_event(self, event) + } + + fn handle_before_instruction( + &mut self, + ctx: &TraceEvent, + extra: Option<&InstructionExtraInformation>, + ) -> Result<(), MovyError> { + SuiFuzzTracer::handle_before_instruction(self, ctx, extra) } } diff --git a/crates/movy-replay/src/tracer/mod.rs b/crates/movy-replay/src/tracer/mod.rs index 1cb5e43..617aed1 100644 --- a/crates/movy-replay/src/tracer/mod.rs +++ b/crates/movy-replay/src/tracer/mod.rs @@ -5,7 +5,7 @@ pub mod fuzz; pub mod op; pub mod oracle; pub mod tree; - +pub mod trace; #[derive(Default)] pub struct NopTracer; diff --git a/crates/movy-replay/src/tracer/op.rs b/crates/movy-replay/src/tracer/op.rs index db01123..c4c50f1 100644 --- a/crates/movy-replay/src/tracer/op.rs +++ b/crates/movy-replay/src/tracer/op.rs @@ -3,6 +3,7 @@ use std::fmt::Display; use alloy_primitives::U256; use color_eyre::eyre::eyre; use move_binary_format::file_format::Bytecode; +use move_trace_format::{format::TraceValue, value::{SerializableMoveValue, SimplifiedMoveStruct}}; use move_vm_types::values::IntegerValue; use movy_types::error::MovyError; use serde::{Deserialize, Serialize}; @@ -45,6 +46,40 @@ impl From for Magic { } } +impl TryFrom<&SerializableMoveValue> for Magic { + type Error = MovyError; + + fn try_from(value: &SerializableMoveValue) -> Result { + match value { + SerializableMoveValue::Bool(v) => Ok(Self::U8(if *v { 1 } else { 0 })), + SerializableMoveValue::U8(v) => Ok(Self::U8(*v)), + SerializableMoveValue::U16(v) => Ok(Self::U16(*v)), + SerializableMoveValue::U32(v) => Ok(Self::U32(*v)), + SerializableMoveValue::U64(v) => Ok(Self::U64(*v)), + SerializableMoveValue::U128(v) => Ok(Self::U128(*v)), + SerializableMoveValue::U256(v) => Ok(Self::U256(U256::from_be_bytes(v.to_be_bytes()))), + SerializableMoveValue::Address(bytes) => Ok(Self::Bytes(bytes.to_vec())), + SerializableMoveValue::Struct(data) => { + // TODO: more generic way to serialize struct into bytes + let SimplifiedMoveStruct { type_, .. } = data; + let mut bytes = type_.address.to_vec(); + bytes.extend_from_slice(type_.module.as_bytes()); + bytes.extend_from_slice(type_.name.as_bytes()); + Ok(Self::Bytes(bytes)) + } + _ => Err(eyre!("TraceValue is not an integer {:?}", value).into()), + } + } +} + +impl TryFrom<&TraceValue> for Magic { + type Error = MovyError; + + fn try_from(value: &TraceValue) -> Result { + Magic::try_from(value.snapshot()) + } +} + #[derive(Serialize, Deserialize, PartialEq, Eq, Debug, Clone, Copy)] pub enum CmpOp { LT, diff --git a/crates/movy-replay/src/tracer/oracle.rs b/crates/movy-replay/src/tracer/oracle.rs index a98544e..25d9db8 100644 --- a/crates/movy-replay/src/tracer/oracle.rs +++ b/crates/movy-replay/src/tracer/oracle.rs @@ -1,5 +1,4 @@ -use move_trace_format::format::TraceEvent; -use move_vm_stack::Stack; +use move_trace_format::{format::TraceEvent}; use movy_types::{ error::MovyError, input::{FunctionIdent, MoveSequence}, @@ -7,7 +6,10 @@ use movy_types::{ }; use sui_types::effects::TransactionEffects; -use crate::tracer::concolic::ConcolicState; +use crate::tracer::{ + concolic::ConcolicState, + trace::TraceState, +}; pub trait SuiGeneralOracle { fn pre_execution( @@ -20,7 +22,7 @@ pub trait SuiGeneralOracle { fn event( &mut self, event: &TraceEvent, - stack: Option<&Stack>, + trace_state: &TraceState, symbol_stack: &ConcolicState, current_function: Option<&FunctionIdent>, state: &mut S, @@ -47,7 +49,7 @@ impl SuiGeneralOracle for () { fn event( &mut self, _event: &TraceEvent, - _stack: Option<&Stack>, + _trace_state: &TraceState, _symbol_stack: &ConcolicState, _current_function: Option<&movy_types::input::FunctionIdent>, _state: &mut S, @@ -83,18 +85,19 @@ where fn event( &mut self, event: &TraceEvent, - stack: Option<&Stack>, + trace_state: &TraceState, symbol_stack: &ConcolicState, current_function: Option<&movy_types::input::FunctionIdent>, state: &mut S, ) -> Result, MovyError> { Ok(self .0 - .event(event, stack, symbol_stack, current_function, state)? + .event(event, trace_state, symbol_stack, current_function.clone(), state)? .into_iter() .chain( self.1 - .event(event, stack, symbol_stack, current_function, state)?, + .event(event, trace_state, symbol_stack, current_function, state)? + .into_iter(), ) .collect()) } @@ -144,7 +147,7 @@ where fn event( &mut self, event: &TraceEvent, - stack: Option<&Stack>, + trace_state: &TraceState, symbol_stack: &ConcolicState, current_function: Option<&FunctionIdent>, state: &mut S, @@ -153,7 +156,7 @@ where return Ok(vec![]); } self.oracle - .event(event, stack, symbol_stack, current_function, state) + .event(event, trace_state, symbol_stack, current_function, state) } fn done_execution( diff --git a/crates/movy-replay/src/tracer/trace.rs b/crates/movy-replay/src/tracer/trace.rs new file mode 100644 index 0000000..a8ebc47 --- /dev/null +++ b/crates/movy-replay/src/tracer/trace.rs @@ -0,0 +1,159 @@ +// Copyright (c) The Move Contributors +// SPDX-License-Identifier: Apache-2.0 + +//! This module contains the implementation of the memory tracer. The memory tracer is a tracer +//! that takes a stream of trace events, and uses these events to create a snapshot of the memory +//! state (operand stack, locals, and globals) at each point in time during execution. +//! +//! The memory tracer then emits `External` events with the current VM state for every instruction, +//! and open/close frame event that is has built up. +//! +//! The memory tracer is useful for debugging, and as an example of how to build up this +//! state for more advanced analysis and also using the custom tracing trait. +use core::fmt; +use move_trace_format::{ + format::{DataLoad, Effect, Location, Read, TraceEvent, TraceIndex, TraceValue, Write}, + interface::{Tracer, Writer}, + value::SerializableMoveValue, +}; +use move_vm_stack::Stack; +use std::collections::BTreeMap; + +#[derive(Debug, Clone)] +pub struct TraceState { + // Tracks "global memory" state (i.e., references out in to global memory/references returned + // from native functions). + pub loaded_state: BTreeMap, + // The current state (i.e., values) of the VM's operand stack. + pub operand_stack: Vec, + // The current call stack indexed by frame id. Maps from the frame id to the current state of + // the frame's locals. The bool indicates if the frame is native or not. + pub call_stack: BTreeMap, bool)>, +} + +impl TraceState { + pub fn new() -> Self { + Self { + loaded_state: BTreeMap::new(), + operand_stack: vec![], + call_stack: BTreeMap::new(), + } + } + + /// Apply an event to the state machine and update the locals state accordingly. + fn apply_event(&mut self, event: &TraceEvent) { + match event { + TraceEvent::OpenFrame { frame, .. } => { + let mut locals = BTreeMap::new(); + for (i, p) in frame.parameters.iter().enumerate() { + // NB: parameters are passed directly, so we just pop to make sure they aren't also + // left on the operand stack. For the initial call, these pops may (should) fail, but that + // is fine as we already have the values in the parameter list. + self.operand_stack.pop(); + locals.insert(i, p.clone()); + } + + self.call_stack + .insert(frame.frame_id, (locals, frame.is_native)); + } + TraceEvent::CloseFrame { .. } => { + self.call_stack + .pop_last() + .expect("Unbalanced call stack in memory tracer -- this should never happen"); + } + TraceEvent::Effect(ef) => match &**ef { + Effect::ExecutionError(_) => (), + Effect::Push(value) => { + self.operand_stack.push(value.clone()); + } + Effect::Pop(_) => { + self.operand_stack.pop().expect( + "Tried to pop off the empty operand stack -- this should never happen", + ); + } + Effect::Read(Read { + location, + root_value_read: _, + moved, + }) => { + if *moved { + match location { + Location::Local(frame_idx, idx) => { + let frame = self.call_stack.get_mut(frame_idx).unwrap(); + frame.0.remove(idx); + } + Location::Indexed(..) => { + panic!("Cannot move from indexed location"); + } + Location::Global(..) => { + panic!("Cannot move from global location"); + } + } + } + } + Effect::Write(Write { + location, + root_value_after_write: value_written, + }) => match location { + Location::Local(frame_idx, idx) => { + let frame = self.call_stack.get_mut(frame_idx).unwrap(); + frame.0.insert(*idx, value_written.clone()); + } + Location::Indexed(location, _idx) => { + let val = self.get_mut_location(location); + *val = value_written.clone().snapshot().clone(); + } + Location::Global(id) => { + let val = self.loaded_state.get_mut(id).unwrap(); + *val = value_written.snapshot().clone(); + } + }, + Effect::DataLoad(DataLoad { + location, snapshot, .. + }) => { + let Location::Global(id) = location else { + unreachable!("Dataload by reference must have a global location"); + }; + self.loaded_state.insert(*id, snapshot.clone()); + } + }, + // External events are treated opaquely unless they define a trace boundary. + TraceEvent::External(external) => { + if external.as_str().is_some_and(|s| s == "MoveCallStart") { + // New trace boundary. Reset reconstructed runtime state to avoid + // leaking values across calls/txns. + self.loaded_state.clear(); + self.operand_stack.clear(); + self.call_stack.clear(); + } + } + // Instructions + _ => () + } + } + + /// Given a reference "location" return a mutable reference to the value it points to so that + /// it can be updated. + fn get_mut_location(&mut self, location: &Location) -> &mut SerializableMoveValue { + match location { + Location::Local(frame_idx, idx) => { + let frame = self.call_stack.get_mut(frame_idx).unwrap(); + frame.0.get_mut(idx).unwrap().value_mut().unwrap() + } + Location::Indexed(loc, _offset) => self.get_mut_location(loc), + Location::Global(id) => self.loaded_state.get_mut(id).unwrap(), + } + } +} + +impl Default for TraceState { + fn default() -> Self { + Self::new() + } +} + +impl Tracer for TraceState { + fn notify(&mut self, event: &TraceEvent, mut write: &mut Writer<'_>, _stack: Option<&Stack>) { + self.apply_event(event); + } +} diff --git a/crates/movy-replay/src/tracer/tree.rs b/crates/movy-replay/src/tracer/tree.rs index abde75e..b1b7850 100644 --- a/crates/movy-replay/src/tracer/tree.rs +++ b/crates/movy-replay/src/tracer/tree.rs @@ -5,7 +5,6 @@ use move_trace_format::{ format::{Frame, TraceEvent, TraceValue}, interface::Tracer, }; -use move_vm_stack::Stack; use tracing::warn; #[derive(Debug, Clone)] @@ -132,7 +131,7 @@ impl Tracer for TreeTracer { &mut self, event: &move_trace_format::format::TraceEvent, _writer: &mut move_trace_format::interface::Writer<'_>, - _stack: Option<&Stack>, + _stack: Option<&move_vm_stack::Stack>, ) { let inner = &mut self.inner; inner.evs.push(event.clone());