Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Detect write in storage ops in view methods #1771

Merged
merged 17 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions framework/meta-lib/src/contract/sc_config/wasm_build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(
Expand All @@ -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(),
Expand Down
10 changes: 10 additions & 0 deletions framework/meta-lib/src/tools/report_creator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
}
190 changes: 150 additions & 40 deletions framework/meta-lib/src/tools/wasm_extractor.rs
Original file line number Diff line number Diff line change
@@ -1,84 +1,185 @@
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<usize, HashSet<usize>>;

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<String>,
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<usize>,
pub view_endpoints: HashMap<String, usize>,
}

impl WasmInfo {
pub fn extract_wasm_info(
output_wasm_path: &str,
extract_imports_enabled: bool,
check_ei: &Option<EIVersion>,
) -> Result<WasmInfo, BinaryReaderError> {
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<usize> = 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<u8>,
extract_imports_enabled: bool,
import_extraction_enabled: bool,
check_ei: &Option<EIVersion>,
view_endpoints: Vec<&str>,
) -> Result<WasmInfo, BinaryReaderError> {
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,
})
}

Expand All @@ -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<String> {
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<usize>,
visited: &mut HashSet<usize>,
) {
// 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<String>, check_ei: &Option<EIVersion>) -> bool {
fn is_ei_valid(imports: &[String], check_ei: &Option<EIVersion>) -> bool {
if let Some(ei) = check_ei {
let mut num_errors = 0;
for import in imports {
Expand All @@ -138,7 +248,7 @@ fn is_ei_valid(imports: Vec<String>, check_ei: &Option<EIVersion>) -> 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");
Expand Down
Loading
Loading