Skip to main content

gear_core/code/
utils.rs

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