diff --git a/framework/meta-lib/src/contract/sc_config/wasm_build.rs b/framework/meta-lib/src/contract/sc_config/wasm_build.rs index 5b318da9a0..17312dbb04 100644 --- a/framework/meta-lib/src/contract/sc_config/wasm_build.rs +++ b/framework/meta-lib/src/contract/sc_config/wasm_build.rs @@ -130,13 +130,21 @@ impl ContractVariant { fn extract_wasm_info(&self, build_args: &BuildArgs, output_path: &str) -> WasmInfo { let output_wasm_path = format!("{output_path}/{}", self.wasm_output_name(build_args)); + let abi = ContractAbiJson::from(&self.abi); + let mut view_endpoints: Vec<&str> = Vec::new(); + for endpoint in &abi.endpoints { + if let crate::abi_json::EndpointMutabilityAbiJson::Readonly = endpoint.mutability { + view_endpoints.push(&endpoint.name); + } + } + if !build_args.extract_imports { return WasmInfo::extract_wasm_info( &output_wasm_path, build_args.extract_imports, &self.settings.check_ei, - ) - .expect("error occured while extracting imports from .wasm "); + view_endpoints, + ); } let output_imports_json_path = format!( @@ -146,9 +154,12 @@ impl ContractVariant { ); print_extract_imports(&output_imports_json_path); - let wasm_data = - WasmInfo::extract_wasm_info(&output_wasm_path, true, &self.settings.check_ei) - .expect("error occured while extracting imports from .wasm "); + let wasm_data = WasmInfo::extract_wasm_info( + &output_wasm_path, + true, + &self.settings.check_ei, + view_endpoints, + ); write_imports_output( output_imports_json_path.as_str(), diff --git a/framework/meta-lib/src/tools/report_creator.rs b/framework/meta-lib/src/tools/report_creator.rs index 9555814075..c45739c594 100644 --- a/framework/meta-lib/src/tools/report_creator.rs +++ b/framework/meta-lib/src/tools/report_creator.rs @@ -7,3 +7,13 @@ pub struct ReportCreator { } impl ReportCreator {} + +impl Default for ReportCreator { + fn default() -> Self { + ReportCreator { + path: String::new(), + has_allocator: false, + has_panic: PanicReport::None, + } + } +} diff --git a/framework/meta-lib/src/tools/wasm_extractor.rs b/framework/meta-lib/src/tools/wasm_extractor.rs index 733ca3fad0..eba84b4ada 100644 --- a/framework/meta-lib/src/tools/wasm_extractor.rs +++ b/framework/meta-lib/src/tools/wasm_extractor.rs @@ -1,22 +1,38 @@ use colored::Colorize; -use std::fs; +use std::{ + collections::{HashMap, HashSet}, + fs, +}; use wasmparser::{ - BinaryReaderError, DataSectionReader, FunctionBody, ImportSectionReader, Operator, Parser, - Payload, + BinaryReaderError, DataSectionReader, ExportSectionReader, FunctionBody, ImportSectionReader, + Operator, Parser, Payload, }; use crate::ei::EIVersion; -use super::{panic_report::PanicReport, report_creator::ReportCreator}; +use super::report_creator::ReportCreator; + +type CallGraph = HashMap>; const ERROR_FAIL_ALLOCATOR: &[u8; 27] = b"memory allocation forbidden"; +const WRITE_OP: &[&str] = &[ + "mBufferStorageStore", + "storageStore", + "int64storageStore", + "bigIntStorageStoreUnsigned", + "smallIntStorageStoreUnsigned", + "smallIntStorageStoreSigned", +]; +#[derive(Default)] pub struct WasmInfo { pub imports: Vec, pub ei_check: bool, pub memory_grow_flag: bool, - pub has_format: bool, pub report: ReportCreator, + pub call_graph: CallGraph, + pub write_index_functions: HashSet, + pub view_endpoints: HashMap, } impl WasmInfo { @@ -24,61 +40,146 @@ impl WasmInfo { output_wasm_path: &str, extract_imports_enabled: bool, check_ei: &Option, - ) -> Result { + view_endpoints: Vec<&str>, + ) -> WasmInfo { let wasm_data = fs::read(output_wasm_path) .expect("error occured while extracting information from .wasm: file not found"); - populate_wasm_info( + let wasm_info = populate_wasm_info( output_wasm_path.to_string(), wasm_data, extract_imports_enabled, check_ei, - ) + view_endpoints, + ); + + wasm_info.expect("error occured while extracting information from .wasm file") + } + + fn create_call_graph(&mut self, body: FunctionBody) { + let mut instructions_reader = body + .get_operators_reader() + .expect("Failed to get operators reader"); + + let mut call_functions = HashSet::new(); + while let Ok(op) = instructions_reader.read() { + if let Operator::Call { function_index } = op { + let function_usize: usize = function_index.try_into().unwrap(); + call_functions.insert(function_usize); + } + } + + self.call_graph + .insert(self.call_graph.len(), call_functions); + } + + pub fn process_imports( + &mut self, + import_section: ImportSectionReader, + import_extraction_enabled: bool, + ) { + for (index, import) in import_section.into_iter().flatten().enumerate() { + if import_extraction_enabled { + self.imports.push(import.name.to_string()); + } + self.call_graph.insert(index, HashSet::new()); + if WRITE_OP.contains(&import.name) { + self.write_index_functions.insert(index); + } + } + + self.imports.sort(); + } + + pub fn detect_write_operations_in_views(&mut self) { + let mut visited: HashSet = HashSet::new(); + for index in self.view_endpoints.values() { + mark_write( + *index, + &self.call_graph, + &mut self.write_index_functions, + &mut visited, + ); + } + + for (name, index) in &self.view_endpoints { + if self.write_index_functions.contains(index) { + println!( + "{} {}", + "Write storage operation in VIEW endpoint:" + .to_string() + .red() + .bold(), + name.red().bold() + ); + } + } + } + + fn parse_export_section( + &mut self, + export_section: ExportSectionReader, + view_endpoints: &[&str], + ) { + for export in export_section { + let export = export.expect("Failed to read export section"); + if let wasmparser::ExternalKind::Func = export.kind { + if view_endpoints.contains(&export.name) { + self.view_endpoints + .insert(export.name.to_owned(), export.index.try_into().unwrap()); + } + } + } } } pub(crate) fn populate_wasm_info( path: String, wasm_data: Vec, - extract_imports_enabled: bool, + import_extraction_enabled: bool, check_ei: &Option, + view_endpoints: Vec<&str>, ) -> Result { - let mut imports = Vec::new(); - let mut allocator_trigger = false; - let mut ei_check = false; - let mut memory_grow_flag = false; - let mut has_panic: PanicReport = PanicReport::default(); + let mut wasm_info = WasmInfo::default(); let parser = Parser::new(0); for payload in parser.parse_all(&wasm_data) { match payload? { Payload::ImportSection(import_section) => { - imports = extract_imports(import_section, extract_imports_enabled); - ei_check |= is_ei_valid(imports.clone(), check_ei); + wasm_info.process_imports(import_section, import_extraction_enabled); + wasm_info.ei_check |= is_ei_valid(&wasm_info.imports, check_ei); }, Payload::DataSection(data_section) => { - allocator_trigger |= is_fail_allocator_triggered(data_section.clone()); - has_panic.max_severity(data_section); + wasm_info.report.has_allocator |= is_fail_allocator_triggered(data_section.clone()); + wasm_info.report.has_panic.max_severity(data_section); }, Payload::CodeSectionEntry(code_section) => { - memory_grow_flag |= is_mem_grow(code_section); + wasm_info.memory_grow_flag |= is_mem_grow(&code_section); + wasm_info.create_call_graph(code_section); + }, + Payload::ExportSection(export_section) => { + wasm_info.parse_export_section(export_section, &view_endpoints); }, _ => (), } } + wasm_info.detect_write_operations_in_views(); + let report = ReportCreator { path, - has_allocator: allocator_trigger, - has_panic, + has_allocator: wasm_info.report.has_allocator, + has_panic: wasm_info.report.has_panic, }; Ok(WasmInfo { - imports, - ei_check, - memory_grow_flag, - has_format: true, + imports: wasm_info.imports, + ei_check: wasm_info.ei_check, + memory_grow_flag: wasm_info.memory_grow_flag, + call_graph: wasm_info.call_graph, report, + write_index_functions: wasm_info.write_index_functions, + view_endpoints: wasm_info.view_endpoints, }) } @@ -103,25 +204,34 @@ fn is_fail_allocator_triggered(data_section: DataSectionReader) -> bool { false } -pub fn extract_imports( - import_section: ImportSectionReader, - import_extraction_enabled: bool, -) -> Vec { - if !import_extraction_enabled { - return Vec::new(); - } - - let mut import_names = Vec::new(); - for import in import_section.into_iter().flatten() { - import_names.push(import.name.to_string()); +fn mark_write( + func: usize, + call_graph: &CallGraph, + write_functions: &mut HashSet, + visited: &mut HashSet, +) { + // Return early to prevent cycles. + if visited.contains(&func) { + return; } - import_names.sort(); + visited.insert(func); - import_names + if let Some(callees) = call_graph.get(&func) { + for &callee in callees { + if write_functions.contains(&callee) { + write_functions.insert(func); + } else { + mark_write(callee, call_graph, write_functions, visited); + if write_functions.contains(&callee) { + write_functions.insert(func); + } + } + } + } } -fn is_ei_valid(imports: Vec, check_ei: &Option) -> bool { +fn is_ei_valid(imports: &[String], check_ei: &Option) -> bool { if let Some(ei) = check_ei { let mut num_errors = 0; for import in imports { @@ -138,7 +248,7 @@ fn is_ei_valid(imports: Vec, check_ei: &Option) -> bool { false } -fn is_mem_grow(code_section: FunctionBody) -> bool { +fn is_mem_grow(code_section: &FunctionBody) -> bool { let mut instructions_reader = code_section .get_operators_reader() .expect("Failed to get operators reader"); diff --git a/framework/meta-lib/src/tools/wasm_extractor_test.rs b/framework/meta-lib/src/tools/wasm_extractor_test.rs index 030cddc503..346d0d855e 100644 --- a/framework/meta-lib/src/tools/wasm_extractor_test.rs +++ b/framework/meta-lib/src/tools/wasm_extractor_test.rs @@ -1,9 +1,161 @@ #[cfg(test)] pub mod tests { + use std::collections::{HashMap, HashSet}; + use wat::Parser; use crate::tools::{panic_report::PanicReport, wasm_extractor::populate_wasm_info}; + const ADDER_WITH_ERR_IN_VIEW: &str = r#" +(module $adder_wasm.wasm + (type (;0;) (func (param i32 i32))) + (type (;1;) (func (result i32))) + (type (;2;) (func (param i32 i32) (result i32))) + (type (;3;) (func (param i32 i32 i32) (result i32))) + (type (;4;) (func)) + (type (;5;) (func (param i32))) + (type (;6;) (func (param i32 i32 i32))) + (type (;7;) (func (param i32) (result i32))) + (import "env" "bigIntGetUnsignedArgument" (func $bigIntGetUnsignedArgument (;0;) (type 0))) + (import "env" "getNumArguments" (func $getNumArguments (;1;) (type 1))) + (import "env" "signalError" (func $signalError (;2;) (type 0))) + (import "env" "mBufferFromBigIntUnsigned" (func $mBufferFromBigIntUnsigned (;3;) (type 2))) + (import "env" "mBufferStorageStore" (func $mBufferStorageStore (;4;) (type 2))) + (import "env" "mBufferStorageLoad" (func $mBufferStorageLoad (;5;) (type 2))) + (import "env" "mBufferToBigIntUnsigned" (func $mBufferToBigIntUnsigned (;6;) (type 2))) + (import "env" "mBufferSetBytes" (func $mBufferSetBytes (;7;) (type 3))) + (import "env" "checkNoPayment" (func $checkNoPayment (;8;) (type 4))) + (import "env" "bigIntFinishUnsigned" (func $bigIntFinishUnsigned (;9;) (type 5))) + (import "env" "bigIntAdd" (func $bigIntAdd (;10;) (type 6))) + (func $_ZN13multiversx_sc2io16arg_nested_tuple15load_single_arg17hcaef680f5560198bE (;11;) (type 1) (result i32) + (local i32) + i32.const 0 + call $_ZN26multiversx_sc_wasm_adapter3api13managed_types19static_var_api_node11next_handle17h315bf8f89178ffe1E + local.tee 0 + call $bigIntGetUnsignedArgument + local.get 0 + ) + (func $_ZN26multiversx_sc_wasm_adapter3api13managed_types19static_var_api_node11next_handle17h315bf8f89178ffe1E (;12;) (type 1) (result i32) + (local i32) + i32.const 0 + i32.const 0 + i32.load offset=131100 + i32.const -1 + i32.add + local.tee 0 + i32.store offset=131100 + local.get 0 + ) + (func $_ZN13multiversx_sc2io16arg_nested_tuple22check_num_arguments_eq17h8cbbe81aa680cf46E (;13;) (type 5) (param i32) + block ;; label = @1 + call $getNumArguments + local.get 0 + i32.ne + br_if 0 (;@1;) + return + end + i32.const 131072 + i32.const 25 + call $signalError + unreachable + ) + (func $_ZN13multiversx_sc7storage7mappers19single_value_mapper31SingleValueMapper$LT$SA$C$T$GT$3set17h00fe05a7d5154b39E (;14;) (type 0) (param i32 i32) + (local i32) + call $_ZN26multiversx_sc_wasm_adapter3api13managed_types19static_var_api_node11next_handle17h315bf8f89178ffe1E + local.tee 2 + local.get 1 + call $mBufferFromBigIntUnsigned + drop + local.get 0 + local.get 2 + call $mBufferStorageStore + drop + ) + (func $_ZN13multiversx_sc7storage7mappers19single_value_mapper35SingleValueMapper$LT$SA$C$T$C$A$GT$3get17hb9977d4c8d45f0d8E (;15;) (type 7) (param i32) (result i32) + (local i32) + local.get 0 + call $_ZN26multiversx_sc_wasm_adapter3api13managed_types19static_var_api_node11next_handle17h315bf8f89178ffe1E + local.tee 1 + call $mBufferStorageLoad + drop + local.get 1 + call $_ZN26multiversx_sc_wasm_adapter3api13managed_types19static_var_api_node11next_handle17h315bf8f89178ffe1E + local.tee 0 + call $mBufferToBigIntUnsigned + drop + local.get 0 + ) + (func $_ZN34_$LT$C$u20$as$u20$adder..Adder$GT$3sum17h7cc7cc0602a1f97fE (;16;) (type 1) (result i32) + (local i32) + call $_ZN26multiversx_sc_wasm_adapter3api13managed_types19static_var_api_node11next_handle17h315bf8f89178ffe1E + local.tee 0 + i32.const 131097 + i32.const 3 + call $mBufferSetBytes + drop + local.get 0 + ) + (func $init (;17;) (type 4) + (local i32) + call $checkNoPayment + i32.const 1 + call $_ZN13multiversx_sc2io16arg_nested_tuple22check_num_arguments_eq17h8cbbe81aa680cf46E + call $_ZN13multiversx_sc2io16arg_nested_tuple15load_single_arg17hcaef680f5560198bE + local.set 0 + call $_ZN34_$LT$C$u20$as$u20$adder..Adder$GT$3sum17h7cc7cc0602a1f97fE + local.get 0 + call $_ZN13multiversx_sc7storage7mappers19single_value_mapper31SingleValueMapper$LT$SA$C$T$GT$3set17h00fe05a7d5154b39E + ) + (func $getSum (;18;) (type 4) + call $checkNoPayment + i32.const 0 + call $_ZN13multiversx_sc2io16arg_nested_tuple22check_num_arguments_eq17h8cbbe81aa680cf46E + call $_ZN34_$LT$C$u20$as$u20$adder..Adder$GT$3sum17h7cc7cc0602a1f97fE + call $_ZN13multiversx_sc7storage7mappers19single_value_mapper35SingleValueMapper$LT$SA$C$T$C$A$GT$3get17hb9977d4c8d45f0d8E + call $bigIntFinishUnsigned + ) + (func $add (;19;) (type 4) + (local i32 i32 i32) + call $checkNoPayment + i32.const 1 + call $_ZN13multiversx_sc2io16arg_nested_tuple22check_num_arguments_eq17h8cbbe81aa680cf46E + call $_ZN13multiversx_sc2io16arg_nested_tuple15load_single_arg17hcaef680f5560198bE + local.set 0 + call $_ZN34_$LT$C$u20$as$u20$adder..Adder$GT$3sum17h7cc7cc0602a1f97fE + local.tee 1 + call $_ZN13multiversx_sc7storage7mappers19single_value_mapper35SingleValueMapper$LT$SA$C$T$C$A$GT$3get17hb9977d4c8d45f0d8E + local.tee 2 + local.get 2 + local.get 0 + call $bigIntAdd + local.get 1 + local.get 2 + call $_ZN13multiversx_sc7storage7mappers19single_value_mapper31SingleValueMapper$LT$SA$C$T$GT$3set17h00fe05a7d5154b39E + ) + (func $callBack (;20;) (type 4)) + (table (;0;) 1 1 funcref) + (memory (;0;) 3) + (global $__stack_pointer (;0;) (mut i32) i32.const 131072) + (global (;1;) i32 i32.const 131104) + (global (;2;) i32 i32.const 131104) + (export "memory" (memory 0)) + (export "init" (func $init)) + (export "getSum" (func $getSum)) + (export "add" (func $add)) + (export "callBack" (func $callBack)) + (export "upgrade" (func $init)) + (export "__data_end" (global 1)) + (export "__heap_base" (global 2)) + (data $.rodata (;0;) (i32.const 131072) "wrong number of argumentssum") + (data $.data (;1;) (i32.const 131100) "8\ff\ff\ff") + (@producers + (language "Rust" "") + (processed-by "rustc" "1.80.1 (3f5fd8dd4 2024-08-06)") + ) + (@custom "target_features" (after data) "\02+\0fmutable-globals+\08sign-ext") +) +"#; + const EMPTY_WITH_FAIL_ALLOCATOR: &str = r#" (module $empty_wasm.wasm (type (;0;) (func (result i32))) @@ -256,8 +408,9 @@ pub mod tests { #[test] fn test_empty() { if let Ok(content) = Parser::new().parse_bytes(None, EMPTY_DBG_WAT.as_bytes()) { - let wasm_info = populate_wasm_info(String::new(), content.to_vec(), false, &None) - .expect("Unable to parse WASM content."); + let wasm_info = + populate_wasm_info(String::new(), content.to_vec(), false, &None, Vec::new()) + .expect("Unable to parse WASM content."); assert!(!wasm_info.memory_grow_flag); assert!(!wasm_info.report.has_allocator); assert_eq!( @@ -270,8 +423,9 @@ pub mod tests { #[test] fn test_empty_with_mem_grow() { if let Ok(content) = Parser::new().parse_bytes(None, EMPTY_WITH_MEM_GROW.as_bytes()) { - let wasm_info = populate_wasm_info(String::new(), content.to_vec(), false, &None) - .expect("Unable to parse WASM content."); + let wasm_info = + populate_wasm_info(String::new(), content.to_vec(), false, &None, Vec::new()) + .expect("Unable to parse WASM content."); assert!(wasm_info.memory_grow_flag); assert!(!wasm_info.report.has_allocator); assert_eq!( @@ -284,8 +438,9 @@ pub mod tests { #[test] fn test_empty_with_fail_allocator() { if let Ok(content) = Parser::new().parse_bytes(None, EMPTY_WITH_FAIL_ALLOCATOR.as_bytes()) { - let wasm_info = populate_wasm_info(String::new(), content.to_vec(), false, &None) - .expect("Unable to parse WASM content."); + let wasm_info = + populate_wasm_info(String::new(), content.to_vec(), false, &None, Vec::new()) + .expect("Unable to parse WASM content."); assert!(!wasm_info.memory_grow_flag); assert!(wasm_info.report.has_allocator); assert_eq!( @@ -294,4 +449,54 @@ pub mod tests { ); } } + + #[test] + fn test_adder_with_write_op_in_view() { + let view_endpoints: Vec<&str> = Vec::from(["getSum", "add"]); + + let expected_view_index: HashMap = + HashMap::from([("getSum".to_string(), 18), ("add".to_string(), 19)]); + let expected_write_index_functions: HashSet = HashSet::from([4, 19, 14]); + let expected_call_graph: HashMap> = HashMap::from([ + (0, HashSet::new()), + (1, HashSet::new()), + (2, HashSet::new()), + (3, HashSet::new()), + (4, HashSet::new()), + (5, HashSet::new()), + (6, HashSet::new()), + (7, HashSet::new()), + (8, HashSet::new()), + (9, HashSet::new()), + (10, HashSet::new()), + (11, HashSet::from([12, 0])), + (12, HashSet::new()), + (13, HashSet::from([1, 2])), + (14, HashSet::from([12, 3, 4])), + (15, HashSet::from([12, 5, 6])), + (16, HashSet::from([12, 7])), + (17, HashSet::from([8, 13, 11, 16, 14])), + (18, HashSet::from([8, 13, 16, 15, 9])), + (19, HashSet::from([8, 13, 11, 16, 15, 10, 14])), + (20, HashSet::new()), + ]); + + if let Ok(content) = Parser::new().parse_bytes(None, ADDER_WITH_ERR_IN_VIEW.as_bytes()) { + let wasm_info = populate_wasm_info( + String::new(), + content.to_vec(), + false, + &None, + view_endpoints, + ) + .expect("Unable to parse WASM content."); + + assert_eq!( + expected_write_index_functions, + wasm_info.write_index_functions + ); + assert_eq!(expected_call_graph, wasm_info.call_graph); + assert_eq!(expected_view_index, wasm_info.view_endpoints); + } + } }