gear_core/code/
utils.rs

1// This file is part of Gear.
2
3// Copyright (C) 2024-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//! Module that contains functions to check code.
20
21use crate::{
22    code::{GENERIC_OS_PAGE_SIZE, errors::*},
23    env::WasmEntryPoint,
24    message::DispatchKind,
25    pages::{WasmPage, WasmPagesAmount},
26};
27use alloc::collections::BTreeSet;
28use gear_wasm_instrument::{
29    ConstExpr, ElementItems, Export, Global, Instruction, Module, STACK_END_EXPORT_NAME,
30    SyscallName,
31};
32use wasmparser::{ExternalKind, Payload, TypeRef, ValType};
33
34/// Defines maximal permitted count of memory pages.
35pub const MAX_WASM_PAGES_AMOUNT: u16 = u16::MAX / 2 + 1; // 2GB
36/// Reference type size in bytes.
37pub(crate) const REF_TYPE_SIZE: u32 = 4;
38
39/// Name of exports allowed on chain.
40pub const ALLOWED_EXPORTS: [&str; 6] = [
41    "init",
42    "handle",
43    "handle_reply",
44    "handle_signal",
45    "state",
46    "metahash",
47];
48
49/// Name of exports required on chain (only 1 of these is required).
50pub const REQUIRED_EXPORTS: [&str; 2] = ["init", "handle"];
51
52pub fn get_static_pages(module: &Module) -> Result<WasmPagesAmount, CodeError> {
53    // get initial memory size from memory import
54    let static_pages = module
55        .import_section
56        .as_ref()
57        .ok_or(SectionError::NotFound(SectionName::Import))?
58        .iter()
59        .find_map(|entry| match entry.ty {
60            TypeRef::Memory(mem_ty) => Some(mem_ty.initial as u32),
61            _ => None,
62        })
63        .map(WasmPagesAmount::try_from)
64        .ok_or(MemoryError::EntryNotFound)?
65        .map_err(|_| MemoryError::InvalidStaticPageCount)?;
66
67    if static_pages > WasmPagesAmount::from(MAX_WASM_PAGES_AMOUNT) {
68        Err(MemoryError::InvalidStaticPageCount)?;
69    }
70
71    Ok(static_pages)
72}
73
74pub fn get_exports(module: &Module) -> BTreeSet<DispatchKind> {
75    let mut entries = BTreeSet::new();
76
77    for entry in module
78        .export_section
79        .as_ref()
80        .expect("Exports section has been checked for already")
81    {
82        if let ExternalKind::Func = entry.kind
83            && let Some(entry) = DispatchKind::try_from_entry(&entry.name)
84        {
85            entries.insert(entry);
86        }
87    }
88
89    entries
90}
91
92pub fn check_exports(module: &Module) -> Result<(), CodeError> {
93    let types = module
94        .type_section
95        .as_ref()
96        .ok_or(SectionError::NotFound(SectionName::Type))?;
97
98    let funcs = module
99        .function_section
100        .as_ref()
101        .ok_or(SectionError::NotFound(SectionName::Function))?;
102
103    let import_count = module.import_count(|ty| matches!(ty, TypeRef::Func(_)));
104
105    let exports = module
106        .export_section
107        .as_ref()
108        .ok_or(SectionError::NotFound(SectionName::Export))?;
109
110    let mut entry_point_found = false;
111    for (export_index, export) in exports.iter().enumerate() {
112        let ExternalKind::Func = export.kind else {
113            continue;
114        };
115
116        let index = export.index.checked_sub(import_count as u32).ok_or(
117            ExportError::ExportReferencesToImportFunction(export_index as u32, export.index),
118        )?;
119
120        // Panic is impossible, unless the Module structure is invalid.
121        let type_id = funcs
122            .get(index as usize)
123            .copied()
124            .unwrap_or_else(|| unreachable!("Module structure is invalid"))
125            as usize;
126
127        // Panic is impossible, unless the Module structure is invalid.
128        let func_type = types
129            .get(type_id)
130            .unwrap_or_else(|| unreachable!("Module structure is invalid"));
131
132        if !ALLOWED_EXPORTS.contains(&&*export.name) {
133            Err(ExportError::ExcessExport(export_index as u32))?;
134        }
135
136        if !(func_type.params().is_empty() && func_type.results().is_empty()) {
137            Err(ExportError::InvalidExportFnSignature(export_index as u32))?;
138        }
139
140        if REQUIRED_EXPORTS.contains(&&*export.name) {
141            entry_point_found = true;
142        }
143    }
144
145    entry_point_found
146        .then_some(())
147        .ok_or(ExportError::RequiredExportNotFound)
148        .map_err(CodeError::Export)
149}
150
151pub fn check_imports(module: &Module) -> Result<(), CodeError> {
152    let types = module
153        .type_section
154        .as_ref()
155        .ok_or(SectionError::NotFound(SectionName::Type))?;
156
157    let imports = module
158        .import_section
159        .as_ref()
160        .ok_or(SectionError::NotFound(SectionName::Import))?;
161
162    let syscalls = SyscallName::instrumentable_map();
163
164    let mut visited_imports = BTreeSet::new();
165
166    for (import_index, import) in imports.iter().enumerate() {
167        let import_index: u32 = import_index
168            .try_into()
169            .unwrap_or_else(|_| unreachable!("Import index should fit in u32"));
170
171        match import.ty {
172            TypeRef::Func(i) => {
173                // Panic is impossible, unless the Module structure is invalid.
174                let &func_type = &types
175                    .get(i as usize)
176                    .unwrap_or_else(|| unreachable!("Module structure is invalid"));
177
178                let syscall = syscalls
179                    .get(import.name.as_ref())
180                    .ok_or(ImportError::UnknownImport(import_index))?;
181
182                if !visited_imports.insert(*syscall) {
183                    Err(ImportError::DuplicateImport(import_index))?;
184                }
185
186                let signature = syscall.signature();
187                let signature_func_type = signature.func_type();
188
189                if signature_func_type != *func_type {
190                    Err(ImportError::InvalidImportFnSignature(import_index))?;
191                }
192            }
193            TypeRef::Global(_) => Err(ImportError::UnexpectedImportKind {
194                kind: &"Global",
195                index: import_index,
196            })?,
197            TypeRef::Table(_) => Err(ImportError::UnexpectedImportKind {
198                kind: &"Table",
199                index: import_index,
200            })?,
201            _ => continue,
202        }
203    }
204
205    Ok(())
206}
207
208fn get_export_entry_with_index<'a>(module: &'a Module, name: &str) -> Option<(u32, &'a Export)> {
209    module
210        .export_section
211        .as_ref()?
212        .iter()
213        .enumerate()
214        .find_map(|(export_index, export)| {
215            (export.name == name).then_some((export_index as u32, export))
216        })
217}
218
219fn get_export_global_with_index(module: &Module, name: &str) -> Option<(u32, u32)> {
220    let (export_index, export) = get_export_entry_with_index(module, name)?;
221    match export.kind {
222        ExternalKind::Global => Some((export_index, export.index)),
223        _ => None,
224    }
225}
226
227fn get_init_expr_const_i32(init_expr: &ConstExpr) -> Option<i32> {
228    match init_expr.instructions.as_slice() {
229        [Instruction::I32Const(value)] => Some(*value),
230        _ => None,
231    }
232}
233
234fn get_export_global_entry(
235    module: &Module,
236    export_index: u32,
237    global_index: u32,
238) -> Result<&Global, CodeError> {
239    let index = global_index
240        .checked_sub(module.import_count(|ty| matches!(ty, TypeRef::Global(_))) as u32)
241        .ok_or(ExportError::ExportReferencesToImportGlobal(
242            export_index,
243            global_index,
244        ))? as usize;
245
246    module
247        .global_section
248        .as_ref()
249        .and_then(|s| s.get(index))
250        .ok_or(ExportError::IncorrectGlobalIndex(global_index, export_index).into())
251}
252
253/// Check that data segments are not overlapping with stack and are inside static pages.
254pub fn check_data_section(
255    module: &Module,
256    static_pages: WasmPagesAmount,
257    stack_end: Option<WasmPage>,
258    data_section_amount_limit: Option<u32>,
259) -> Result<(), CodeError> {
260    let Some(data_section) = &module.data_section else {
261        // No data section - nothing to check.
262        return Ok(());
263    };
264
265    // Check that data segments amount does not exceed the limit.
266    if let Some(data_segments_amount_limit) = data_section_amount_limit {
267        let number_of_data_segments = data_section.len() as u32;
268        if number_of_data_segments > data_segments_amount_limit {
269            Err(DataSectionError::DataSegmentsAmountLimit {
270                limit: data_segments_amount_limit,
271                actual: number_of_data_segments,
272            })?;
273        }
274    }
275
276    for data_segment in data_section {
277        let data_segment_offset = get_init_expr_const_i32(&data_segment.offset_expr)
278            .ok_or(DataSectionError::Initialization)? as u32;
279
280        if let Some(stack_end_offset) = stack_end.map(|p| p.offset()) {
281            // Checks, that each data segment does not overlap the user stack.
282            (data_segment_offset >= stack_end_offset)
283                .then_some(())
284                .ok_or(DataSectionError::GearStackOverlaps(
285                    data_segment_offset,
286                    stack_end_offset,
287                ))?;
288        }
289
290        let Some(size) = u32::try_from(data_segment.data.len())
291            .map_err(|_| DataSectionError::EndAddressOverflow(data_segment_offset))?
292            .checked_sub(1)
293        else {
294            // Zero size data segment - strange, but allowed.
295            continue;
296        };
297
298        let data_segment_last_byte_offset = data_segment_offset
299            .checked_add(size)
300            .ok_or(DataSectionError::EndAddressOverflow(data_segment_offset))?;
301
302        ((data_segment_last_byte_offset as u64) < static_pages.offset())
303            .then_some(())
304            .ok_or(DataSectionError::EndAddressOutOfStaticMemory(
305                data_segment_offset,
306                data_segment_last_byte_offset,
307                static_pages.offset(),
308            ))?;
309    }
310
311    Ok(())
312}
313
314fn get_stack_end_offset(module: &Module) -> Result<Option<u32>, CodeError> {
315    let Some((export_index, global_index)) =
316        get_export_global_with_index(module, STACK_END_EXPORT_NAME)
317    else {
318        return Ok(None);
319    };
320
321    Ok(Some(
322        get_init_expr_const_i32(
323            &get_export_global_entry(module, export_index, global_index)?.init_expr,
324        )
325        .ok_or(StackEndError::Initialization)? as u32,
326    ))
327}
328
329pub fn check_and_canonize_gear_stack_end(
330    module: &mut Module,
331    static_pages: WasmPagesAmount,
332) -> Result<Option<WasmPage>, CodeError> {
333    let Some(stack_end_offset) = get_stack_end_offset(module)? else {
334        return Ok(None);
335    };
336
337    // Remove stack end export from module.
338    // Panic below is impossible, because we have checked above, that export section exists.
339    module
340        .export_section
341        .as_mut()
342        .unwrap_or_else(|| unreachable!("Cannot find export section"))
343        .retain(|export| export.name != STACK_END_EXPORT_NAME);
344
345    if !stack_end_offset.is_multiple_of(WasmPage::SIZE) {
346        return Err(StackEndError::NotAligned(stack_end_offset).into());
347    }
348
349    let stack_end = WasmPage::from_offset(stack_end_offset);
350    if stack_end > static_pages {
351        return Err(StackEndError::OutOfStatic(stack_end_offset, static_pages.offset()).into());
352    }
353
354    Ok(Some(stack_end))
355}
356
357/// Checks that module:
358/// 1) Does not have exports to mutable globals.
359/// 2) Does not have exports to imported globals.
360/// 3) Does not have exports with incorrect global index.
361pub fn check_mut_global_exports(module: &Module) -> Result<(), CodeError> {
362    let Some(export_section) = &module.export_section else {
363        return Ok(());
364    };
365
366    export_section
367        .iter()
368        .enumerate()
369        .filter_map(|(export_index, export)| match export.kind {
370            ExternalKind::Global => Some((export_index as u32, export.index)),
371            _ => None,
372        })
373        .try_for_each(|(export_index, global_index)| {
374            let entry = get_export_global_entry(module, export_index, global_index)?;
375            if entry.ty.mutable {
376                Err(ExportError::MutableGlobalExport(global_index, export_index).into())
377            } else {
378                Ok(())
379            }
380        })
381}
382
383pub fn check_start_section(module: &Module) -> Result<(), CodeError> {
384    if module.start_section.is_some() {
385        log::debug!("Found start section in program code, which is not allowed");
386        Err(SectionError::NotSupported(SectionName::Start))?
387    } else {
388        Ok(())
389    }
390}
391
392/// Calculates the instantiated data section size based on the number of heuristic memory pages (see `GENERIC_OS_PAGE_SIZE`).
393/// That is, the size of the instantiated data section is the size of the section after the module is instantiated
394/// in the executor's memory. Additionally, the number of heuristic pages used during instantiation is considered,
395/// as each page contributes to the total weight during instantiation.
396pub fn get_data_section_size(module: &Module) -> Result<u32, CodeError> {
397    let Some(data_section) = &module.data_section else {
398        // No data section
399        return Ok(0);
400    };
401
402    let mut used_pages = BTreeSet::new();
403    for data_segment in data_section {
404        let data_segment_offset = get_init_expr_const_i32(&data_segment.offset_expr)
405            .ok_or(DataSectionError::Initialization)? as u32;
406        let data_segment_start = data_segment_offset / GENERIC_OS_PAGE_SIZE;
407
408        let data_segment_size = data_segment.data.len() as u32;
409
410        if data_segment_size == 0 {
411            // Zero size data segment
412            continue;
413        }
414
415        let data_segment_end = data_segment_offset // We should use `offset` here and not `start`
416                .saturating_add(data_segment_size.saturating_sub(1)) // Round up to the nearest whole number
417                / GENERIC_OS_PAGE_SIZE;
418
419        used_pages.extend(data_segment_start..=data_segment_end);
420    }
421
422    Ok(used_pages.len() as u32 * GENERIC_OS_PAGE_SIZE)
423}
424
425/// Calculates the amount of bytes in the global section will be initialized during module instantiation.
426pub fn get_instantiated_global_section_size(module: &Module) -> Result<u32, CodeError> {
427    let Some(global_section) = &module.global_section else {
428        // No element section
429        return Ok(0);
430    };
431
432    Ok(global_section.iter().fold(0, |total_bytes, global| {
433        let value_size = match global.ty.content_type {
434            ValType::I32 => size_of::<i32>(),
435            ValType::I64 => size_of::<i64>(),
436            ValType::F32 | ValType::F64 | ValType::V128 | ValType::Ref(_) => {
437                unreachable!("f32/64, SIMD and reference types are not supported")
438            }
439        } as u32;
440        total_bytes.saturating_add(value_size)
441    }))
442}
443
444/// Calculates the amount of bytes in the table section that will be allocated during module instantiation.
445pub fn get_instantiated_table_section_size(module: &Module) -> u32 {
446    let Some(table) = &module.table_section else {
447        return 0;
448    };
449
450    // Tables may hold only reference types, which are 4 bytes long.
451    (table.ty.initial as u32).saturating_mul(REF_TYPE_SIZE)
452}
453
454/// Calculates the amount of bytes in the table/element section that will be initialized during module instantiation.
455pub fn get_instantiated_element_section_size(module: &Module) -> Result<u32, CodeError> {
456    if module.table_section.is_none() {
457        return Ok(0);
458    }
459
460    let Some(element_section) = &module.element_section else {
461        // No element section
462        return Ok(0);
463    };
464
465    Ok(element_section.iter().fold(0, |total_bytes, segment| {
466        let count = match &segment.items {
467            ElementItems::Functions(section) => section.len(),
468        } as u32;
469        // Tables may hold only reference types, which are 4 bytes long.
470        total_bytes.saturating_add(count.saturating_mul(REF_TYPE_SIZE))
471    }))
472}
473
474pub struct CodeTypeSectionSizes {
475    pub code_section: u32,
476    pub type_section: u32,
477}
478
479// Calculate the size of the code and type sections in bytes.
480pub fn get_code_type_sections_sizes(code_bytes: &[u8]) -> Result<CodeTypeSectionSizes, CodeError> {
481    let mut code_section_size = 0;
482    let mut type_section_size = 0;
483
484    let parser = wasmparser::Parser::new(0);
485
486    for item in parser.parse_all(code_bytes) {
487        let item = item.map_err(CodeError::Validation)?;
488        match item {
489            Payload::CodeSectionStart { size, .. } => {
490                code_section_size = size;
491            }
492            Payload::TypeSection(t) => {
493                type_section_size += t.range().len() as u32;
494            }
495            _ => {}
496        }
497    }
498
499    Ok(CodeTypeSectionSizes {
500        code_section: code_section_size,
501        type_section: type_section_size,
502    })
503}