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#![cfg_attr(not(feature = "std"), no_std)]
20#![allow(clippy::items_after_test_module)]
21
22extern crate alloc;
23
24use alloc::vec;
25use gwasm_instrument::{
26    parity_wasm::{
27        builder,
28        elements::{
29            self, BlockType, ImportCountType, Instruction, Instructions, Local, Module, ValueType,
30        },
31    },
32    InjectionConfig,
33};
34
35pub use crate::{gas_metering::Rules, syscalls::SyscallName};
36pub use gwasm_instrument::{self as wasm_instrument, gas_metering, parity_wasm, utils};
37
38#[cfg(test)]
39mod tests;
40
41pub mod syscalls;
42
43// TODO #3057
44/// Gas global export name in WASM module.
45pub const GLOBAL_NAME_GAS: &str = "gear_gas";
46
47/// `__gear_stack_end` export is inserted by wasm-proc or wasm-builder,
48/// it indicates the end of program stack memory.
49pub const STACK_END_EXPORT_NAME: &str = "__gear_stack_end";
50/// `__gear_stack_height` export is inserted by gwasm-instrument,
51/// it points to stack height global that is used by
52/// [`gwasm_instrument::stack_limiter`].
53pub const STACK_HEIGHT_EXPORT_NAME: &str = "__gear_stack_height";
54
55/// System break code for [`SyscallName::SystemBreak`] syscall.
56#[derive(Debug, Clone, Copy)]
57pub enum SystemBreakCode {
58    OutOfGas = 0,
59    StackLimitExceeded = 1,
60}
61
62/// The error type returned when a conversion from `i32` or `u32` to
63/// [`SystemBreakCode`] fails.
64#[derive(Clone, Debug, derive_more::Display)]
65#[display(fmt = "Unsupported system break code")]
66pub struct SystemBreakCodeTryFromError;
67
68impl TryFrom<i32> for SystemBreakCode {
69    type Error = SystemBreakCodeTryFromError;
70
71    fn try_from(value: i32) -> Result<Self, Self::Error> {
72        match value {
73            0 => Ok(Self::OutOfGas),
74            1 => Ok(Self::StackLimitExceeded),
75            _ => Err(SystemBreakCodeTryFromError),
76        }
77    }
78}
79
80impl TryFrom<u32> for SystemBreakCode {
81    type Error = SystemBreakCodeTryFromError;
82
83    fn try_from(value: u32) -> Result<Self, Self::Error> {
84        SystemBreakCode::try_from(value as i32)
85    }
86}
87
88/// WASM module instrumentation error.
89#[derive(Debug, PartialEq, Eq, derive_more::Display)]
90pub enum InstrumentationError {
91    /// Error occurred during injecting `gr_system_break` import.
92    #[display(fmt = "The WASM module already has `gr_system_break` import")]
93    SystemBreakImportAlreadyExists,
94    /// Error occurred during stack height instrumentation.
95    #[display(fmt = "Failed to inject stack height limits")]
96    StackLimitInjection,
97    /// Error occurred during injecting `gear_gas` global.
98    #[display(fmt = "The WASM module already has `gear_gas` global")]
99    GasGlobalAlreadyExists,
100    /// Error occurred during calculating the cost of the `gas_charge` function.
101    #[display(
102        fmt = "An overflow occurred while calculating the cost of the `gas_charge` function"
103    )]
104    CostCalculationOverflow,
105    /// Error occurred while trying to get the instruction cost.
106    #[display(fmt = "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/non-deterministic
111    /// instructions (floats, memory grow, etc.).
112    #[display(fmt = "Failed to inject instructions for gas metrics: may be in case \
113        program contains unsupported instructions (floats, memory grow, etc.)")]
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 = wasm_instrument::inject_stack_limiter_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: elements::Module,
195    break_module_name: &str,
196) -> Result<(u32, elements::Module), InstrumentationError> {
197    if module
198        .import_section()
199        .map(|section| {
200            section.entries().iter().any(|entry| {
201                entry.module() == break_module_name
202                    && entry.field() == SyscallName::SystemBreak.to_str()
203            })
204        })
205        .unwrap_or(false)
206    {
207        return Err(InstrumentationError::SystemBreakImportAlreadyExists);
208    }
209
210    let mut mbuilder = builder::from_module(module);
211
212    // fn gr_system_break(code: u32) -> !;
213    let import_sig =
214        mbuilder.push_signature(builder::signature().with_param(ValueType::I32).build_sig());
215
216    // back to plain module
217    let module = mbuilder
218        .import()
219        .module(break_module_name)
220        .field(SyscallName::SystemBreak.to_str())
221        .external()
222        .func(import_sig)
223        .build()
224        .build();
225
226    let import_count = module.import_count(ImportCountType::Function);
227    let inserted_index = import_count as u32 - 1;
228
229    let module = utils::rewrite_sections_after_insertion(module, inserted_index, 1)
230        .expect("Failed to rewrite sections");
231
232    Ok((inserted_index, module))
233}
234
235fn inject_gas_limiter<R: Rules>(
236    module: Module,
237    rules: &R,
238    gr_system_break_index: u32,
239) -> Result<Module, InstrumentationError> {
240    if module
241        .export_section()
242        .map(|section| {
243            section
244                .entries()
245                .iter()
246                .any(|entry| entry.field() == GLOBAL_NAME_GAS)
247        })
248        .unwrap_or(false)
249    {
250        return Err(InstrumentationError::GasGlobalAlreadyExists);
251    }
252
253    let gas_charge_index = module.functions_space();
254    let gas_index = module.globals_space() as u32;
255
256    let mut mbuilder = builder::from_module(module);
257
258    mbuilder.push_global(
259        builder::global()
260            .value_type()
261            .i64()
262            .init_expr(Instruction::I64Const(0))
263            .mutable()
264            .build(),
265    );
266
267    mbuilder.push_export(
268        builder::export()
269            .field(GLOBAL_NAME_GAS)
270            .internal()
271            .global(gas_index)
272            .build(),
273    );
274
275    // This const is introduced to avoid future errors in code if some other
276    // `I64Const` instructions appear in gas charge function body.
277    const GAS_CHARGE_COST_PLACEHOLDER: i64 = 1248163264128;
278
279    let mut elements = vec![
280        // I. Put global with value of current gas counter of any type.
281        Instruction::GetGlobal(gas_index),
282        // II. Calculating total gas to charge as sum of:
283        //  - `gas_charge(..)` argument;
284        //  - `gas_charge(..)` call cost.
285        //
286        // Setting the sum into local with index 1 with keeping it on stack.
287        Instruction::GetLocal(0),
288        Instruction::I64ExtendUI32,
289        Instruction::I64Const(GAS_CHARGE_COST_PLACEHOLDER),
290        Instruction::I64Add,
291        Instruction::TeeLocal(1),
292        // III. Validating left amount of gas.
293        //
294        // In case of requested value is bigger than actual gas counter value,
295        // than we call `out_of_gas()` that will terminate execution.
296        Instruction::I64LtU,
297        Instruction::If(BlockType::NoResult),
298        Instruction::I32Const(SystemBreakCode::OutOfGas as i32),
299        Instruction::Call(gr_system_break_index),
300        Instruction::End,
301        // IV. Calculating new global value by subtraction.
302        //
303        // Result is stored back into global.
304        Instruction::GetGlobal(gas_index),
305        Instruction::GetLocal(1),
306        Instruction::I64Sub,
307        Instruction::SetGlobal(gas_index),
308        // V. Ending `gas_charge()` function.
309        Instruction::End,
310    ];
311
312    // determine cost for successful execution
313    let mut block_of_code = false;
314
315    let cost_blocks = elements
316        .iter()
317        .filter(|instruction| match instruction {
318            Instruction::If(_) => {
319                block_of_code = true;
320                true
321            }
322            Instruction::End => {
323                block_of_code = false;
324                false
325            }
326            _ => !block_of_code,
327        })
328        .try_fold(0u64, |cost, instruction| {
329            rules
330                .instruction_cost(instruction)
331                .and_then(|c| cost.checked_add(c.into()))
332        })
333        .ok_or(InstrumentationError::CostCalculationOverflow)?;
334
335    let cost_push_arg = rules
336        .instruction_cost(&Instruction::I32Const(0))
337        .map(|c| c as u64)
338        .ok_or(InstrumentationError::InstructionCostNotFound)?;
339
340    let cost_call = rules
341        .instruction_cost(&Instruction::Call(0))
342        .map(|c| c as u64)
343        .ok_or(InstrumentationError::InstructionCostNotFound)?;
344
345    let cost_local_var = rules.call_per_local_cost() as u64;
346
347    let cost = cost_push_arg + cost_call + cost_local_var + cost_blocks;
348
349    // the cost is added to gas_to_charge which cannot
350    // exceed u32::MAX value. This check ensures
351    // there is no u64 overflow.
352    if cost > u64::MAX - u64::from(u32::MAX) {
353        return Err(InstrumentationError::CostCalculationOverflow);
354    }
355
356    // update cost for 'gas_charge' function itself
357    let cost_instr = elements
358        .iter_mut()
359        .find(|i| **i == Instruction::I64Const(GAS_CHARGE_COST_PLACEHOLDER))
360        .expect("Const for cost of the fn not found");
361    *cost_instr = Instruction::I64Const(cost as i64);
362
363    // gas_charge function
364    mbuilder.push_function(
365        builder::function()
366            .signature()
367            .with_param(ValueType::I32)
368            .build()
369            .body()
370            .with_locals(vec![Local::new(1, ValueType::I64)])
371            .with_instructions(Instructions::new(elements))
372            .build()
373            .build(),
374    );
375
376    // back to plain module
377    let module = mbuilder.build();
378
379    gas_metering::post_injection_handler(module, rules, gas_charge_index)
380        .map_err(|_| InstrumentationError::GasInjection)
381}