1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
// This file is part of Gear.
// Copyright (C) 2023-2024 Gear Technologies Inc.
// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
//! Lazy-pages memory accesses processing main logic.
use crate::{
common::{Error, LazyPagesExecutionContext, LazyPagesRuntimeContext},
mprotect,
pages::{GearPage, PagesAmountTrait},
};
use gear_lazy_pages_common::Status;
use std::slice;
/// `process_lazy_pages` use struct which implements this trait,
/// to process in custom logic two cases: host function call and signal.
pub(crate) trait AccessHandler {
type Pages;
type Output;
/// Returns whether it is write access
fn is_write(&self) -> bool;
/// Returns whether gas exceeded status is allowed for current access.
fn check_status_is_gas_exceeded() -> Result<(), Error>;
/// Returns whether stack memory access can appear for the case.
fn check_stack_memory_access() -> Result<(), Error>;
/// Returns whether write accessed memory access is allowed for the case.
fn check_write_accessed_memory_access() -> Result<(), Error>;
/// Returns whether already accessed memory read access is allowed for the case.
fn check_read_from_accessed_memory() -> Result<(), Error>;
/// Charge for accessed gear page.
fn charge_for_page_access(
&mut self,
page: GearPage,
is_already_accessed: bool,
) -> Result<Status, Error>;
/// Charge for one gear page data loading.
fn charge_for_page_data_loading(&mut self) -> Result<Status, Error>;
/// Get the biggest page from `pages`.
fn last_page(pages: &Self::Pages) -> Option<GearPage>;
/// Apply `f` for all `pages`.
fn process_pages(
pages: Self::Pages,
process_one: impl FnMut(GearPage) -> Result<(), Error>,
) -> Result<(), Error>;
/// Drops and returns output.
fn into_output(self, ctx: &mut LazyPagesExecutionContext) -> Result<Self::Output, Error>;
}
/// Lazy-pages accesses processing main function.
/// Acts differently for signals and host functions accesses,
/// but main logic is the same:
/// It removes read and write protections for page,
/// then it loads wasm page data from storage to wasm page memory location.
/// If native page size is bigger than gear page size, then this will be done
/// for all gear pages from accessed native page.
/// 1) Set new access pages protection accordingly it is read or write access.
/// 2) If some page contains data in storage, then load this data and place it in
/// program's wasm memory.
/// 3) Charge gas for access and data loading.
pub(crate) fn process_lazy_pages<H: AccessHandler>(
rt_ctx: &mut LazyPagesRuntimeContext,
exec_ctx: &mut LazyPagesExecutionContext,
mut handler: H,
pages: H::Pages,
) -> Result<H::Output, Error> {
let wasm_mem_size = exec_ctx.wasm_mem_size.offset(rt_ctx);
unsafe {
if let Some(last_page) = H::last_page(&pages) {
// Check that all pages are inside wasm memory.
if last_page.end_offset(rt_ctx) as usize >= wasm_mem_size {
return Err(Error::OutOfWasmMemoryAccess);
}
} else {
// Accessed pages are empty - nothing to do.
return handler.into_output(exec_ctx);
}
if exec_ctx.status != Status::Normal {
H::check_status_is_gas_exceeded()?;
return handler.into_output(exec_ctx);
}
let stack_end = exec_ctx.stack_end;
let wasm_mem_addr = exec_ctx.wasm_mem_addr.ok_or(Error::WasmMemAddrIsNotSet)?;
// Returns `true` if new status is not `Normal`.
let update_status = |ctx: &mut LazyPagesExecutionContext, status| {
ctx.status = status;
// If new status is not [Status::Normal], then unprotect lazy-pages
// and continue work until the end of current wasm block. We don't care
// about future program execution correctness, because gas limit or allowance exceed.
match status {
Status::Normal => Ok(false),
Status::GasLimitExceeded => {
log::trace!(
"Gas limit or allowance exceed, so removes protection from all wasm memory \
and continues execution until the end of current wasm block"
);
mprotect::mprotect_interval(wasm_mem_addr, wasm_mem_size, true, true)
.map(|_| true)
}
}
};
let page_size = GearPage::size(rt_ctx) as usize;
let process_one = |page: GearPage| {
let page_offset = page.offset(rt_ctx);
let page_buffer_ptr = (wasm_mem_addr as *mut u8).add(page_offset as usize);
let protect_page = |prot_write| {
mprotect::mprotect_interval(page_buffer_ptr as usize, page_size, true, prot_write)
};
if page_offset < stack_end.offset(rt_ctx) {
// Nothing to do, page has r/w accesses and data is in correct state.
H::check_stack_memory_access()?;
} else if exec_ctx.is_write_accessed(page) {
// Nothing to do, page has r/w accesses and data is in correct state.
H::check_write_accessed_memory_access()?;
} else if exec_ctx.is_accessed(page) {
if handler.is_write() {
// Charges for page write access
let status = handler.charge_for_page_access(page, true)?;
if update_status(exec_ctx, status)? {
return Ok(());
}
// Sets read/write protection access for page and add page to write accessed
protect_page(true)?;
exec_ctx.set_write_accessed(page)?;
} else {
// Nothing to do, page has read accesses and data is in correct state.
H::check_read_from_accessed_memory()?;
}
} else {
// Charge for page access.
let status = handler.charge_for_page_access(page, false)?;
if update_status(exec_ctx, status)? {
return Ok(());
}
let unprotected = if rt_ctx
.page_has_data_in_storage(&mut exec_ctx.program_storage_prefix, page)
{
// Charge for page data loading from storage.
let status = handler.charge_for_page_data_loading()?;
if update_status(exec_ctx, status)? {
return Ok(());
}
// Set read/write access, in order to write page data to program memory.
protect_page(true)?;
// Load and write data to memory.
let buffer_as_slice = slice::from_raw_parts_mut(page_buffer_ptr, page_size);
if !rt_ctx.load_page_data_from_storage(
&mut exec_ctx.program_storage_prefix,
page,
buffer_as_slice,
)? {
let err_msg = format!(
"process_lazy_pages: `read` returns, that page has no data, but `exist` returns that there is one. \
Page - {page:?}, program storage prefix - {:?}",
&exec_ctx.program_storage_prefix,
);
log::error!("{err_msg}");
unreachable!("{err_msg}")
}
true
} else {
false
};
exec_ctx.set_accessed(page);
if handler.is_write() {
if !unprotected {
protect_page(true)?;
}
exec_ctx.set_write_accessed(page)?;
} else {
// Set only read access for page.
protect_page(false)?;
}
}
Ok(())
};
H::process_pages(pages, process_one)?;
handler.into_output(exec_ctx)
}
}