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

feat(wasm-gen): add limits for recursions and heavy loops #3391

Merged
merged 22 commits into from
Nov 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4d78c6b
add incomplete implementation
StackOverflowExcept1on Oct 4, 2023
eaa0976
complete impl & use it in wasm-gen
StackOverflowExcept1on Oct 4, 2023
98a3f9f
Merge remote-tracking branch 'origin/master' into av/wasm-gen-recursions
StackOverflowExcept1on Oct 6, 2023
eb665dc
move instructions into 2 functions: prolog and epilog
StackOverflowExcept1on Oct 6, 2023
4f0cae8
exit from func using `return` instruction
StackOverflowExcept1on Oct 7, 2023
f6de6e2
[ci skip] cleanup
StackOverflowExcept1on Oct 8, 2023
85dd6e7
also instrument loops
StackOverflowExcept1on Oct 9, 2023
1517c20
[ci skip] enable remove_recursion
StackOverflowExcept1on Oct 9, 2023
fbb7208
Merge remote-tracking branch 'origin/master' into av/wasm-gen-recursions
StackOverflowExcept1on Oct 20, 2023
7256ce6
fix fmt
StackOverflowExcept1on Oct 20, 2023
da9cb01
add changes
StackOverflowExcept1on Nov 9, 2023
7b01c11
Merge remote-tracking branch 'origin/master' into av/wasm-gen-recursions
StackOverflowExcept1on Nov 9, 2023
87b97dc
fix cargo fmt
StackOverflowExcept1on Nov 9, 2023
4802c06
refactoring
StackOverflowExcept1on Nov 13, 2023
28c0519
Merge remote-tracking branch 'origin/master' into av/wasm-gen-recursions
StackOverflowExcept1on Nov 13, 2023
3bd71a8
fix doc test
StackOverflowExcept1on Nov 13, 2023
d793944
move enabling gr_gas_available syscall to config
StackOverflowExcept1on Nov 15, 2023
00bf360
more docs
StackOverflowExcept1on Nov 15, 2023
7d42eb0
fix fmt
StackOverflowExcept1on Nov 15, 2023
4a4cf6e
fixes
StackOverflowExcept1on Nov 15, 2023
f7d8d1c
Merge remote-tracking branch 'origin/master' into av/wasm-gen-recursions
StackOverflowExcept1on Nov 15, 2023
60a89cb
revert changes
StackOverflowExcept1on Nov 17, 2023
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
13 changes: 12 additions & 1 deletion utils/wasm-gen/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@
//! let wasm_gen_config = GearWasmGeneratorConfig {
//! memory_config: memory_pages_config,
//! entry_points_config: entry_points_set,
//! remove_recursions: true,
//! remove_recursions: false,
//! critical_gas_limit: Some(1_000_000),
//! syscalls_config,
//! };
//! ```
Expand Down Expand Up @@ -140,6 +141,13 @@ pub struct StandardGearWasmConfigsBundle<T = [u8; 32]> {
pub existing_addresses: Option<NonEmpty<T>>,
/// Flag which signals whether recursions must be removed.
pub remove_recursion: bool,
/// If the limit is set to `Some(_)`, programs will try to stop execution
/// after reaching a critical gas limit, which can be useful to exit from
/// heavy loops and recursions that waste all gas.
///
/// The `gr_gas_available` syscall is called at the beginning of each
/// function and for each control instruction (blocks, loops, conditions).
pub critical_gas_limit: Option<u64>,
/// Injection type for each syscall.
pub injection_types: SysCallsInjectionTypes,
/// Config of gear wasm call entry-points (exports).
Expand All @@ -158,6 +166,7 @@ impl<T> Default for StandardGearWasmConfigsBundle<T> {
log_info: Some("StandardGearWasmConfigsBundle".into()),
existing_addresses: None,
remove_recursion: false,
critical_gas_limit: Some(1_000_000),
injection_types: SysCallsInjectionTypes::all_once(),
entry_points_set: Default::default(),
initial_pages: DEFAULT_INITIAL_SIZE,
Expand All @@ -173,6 +182,7 @@ impl<T: Into<Hash>> ConfigsBundle for StandardGearWasmConfigsBundle<T> {
log_info,
existing_addresses,
remove_recursion,
critical_gas_limit,
injection_types,
entry_points_set,
initial_pages,
Expand All @@ -199,6 +209,7 @@ impl<T: Into<Hash>> ConfigsBundle for StandardGearWasmConfigsBundle<T> {
upper_limit: None,
};
let gear_wasm_generator_config = GearWasmGeneratorConfigBuilder::new()
.with_critical_gas_limit(critical_gas_limit)
.with_recursions_removed(remove_recursion)
.with_syscalls_config(syscalls_config_builder.build())
.with_entry_points_config(entry_points_set)
Expand Down
10 changes: 10 additions & 0 deletions utils/wasm-gen/src/config/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,13 @@ impl GearWasmGeneratorConfigBuilder {
self
}

/// Defines whether programs should have a critical gas limit.
pub fn with_critical_gas_limit(mut self, critical_gas_limit: Option<u64>) -> Self {
self.0.critical_gas_limit = critical_gas_limit;

self
}

/// Build the gear wasm generator.
pub fn build(self) -> GearWasmGeneratorConfig {
self.0
Expand All @@ -81,6 +88,9 @@ pub struct GearWasmGeneratorConfig {
/// Flag, signalizing whether recursions
/// should be removed from resulting module.
pub remove_recursions: bool,
/// The critical gas limit after which the program
/// will attempt to terminate successfully.
pub critical_gas_limit: Option<u64>,
}

/// Memory pages config used by [`crate::MemoryGenerator`].
Expand Down
8 changes: 8 additions & 0 deletions utils/wasm-gen/src/generator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,14 @@ impl<'a, 'b> GearWasmGenerator<'a, 'b> {
.into_wasm_module()
.into_inner();

let module = if let Some(critical_gas_limit) = config.critical_gas_limit {
log::trace!("Injecting critical gas limit");
utils::inject_critical_gas_limit(module, critical_gas_limit)
} else {
log::trace!("Critical gas limit is not set");
module
};

Ok(if config.remove_recursions {
log::trace!("Removing recursions");
utils::remove_recursion(module)
Expand Down
34 changes: 34 additions & 0 deletions utils/wasm-gen/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,40 @@ use std::{mem, num::NonZeroUsize};

const UNSTRUCTURED_SIZE: usize = 1_000_000;

#[test]
fn inject_critical_gas_limit_works() {
techraed marked this conversation as resolved.
Show resolved Hide resolved
let wat1 = r#"
(module
(memory $memory0 (import "env" "memory") 16)
(export "handle" (func $handle))
(func $handle
call $f
drop
)
(func $f (result i64)
call $f
)
(func $g
(loop $my_loop
br $my_loop
)
)
)"#;

let wasm_bytes = wat::parse_str(wat1).expect("invalid wat");
let module =
parity_wasm::deserialize_buffer::<Module>(&wasm_bytes).expect("invalid wasm bytes");
let module_with_critical_gas_limit = utils::inject_critical_gas_limit(module, 1_000_000);

let wasm_bytes = module_with_critical_gas_limit
.into_bytes()
.expect("invalid pw module");
assert!(wasmparser::validate(&wasm_bytes).is_ok());

let wat = wasmprinter::print_bytes(&wasm_bytes).expect("failed printing bytes");
println!("wat = {wat}");
}

#[test]
fn remove_trivial_recursions() {
let wat1 = r#"
Expand Down
225 changes: 221 additions & 4 deletions utils/wasm-gen/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,16 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

use gear_wasm_instrument::parity_wasm::{
builder,
elements::{self, FuncBody, ImportCountType, Instruction, Module, Type, ValueType},
use crate::wasm::PageCount as WasmPageCount;
use gear_wasm_instrument::{
parity_wasm::{
builder,
elements::{
BlockType, External, FuncBody, ImportCountType, Instruction, Instructions, Internal,
Module, Section, Type, ValueType,
},
},
syscalls::SysCallName,
};
use gsys::HashWithValue;
use std::{
Expand Down Expand Up @@ -95,7 +102,7 @@ pub fn remove_recursion(module: Module) -> Module {
.with_results(signature.results().to_vec())
.build()
.body()
.with_instructions(elements::Instructions::new(body))
.with_instructions(Instructions::new(body))
.build()
.build(),
)
Expand Down Expand Up @@ -215,6 +222,216 @@ fn find_recursion_impl<Callback>(
path.pop();
}

/// Injects a critical gas limit to a given wasm module.
///
/// Code before injection gas limiter:
/// ```ignore
/// fn func() {
/// func();
/// loop { }
/// }
/// ```
///
/// Code after injection gas limiter:
/// ```ignore
/// use gcore::exec;
///
/// const CRITICAL_GAS_LIMIT: u64 = 1_000_000;
///
/// fn func() {
/// // exit from recursions
/// if exec::gas_available() <= CRITICAL_GAS_LIMIT {
/// return;
/// }
/// func();
/// loop {
/// // exit from heavy loops
/// if exec::gas_available() <= CRITICAL_GAS_LIMIT {
/// return;
/// }
/// }
/// }
/// ```
pub fn inject_critical_gas_limit(module: Module, critical_gas_limit: u64) -> Module {
techraed marked this conversation as resolved.
Show resolved Hide resolved
// get initial memory size of program
let Some(mem_size) = module
.import_section()
.and_then(|section| {
section
.entries()
.iter()
.find_map(|entry| match entry.external() {
External::Memory(mem_ty) => Some(mem_ty.limits().initial()),
_ => None,
})
})
.map(Into::<WasmPageCount>::into)
.map(|page_count| page_count.memory_size())
else {
return module;
};

// store available gas pointer on the last memory page
let gas_ptr = mem_size - mem::size_of::<u64>() as u32;

// add gr_gas_available import if needed
let maybe_gr_gas_available_index = module.import_section().and_then(|section| {
section
.entries()
.iter()
.filter(|entry| matches!(entry.external(), External::Function(_)))
.enumerate()
.find_map(|(i, entry)| {
(entry.module() == "env" && entry.field() == SysCallName::GasAvailable.to_str())
.then_some(i as u32)
})
});
// sections should only be rewritten if the module did not previously have gr_gas_available import
let rewrite_sections = maybe_gr_gas_available_index.is_none();

let (gr_gas_available_index, mut module) = match maybe_gr_gas_available_index {
Some(gr_gas_available_index) => (gr_gas_available_index, module),
None => {
let mut mbuilder = builder::from_module(module);

// fn gr_gas_available(gas: *mut u64);
let import_sig = mbuilder
.push_signature(builder::signature().with_param(ValueType::I32).build_sig());

mbuilder.push_import(
builder::import()
.module("env")
.field(SysCallName::GasAvailable.to_str())
.external()
.func(import_sig)
.build(),
);

// back to plain module
let module = mbuilder.build();

let import_count = module.import_count(ImportCountType::Function);
let gr_gas_available_index = import_count as u32 - 1;

(gr_gas_available_index, module)
}
};

let (Some(type_section), Some(function_section)) =
(module.type_section(), module.function_section())
else {
return module;
};

let types = type_section.types().to_vec();
let signature_entries = function_section.entries().to_vec();

let Some(code_section) = module.code_section_mut() else {
return module;
};

for (index, func_body) in code_section.bodies_mut().iter_mut().enumerate() {
let signature_index = &signature_entries[index];
let signature = &types[signature_index.type_ref() as usize];
let Type::Function(signature) = signature;
let results = signature.results();

// create the body of the gas limiter:
let mut body = Vec::with_capacity(results.len() + 9);
body.extend_from_slice(&[
// gr_gas_available(gas_ptr)
Instruction::I32Const(gas_ptr as i32),
Instruction::Call(gr_gas_available_index),
// gas_available = *gas_ptr
Instruction::I32Const(gas_ptr as i32),
Instruction::I64Load(3, 0),
Instruction::I64Const(critical_gas_limit as i64),
// if gas_available <= critical_gas_limit { return result; }
Instruction::I64LeU,
Instruction::If(BlockType::NoResult),
]);

// exit the current function with dummy results
for result in results {
let instruction = match result {
ValueType::I32 => Instruction::I32Const(u32::MAX as i32),
ValueType::I64 => Instruction::I64Const(u64::MAX as i64),
ValueType::F32 | ValueType::F64 => unreachable!("f32/64 types are not supported"),
};

body.push(instruction);
}

body.extend_from_slice(&[Instruction::Return, Instruction::End]);

let instructions = func_body.code_mut().elements_mut();

let original_instructions =
mem::replace(instructions, Vec::with_capacity(instructions.len()));
let new_instructions = instructions;

// insert gas limiter at the beginning of each function to limit recursions
new_instructions.extend_from_slice(&body);

// also insert gas limiter at the beginning of each block, loop and condition
// to limit control instructions
for instruction in original_instructions {
match instruction {
Instruction::Block(_) | Instruction::Loop(_) | Instruction::If(_) => {
new_instructions.push(instruction);
new_instructions.extend_from_slice(&body);
}
Instruction::Call(call_index)
if rewrite_sections && call_index >= gr_gas_available_index =>
{
// fix function indexes if import gr_gas_available was inserted
new_instructions.push(Instruction::Call(call_index + 1));
}
_ => {
new_instructions.push(instruction);
}
}
}
}

// fix other sections if import gr_gas_available was inserted
if rewrite_sections {
let sections = module.sections_mut();
sections.retain(|section| !matches!(section, Section::Custom(_)));

for section in sections {
match section {
Section::Export(export_section) => {
for export in export_section.entries_mut() {
if let Internal::Function(func_index) = export.internal_mut() {
if *func_index >= gr_gas_available_index {
*func_index += 1
}
}
}
}
Section::Element(elements_section) => {
for segment in elements_section.entries_mut() {
for func_index in segment.members_mut() {
if *func_index >= gr_gas_available_index {
*func_index += 1
}
}
}
}
Section::Start(start_idx) => {
if *start_idx >= gr_gas_available_index {
*start_idx += 1;
}
}
_ => {}
}
}
}

module
}

pub(crate) fn hash_with_value_to_vec(hash_with_value: &HashWithValue) -> Vec<u8> {
let address_data_size = mem::size_of::<HashWithValue>();
let address_data_slice = unsafe {
Expand Down