gear_wasm_instrument/
lib.rs

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