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