Skip to main content

gear_wasm_instrument/
lib.rs

1// Copyright (C) Gear Technologies Inc.
2// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
3
4#![recursion_limit = "4096"]
5#![cfg_attr(not(feature = "std"), no_std)]
6#![allow(clippy::items_after_test_module)]
7
8extern crate alloc;
9
10pub use crate::{
11    gas_metering::Rules,
12    syscalls::{SyscallKind, SyscallName},
13};
14pub use module::{
15    BrTable, ConstExpr, Data, Element, ElementItems, Export, Function, GEAR_SUPPORTED_FEATURES,
16    Global, Import, Instruction, MemArg, Module, ModuleBuilder, ModuleError, Name, Table,
17};
18pub use wasmparser::{
19    BlockType, ExternalKind, FuncType, GlobalType, MemoryType, RefType, TableType, TypeRef, ValType,
20};
21
22use crate::stack_limiter::InjectionConfig;
23use alloc::{string::ToString, vec};
24
25mod module;
26#[cfg(test)]
27mod tests;
28
29pub mod gas_metering;
30pub mod stack_limiter;
31pub mod syscalls;
32
33// TODO #3057
34/// Gas global export name in WASM module.
35pub const GLOBAL_NAME_GAS: &str = "gear_gas";
36
37/// `__gear_stack_end` export is inserted by wasm-proc or wasm-builder,
38/// it indicates the end of program stack memory.
39pub const STACK_END_EXPORT_NAME: &str = "__gear_stack_end";
40/// `__gear_stack_height` export is inserted by gear-wasm-instrument,
41/// it points to stack height global that is used by [`stack_limiter`].
42pub const STACK_HEIGHT_EXPORT_NAME: &str = "__gear_stack_height";
43
44/// System break code for [`SyscallName::SystemBreak`] syscall.
45#[derive(Debug, Clone, Copy)]
46pub enum SystemBreakCode {
47    OutOfGas = 0,
48    StackLimitExceeded = 1,
49}
50
51/// The error type returned when a conversion from `i32` or `u32` to
52/// [`SystemBreakCode`] fails.
53#[derive(Clone, Debug, derive_more::Display)]
54#[display("Unsupported system break code")]
55pub struct SystemBreakCodeTryFromError;
56
57impl TryFrom<i32> for SystemBreakCode {
58    type Error = SystemBreakCodeTryFromError;
59
60    fn try_from(value: i32) -> Result<Self, Self::Error> {
61        match value {
62            0 => Ok(Self::OutOfGas),
63            1 => Ok(Self::StackLimitExceeded),
64            _ => Err(SystemBreakCodeTryFromError),
65        }
66    }
67}
68
69impl TryFrom<u32> for SystemBreakCode {
70    type Error = SystemBreakCodeTryFromError;
71
72    fn try_from(value: u32) -> Result<Self, Self::Error> {
73        SystemBreakCode::try_from(value as i32)
74    }
75}
76
77/// WASM module instrumentation error.
78#[derive(Debug, PartialEq, Eq, derive_more::Display)]
79pub enum InstrumentationError {
80    /// Error occurred during injecting `gr_system_break` import.
81    #[display("The WASM module already has `gr_system_break` import")]
82    SystemBreakImportAlreadyExists,
83    /// Error occurred during stack height instrumentation.
84    #[display("Failed to inject stack height limits")]
85    StackLimitInjection,
86    /// Error occurred during injecting `gear_gas` global.
87    #[display("The WASM module already has `gear_gas` global")]
88    GasGlobalAlreadyExists,
89    /// Error occurred during calculating the cost of the `gas_charge` function.
90    #[display("An overflow occurred while calculating the cost of the `gas_charge` function")]
91    CostCalculationOverflow,
92    /// Error occurred while trying to get the instruction cost.
93    #[display("Failed to get instruction cost")]
94    InstructionCostNotFound,
95    /// Error occurred during injecting gas metering instructions.
96    ///
97    /// This might be due to program contained unsupported instructions (memory grow, etc.).
98    #[display(
99        "Failed to inject instructions for gas metrics: may be in case \
100        program contains unsupported instructions (memory grow, etc.)"
101    )]
102    GasInjection,
103}
104
105/// This is an auxiliary builder that allows to instrument WASM module.
106pub struct InstrumentationBuilder<'a, R, GetRulesFn>
107where
108    R: Rules,
109    GetRulesFn: FnMut(&Module) -> R,
110{
111    /// name of module to import syscalls
112    module_name: &'a str,
113    /// configuration of stack_limiter
114    stack_limiter: Option<(u32, bool)>,
115    /// configuration of gas limiter
116    gas_limiter: Option<GetRulesFn>,
117}
118
119impl<'a, R, GetRulesFn> InstrumentationBuilder<'a, R, GetRulesFn>
120where
121    R: Rules,
122    GetRulesFn: FnMut(&Module) -> R,
123{
124    /// Creates a new [`InstrumentationBuilder`] with the given module name to
125    /// import syscalls.
126    pub fn new(module_name: &'a str) -> Self {
127        Self {
128            module_name,
129            stack_limiter: None,
130            gas_limiter: None,
131        }
132    }
133
134    /// Whether to insert a stack limiter into WASM module.
135    pub fn with_stack_limiter(&mut self, stack_limit: u32, export_stack_height: bool) -> &mut Self {
136        self.stack_limiter = Some((stack_limit, export_stack_height));
137        self
138    }
139
140    /// Whether to insert a gas limiter into WASM module.
141    pub fn with_gas_limiter(&mut self, get_gas_rules: GetRulesFn) -> &mut Self {
142        self.gas_limiter = Some(get_gas_rules);
143        self
144    }
145
146    /// Performs instrumentation of a given WASM module depending
147    /// on the parameters with which the [`InstrumentationBuilder`] was created.
148    pub fn instrument(&mut self, module: Module) -> Result<Module, InstrumentationError> {
149        if let (None, None) = (self.stack_limiter, &self.gas_limiter) {
150            return Ok(module);
151        }
152
153        let (gr_system_break_index, mut module) =
154            inject_system_break_import(module, self.module_name)?;
155
156        if let Some((stack_limit, export_stack_height)) = self.stack_limiter {
157            let injection_config = InjectionConfig {
158                stack_limit,
159                injection_fn: |_| {
160                    [
161                        Instruction::I32Const(SystemBreakCode::StackLimitExceeded as i32),
162                        Instruction::Call(gr_system_break_index),
163                    ]
164                },
165                stack_height_export_name: export_stack_height.then_some(STACK_HEIGHT_EXPORT_NAME),
166            };
167
168            module = stack_limiter::inject_with_config(module, injection_config)
169                .map_err(|_| InstrumentationError::StackLimitInjection)?;
170        }
171
172        if let Some(ref mut get_gas_rules) = self.gas_limiter {
173            let gas_rules = get_gas_rules(&module);
174            module = inject_gas_limiter(module, &gas_rules, gr_system_break_index)?;
175        }
176
177        Ok(module)
178    }
179}
180
181fn inject_system_break_import(
182    module: Module,
183    break_module_name: &str,
184) -> Result<(u32, Module), InstrumentationError> {
185    if module
186        .import_section
187        .as_ref()
188        .map(|section| {
189            section.iter().any(|entry| {
190                entry.module == break_module_name && entry.name == SyscallName::SystemBreak.to_str()
191            })
192        })
193        .unwrap_or(false)
194    {
195        return Err(InstrumentationError::SystemBreakImportAlreadyExists);
196    }
197
198    let inserted_index = module.import_count(|ty| matches!(ty, TypeRef::Func(_))) as u32;
199
200    let mut mbuilder = ModuleBuilder::from_module(module);
201    // fn gr_system_break(code: u32) -> !;
202    let import_idx = mbuilder.push_type(FuncType::new([ValType::I32], []));
203
204    // back to plain module
205    mbuilder.push_import(Import::func(
206        break_module_name.to_string(),
207        SyscallName::SystemBreak.to_str(),
208        import_idx,
209    ));
210
211    let module = mbuilder
212        .shift_func_index(inserted_index)
213        .shift_all()
214        .build();
215
216    Ok((inserted_index, module))
217}
218
219fn inject_gas_limiter<R: Rules>(
220    module: Module,
221    rules: &R,
222    gr_system_break_index: u32,
223) -> Result<Module, InstrumentationError> {
224    if module
225        .export_section
226        .as_ref()
227        .map(|section| section.iter().any(|entry| entry.name == GLOBAL_NAME_GAS))
228        .unwrap_or(false)
229    {
230        return Err(InstrumentationError::GasGlobalAlreadyExists);
231    }
232
233    let gas_charge_index = module.functions_space();
234    let gas_index = module.globals_space() as u32;
235
236    let mut mbuilder = ModuleBuilder::from_module(module);
237
238    mbuilder.push_global(Global::i64_value_mut(0));
239    mbuilder.push_export(Export::global(GLOBAL_NAME_GAS, gas_index));
240
241    // This const is introduced to avoid future errors in code if some other
242    // `I64Const` instructions appear in gas charge function body.
243    const GAS_CHARGE_COST_PLACEHOLDER: i64 = 1248163264128;
244
245    let mut elements = vec![
246        // I. Put global with value of current gas counter of any type.
247        Instruction::GlobalGet(gas_index),
248        // II. Calculating total gas to charge as sum of:
249        //  - `gas_charge(..)` argument;
250        //  - `gas_charge(..)` call cost.
251        //
252        // Setting the sum into local with index 1 with keeping it on stack.
253        Instruction::LocalGet(0),
254        Instruction::I64ExtendI32U,
255        Instruction::I64Const(GAS_CHARGE_COST_PLACEHOLDER),
256        Instruction::I64Add,
257        Instruction::LocalTee(1),
258        // III. Validating left amount of gas.
259        //
260        // In case of requested value is bigger than actual gas counter value,
261        // than we call `out_of_gas()` that will terminate execution.
262        Instruction::I64LtU,
263        Instruction::If(BlockType::Empty),
264        Instruction::I32Const(SystemBreakCode::OutOfGas as i32),
265        Instruction::Call(gr_system_break_index),
266        Instruction::End,
267        // IV. Calculating new global value by subtraction.
268        //
269        // Result is stored back into global.
270        Instruction::GlobalGet(gas_index),
271        Instruction::LocalGet(1),
272        Instruction::I64Sub,
273        Instruction::GlobalSet(gas_index),
274        // V. Ending `gas_charge()` function.
275        Instruction::End,
276    ];
277
278    // determine cost for successful execution
279    let mut block_of_code = false;
280
281    let cost_blocks = elements
282        .iter()
283        .filter(|instruction| match instruction {
284            Instruction::If { .. } => {
285                block_of_code = true;
286                true
287            }
288            Instruction::End => {
289                block_of_code = false;
290                false
291            }
292            _ => !block_of_code,
293        })
294        .try_fold(0u64, |cost, instruction| {
295            rules
296                .instruction_cost(instruction)
297                .and_then(|c| cost.checked_add(c.into()))
298        })
299        .ok_or(InstrumentationError::CostCalculationOverflow)?;
300
301    let cost_push_arg = rules
302        .instruction_cost(&Instruction::I32Const(0))
303        .map(|c| c as u64)
304        .ok_or(InstrumentationError::InstructionCostNotFound)?;
305
306    let cost_call = rules
307        .instruction_cost(&Instruction::Call(0))
308        .map(|c| c as u64)
309        .ok_or(InstrumentationError::InstructionCostNotFound)?;
310
311    let cost_local_var = rules.call_per_local_cost() as u64;
312
313    let cost = cost_push_arg + cost_call + cost_local_var + cost_blocks;
314
315    // the cost is added to gas_to_charge which cannot
316    // exceed u32::MAX value. This check ensures
317    // there is no u64 overflow.
318    if cost > u64::MAX - u64::from(u32::MAX) {
319        return Err(InstrumentationError::CostCalculationOverflow);
320    }
321
322    // update cost for 'gas_charge' function itself
323    let cost_instr = elements
324        .iter_mut()
325        .find(|i| **i == Instruction::I64Const(GAS_CHARGE_COST_PLACEHOLDER))
326        .expect("Const for cost of the fn not found");
327    *cost_instr = Instruction::I64Const(cost as i64);
328
329    // gas_charge function
330    mbuilder.add_func(
331        FuncType::new([ValType::I32], []),
332        Function {
333            locals: vec![(1, ValType::I64)],
334            instructions: elements,
335        },
336    );
337
338    // back to plain module
339    let module = mbuilder.build();
340
341    gas_metering::post_injection_handler(module, rules, gas_charge_index)
342        .inspect_err(|err| {
343            log::debug!("post_injection_handler failed: {err:?}");
344        })
345        .map_err(|_| InstrumentationError::GasInjection)
346}