gear_lazy_pages/
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//! Lazy-pages support.
20//! In runtime data for program Wasm memory pages can be loaded in lazy manner.
21//! All pages, which is supposed to be lazy, must be mprotected before program execution.
22//! During execution data from storage is loaded for all pages, which has been accessed
23//! and which has data in storage.
24//! See also `process::process_lazy_pages`, `signal`, `host_func` for more information.
25//!
26//! Note: currently we restrict twice write signal from same page during one execution.
27//! It's not necessary behavior, but more simple and safe.
28
29#![allow(clippy::items_after_test_module)]
30#![doc(html_logo_url = "https://gear-tech.io/logo.png")]
31#![doc(html_favicon_url = "https://gear-tech.io/favicon.ico")]
32#![cfg_attr(docsrs, feature(doc_cfg))]
33
34mod common;
35mod globals;
36mod host_func;
37mod init_flag;
38mod mprotect;
39mod pages;
40mod process;
41mod signal;
42mod sys;
43
44#[cfg(test)]
45mod tests;
46
47pub use common::{Error as LazyPagesError, LazyPagesStorage, LazyPagesVersion};
48pub use host_func::pre_process_memory_accesses;
49pub use signal::{ExceptionInfo, UserSignalHandler};
50
51use crate::{
52    common::{ContextError, CostNo, Costs, LazyPagesContext, PagePrefix, PageSizes},
53    globals::{GlobalNo, GlobalsContext},
54    init_flag::InitializationFlag,
55    pages::{
56        GearPagesAmount, GearSizeNo, PagesAmountTrait, SIZES_AMOUNT, SizeNumber, WasmPage,
57        WasmPagesAmount, WasmSizeNo,
58    },
59    signal::DefaultUserSignalHandler,
60};
61use common::{LazyPagesExecutionContext, LazyPagesRuntimeContext};
62use gear_lazy_pages_common::{GlobalsAccessConfig, LazyPagesInitContext, Status};
63use mprotect::MprotectError;
64use numerated::iterators::IntervalIterator;
65use pages::GearPage;
66use std::{cell::RefCell, convert::TryInto, num::NonZero};
67
68/// Initialize lazy-pages once for process.
69static LAZY_PAGES_INITIALIZED: InitializationFlag = InitializationFlag::new();
70
71thread_local! {
72    // NOTE: here we suppose, that each program is executed in separate thread.
73    // Or may be in one thread but consequentially.
74
75    static LAZY_PAGES_CONTEXT: RefCell<LazyPagesContext> = RefCell::new(Default::default());
76}
77
78#[derive(Debug, derive_more::Display, derive_more::From)]
79pub enum Error {
80    #[display("WASM memory native address {_0:#x} is not aligned to the native page size")]
81    WasmMemAddrIsNotAligned(usize),
82    Mprotect(MprotectError),
83    #[from(skip)]
84    #[display("Wasm memory end addr is out of usize: begin addr = {_0:#x}, size = {_1:#x}")]
85    WasmMemoryEndAddrOverflow(usize, usize),
86    #[display("Prefix of storage with memory pages was not set")]
87    MemoryPagesPrefixNotSet,
88    #[display("Memory size must be null when memory host addr is not set")]
89    MemorySizeIsNotNull,
90    #[display("Wasm mem size is too big")]
91    WasmMemSizeOverflow,
92    #[display("Stack end offset cannot be bigger than memory size")]
93    StackEndBiggerThanMemSize,
94    #[display("Stack end offset is too big")]
95    StackEndOverflow,
96    #[display("Wasm addr and size are not changed, so host func call is needless")]
97    NothingToChange,
98    #[display("Wasm memory addr must be set, when trying to change something in lazy pages")]
99    WasmMemAddrIsNotSet,
100    GlobalContext(ContextError),
101    #[from(skip)]
102    #[display("Wrong costs amount: get {_0}, must be {_1}")]
103    WrongCostsAmount(usize, usize),
104}
105
106fn check_memory_interval(addr: usize, size: usize) -> Result<(), Error> {
107    addr.checked_add(size)
108        .ok_or(Error::WasmMemoryEndAddrOverflow(addr, size))
109        .map(|_| ())
110}
111
112pub fn initialize_for_program(
113    wasm_mem_addr: Option<usize>,
114    wasm_mem_size: u32,
115    stack_end: Option<u32>,
116    program_key: Vec<u8>,
117    globals_config: Option<GlobalsAccessConfig>,
118    costs: Vec<u64>,
119) -> Result<(), Error> {
120    // Initialize new execution context
121    LAZY_PAGES_CONTEXT.with(|ctx| {
122        let mut ctx = ctx.borrow_mut();
123        let runtime_ctx = ctx.runtime_context_mut()?;
124
125        // Check wasm program memory host address
126        if let Some(addr) = wasm_mem_addr
127            && !addr.is_multiple_of(region::page::size())
128        {
129            return Err(Error::WasmMemAddrIsNotAligned(addr));
130        }
131
132        // Check stack_end is less or equal than wasm memory size
133        let stack_end = stack_end.unwrap_or_default();
134        if wasm_mem_size < stack_end {
135            return Err(Error::StackEndBiggerThanMemSize);
136        }
137
138        let wasm_mem_size =
139            WasmPagesAmount::new(runtime_ctx, wasm_mem_size).ok_or(Error::WasmMemSizeOverflow)?;
140        let wasm_mem_size_in_bytes = wasm_mem_size.offset(runtime_ctx);
141
142        // Check wasm program memory size
143        if let Some(addr) = wasm_mem_addr {
144            check_memory_interval(addr, wasm_mem_size_in_bytes)?;
145        } else if wasm_mem_size_in_bytes != 0 {
146            return Err(Error::MemorySizeIsNotNull);
147        }
148
149        let stack_end = WasmPage::new(runtime_ctx, stack_end).ok_or(Error::StackEndOverflow)?;
150
151        let costs: Costs = costs.try_into().map_err(|costs: Vec<u64>| {
152            Error::WrongCostsAmount(costs.len(), CostNo::Amount as usize)
153        })?;
154
155        let execution_ctx = LazyPagesExecutionContext {
156            costs,
157            wasm_mem_addr,
158            wasm_mem_size,
159            program_storage_prefix: PagePrefix::new_from_program_prefix(
160                [runtime_ctx.pages_storage_prefix.as_slice(), &program_key].concat(),
161            ),
162            accessed_pages: Default::default(),
163            write_accessed_pages: Default::default(),
164            stack_end,
165            globals_context: globals_config.map(|cfg| GlobalsContext {
166                names: runtime_ctx.global_names.clone(),
167                access_ptr: cfg.access_ptr,
168                access_mod: cfg.access_mod,
169            }),
170            status: Status::Normal,
171        };
172
173        // Set protection if wasm memory exist.
174        if let Some(addr) = wasm_mem_addr {
175            let stack_end_offset = execution_ctx.stack_end.offset(runtime_ctx) as usize;
176            // `+` and `-` are safe because we checked
177            // that `stack_end` is less or equal to `wasm_mem_size` and wasm end addr fits usize.
178            let addr = addr + stack_end_offset;
179            let size = wasm_mem_size_in_bytes - stack_end_offset;
180            if size != 0 {
181                mprotect::mprotect_interval(addr, size, false, false)?;
182            }
183        }
184
185        ctx.set_execution_context(execution_ctx);
186
187        log::trace!("Initialize lazy-pages for current program: {ctx:?}");
188
189        Ok(())
190    })
191}
192
193/// Protect lazy pages, after they had been unprotected.
194pub fn set_lazy_pages_protection() -> Result<(), Error> {
195    LAZY_PAGES_CONTEXT.with(|ctx| {
196        let ctx = ctx.borrow();
197        let (rt_ctx, exec_ctx) = ctx.contexts()?;
198        let mem_addr = exec_ctx.wasm_mem_addr.ok_or(Error::WasmMemAddrIsNotSet)?;
199
200        // Set r/w protection for all pages except stack pages and write accessed pages.
201        let start: GearPage = exec_ctx.stack_end.to_page(rt_ctx);
202        let end: GearPagesAmount = exec_ctx.wasm_mem_size.convert(rt_ctx);
203        let interval = start.to_end_interval(rt_ctx, end).unwrap_or_else(|| {
204            let err_msg = format!(
205                "set_lazy_pages_protection: `stack_end` must be less or equal to `wasm_mem_size`. \
206                Stack end start - {start:?}, wasm memory size - {end:?}",
207            );
208
209            log::error!("{err_msg}");
210            unreachable!("{err_msg}")
211        });
212        let pages = exec_ctx.write_accessed_pages.voids(interval);
213        mprotect::mprotect_pages(mem_addr, pages, rt_ctx, false, false)?;
214
215        // Set only write protection for already accessed, but not write accessed pages.
216        let pages = exec_ctx
217            .accessed_pages
218            .difference(&exec_ctx.write_accessed_pages);
219        mprotect::mprotect_pages(mem_addr, pages, rt_ctx, true, false)?;
220
221        // After that protections are:
222        // 1) Only execution protection for stack pages.
223        // 2) Only execution protection for write accessed pages.
224        // 3) Read and execution protection for accessed, but not write accessed pages.
225        // 4) r/w/e protections for all other WASM memory.
226
227        Ok(())
228    })
229}
230
231/// Unset lazy pages read/write protections.
232pub fn unset_lazy_pages_protection() -> Result<(), Error> {
233    LAZY_PAGES_CONTEXT.with(|ctx| {
234        let ctx = ctx.borrow();
235        let (rt_ctx, exec_ctx) = ctx.contexts()?;
236        let addr = exec_ctx.wasm_mem_addr.ok_or(Error::WasmMemAddrIsNotSet)?;
237        let size = exec_ctx.wasm_mem_size.offset(rt_ctx);
238        mprotect::mprotect_interval(addr, size, true, true)?;
239        Ok(())
240    })
241}
242
243/// Set current wasm memory begin addr in global context
244pub fn change_wasm_mem_addr_and_size(addr: Option<usize>, size: Option<u32>) -> Result<(), Error> {
245    if matches!((addr, size), (None, None)) {
246        return Err(Error::NothingToChange);
247    }
248
249    LAZY_PAGES_CONTEXT.with(|ctx| {
250        let mut ctx = ctx.borrow_mut();
251        let (rt_ctx, exec_ctx) = ctx.contexts_mut()?;
252
253        let addr = match addr {
254            Some(addr) => match addr % region::page::size() {
255                0 => addr,
256                _ => return Err(Error::WasmMemAddrIsNotAligned(addr)),
257            },
258
259            None => match exec_ctx.wasm_mem_addr {
260                Some(addr) => addr,
261                None => return Err(Error::WasmMemAddrIsNotSet),
262            },
263        };
264
265        let size = match size {
266            Some(raw) => WasmPagesAmount::new(rt_ctx, raw).ok_or(Error::WasmMemSizeOverflow)?,
267            None => exec_ctx.wasm_mem_size,
268        };
269
270        check_memory_interval(addr, size.offset(rt_ctx))?;
271
272        exec_ctx.wasm_mem_addr = Some(addr);
273        exec_ctx.wasm_mem_size = size;
274
275        Ok(())
276    })
277}
278
279/// Returns vec of lazy-pages which has been accessed
280pub fn write_accessed_pages() -> Result<Vec<u32>, Error> {
281    LAZY_PAGES_CONTEXT.with(|ctx| {
282        ctx.borrow()
283            .execution_context()
284            .map(|ctx| {
285                ctx.write_accessed_pages
286                    .iter()
287                    .flat_map(IntervalIterator::from)
288                    .map(|p| p.raw())
289                    .collect()
290            })
291            .map_err(Into::into)
292    })
293}
294
295pub fn status() -> Result<Status, Error> {
296    LAZY_PAGES_CONTEXT.with(|ctx| {
297        ctx.borrow()
298            .execution_context()
299            .map(|ctx| ctx.status)
300            .map_err(Into::into)
301    })
302}
303
304#[derive(Debug, Clone, derive_more::Display)]
305pub enum InitError {
306    #[display("Wrong page sizes amount: get {_0}, must be {_1}")]
307    WrongSizesAmount(usize, usize),
308    #[display("Wrong global names: expected {_0}, found {_1}")]
309    WrongGlobalNames(String, String),
310    #[display("Not suitable page sizes")]
311    NotSuitablePageSizes,
312    #[display("Can not set signal handler: {_0}")]
313    CanNotSetUpSignalHandler(String),
314    #[display("Failed to init for thread: {_0}")]
315    InitForThread(String),
316    #[display("Provided by runtime memory page size cannot be zero")]
317    ZeroPageSize,
318}
319
320/// Initialize lazy-pages once for process:
321/// 1) checks whether lazy-pages is supported in current environment
322/// 2) set signals handler
323///
324/// # Safety
325/// See [`sys::setup_signal_handler`]
326unsafe fn init_for_process<H: UserSignalHandler>() -> Result<(), InitError> {
327    use InitError::*;
328
329    #[cfg(target_vendor = "apple")]
330    {
331        // Support debugging under lldb on Darwin.
332        // When SIGBUS appears lldb will stuck on it forever, without this code.
333        // See also: https://github.com/mono/mono/commit/8e75f5a28e6537e56ad70bf870b86e22539c2fb7.
334
335        use mach::{
336            exception_types::*, kern_return::*, mach_types::*, port::*, thread_status::*, traps::*,
337        };
338
339        unsafe extern "C" {
340            // See https://web.mit.edu/darwin/src/modules/xnu/osfmk/man/task_set_exception_ports.html
341            fn task_set_exception_ports(
342                task: task_t,
343                exception_mask: exception_mask_t,
344                new_port: mach_port_t,
345                behavior: exception_behavior_t,
346                new_flavor: thread_state_flavor_t,
347            ) -> kern_return_t;
348        }
349
350        #[cfg(target_arch = "x86_64")]
351        static MACHINE_THREAD_STATE: i32 = x86_THREAD_STATE64;
352
353        // Took const value from https://opensource.apple.com/source/cctools/cctools-870/include/mach/arm/thread_status.h
354        // ```
355        // #define ARM_THREAD_STATE64		6
356        // ```
357        #[cfg(target_arch = "aarch64")]
358        static MACHINE_THREAD_STATE: i32 = 6;
359
360        unsafe {
361            task_set_exception_ports(
362                mach_task_self(),
363                EXC_MASK_BAD_ACCESS,
364                MACH_PORT_NULL,
365                EXCEPTION_STATE_IDENTITY as exception_behavior_t,
366                MACHINE_THREAD_STATE,
367            )
368        };
369    }
370
371    LAZY_PAGES_INITIALIZED.get_or_init(|| {
372        if let Err(err) = unsafe { sys::setup_signal_handler::<H>() } {
373            return Err(CanNotSetUpSignalHandler(err.to_string()));
374        }
375
376        log::trace!("Successfully initialize lazy-pages for process");
377
378        Ok(())
379    })
380}
381
382#[cfg(test)]
383pub(crate) fn reset_init_flag() {
384    LAZY_PAGES_INITIALIZED.reset();
385}
386
387/// Initialize lazy-pages for current thread.
388pub fn init_with_handler<H: UserSignalHandler, S: LazyPagesStorage + 'static>(
389    _version: LazyPagesVersion,
390    ctx: LazyPagesInitContext,
391    pages_storage: S,
392) -> Result<(), InitError> {
393    use InitError::*;
394
395    let LazyPagesInitContext {
396        page_sizes,
397        global_names,
398        pages_storage_prefix,
399    } = ctx;
400
401    // Check that sizes are not zero
402    let page_sizes = page_sizes
403        .into_iter()
404        .map(TryInto::<NonZero<u32>>::try_into)
405        .collect::<Result<Vec<_>, _>>()
406        .map_err(|_| ZeroPageSize)?;
407
408    let page_sizes: PageSizes = match page_sizes.try_into() {
409        Ok(sizes) => sizes,
410        Err(sizes) => return Err(WrongSizesAmount(sizes.len(), SIZES_AMOUNT)),
411    };
412
413    // Check sizes suitability
414    let wasm_page_size = page_sizes[WasmSizeNo::SIZE_NO];
415    let gear_page_size = page_sizes[GearSizeNo::SIZE_NO];
416    let native_page_size = region::page::size();
417    if wasm_page_size < gear_page_size
418        || (gear_page_size.get() as usize) < native_page_size
419        || !u32::is_power_of_two(wasm_page_size.get())
420        || !u32::is_power_of_two(gear_page_size.get())
421        || !usize::is_power_of_two(native_page_size)
422    {
423        return Err(NotSuitablePageSizes);
424    }
425
426    // TODO: check globals from context issue #3057
427    // we only need to check the globals that are used to keep the state consistent in older runtimes.
428    if global_names[GlobalNo::Gas as usize].as_str() != "gear_gas" {
429        return Err(WrongGlobalNames(
430            "gear_gas".to_string(),
431            global_names[GlobalNo::Gas as usize].to_string(),
432        ));
433    }
434
435    // Set version even if it has been already set, because it can be changed after runtime upgrade.
436    LAZY_PAGES_CONTEXT.with(|ctx| {
437        ctx.borrow_mut()
438            .set_runtime_context(LazyPagesRuntimeContext {
439                page_sizes,
440                global_names,
441                pages_storage_prefix,
442                program_storage: Box::new(pages_storage),
443            })
444    });
445
446    // TODO: remove after usage of `wasmer::Store::set_trap_handler` for lazy-pages
447    // we capture executor signal handler first to call it later
448    // if our handler is not effective
449    wasmer_vm::init_traps();
450
451    unsafe { init_for_process::<H>()? }
452
453    unsafe { sys::init_for_thread().map_err(InitForThread)? }
454
455    Ok(())
456}
457
458pub fn init<S: LazyPagesStorage + 'static>(
459    version: LazyPagesVersion,
460    ctx: LazyPagesInitContext,
461    pages_storage: S,
462) -> Result<(), InitError> {
463    init_with_handler::<DefaultUserSignalHandler, S>(version, ctx, pages_storage)
464}