1#![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
68static LAZY_PAGES_INITIALIZED: InitializationFlag = InitializationFlag::new();
70
71thread_local! {
72 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 LAZY_PAGES_CONTEXT.with(|ctx| {
122 let mut ctx = ctx.borrow_mut();
123 let runtime_ctx = ctx.runtime_context_mut()?;
124
125 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 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 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 if let Some(addr) = wasm_mem_addr {
175 let stack_end_offset = execution_ctx.stack_end.offset(runtime_ctx) as usize;
176 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
193pub 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 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 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 Ok(())
228 })
229}
230
231pub 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
243pub 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
279pub 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
320unsafe fn init_for_process<H: UserSignalHandler>() -> Result<(), InitError> {
327 use InitError::*;
328
329 #[cfg(target_vendor = "apple")]
330 {
331 use mach::{
336 exception_types::*, kern_return::*, mach_types::*, port::*, thread_status::*, traps::*,
337 };
338
339 unsafe extern "C" {
340 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 #[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
387pub 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 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 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 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 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 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}