diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index c06562cfd95..0f836b80c71 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -111,3 +111,6 @@ jobs: - name: "Check fuzzer competence with mutation test" run: ./scripts/check-fuzzer.sh + + - name: "Check lazy pages fuzzer with smoke test" + run: ./scripts/check-lazy-pages-fuzzer.sh diff --git a/.gitignore b/.gitignore index 28d7bb82e4a..9b03d8e4bc4 100644 --- a/.gitignore +++ b/.gitignore @@ -11,5 +11,7 @@ weight-dumps/ .log *.meta.txt .terraform* -utils/runtime-fuzzer/fuzz/corpus/* -utils/runtime-fuzzer/fuzz/coverage/* +utils/**/fuzz/corpus/* +utils/**/fuzz/coverage/* +utils/**/fuzz/artifacts/* +utils/**/fuzz/fuzz/* diff --git a/Cargo.lock b/Cargo.lock index 91a2f7b2ca8..9ac093c7ec4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6349,6 +6349,35 @@ dependencies = [ "smallvec", ] +[[package]] +name = "lazy-pages-fuzzer" +version = "0.1.0" +dependencies = [ + "anyhow", + "arbitrary", + "derive_more", + "gear-lazy-pages", + "gear-lazy-pages-common", + "gear-wasm-gen", + "gear-wasm-instrument", + "log", + "region", + "wasmer", + "wasmi 0.13.2", + "wasmprinter", + "wat", +] + +[[package]] +name = "lazy-pages-fuzzer-fuzz" +version = "0.1.0" +dependencies = [ + "gear-utils", + "lazy-pages-fuzzer", + "libfuzzer-sys", + "log", +] + [[package]] name = "lazy_static" version = "1.5.0" diff --git a/Cargo.toml b/Cargo.toml index a261d67604f..a37218f28ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -101,7 +101,8 @@ members = [ "runtime/*", "runtime-interface/sandbox", "utils/*", - "utils/runtime-fuzzer/fuzz" + "utils/runtime-fuzzer/fuzz", + "utils/lazy-pages-fuzzer/fuzz" ] [workspace.dependencies] @@ -272,6 +273,7 @@ wasm-smith = { version = "0.12.21", git = "https://github.com/gear-tech/wasm-too # Common executors between `sandbox-host` and `calc-stack-height` sandbox-wasmer = { package = "wasmer", version = "2.2", features = ["singlepass"] } sandbox-wasmer-types = { package = "wasmer-types", version = "2.2" } +sandbox-wasmi = { package = "wasmi", git = "https://github.com/gear-tech/wasmi", branch = "v0.13.2-sign-ext", features = ["virtual_memory"] } # Substrate deps frame-benchmarking = { version = "4.0.0-dev", git = "https://github.com/gear-tech/polkadot-sdk.git", branch = "gear-v1.3.0", default-features = false } diff --git a/lazy-pages/src/common.rs b/lazy-pages/src/common.rs index cb8e69cd430..33c78c75414 100644 --- a/lazy-pages/src/common.rs +++ b/lazy-pages/src/common.rs @@ -30,7 +30,7 @@ use std::{fmt, mem, num::NonZeroU32}; // TODO: investigate error allocations #2441 #[derive(Debug, derive_more::Display, derive_more::From)] -pub(crate) enum Error { +pub enum Error { #[display(fmt = "Accessed memory interval is out of wasm memory")] OutOfWasmMemoryAccess, #[display(fmt = "Signals cannot come from WASM program virtual stack memory")] diff --git a/lazy-pages/src/lib.rs b/lazy-pages/src/lib.rs index 4271ce8939c..bf48f89ade1 100644 --- a/lazy-pages/src/lib.rs +++ b/lazy-pages/src/lib.rs @@ -53,14 +53,15 @@ use crate::{ WasmSizeNo, SIZES_AMOUNT, }, }; -pub use common::LazyPagesVersion; +pub use common::{Error as LazyPagesError, LazyPagesVersion}; use common::{LazyPagesExecutionContext, LazyPagesRuntimeContext}; use gear_lazy_pages_common::{GlobalsAccessConfig, LazyPagesInitContext, Status}; pub use host_func::pre_process_memory_accesses; use mprotect::MprotectError; use numerated::iterators::IntervalIterator; use pages::GearPage; -use signal::{DefaultUserSignalHandler, UserSignalHandler}; +use signal::DefaultUserSignalHandler; +pub use signal::{ExceptionInfo, UserSignalHandler}; use std::{cell::RefCell, convert::TryInto, num::NonZeroU32}; /// Initialize lazy-pages once for process. @@ -377,7 +378,7 @@ pub(crate) fn reset_init_flag() { } /// Initialize lazy-pages for current thread. -fn init_with_handler( +pub fn init_with_handler( _version: LazyPagesVersion, ctx: LazyPagesInitContext, pages_storage: S, diff --git a/lazy-pages/src/signal.rs b/lazy-pages/src/signal.rs index 68b4a7f2492..b3166312fb1 100644 --- a/lazy-pages/src/signal.rs +++ b/lazy-pages/src/signal.rs @@ -28,7 +28,7 @@ use crate::{ use gear_lazy_pages_common::Status; use std::convert::TryFrom; -pub(crate) trait UserSignalHandler { +pub trait UserSignalHandler { /// # Safety /// /// It's expected handler calls syscalls to protect memory @@ -44,7 +44,7 @@ impl UserSignalHandler for DefaultUserSignalHandler { } #[derive(Debug)] -pub(crate) struct ExceptionInfo { +pub struct ExceptionInfo { /// Address where fault is occurred pub fault_addr: *const (), pub is_write: Option, diff --git a/sandbox/host/Cargo.toml b/sandbox/host/Cargo.toml index 3d466dca893..54128fee56c 100644 --- a/sandbox/host/Cargo.toml +++ b/sandbox/host/Cargo.toml @@ -20,7 +20,7 @@ thiserror.workspace = true log = { workspace = true, features = ["std"] } sandbox-wasmer.workspace = true sandbox-wasmer-types.workspace = true -wasmi = { git = "https://github.com/gear-tech/wasmi", branch = "v0.13.2-sign-ext", features = ["virtual_memory"] } +sandbox-wasmi = { workspace = true, features = ["virtual_memory"] } sp-allocator = { workspace = true, features = ["std"] } sp-wasm-interface-common = { workspace = true, features = ["std"] } gear-sandbox-env = { workspace = true, features = ["std"] } diff --git a/sandbox/host/src/error.rs b/sandbox/host/src/error.rs index b3b6615b716..28d1945e88e 100644 --- a/sandbox/host/src/error.rs +++ b/sandbox/host/src/error.rs @@ -18,8 +18,6 @@ //! Rust executor possible errors. -use wasmi; - /// Result type alias. pub type Result = std::result::Result; @@ -28,7 +26,7 @@ pub type Result = std::result::Result; #[allow(missing_docs)] pub enum Error { #[error(transparent)] - Wasmi(#[from] wasmi::Error), + Wasmi(#[from] sandbox_wasmi::Error), #[error("Sandbox error: {0}")] Sandbox(String), @@ -109,7 +107,7 @@ pub enum Error { AbortedDueToTrap(MessageWithBacktrace), } -impl wasmi::HostError for Error {} +impl sandbox_wasmi::HostError for Error {} impl From<&'static str> for Error { fn from(err: &'static str) -> Error { diff --git a/sandbox/host/src/sandbox.rs b/sandbox/host/src/sandbox.rs index b2d2fdd41ba..1cf053bb922 100644 --- a/sandbox/host/src/sandbox.rs +++ b/sandbox/host/src/sandbox.rs @@ -176,7 +176,7 @@ pub struct GuestExternals<'a> { /// Module instance in terms of selected backend enum BackendInstance { /// Wasmi module instance - Wasmi(wasmi::ModuleRef), + Wasmi(sandbox_wasmi::ModuleRef), /// Wasmer module instance Wasmer(sandbox_wasmer::Instance), diff --git a/sandbox/host/src/sandbox/wasmi_backend.rs b/sandbox/host/src/sandbox/wasmi_backend.rs index 82f1883babf..1329f8a10f9 100644 --- a/sandbox/host/src/sandbox/wasmi_backend.rs +++ b/sandbox/host/src/sandbox/wasmi_backend.rs @@ -22,11 +22,11 @@ use std::fmt; use codec::{Decode, Encode}; use gear_sandbox_env::HostError; -use sp_wasm_interface_common::{util, Pointer, ReturnValue, Value, WordSize}; -use wasmi::{ +use sandbox_wasmi::{ memory_units::Pages, ImportResolver, MemoryInstance, Module, ModuleInstance, RuntimeArgs, RuntimeValue, Trap, TrapCode, }; +use sp_wasm_interface_common::{util, Pointer, ReturnValue, Value, WordSize}; use crate::{ error::{self, Error}, @@ -48,7 +48,7 @@ impl fmt::Display for CustomHostError { } } -impl wasmi::HostError for CustomHostError {} +impl sandbox_wasmi::HostError for CustomHostError {} /// Construct trap error from specified message fn trap(msg: &'static str) -> Trap { @@ -60,32 +60,38 @@ impl ImportResolver for Imports { &self, module_name: &str, field_name: &str, - signature: &wasmi::Signature, - ) -> std::result::Result { + signature: &sandbox_wasmi::Signature, + ) -> std::result::Result { let idx = self.func_by_name(module_name, field_name).ok_or_else(|| { - wasmi::Error::Instantiation(format!("Export {}:{} not found", module_name, field_name)) + sandbox_wasmi::Error::Instantiation(format!( + "Export {}:{} not found", + module_name, field_name + )) })?; - Ok(wasmi::FuncInstance::alloc_host(signature.clone(), idx.0)) + Ok(sandbox_wasmi::FuncInstance::alloc_host( + signature.clone(), + idx.0, + )) } fn resolve_memory( &self, module_name: &str, field_name: &str, - _memory_type: &wasmi::MemoryDescriptor, - ) -> std::result::Result { + _memory_type: &sandbox_wasmi::MemoryDescriptor, + ) -> std::result::Result { let mem = self .memory_by_name(module_name, field_name) .ok_or_else(|| { - wasmi::Error::Instantiation(format!( + sandbox_wasmi::Error::Instantiation(format!( "Export {}:{} not found", module_name, field_name )) })?; let wrapper = mem.as_wasmi().ok_or_else(|| { - wasmi::Error::Instantiation(format!( + sandbox_wasmi::Error::Instantiation(format!( "Unsupported non-wasmi export {}:{}", module_name, field_name )) @@ -103,9 +109,9 @@ impl ImportResolver for Imports { &self, module_name: &str, field_name: &str, - _global_type: &wasmi::GlobalDescriptor, - ) -> std::result::Result { - Err(wasmi::Error::Instantiation(format!( + _global_type: &sandbox_wasmi::GlobalDescriptor, + ) -> std::result::Result { + Err(sandbox_wasmi::Error::Instantiation(format!( "Export {}:{} not found", module_name, field_name ))) @@ -115,9 +121,9 @@ impl ImportResolver for Imports { &self, module_name: &str, field_name: &str, - _table_type: &wasmi::TableDescriptor, - ) -> std::result::Result { - Err(wasmi::Error::Instantiation(format!( + _table_type: &sandbox_wasmi::TableDescriptor, + ) -> std::result::Result { + Err(sandbox_wasmi::Error::Instantiation(format!( "Export {}:{} not found", module_name, field_name ))) @@ -138,11 +144,11 @@ pub fn new_memory(initial: u32, maximum: Option) -> crate::error::Result Self { + fn new(memory: sandbox_wasmi::MemoryRef) -> Self { Self(memory) } } @@ -198,7 +204,7 @@ impl MemoryTransfer for MemoryWrapper { } } -impl<'a> wasmi::Externals for GuestExternals<'a> { +impl<'a> sandbox_wasmi::Externals for GuestExternals<'a> { fn invoke_index( &mut self, index: usize, @@ -339,7 +345,7 @@ pub fn instantiate( /// Invoke a function within a sandboxed module pub fn invoke( instance: &SandboxInstance, - module: &wasmi::ModuleRef, + module: &sandbox_wasmi::ModuleRef, export_name: &str, args: &[Value], sandbox_context: &mut dyn SandboxContext, @@ -352,7 +358,7 @@ pub fn invoke( .invoke_export(export_name, &args, guest_externals) .map(|result| result.map(Into::into)) .map_err(|error| { - if matches!(error, wasmi::Error::Trap(Trap::Code(TrapCode::StackOverflow))) { + if matches!(error, sandbox_wasmi::Error::Trap(Trap::Code(TrapCode::StackOverflow))) { // Panic stops process queue execution in that case. // This allows to avoid error lead to consensus failures, that must be handled // in node binaries forever. If this panic occur, then we must increase stack memory size, @@ -367,7 +373,7 @@ pub fn invoke( } /// Get global value by name -pub fn get_global(instance: &wasmi::ModuleRef, name: &str) -> Option { +pub fn get_global(instance: &sandbox_wasmi::ModuleRef, name: &str) -> Option { Some(Into::into( instance.export_by_name(name)?.as_global()?.get(), )) @@ -375,7 +381,7 @@ pub fn get_global(instance: &wasmi::ModuleRef, name: &str) -> Option { /// Set global value by name pub fn set_global( - instance: &wasmi::ModuleRef, + instance: &sandbox_wasmi::ModuleRef, name: &str, value: Value, ) -> std::result::Result, error::Error> { diff --git a/scripts/check-fuzzer.sh b/scripts/check-fuzzer.sh index ab624d796df..1350209b465 100755 --- a/scripts/check-fuzzer.sh +++ b/scripts/check-fuzzer.sh @@ -6,6 +6,7 @@ SCRIPTS="$(cd "$(dirname "$SELF")"/ && pwd)" . "$SCRIPTS"/fuzzer_consts.sh main() { + echo " >> Checking runtime fuzzer" echo " >> Getting random bytes from /dev/urandom" # Fuzzer expects a minimal input size of 350 KiB. Without providing a corpus of the same or larger # size fuzzer will stuck for a long time with trying to test the target using 0..100 bytes. diff --git a/scripts/check-lazy-pages-fuzzer.sh b/scripts/check-lazy-pages-fuzzer.sh new file mode 100755 index 00000000000..f59e0f45807 --- /dev/null +++ b/scripts/check-lazy-pages-fuzzer.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env sh + +SELF="$0" +SCRIPTS="$(cd "$(dirname "$SELF")"/ && pwd)" + +. "$SCRIPTS"/fuzzer_consts.sh + +RUN_DURATION_SECS=10 +PROCESS_NAME="lazy-pages-fuzzer-fuzz" +OUTPUT_FILE="lazy_pages_fuzz_run" + +main() { + echo " >> Checking lazy pages fuzzer" + echo " >> Getting random bytes from /dev/urandom" + # Fuzzer expects a minimal input size of 350 KiB. Without providing a corpus of the same or larger + # size fuzzer will stuck for a long time with trying to test the target using 0..100 bytes. + mkdir -p utils/lazy-pages-fuzzer/fuzz/corpus/main + dd if=/dev/urandom of=utils/lazy-pages-fuzzer/fuzz/corpus/main/check-fuzzer-bytes bs=1 count="$INITIAL_INPUT_SIZE" + + # Remove lazy pages fuzzer run file + rm -f $OUTPUT_FILE + + # Build lazy pages fuzzer + LAZY_PAGES_FUZZER_ONLY_BUILD=1 ./scripts/gear.sh test lazy-pages-fuzz + + echo " >> Running lazy pages fuzzer for ${RUN_DURATION_SECS} seconds" + + # Run lazy pages fuzzer for a few seconds + ( RUST_LOG="error,lazy_pages_fuzzer::lazy_pages=trace" RUST_BACKTRACE=1 ./scripts/gear.sh test lazy-pages-fuzz "" > $OUTPUT_FILE 2>&1 ) & \ + sleep ${RUN_DURATION_SECS} ; \ + kill -s KILL $(pidof $PROCESS_NAME) 2> /dev/null ; \ + echo " >> Lazy pages fuzzer run completed" ; + + # Trim output after SIGKILL backtrace + OUTPUT=$(sed '/SIGKILL/,$d' $OUTPUT_FILE) + + if echo $OUTPUT | grep -q 'SIG: Unprotect WASM memory at address' && \ + ! echo $OUTPUT | grep -iq "ERROR" + then + echo "Success" + exit 0 + else + echo "Failed" + exit 1 + fi +} + +main diff --git a/scripts/gear.sh b/scripts/gear.sh index dc3b0de4c78..93fd91359b8 100755 --- a/scripts/gear.sh +++ b/scripts/gear.sh @@ -302,6 +302,10 @@ case "$COMMAND" in header "Running fuzzer for runtime panic checks" run_fuzzer "$ROOT_DIR" "$1" "$2"; ;; + lazy-pages-fuzz) + header "Running lazy pages fuzzer smoke test" + run_lazy_pages_fuzzer "$ROOT_DIR" "$1" "$2"; ;; + fuzzer-tests) header "Running runtime-fuzzer crate tests" run_fuzzer_tests ;; diff --git a/scripts/src/test.sh b/scripts/src/test.sh index 1caed45c196..a5e33d354e8 100755 --- a/scripts/src/test.sh +++ b/scripts/src/test.sh @@ -102,6 +102,24 @@ run_fuzzer() { RUST_LOG="$LOG_TARGETS" cargo fuzz run --release --sanitizer=none main $CORPUS_DIR -- -rss_limit_mb=$RSS_LIMIT_MB -max_len=$MAX_LEN -len_control=0 } +run_lazy_pages_fuzzer() { + . $(dirname "$SELF")/fuzzer_consts.sh + + ROOT_DIR="$1" + CORPUS_DIR="$2" + + # Navigate to lazy pages fuzzer dir + cd $ROOT_DIR/utils/lazy-pages-fuzzer + + # Build/run fuzzer + if [ -n "$LAZY_PAGES_FUZZER_ONLY_BUILD" ] + then + cargo fuzz build --release --sanitizer=none lazy-pages-fuzzer-fuzz $CORPUS_DIR + else + cargo fuzz run --release --sanitizer=none lazy-pages-fuzzer-fuzz $CORPUS_DIR -- -rss_limit_mb=$RSS_LIMIT_MB -max_len=$MAX_LEN -len_control=0 + fi +} + run_fuzzer_tests() { # This includes property tests for runtime-fuzzer. cargo nextest run -p runtime-fuzzer diff --git a/utils/crates-io/src/handler.rs b/utils/crates-io/src/handler.rs index c457c86c805..d86b18362a2 100644 --- a/utils/crates-io/src/handler.rs +++ b/utils/crates-io/src/handler.rs @@ -187,12 +187,12 @@ mod sandbox_host { /// Replace the wasmi module to the crates-io version. pub fn patch(manifest: &mut DocumentMut) { - let Some(wasmi) = manifest["dependencies"]["wasmi"].as_inline_table_mut() else { + let Some(wasmi) = manifest["dependencies"]["sandbox-wasmi"].as_inline_table_mut() else { return; }; + wasmi.insert("package", "wasmi".into()); wasmi.insert("version", "0.13.2".into()); - wasmi.remove("branch"); - wasmi.remove("git"); + wasmi.remove("workspace"); } } diff --git a/utils/lazy-pages-fuzzer/Cargo.toml b/utils/lazy-pages-fuzzer/Cargo.toml new file mode 100644 index 00000000000..d602ef5bc6b --- /dev/null +++ b/utils/lazy-pages-fuzzer/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "lazy-pages-fuzzer" +version = "0.1.0" +authors.workspace = true +edition.workspace = true + +[dependencies] +anyhow.workspace = true +arbitrary = { workspace = true, features = ["derive"] } +derive_more.workspace = true +gear-wasm-gen.workspace = true +gear-wasm-instrument.workspace = true +gear-lazy-pages.workspace = true +gear-lazy-pages-common.workspace = true +log.workspace = true +region.workspace = true +sandbox-wasmer.workspace = true +sandbox-wasmi.workspace = true +wasmprinter.workspace = true +wat.workspace = true diff --git a/utils/lazy-pages-fuzzer/fuzz/Cargo.toml b/utils/lazy-pages-fuzzer/fuzz/Cargo.toml new file mode 100644 index 00000000000..cfeaaa77e56 --- /dev/null +++ b/utils/lazy-pages-fuzzer/fuzz/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "lazy-pages-fuzzer-fuzz" +version = "0.1.0" +authors.workspace = true +edition.workspace = true + +[package.metadata] +cargo-fuzz = true + +[dependencies] +libfuzzer-sys.workspace = true +lazy-pages-fuzzer = { path = ".." } +gear-utils.workspace = true +log.workspace = true + +[[bin]] +name = "lazy-pages-fuzzer-fuzz" +path = "fuzz_targets/main.rs" +test = false +doc = false diff --git a/utils/lazy-pages-fuzzer/fuzz/fuzz_targets/main.rs b/utils/lazy-pages-fuzzer/fuzz/fuzz_targets/main.rs new file mode 100644 index 00000000000..f892b61af57 --- /dev/null +++ b/utils/lazy-pages-fuzzer/fuzz/fuzz_targets/main.rs @@ -0,0 +1,31 @@ +// This file is part of Gear. + +// Copyright (C) 2021-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 . + +#![no_main] + +use lazy_pages_fuzzer::GeneratedModule; +use libfuzzer_sys::{fuzz_target, Corpus}; + +fuzz_target!(|generated_module: GeneratedModule<'_>| -> Corpus { + gear_utils::init_default_logger(); + + match lazy_pages_fuzzer::run(generated_module) { + Err(_) => Corpus::Reject, + Ok(_) => Corpus::Keep, + } +}); diff --git a/utils/lazy-pages-fuzzer/src/config.rs b/utils/lazy-pages-fuzzer/src/config.rs new file mode 100644 index 00000000000..2eaf40fd02d --- /dev/null +++ b/utils/lazy-pages-fuzzer/src/config.rs @@ -0,0 +1,86 @@ +// This file is part of Gear. + +// Copyright (C) 2021-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 . + +use std::num::{NonZeroU32, NonZeroUsize}; + +use gear_wasm_gen::{ + ConfigsBundle, GearWasmGeneratorConfig, InstructionKind, MemoryPagesConfig, SelectableParams, + SyscallsConfigBuilder, SyscallsInjectionTypes, +}; +use gear_wasm_instrument::{ + gas_metering::MemoryGrowCost, parity_wasm::elements::Instruction, Rules, +}; + +use crate::{ + generate::{InjectGlobalsConfig, InjectMemoryAccessesConfig}, + INITIAL_PAGES, +}; + +#[derive(Debug, Default, Clone)] +pub struct FuzzerConfigBundle { + pub memory_accesses: InjectMemoryAccessesConfig, + pub globals: InjectGlobalsConfig, +} + +impl ConfigsBundle for FuzzerConfigBundle { + fn into_parts(self) -> (GearWasmGeneratorConfig, SelectableParams) { + use InstructionKind::*; + ( + GearWasmGeneratorConfig { + memory_config: MemoryPagesConfig { + initial_size: INITIAL_PAGES, + upper_limit: None, + stack_end_page: None, + }, + syscalls_config: SyscallsConfigBuilder::new(SyscallsInjectionTypes::all_never()) + .build(), + remove_recursions: false, + ..Default::default() + }, + SelectableParams { + allowed_instructions: vec![ + Numeric, Reference, Parametric, Variable, Table, Memory, Control, + ], + max_instructions: 500, + min_funcs: NonZeroUsize::new(3).expect("non zero value"), + max_funcs: NonZeroUsize::new(5).expect("non zero value"), + }, + ) + } +} + +/// Dummy cost rules for the fuzzer +/// We don't care about the actual costs, just that they are non-zero +pub struct DummyCostRules; + +impl Rules for DummyCostRules { + fn instruction_cost(&self, _instruction: &Instruction) -> Option { + const DUMMY_COST: u32 = 13; + Some(DUMMY_COST) + } + + fn memory_grow_cost(&self) -> MemoryGrowCost { + const DUMMY_MEMORY_GROW_COST: u32 = 1242; + MemoryGrowCost::Linear(NonZeroU32::new(DUMMY_MEMORY_GROW_COST).unwrap()) + } + + fn call_per_local_cost(&self) -> u32 { + const DUMMY_COST_PER_CALL: u32 = 132; + DUMMY_COST_PER_CALL + } +} diff --git a/utils/lazy-pages-fuzzer/src/generate.rs b/utils/lazy-pages-fuzzer/src/generate.rs new file mode 100644 index 00000000000..8e8f95b4a83 --- /dev/null +++ b/utils/lazy-pages-fuzzer/src/generate.rs @@ -0,0 +1,99 @@ +// This file is part of Gear. + +// Copyright (C) 2021-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 . + +use std::fmt; + +use anyhow::{Context as _, Result}; +use arbitrary::{Arbitrary, Unstructured}; +use gear_wasm_gen::generate_gear_program_module; +use gear_wasm_instrument::{parity_wasm::elements::Module, InstrumentationBuilder}; + +use crate::{ + config::{DummyCostRules, FuzzerConfigBundle}, + MODULE_ENV, +}; + +use globals::InjectGlobals; +pub use globals::{InjectGlobalsConfig, GLOBAL_NAME_PREFIX}; +mod globals; + +use mem_accesses::InjectMemoryAccesses; +pub use mem_accesses::InjectMemoryAccessesConfig; +mod mem_accesses; + +pub struct GeneratedModule<'u> { + u: Unstructured<'u>, + module: Module, + config: FuzzerConfigBundle, +} + +impl<'u> Arbitrary<'u> for GeneratedModule<'u> { + fn arbitrary(u: &mut Unstructured<'u>) -> arbitrary::Result { + let mut u = Unstructured::new( + u.peek_bytes(u.len()) + .ok_or(arbitrary::Error::NotEnoughData)?, + ); + + let config = FuzzerConfigBundle::default(); + + Ok(GeneratedModule { + module: generate_gear_program_module(&mut u, config.clone())?, + u, + config, + }) + } +} + +impl GeneratedModule<'_> { + pub fn enhance(self) -> Result { + let GeneratedModule { module, config, u } = self; + + let (module, u) = InjectMemoryAccesses::new(u, config.memory_accesses.clone()) + .inject(module) + .context("injected memory accesses")?; + + let (module, u) = InjectGlobals::new(u, config.globals.clone()) + .inject(module) + .context("injected globals")?; + + let module = InstrumentationBuilder::new(MODULE_ENV) + .with_gas_limiter(|_| DummyCostRules) + .instrument(module) + .map_err(anyhow::Error::msg)?; + + Ok(GeneratedModule { u, module, config }) + } + + pub fn module(self) -> Module { + self.module + } +} + +impl fmt::Debug for GeneratedModule<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let module_str = wasmprinter::print_bytes( + self.module + .clone() + .into_bytes() + .expect("failed to serialize"), + ) + .expect("failed to print module"); + + write!(f, "{}", module_str) + } +} diff --git a/utils/lazy-pages-fuzzer/src/generate/globals.rs b/utils/lazy-pages-fuzzer/src/generate/globals.rs new file mode 100644 index 00000000000..7af17bb9b83 --- /dev/null +++ b/utils/lazy-pages-fuzzer/src/generate/globals.rs @@ -0,0 +1,217 @@ +// This file is part of Gear. + +// Copyright (C) 2021-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 . + +use anyhow::Result; +use arbitrary::{Arbitrary, Unstructured}; + +use gear_wasm_instrument::parity_wasm::{ + builder, + elements::{Instruction, Module}, +}; + +pub const GLOBAL_NAME_PREFIX: &str = "gear_fuzz_"; +pub const INITIAL_GLOBAL_VALUE: i64 = 0; + +#[derive(Debug, Clone)] +pub struct InjectGlobalsConfig { + pub max_global_number: usize, + pub max_access_per_func: usize, +} + +impl Default for InjectGlobalsConfig { + fn default() -> Self { + InjectGlobalsConfig { + max_global_number: 10, + max_access_per_func: 10, + } + } +} + +pub struct InjectGlobals<'u> { + unstructured: Unstructured<'u>, + config: InjectGlobalsConfig, +} + +impl<'u> InjectGlobals<'u> { + pub fn new(unstructured: Unstructured<'_>, config: InjectGlobalsConfig) -> InjectGlobals<'_> { + InjectGlobals { + unstructured, + config, + } + } + + pub fn inject(mut self, mut module: Module) -> Result<(Module, Unstructured<'u>)> { + let global_names: Vec<_> = ('a'..='z') + .take(self.config.max_global_number) + .map(|ch| format!("{GLOBAL_NAME_PREFIX}{ch}")) + .collect(); + + let mut next_global_idx = module.globals_space() as u32; + + let code_section = module + .code_section_mut() + .ok_or_else(|| anyhow::Error::msg("No code section found"))?; + + // Insert global access instructions + for function in code_section.bodies_mut() { + let count_per_func = self + .unstructured + .int_in_range(1..=self.config.max_access_per_func)?; + + for _ in 0..=count_per_func { + let array_idx = self.unstructured.choose_index(global_names.len())? as u32; + let global_idx = next_global_idx + array_idx; + + let insert_at_pos = self + .unstructured + .choose_index(function.code().elements().len())?; + let is_set = bool::arbitrary(&mut self.unstructured)?; + + let instructions = if is_set { + [ + Instruction::I64Const(self.unstructured.int_in_range(0..=i64::MAX)?), + Instruction::SetGlobal(global_idx), + ] + } else { + [Instruction::GetGlobal(global_idx), Instruction::Drop] + }; + + for instr in instructions.into_iter().rev() { + function + .code_mut() + .elements_mut() + .insert(insert_at_pos, instr.clone()); + } + } + } + + // Add global exports + let mut builder = builder::from_module(module); + for global in global_names.iter() { + builder.push_export( + builder::export() + .field(global) + .internal() + .global(next_global_idx) + .build(), + ); + builder.push_global( + builder::global() + .mutable() + .value_type() + .i64() + .init_expr(Instruction::I64Const(INITIAL_GLOBAL_VALUE)) + .build(), + ); + + next_global_idx += 1; + } + + Ok((builder.build(), self.unstructured)) + } +} + +#[cfg(test)] +mod tests { + use gear_wasm_instrument::parity_wasm::elements::Internal; + + use super::*; + + const TEST_PROGRAM_WAT: &str = r#" + (module + (func (export "main") (result i32) + i32.const 42 + ) + ) + "#; + + #[test] + fn test_inject_globals() { + let unstructured = Unstructured::new(&[0u8; 32]); + let config = InjectGlobalsConfig { + max_global_number: 3, + max_access_per_func: 3, + }; + let globals = InjectGlobals::new(unstructured, config); + + let wasm = wat::parse_str(TEST_PROGRAM_WAT).unwrap(); + let module = Module::from_bytes(wasm).unwrap(); + let (module, _) = globals.inject(module).unwrap(); + + assert_eq!(module.globals_space(), 3); + assert_eq!( + module + .export_section() + .unwrap() + .entries() + .iter() + .filter(|export| { matches!(export.internal(), Internal::Global(_)) }) + .count(), + 3 + ); + } + + #[test] + fn test_globals_modified() { + // Precomputed value of the global after running the program + const EXPECTED_GLOBAL_VALUE: i64 = 217020518514230019; + + let unstructured = Unstructured::new(&[3u8; 32]); + let config = InjectGlobalsConfig { + max_global_number: 3, + max_access_per_func: 3, + }; + let globals = InjectGlobals::new(unstructured, config); + + let wasm = wat::parse_str(TEST_PROGRAM_WAT).unwrap(); + let module = Module::from_bytes(wasm).unwrap(); + let (module, _) = globals.inject(module).unwrap(); + + let module = sandbox_wasmi::Module::from_buffer(module.into_bytes().unwrap()).unwrap(); + + let instance = + sandbox_wasmi::ModuleInstance::new(&module, &sandbox_wasmi::ImportsBuilder::default()) + .unwrap() + .assert_no_start(); + + let gear_fuzz_a: i64 = instance + .export_by_name("gear_fuzz_a") + .unwrap() + .as_global() + .unwrap() + .get() + .try_into() + .unwrap(); + assert_eq!(gear_fuzz_a, INITIAL_GLOBAL_VALUE); + + let _ = instance + .invoke_export("main", &[], &mut sandbox_wasmi::NopExternals) + .unwrap(); + + // Assert that global was modified (initially 0) + let gear_fuzz_a: i64 = instance + .export_by_name("gear_fuzz_a") + .unwrap() + .as_global() + .unwrap() + .get() + .try_into() + .unwrap(); + assert_eq!(gear_fuzz_a, EXPECTED_GLOBAL_VALUE); + } +} diff --git a/utils/lazy-pages-fuzzer/src/generate/mem_accesses.rs b/utils/lazy-pages-fuzzer/src/generate/mem_accesses.rs new file mode 100644 index 00000000000..aafef772fd2 --- /dev/null +++ b/utils/lazy-pages-fuzzer/src/generate/mem_accesses.rs @@ -0,0 +1,228 @@ +// This file is part of Gear. + +// Copyright (C) 2021-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 . + +use arbitrary::Unstructured; +use derive_more::{Display, Error, From}; +use gear_wasm_instrument::parity_wasm::elements::{External, Instruction, Module}; + +use crate::OS_PAGE_SIZE; + +#[derive(Debug, Clone)] +pub struct InjectMemoryAccessesConfig { + pub max_accesses_per_func: usize, +} + +impl Default for InjectMemoryAccessesConfig { + fn default() -> Self { + InjectMemoryAccessesConfig { + max_accesses_per_func: 10, + } + } +} + +#[derive(Debug, Display, Error, From)] +pub enum InjectMemoryAccessesError { + #[display(fmt = "No memory imports found")] + NoMemoryImports, + #[display(fmt = "No code section found")] + NoCodeSection, + #[display(fmt = "")] + Arbitrary(arbitrary::Error), +} + +// TODO: different word size accesses (#4042) +enum MemoryAccess { + ReadI32, + WriteI32, +} + +pub struct InjectMemoryAccesses<'u> { + unstructured: Unstructured<'u>, + config: InjectMemoryAccessesConfig, +} + +impl<'u> InjectMemoryAccesses<'u> { + pub fn new( + unstructured: Unstructured<'_>, + config: InjectMemoryAccessesConfig, + ) -> InjectMemoryAccesses<'_> { + InjectMemoryAccesses { + unstructured, + config, + } + } + + fn generate_access_instructions( + u: &mut Unstructured, + target_addr: usize, + ) -> Result, InjectMemoryAccessesError> { + use MemoryAccess::*; + // Dummy value to write to memory + const DUMMY_VALUE: u32 = 0xB6B6B6B6; + + Ok(match u.choose(&[ReadI32, WriteI32])? { + ReadI32 => vec![ + Instruction::I32Const(target_addr as i32), + Instruction::I32Load(0, 0), + Instruction::Drop, + ], + WriteI32 => vec![ + Instruction::I32Const(target_addr as i32), + Instruction::I32Const(DUMMY_VALUE as i32), + Instruction::I32Store(0, 0), + ], + }) + } + + pub fn inject( + mut self, + mut module: Module, + ) -> Result<(Module, Unstructured<'u>), InjectMemoryAccessesError> { + let import_section = module + .import_section() + .ok_or(InjectMemoryAccessesError::NoMemoryImports)?; + let initial_memory_limit = import_section + .entries() + .iter() + .filter_map(|import| { + if let External::Memory(import) = import.external() { + Some(import.limits().initial()) + } else { + None + } + }) + .next() + .ok_or(InjectMemoryAccessesError::NoMemoryImports)?; + + let code_section = module + .code_section_mut() + .ok_or(InjectMemoryAccessesError::NoCodeSection)?; + + for function in code_section.bodies_mut() { + let access_count = self + .unstructured + .int_in_range(1..=self.config.max_accesses_per_func)?; + + for _ in 0..=access_count { + let target_addr = self + .unstructured + .choose_index(initial_memory_limit as usize)? + .saturating_mul(OS_PAGE_SIZE); + + let code_len = function.code().elements().len(); + let insert_at_pos = self + .unstructured + .choose_index(code_len) + .ok() + .unwrap_or_default(); + + let instrs = + Self::generate_access_instructions(&mut self.unstructured, target_addr)?; + + for instr in instrs.into_iter().rev() { + function + .code_mut() + .elements_mut() + .insert(insert_at_pos, instr); + } + } + } + + Ok((module, self.unstructured)) + } +} + +#[cfg(test)] +mod tests { + use std::hash::{DefaultHasher, Hash, Hasher}; + + use super::*; + + const TEST_PROGRAM_WAT: &str = r#" + (module + (import "env" "memory" (memory 1)) + (func (export "main") (result i32) + i32.const 42 + ) + ) + "#; + + struct Resolver { + memory: sandbox_wasmi::MemoryRef, + } + + impl sandbox_wasmi::ModuleImportResolver for Resolver { + fn resolve_memory( + &self, + _field_name: &str, + _memory_type: &sandbox_wasmi::MemoryDescriptor, + ) -> Result { + Ok(self.memory.clone()) + } + } + + fn calculate_slice_hash(slice: &[u8]) -> u64 { + let mut s = DefaultHasher::new(); + for b in slice { + b.hash(&mut s); + } + s.finish() + } + + #[test] + fn test_memory_accesses() { + let unstructured = Unstructured::new(&[1u8; 32]); + let config = InjectMemoryAccessesConfig { + max_accesses_per_func: 10, + }; + + let wasm = wat::parse_str(TEST_PROGRAM_WAT).unwrap(); + let module = Module::from_bytes(wasm).unwrap(); + + let (module, _) = InjectMemoryAccesses::new(unstructured, config) + .inject(module) + .unwrap(); + + let memory = + sandbox_wasmi::MemoryInstance::alloc(sandbox_wasmi::memory_units::Pages(1), None) + .unwrap(); + + let original_mem_hash = { + let mem_slice = memory.direct_access(); + calculate_slice_hash(mem_slice.as_ref()) + }; + + let resolver = Resolver { memory }; + let imports = sandbox_wasmi::ImportsBuilder::new().with_resolver("env", &resolver); + + let module = sandbox_wasmi::Module::from_buffer(module.into_bytes().unwrap()).unwrap(); + let instance = sandbox_wasmi::ModuleInstance::new(&module, &imports) + .unwrap() + .assert_no_start(); + let _ = instance + .invoke_export("main", &[], &mut sandbox_wasmi::NopExternals) + .unwrap(); + + let mem_hash = { + let mem_slice = resolver.memory.direct_access(); + calculate_slice_hash(mem_slice.as_ref()) + }; + + assert_ne!(original_mem_hash, mem_hash); + } +} diff --git a/utils/lazy-pages-fuzzer/src/globals.rs b/utils/lazy-pages-fuzzer/src/globals.rs new file mode 100644 index 00000000000..efb03ffdddb --- /dev/null +++ b/utils/lazy-pages-fuzzer/src/globals.rs @@ -0,0 +1,68 @@ +// This file is part of Gear. + +// Copyright (C) 2021-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 . + +use std::collections::BTreeMap; + +use anyhow::Result; +use gear_wasm_instrument::parity_wasm::elements::Module; + +use crate::generate::GLOBAL_NAME_PREFIX; + +pub trait InstanceAccessGlobal { + fn set_global(&self, name: &str, value: i64) -> Result<()>; + fn get_global(&self, name: &str) -> Result; + + fn increment_global(&self, name: &str, value: i64) -> Result<()> { + let current_value = self.get_global(name)?; + self.set_global(name, current_value.saturating_add(value)) + } +} + +/// List of generated globals +pub fn globals_list(module: &Module) -> Vec { + module + .export_section() + .map(|section| { + section + .entries() + .iter() + .filter_map(|entry| { + let export_name = entry.field(); + if export_name.starts_with(GLOBAL_NAME_PREFIX) { + Some(export_name.to_string()) + } else { + None + } + }) + .collect() + }) + .unwrap_or_default() +} + +/// Get globals values from instance +pub fn get_globals( + instance: &impl InstanceAccessGlobal, + module: &Module, +) -> Result> { + let mut globals = BTreeMap::new(); + for global_name in globals_list(module) { + let value = instance.get_global(&global_name)?; + globals.insert(global_name, value); + } + Ok(globals) +} diff --git a/utils/lazy-pages-fuzzer/src/lazy_pages.rs b/utils/lazy-pages-fuzzer/src/lazy_pages.rs new file mode 100644 index 00000000000..ec1e9832f82 --- /dev/null +++ b/utils/lazy-pages-fuzzer/src/lazy_pages.rs @@ -0,0 +1,235 @@ +// This file is part of Gear. + +// Copyright (C) 2021-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 . + +use std::{ + cell::RefCell, + collections::{BTreeMap, HashMap}, + mem, + ops::Range, + ptr, +}; + +use gear_lazy_pages::{ + ExceptionInfo, LazyPagesError as Error, LazyPagesVersion, UserSignalHandler, +}; +use gear_lazy_pages_common::LazyPagesInitContext; +use gear_wasm_instrument::GLOBAL_NAME_GAS; + +use crate::{globals::InstanceAccessGlobal, OS_PAGE_SIZE}; + +pub type HostPageAddr = usize; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TouchedPage { + pub write: bool, + pub read: bool, +} + +impl TouchedPage { + fn update(&mut self, other: &Self) { + self.write |= other.write; + self.read |= other.read; + } +} + +pub struct FuzzerLazyPagesContext { + pub memory_range: Range, + pub instance: Box, + pub pages: HashMap, + pub globals_list: Vec, +} + +thread_local! { + static FUZZER_LP_CONTEXT: RefCell> = const { RefCell::new(None) }; +} + +pub fn init_fuzzer_lazy_pages(init: FuzzerLazyPagesContext) { + const PROGRAM_STORAGE_PREFIX: [u8; 32] = *b"dummydummydummydummydummydummydu"; + + let mem_range = init.memory_range.clone(); + + FUZZER_LP_CONTEXT.with(|ctx: &RefCell>| { + *ctx.borrow_mut() = Some(init); + }); + + unsafe { + mprotect_interval( + mem_range.start, + mem_range.end - mem_range.start, + false, + false, + false, + ) + .expect("failed to protect memory") + } + + gear_lazy_pages::init_with_handler::( + LazyPagesVersion::Version1, + LazyPagesInitContext::new(PROGRAM_STORAGE_PREFIX), + (), + ) + .expect("Failed to init lazy-pages"); +} + +pub fn get_touched_pages() -> BTreeMap)> { + let pages = FUZZER_LP_CONTEXT.with(|ctx: &RefCell>| { + let mut borrow = ctx.borrow_mut(); + let ctx = borrow.as_mut().expect("lazy pages initialized"); + mem::take(&mut ctx.pages) + }); + + pages + .into_iter() + .map(|(addr, page)| { + let mut data = vec![0; OS_PAGE_SIZE]; + + // Unprotect page for read + if !page.read { + unsafe { + mprotect_interval(addr, OS_PAGE_SIZE, true, false, false) + .expect("unprotect page"); + } + } + + // SAFETY: these pages still allocated by VM and not freed. + unsafe { + ptr::copy_nonoverlapping(addr as *const u8, data.as_mut_ptr(), OS_PAGE_SIZE); + } + + (addr, (page, data)) + }) + .collect() +} + +struct FuzzerLazyPagesSignalHandler; + +impl UserSignalHandler for FuzzerLazyPagesSignalHandler { + unsafe fn handle(info: ExceptionInfo) -> std::result::Result<(), Error> { + log::debug!("Interrupted, exception info = {:?}", info); + FUZZER_LP_CONTEXT.with(|ctx| { + let mut borrow = ctx.borrow_mut(); + let ctx = borrow.as_mut().ok_or_else(|| Error::WasmMemAddrIsNotSet)?; + user_signal_handler_internal(ctx, info) + }) + } +} + +fn user_signal_handler_internal( + ctx: &mut FuzzerLazyPagesContext, + info: ExceptionInfo, +) -> Result<(), Error> { + let native_addr = info.fault_addr as usize; + let is_write = info.is_write.ok_or_else(|| Error::ReadOrWriteIsUnknown)?; + let wasm_mem_range = &ctx.memory_range; + + if !wasm_mem_range.contains(&native_addr) { + return Err(Error::OutOfWasmMemoryAccess); + } + + log::trace!( + "SIG: Unprotect WASM memory at address: {:#x}, wr: {is_write}", + native_addr + ); + + // On read, simulate data load to memory page + if !is_write { + unsafe { + simulate_data_load(native_addr); + } + } + + unsafe { + // In case of write access, unprotect page for write and protect for read (and vice versa) + mprotect_interval(native_addr, OS_PAGE_SIZE, !is_write, is_write, false) + .expect("mprotect succeeded"); + } + + // Update touched pages + let page = TouchedPage { + write: is_write, + read: !is_write, + }; + ctx.pages + .entry(native_addr) + .and_modify(|prev_access| { + prev_access.update(&page); + }) + .or_insert(page); + + // Increment gas global + ctx.instance + .increment_global(GLOBAL_NAME_GAS, 100) + .expect("gas global exists"); + + // Increment generated globals + for global in &ctx.globals_list { + //TODO: add some randomness to global update process + ctx.instance + .increment_global(global, -42) + .expect("global exists"); + } + + Ok(()) +} + +/// `mprotect` native memory interval [`addr`, `addr` + `size`]. +/// Protection mask is set according to protection arguments. +unsafe fn mprotect_interval( + addr: usize, + size: usize, + allow_read: bool, + allow_write: bool, + allow_exec: bool, +) -> Result<(), Box> { + if size == 0 { + panic!("zero size is restricted for mprotect"); + } + + let mut mask = region::Protection::NONE; + if allow_read { + mask |= region::Protection::READ; + } + if allow_write { + mask |= region::Protection::WRITE; + } + if allow_exec { + mask |= region::Protection::EXECUTE; + } + region::protect(addr as *mut (), size, mask)?; + log::trace!("mprotect interval: {addr:#x}, size: {size:#x}, mask: {mask}"); + Ok(()) +} + +// Simulate data load to memory page. +unsafe fn simulate_data_load(addr: usize) { + const DUMMY_BYTE: u8 = 0xA5; + // SAFETY: these pages still allocated by VM and not freed. + unsafe { + mprotect_interval(addr, OS_PAGE_SIZE, true, true, false).expect("mprotect succeeded"); + memset(addr as *mut u8, DUMMY_BYTE, OS_PAGE_SIZE); + } +} + +// Set memory region to specific value. +unsafe fn memset(addr: *mut u8, value: u8, size: usize) { + let mut addr = addr; + for _ in 0..size { + *addr = value; + addr = addr.add(1); + } +} diff --git a/utils/lazy-pages-fuzzer/src/lib.rs b/utils/lazy-pages-fuzzer/src/lib.rs new file mode 100644 index 00000000000..e704888e393 --- /dev/null +++ b/utils/lazy-pages-fuzzer/src/lib.rs @@ -0,0 +1,102 @@ +// This file is part of Gear. + +// Copyright (C) 2021-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 . + +use std::collections::BTreeMap; + +use anyhow::Result; +use gear_wasm_instrument::parity_wasm::elements::Module; + +mod config; + +pub use generate::GeneratedModule; +mod generate; + +mod globals; + +use lazy_pages::{HostPageAddr, TouchedPage}; +mod lazy_pages; + +use wasmer_backend::WasmerRunner; +mod wasmer_backend; + +use wasmi_backend::WasmiRunner; +mod wasmi_backend; + +const INITIAL_PAGES: u32 = 10; +const WASM_PAGE_SIZE: usize = 0x10_000; +const PROGRAM_GAS: i64 = 1_000_000; +const OS_PAGE_SIZE: usize = 4096; +const MODULE_ENV: &str = "env"; + +trait Runner { + fn run(module: &Module) -> Result; +} + +/// Runs all the fuzz testing internal machinery. +pub fn run(generated_module: GeneratedModule) -> Result<()> { + let module = generated_module.enhance()?.module(); + + let unwrap_error_chain = |res| { + match res { + Ok(res) => res, + Err(e) => { + // Print whole error chain with '#' formatter + panic!("{:#?}", e) + } + } + }; + + let wasmer_res = unwrap_error_chain(WasmerRunner::run(&module)); + let wasmi_res = unwrap_error_chain(WasmiRunner::run(&module)); + + RunResult::verify_equality(wasmer_res, wasmi_res); + + Ok(()) +} + +struct RunResult { + gas_global: i64, + pages: BTreeMap)>, + globals: BTreeMap, +} + +impl RunResult { + fn verify_equality(wasmer_res: Self, wasmi_res: Self) { + assert_eq!(wasmer_res.gas_global, wasmi_res.gas_global); + assert_eq!(wasmer_res.pages.len(), wasmi_res.pages.len()); + + for ( + (wasmer_addr, (wasmer_page_info, wasmer_page_mem)), + (wasmi_addr, (wasmi_page_info, wasmi_page_mem)), + ) in wasmer_res + .pages + .into_iter() + .zip(wasmi_res.pages.into_iter()) + { + let lower_bytes_page_mask = ((INITIAL_PAGES as usize) * WASM_PAGE_SIZE) - 1; + assert_eq!( + lower_bytes_page_mask & wasmer_addr, + lower_bytes_page_mask & wasmi_addr + ); + assert_eq!(wasmer_page_info, wasmi_page_info); + assert_eq!(wasmer_page_mem, wasmi_page_mem); + } + + assert_eq!(wasmer_res.globals, wasmi_res.globals); + } +} diff --git a/utils/lazy-pages-fuzzer/src/wasmer_backend.rs b/utils/lazy-pages-fuzzer/src/wasmer_backend.rs new file mode 100644 index 00000000000..e142aa5bdd9 --- /dev/null +++ b/utils/lazy-pages-fuzzer/src/wasmer_backend.rs @@ -0,0 +1,123 @@ +// This file is part of Gear. + +// Copyright (C) 2021-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 . + +use anyhow::{bail, Context, Result}; + +use gear_wasm_gen::SyscallName; +use gear_wasm_instrument::{parity_wasm::elements::Module, GLOBAL_NAME_GAS}; +use sandbox_wasmer::{ + Exports, Extern, Function, FunctionType, ImportObject, Instance, Memory, MemoryType, + Module as WasmerModule, RuntimeError, Singlepass, Store, Type, Universal, Val, +}; + +use crate::{ + globals::{get_globals, globals_list, InstanceAccessGlobal}, + lazy_pages::{self, FuzzerLazyPagesContext}, + RunResult, Runner, INITIAL_PAGES, MODULE_ENV, PROGRAM_GAS, +}; + +impl InstanceAccessGlobal for Instance { + fn set_global(&self, name: &str, value: i64) -> Result<()> { + let global = self.exports.get_global(name)?; + global.set(Val::I64(value))?; + Ok(()) + } + + fn get_global(&self, name: &str) -> Result { + let global = self.exports.get_global(name)?; + let Val::I64(v) = global.get() else { + bail!("global is not an i64") + }; + + Ok(v) + } +} + +pub struct WasmerRunner; + +impl Runner for WasmerRunner { + fn run(module: &Module) -> Result { + let compiler = Singlepass::default(); + let store = Store::new(&Universal::new(compiler).engine()); + + let wasmer_module = WasmerModule::new( + &store, + module.clone().into_bytes().map_err(anyhow::Error::msg)?, + )?; + + let ty = MemoryType::new(INITIAL_PAGES, None, false); + let m = Memory::new(&store, ty).context("memory allocated")?; + let mem_ptr = m.data_ptr() as usize; + let mem_size = m.data_size() as usize; + let memory = Extern::Memory(m); + + let mut exports = Exports::new(); + exports.insert("memory".to_string(), memory.clone()); + + let host_function_signature = FunctionType::new(vec![Type::I32], vec![]); + let host_function = Function::new(&store, &host_function_signature, |_args| { + Err(RuntimeError::user("out of gas".into())) + }); + + exports.insert( + SyscallName::SystemBreak.to_str(), + Extern::Function(host_function), + ); + + let mut imports = ImportObject::new(); + imports.register(MODULE_ENV, exports); + + let instance = match Instance::new(&wasmer_module, &imports) { + Ok(instance) => instance, + err @ Err(_) => err?, + }; + + lazy_pages::init_fuzzer_lazy_pages(FuzzerLazyPagesContext { + instance: Box::new(instance.clone()), + memory_range: mem_ptr..(mem_ptr + mem_size), + pages: Default::default(), + globals_list: globals_list(module), + }); + + instance + .set_global(GLOBAL_NAME_GAS, PROGRAM_GAS) + .context("failed to set gas")?; + + let init_fn = instance + .exports + .get_function("init") + .context("init function")?; + + match init_fn.call(&[]) { + Ok(_) => {} + Err(e) => { + if e.message().contains("out of gas") { + log::info!("out of gas"); + } else { + Err(e)? + } + } + } + + Ok(RunResult { + gas_global: instance.get_global(GLOBAL_NAME_GAS)?, + pages: lazy_pages::get_touched_pages(), + globals: get_globals(&instance, module).context("failed to get globals")?, + }) + } +} diff --git a/utils/lazy-pages-fuzzer/src/wasmi_backend.rs b/utils/lazy-pages-fuzzer/src/wasmi_backend.rs new file mode 100644 index 00000000000..c03403fa270 --- /dev/null +++ b/utils/lazy-pages-fuzzer/src/wasmi_backend.rs @@ -0,0 +1,163 @@ +// This file is part of Gear. + +// Copyright (C) 2021-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 . + +use anyhow::{bail, Context}; + +use gear_wasm_gen::SyscallName; +use gear_wasm_instrument::{parity_wasm::elements::Module, GLOBAL_NAME_GAS}; +use sandbox_wasmi::{ + memory_units::Pages, ExternVal, FuncInstance, FuncRef, ImportsBuilder, MemoryInstance, + MemoryRef, Module as WasmiModule, ModuleImportResolver, ModuleInstance, ModuleRef, + RuntimeValue, Trap, TrapCode, ValueType, +}; + +use crate::{ + globals::{get_globals, globals_list, InstanceAccessGlobal}, + lazy_pages::{self, FuzzerLazyPagesContext}, + RunResult, Runner, INITIAL_PAGES, MODULE_ENV, PROGRAM_GAS, +}; + +use error::CustomHostError; +mod error; + +struct Resolver { + memory: MemoryRef, +} + +impl ModuleImportResolver for Resolver { + fn resolve_func( + &self, + field_name: &str, + _signature: &sandbox_wasmi::Signature, + ) -> Result { + if field_name == SyscallName::SystemBreak.to_str() { + Ok(FuncInstance::alloc_host( + sandbox_wasmi::Signature::new([ValueType::I32].as_slice(), None), + 0, + )) + } else { + Err(sandbox_wasmi::Error::Instantiation(format!( + "Export '{field_name}' not found" + ))) + } + } + + fn resolve_memory( + &self, + _field_name: &str, + _memory_type: &sandbox_wasmi::MemoryDescriptor, + ) -> Result { + Ok(self.memory.clone()) + } +} + +struct Externals { + gr_system_break_idx: usize, +} + +impl sandbox_wasmi::Externals for Externals { + fn invoke_index( + &mut self, + index: usize, + _args: sandbox_wasmi::RuntimeArgs, + ) -> Result, sandbox_wasmi::Trap> { + Err(if index == self.gr_system_break_idx { + sandbox_wasmi::Trap::host(CustomHostError::from("out of gas")) + } else { + TrapCode::Unreachable.into() + }) + } +} + +impl InstanceAccessGlobal for ModuleRef { + fn set_global(&self, name: &str, value: i64) -> anyhow::Result<()> { + let Some(ExternVal::Global(global)) = self.export_by_name(name) else { + bail!("global '{name}' not found"); + }; + + Ok(global.set(RuntimeValue::I64(value))?) + } + + fn get_global(&self, name: &str) -> anyhow::Result { + let Some(ExternVal::Global(global)) = self.export_by_name(name) else { + bail!("global '{name}' not found"); + }; + + let RuntimeValue::I64(v) = global.get() else { + bail!("global is not an i64"); + }; + + Ok(v) + } +} + +pub struct WasmiRunner; + +impl Runner for WasmiRunner { + fn run(module: &Module) -> anyhow::Result { + let wasmi_module = + WasmiModule::from_buffer(module.clone().into_bytes().map_err(anyhow::Error::msg)?) + .context("failed to load wasm")?; + + let memory = MemoryInstance::alloc(Pages(INITIAL_PAGES as usize), None) + .context("failed to allocate memory")?; + + let mem_ptr = memory.direct_access().as_ref().as_ptr() as usize; + let mem_size = memory.direct_access().as_ref().len(); + + let resolver = Resolver { memory }; + let imports = ImportsBuilder::new().with_resolver(MODULE_ENV, &resolver); + + let instance = ModuleInstance::new(&wasmi_module, &imports) + .context("failed to instantiate wasm module")? + .assert_no_start(); + + instance + .set_global(GLOBAL_NAME_GAS, PROGRAM_GAS) + .context("failed to set gas")?; + + lazy_pages::init_fuzzer_lazy_pages(FuzzerLazyPagesContext { + instance: Box::new(instance.clone()), + memory_range: mem_ptr..(mem_ptr + mem_size), + pages: Default::default(), + globals_list: globals_list(module), + }); + + if let Err(error) = instance.invoke_export( + "init", + &[], + &mut Externals { + gr_system_break_idx: 0, + }, + ) { + if let sandbox_wasmi::Error::Trap(Trap::Host(_)) = error { + log::info!("out of gas"); + } else { + Err(error)?; + } + } + + let result = RunResult { + gas_global: instance.get_global(GLOBAL_NAME_GAS)?, + pages: lazy_pages::get_touched_pages(), + globals: get_globals(&instance, module).context("failed to get globals")?, + }; + + Ok(result) + } +} diff --git a/utils/lazy-pages-fuzzer/src/wasmi_backend/error.rs b/utils/lazy-pages-fuzzer/src/wasmi_backend/error.rs new file mode 100644 index 00000000000..1369aecaea0 --- /dev/null +++ b/utils/lazy-pages-fuzzer/src/wasmi_backend/error.rs @@ -0,0 +1,37 @@ +// This file is part of Gear. + +// Copyright (C) 2021-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 . + +use derive_more::Display; +use sandbox_wasmi::HostError; + +#[derive(Debug, Display)] +#[display(fmt = "{message}")] +pub struct CustomHostError { + message: String, +} + +impl HostError for CustomHostError {} + +impl From for CustomHostError +where + T: Into, +{ + fn from(s: T) -> CustomHostError { + Self { message: s.into() } + } +}