From dae5922f74ab377db06432f501a5cbcdcb9add32 Mon Sep 17 00:00:00 2001 From: Steven Allen Date: Fri, 8 Dec 2023 14:46:27 +0900 Subject: [PATCH] wip --- fvm/src/engine/mod.rs | 13 ++-- fvm/src/kernel/mod.rs | 5 +- fvm/src/syscalls/bind.rs | 92 +++++++++++++++++---------- fvm/src/syscalls/error.rs | 13 ++-- fvm/src/syscalls/mod.rs | 116 +++++++++++++++++----------------- testing/conformance/src/vm.rs | 5 +- 6 files changed, 137 insertions(+), 107 deletions(-) diff --git a/fvm/src/engine/mod.rs b/fvm/src/engine/mod.rs index 8fa9d3e35b..51e9459677 100644 --- a/fvm/src/engine/mod.rs +++ b/fvm/src/engine/mod.rs @@ -18,8 +18,8 @@ use fvm_wasm_instrument::gas_metering::GAS_COUNTER_NAME; use num_traits::Zero; use wasmtime::OptLevel::Speed; use wasmtime::{ - Global, GlobalType, InstanceAllocationStrategy, Linker, Memory, MemoryType, Module, Mutability, - Val, ValType, + Global, GlobalType, InstanceAllocationStrategy, Memory, MemoryType, Module, Mutability, Val, + ValType, }; use crate::gas::{Gas, GasTimer, WasmGasPrices}; @@ -28,6 +28,7 @@ use crate::machine::{Machine, NetworkConfig}; use crate::syscalls::error::Abort; use crate::syscalls::{ charge_for_exec, charge_for_init, record_init_time, update_gas_available, InvocationData, + Linker, }; use crate::Kernel; @@ -497,7 +498,7 @@ impl Engine { /// linker, syscalls, etc. /// /// This returns an `Abort` as it may need to execute initialization code, charge gas, etc. - pub fn instantiate( + pub(crate) fn instantiate( &self, store: &mut wasmtime::Store>, k: &Cid, @@ -513,9 +514,9 @@ impl Engine { .expect("invalid instance cache entry"), Vacant(e) => &mut *e .insert({ - let mut linker = Linker::new(&self.inner.engine); + let mut linker = wasmtime::Linker::new(&self.inner.engine); linker.allow_shadowing(true); - K::bind_syscalls(&mut linker).map_err(Abort::Fatal)?; + K::bind_syscalls(Linker::wrap(&mut linker)).map_err(Abort::Fatal)?; Box::new(Cache { linker }) }) .downcast_mut() @@ -595,7 +596,7 @@ impl Engine { } /// Construct a new wasmtime "store" from the given kernel. - pub fn new_store(&self, mut kernel: K) -> wasmtime::Store> { + pub(crate) fn new_store(&self, mut kernel: K) -> wasmtime::Store> { // Take a new instance and put it into a drop-guard that removes the reservation when // we're done. #[must_use] diff --git a/fvm/src/kernel/mod.rs b/fvm/src/kernel/mod.rs index 0ab18dab93..83ddfa6853 100644 --- a/fvm/src/kernel/mod.rs +++ b/fvm/src/kernel/mod.rs @@ -2,12 +2,11 @@ // SPDX-License-Identifier: Apache-2.0, MIT use ambassador::delegatable_trait; use fvm_shared::event::StampedEvent; -use wasmtime::Linker; use crate::call_manager::CallManager; use crate::machine::limiter::MemoryLimiter; use crate::machine::Machine; -use crate::syscalls::InvocationData; +use crate::syscalls::Linker; mod blocks; mod error; @@ -95,7 +94,7 @@ pub trait Kernel: GasOps + SyscallHandler + 'static { } pub trait SyscallHandler: Sized { - fn bind_syscalls(linker: &mut Linker>) -> anyhow::Result<()>; + fn bind_syscalls(linker: &mut Linker) -> anyhow::Result<()>; } /// Network-related operations. diff --git a/fvm/src/syscalls/bind.rs b/fvm/src/syscalls/bind.rs index b7c0697b27..58155f7d71 100644 --- a/fvm/src/syscalls/bind.rs +++ b/fvm/src/syscalls/bind.rs @@ -4,7 +4,7 @@ use std::mem; use fvm_shared::error::ErrorNumber; use fvm_shared::sys::SyscallSafe; -use wasmtime::{Caller, Linker, WasmTy}; +use wasmtime::{Caller, WasmTy}; use super::context::Memory; use super::error::Abort; @@ -12,23 +12,13 @@ use super::{charge_for_exec, update_gas_available, Context, InvocationData}; use crate::call_manager::backtrace; use crate::kernel::{self, ExecutionError, Kernel, SyscallError}; -/// Binds syscalls to a linker, converting the returned error according to the syscall convention: -/// -/// 1. If the error is a syscall error, it's returned as the first return value. -/// 2. If the error is a fatal error, a Trap is returned. -pub trait BindSyscall { +/// Bind a [`Syscall`] to a [`Linker`]. Import this trait then call `linker.bind(module, name, +/// your_syscall)` on your [`Linker`]. +#[repr(transparent)] +pub struct Linker(wasmtime::Linker>); +impl Linker { /// Bind a syscall to the linker. /// - /// 1. The return type will be automatically adjusted to return `Result` where - /// `u32` is the error code. - /// 2. If the return type is non-empty (i.e., not `()`), an out-pointer will be prepended to the - /// arguments for the return-value. - /// - /// By example: - /// - /// - `fn(u32) -> kernel::Result<()>` will become `fn(u32) -> Result`. - /// - `fn(u32) -> kernel::Result` will become `fn(u32, u32) -> Result`. - /// /// # Example /// /// ```ignore @@ -41,19 +31,52 @@ pub trait BindSyscall { /// let mut linker = wasmtime::Linker::new(&engine); /// linker.bind("my_module", "zero", my_module::zero); /// ``` - fn bind( + pub fn bind_syscall( &mut self, module: &'static str, name: &'static str, - syscall: Func, - ) -> anyhow::Result<&mut Self>; + syscall: impl Syscall, + ) -> anyhow::Result<&mut Self> { + syscall.bind_to(self, module, name)?; + Ok(self) + } + + /// Wrap a wasmtime Linker in our newtype. We use a newtype to hide the underlying + /// implementation. + pub(crate) fn wrap(l: &mut wasmtime::Linker>) -> &mut Self { + // SAFETY: We're transmuting to a transparent wrapper type. + unsafe { &mut *(l as *mut _ as *mut _) } + } } -/// ControlFlow is a general-purpose enum allowing us to pass syscall error up the -/// stack to the actor and treat error handling there (decide when to abort, etc). +/// A [`Syscall`] is a function in the form `fn(Context<'_, K>, I...) -> R` where: +/// +/// - `K` is the kernel type. Constrain this to the precise kernel operations you need, or even +/// a specific kernel implementation. +/// - `I...`, the syscall parameters, are 0-8 types, each one of [`u32`], [`u64`], [`i32`], or +/// [`i64`]. +/// - `R` is one of: +/// - [`kernel::Result`] or [`ControlFlow`] where `T`, the return value type, is +/// [`SyscallSafe`]. +/// - [`Abort`] for syscalls that only abort (revert) the currently running actor. +pub trait Syscall: Send + Sync + 'static { + // This is an implementation detail. See [`Linker`] for the user-facing API. + #[doc(hidden)] + fn bind_to( + self, + linker: &mut Linker, + module: &'static str, + name: &'static str, + ) -> anyhow::Result<()>; +} + +/// ControlFlow is a general-purpose enum for returning a control-flow decision from a syscall. pub enum ControlFlow { + /// Return a value to the actor. Return(T), + /// Fail with the specified syscall error. Error(SyscallError), + /// Abort the running actor (exit, out of gas, or fatal error). Abort(Abort), } @@ -67,11 +90,11 @@ impl From for ControlFlow { } } -/// The helper trait used by `BindSyscall` to convert kernel results with execution errors into +/// The helper trait used by `Syscall` to convert kernel results with execution errors into /// results that can be handled by wasmtime. See the documentation on `BindSyscall` for details. -#[doc(hidden)] pub trait IntoControlFlow: Sized { type Value: SyscallSafe; + #[doc(hidden)] fn into_control_flow(self) -> ControlFlow; } @@ -140,29 +163,29 @@ macro_rules! charge_syscall_gas { macro_rules! impl_bind_syscalls { ($($t:ident)*) => { #[allow(non_snake_case)] - impl<$($t,)* Ret, K, Func> BindSyscall<($($t,)*), Ret, Func> for Linker> + impl<$($t,)* Ret, K, Func> Syscall for Func where K: Kernel, Func: Fn(Context<'_, K> $(, $t)*) -> Ret + Send + Sync + 'static, Ret: IntoControlFlow, $($t: WasmTy+SyscallSafe,)* { - fn bind( - &mut self, + fn bind_to( + self, + linker: &mut Linker, module: &'static str, name: &'static str, - syscall: Func, - ) -> anyhow::Result<&mut Self> { + ) -> anyhow::Result<()> { if mem::size_of::() == 0 { // If we're returning a zero-sized "value", we return no value therefore and expect no out pointer. - self.func_wrap(module, name, move |mut caller: Caller<'_, InvocationData> $(, $t: $t)*| { + linker.0.func_wrap(module, name, move |mut caller: Caller<'_, InvocationData> $(, $t: $t)*| { charge_for_exec(&mut caller)?; let (mut memory, data) = memory_and_data(&mut caller); charge_syscall_gas!(data.kernel); let ctx = Context{kernel: &mut data.kernel, memory: &mut memory}; - let out = syscall(ctx $(, $t)*).into_control_flow(); + let out = self(ctx $(, $t)*).into_control_flow(); let result = match out { ControlFlow::Return(_) => { @@ -182,10 +205,10 @@ macro_rules! impl_bind_syscalls { update_gas_available(&mut caller)?; result - }) + })?; } else { // If we're returning an actual value, we need to write it back into the wasm module's memory. - self.func_wrap(module, name, move |mut caller: Caller<'_, InvocationData>, ret: u32 $(, $t: $t)*| { + linker.0.func_wrap(module, name, move |mut caller: Caller<'_, InvocationData>, ret: u32 $(, $t: $t)*| { charge_for_exec(&mut caller)?; let (mut memory, data) = memory_and_data(&mut caller); @@ -200,7 +223,7 @@ macro_rules! impl_bind_syscalls { } let ctx = Context{kernel: &mut data.kernel, memory: &mut memory}; - let result = match syscall(ctx $(, $t)*).into_control_flow() { + let result = match self(ctx $(, $t)*).into_control_flow() { ControlFlow::Return(value) => { log::trace!("syscall {}::{}: ok", module, name); unsafe { @@ -223,8 +246,9 @@ macro_rules! impl_bind_syscalls { update_gas_available(&mut caller)?; result - }) + })?; } + Ok(()) } } } diff --git a/fvm/src/syscalls/error.rs b/fvm/src/syscalls/error.rs index 1dbd81aaa2..521e358048 100644 --- a/fvm/src/syscalls/error.rs +++ b/fvm/src/syscalls/error.rs @@ -8,16 +8,21 @@ use wasmtime::Trap; use crate::call_manager::NO_DATA_BLOCK_ID; use crate::kernel::{BlockId, ExecutionError}; -/// Represents an actor "abort". +/// Represents an actor "abort". Returning an [`Abort`] from a syscall will cause the currently +/// running actor to exit and may cause part or all of the actor call stack to unwind. #[derive(Debug, thiserror::Error)] pub enum Abort { - /// The actor explicitly aborted with the given exit code (or panicked). + /// The actor explicitly aborted with the given exit code, panicked, or ran out of memory. #[error("exit with code {0} ({2})")] Exit(ExitCode, String, BlockId), - /// The actor ran out of gas. + /// The actor ran out of gas. This will unwind the actor call stack until we reach an actor + /// invocation with a gas limit _less_ than that of the caller's gas limit, or until we reach + /// the top-level call. #[error("out of gas")] OutOfGas, - /// The system failed with a fatal error. + /// The system failed with a fatal error indicating a bug in the FVM. This will abort the entire + /// top-level message and record a + /// [`SYS_ASSERTION_FAILED`][fvm_shared::ExitCode::SYS_ASSERTION_FAILED] exit code on-chain. #[error("fatal error: {0}")] Fatal(anyhow::Error), } diff --git a/fvm/src/syscalls/mod.rs b/fvm/src/syscalls/mod.rs index c06a0e81d3..58bda57e06 100644 --- a/fvm/src/syscalls/mod.rs +++ b/fvm/src/syscalls/mod.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0, MIT use anyhow::{anyhow, Context as _}; use num_traits::Zero; -use wasmtime::{AsContextMut, ExternType, Global, Linker, Module, Val}; +use wasmtime::{AsContextMut, ExternType, Global, Module, Val}; use crate::call_manager::backtrace; use crate::gas::{Gas, GasInstant, GasTimer}; @@ -18,7 +18,7 @@ use crate::{DefaultKernel, Kernel}; pub(crate) mod error; mod actor; -pub mod bind; +mod bind; mod context; mod crypto; mod debug; @@ -32,11 +32,15 @@ mod send; mod sself; mod vm; +pub use bind::{ControlFlow, Linker}; pub use context::{Context, Memory}; pub use error::Abort; +#[cfg(doc)] +pub use bind::{IntoControlFlow, Syscall}; + /// Invocation data attached to a wasm "store" and available to the syscall binding. -pub struct InvocationData { +pub(crate) struct InvocationData { /// The kernel on which this actor is being executed. pub kernel: K, @@ -66,7 +70,7 @@ pub struct InvocationData { /// Updates the global available gas in the Wasm module after a syscall, to account for any /// gas consumption that happened on the host side. -pub fn update_gas_available( +pub(crate) fn update_gas_available( ctx: &mut impl AsContextMut>, ) -> Result<(), Abort> { let mut ctx = ctx.as_context_mut(); @@ -96,7 +100,7 @@ pub fn update_gas_available( } /// Updates the FVM-side gas tracker with newly accrued execution gas charges. -pub fn charge_for_exec( +pub(crate) fn charge_for_exec( ctx: &mut impl AsContextMut>, ) -> Result<(), Abort> { let mut ctx = ctx.as_context_mut(); @@ -171,7 +175,7 @@ pub fn charge_for_exec( /// The Wasm instrumentation machinery via [fvm_wasm_instrument::gas_metering::MemoryGrowCost] /// only charges for growing the memory _beyond_ the initial amount. It's up to us to make sure /// the minimum memory is properly charged for. -pub fn charge_for_init( +pub(crate) fn charge_for_init( ctx: &mut impl AsContextMut>, module: &Module, ) -> crate::kernel::Result { @@ -195,7 +199,7 @@ pub fn charge_for_init( /// /// In practice this includes all the time elapsed since the `InvocationData` was created, /// ie. this is the first time we'll use the `last_charge_time`. -pub fn record_init_time( +pub(crate) fn record_init_time( ctx: &mut impl AsContextMut>, timer: GasTimer, ) { @@ -237,8 +241,6 @@ fn min_table_elements(module: &Module) -> Option { } } -use self::bind::BindSyscall; - impl SyscallHandler for DefaultKernel where K: Kernel @@ -253,77 +255,77 @@ where + RandomnessOps + SelfOps, { - fn bind_syscalls(linker: &mut wasmtime::Linker>) -> anyhow::Result<()> { - linker.bind("vm", "exit", vm::exit)?; - linker.bind("vm", "message_context", vm::message_context)?; - - linker.bind("network", "context", network::context)?; - linker.bind("network", "tipset_cid", network::tipset_cid)?; - - linker.bind("ipld", "block_open", ipld::block_open)?; - linker.bind("ipld", "block_create", ipld::block_create)?; - linker.bind("ipld", "block_read", ipld::block_read)?; - linker.bind("ipld", "block_stat", ipld::block_stat)?; - linker.bind("ipld", "block_link", ipld::block_link)?; - - linker.bind("self", "root", sself::root)?; - linker.bind("self", "set_root", sself::set_root)?; - linker.bind("self", "current_balance", sself::current_balance)?; - linker.bind("self", "self_destruct", sself::self_destruct)?; - - linker.bind("actor", "resolve_address", actor::resolve_address)?; - linker.bind( + fn bind_syscalls(linker: &mut Linker) -> anyhow::Result<()> { + linker.bind_syscall("vm", "exit", vm::exit)?; + linker.bind_syscall("vm", "message_context", vm::message_context)?; + + linker.bind_syscall("network", "context", network::context)?; + linker.bind_syscall("network", "tipset_cid", network::tipset_cid)?; + + linker.bind_syscall("ipld", "block_open", ipld::block_open)?; + linker.bind_syscall("ipld", "block_create", ipld::block_create)?; + linker.bind_syscall("ipld", "block_read", ipld::block_read)?; + linker.bind_syscall("ipld", "block_stat", ipld::block_stat)?; + linker.bind_syscall("ipld", "block_link", ipld::block_link)?; + + linker.bind_syscall("self", "root", sself::root)?; + linker.bind_syscall("self", "set_root", sself::set_root)?; + linker.bind_syscall("self", "current_balance", sself::current_balance)?; + linker.bind_syscall("self", "self_destruct", sself::self_destruct)?; + + linker.bind_syscall("actor", "resolve_address", actor::resolve_address)?; + linker.bind_syscall( "actor", "lookup_delegated_address", actor::lookup_delegated_address, )?; - linker.bind("actor", "get_actor_code_cid", actor::get_actor_code_cid)?; - linker.bind("actor", "next_actor_address", actor::next_actor_address)?; - linker.bind("actor", "create_actor", actor::create_actor)?; + linker.bind_syscall("actor", "get_actor_code_cid", actor::get_actor_code_cid)?; + linker.bind_syscall("actor", "next_actor_address", actor::next_actor_address)?; + linker.bind_syscall("actor", "create_actor", actor::create_actor)?; if cfg!(feature = "upgrade-actor") { // We disable/enable with the feature, but we always compile this code to ensure we don't // accidentally break it. - linker.bind("actor", "upgrade_actor", actor::upgrade_actor)?; + linker.bind_syscall("actor", "upgrade_actor", actor::upgrade_actor)?; } - linker.bind( + linker.bind_syscall( "actor", "get_builtin_actor_type", actor::get_builtin_actor_type, )?; - linker.bind( + linker.bind_syscall( "actor", "get_code_cid_for_type", actor::get_code_cid_for_type, )?; - linker.bind("actor", "balance_of", actor::balance_of)?; + linker.bind_syscall("actor", "balance_of", actor::balance_of)?; // Only wire this syscall when M2 native is enabled. if cfg!(feature = "m2-native") { - linker.bind("actor", "install_actor", actor::install_actor)?; + linker.bind_syscall("actor", "install_actor", actor::install_actor)?; } - linker.bind("crypto", "verify_signature", crypto::verify_signature)?; - linker.bind( + linker.bind_syscall("crypto", "verify_signature", crypto::verify_signature)?; + linker.bind_syscall( "crypto", "recover_secp_public_key", crypto::recover_secp_public_key, )?; - linker.bind("crypto", "hash", crypto::hash)?; + linker.bind_syscall("crypto", "hash", crypto::hash)?; - linker.bind("event", "emit_event", event::emit_event)?; + linker.bind_syscall("event", "emit_event", event::emit_event)?; - linker.bind("rand", "get_chain_randomness", rand::get_chain_randomness)?; - linker.bind("rand", "get_beacon_randomness", rand::get_beacon_randomness)?; + linker.bind_syscall("rand", "get_chain_randomness", rand::get_chain_randomness)?; + linker.bind_syscall("rand", "get_beacon_randomness", rand::get_beacon_randomness)?; - linker.bind("gas", "charge", gas::charge_gas)?; - linker.bind("gas", "available", gas::available)?; + linker.bind_syscall("gas", "charge", gas::charge_gas)?; + linker.bind_syscall("gas", "available", gas::available)?; // Ok, this singled-out syscall should probably be in another category. - linker.bind("send", "send", send::send)?; + linker.bind_syscall("send", "send", send::send)?; - linker.bind("debug", "log", debug::log)?; - linker.bind("debug", "enabled", debug::enabled)?; - linker.bind("debug", "store_artifact", debug::store_artifact)?; + linker.bind_syscall("debug", "log", debug::log)?; + linker.bind_syscall("debug", "enabled", debug::enabled)?; + linker.bind_syscall("debug", "store_artifact", debug::store_artifact)?; Ok(()) } @@ -343,39 +345,39 @@ where + RandomnessOps + SelfOps, { - fn bind_syscalls(linker: &mut Linker>) -> anyhow::Result<()> { + fn bind_syscalls(linker: &mut Linker) -> anyhow::Result<()> { DefaultKernel::::bind_syscalls(linker)?; // Bind the circulating supply call. - linker.bind( + linker.bind_syscall( "network", "total_fil_circ_supply", filecoin::total_fil_circ_supply, )?; // Now bind the crypto syscalls. - linker.bind( + linker.bind_syscall( "crypto", "compute_unsealed_sector_cid", filecoin::compute_unsealed_sector_cid, )?; - linker.bind("crypto", "verify_post", filecoin::verify_post)?; - linker.bind( + linker.bind_syscall("crypto", "verify_post", filecoin::verify_post)?; + linker.bind_syscall( "crypto", "verify_consensus_fault", filecoin::verify_consensus_fault, )?; - linker.bind( + linker.bind_syscall( "crypto", "verify_aggregate_seals", filecoin::verify_aggregate_seals, )?; - linker.bind( + linker.bind_syscall( "crypto", "verify_replica_update", filecoin::verify_replica_update, )?; - linker.bind("crypto", "batch_verify_seals", filecoin::batch_verify_seals)?; + linker.bind_syscall("crypto", "batch_verify_seals", filecoin::batch_verify_seals)?; Ok(()) } diff --git a/testing/conformance/src/vm.rs b/testing/conformance/src/vm.rs index 379bcd8321..b52fee1419 100644 --- a/testing/conformance/src/vm.rs +++ b/testing/conformance/src/vm.rs @@ -5,20 +5,19 @@ use std::sync::{Arc, Mutex}; use anyhow::anyhow; use fvm::kernel::filecoin::{DefaultFilecoinKernel, FilecoinKernel}; -use fvm::syscalls::InvocationData; use fvm::call_manager::{CallManager, DefaultCallManager}; use fvm::gas::price_list_by_network_version; use fvm::machine::limiter::MemoryLimiter; use fvm::machine::{DefaultMachine, Machine, MachineContext, Manifest, NetworkConfig}; use fvm::state_tree::StateTree; +use fvm::syscalls::Linker; use fvm_ipld_blockstore::MemoryBlockstore; use fvm_shared::consensus::ConsensusFault; use fvm_shared::piece::PieceInfo; use fvm_shared::sector::{ AggregateSealVerifyProofAndInfos, RegisteredSealProof, ReplicaUpdateInfo, SealVerifyInfo, }; -use wasmtime::Linker; // We have glob imports here because delegation doesn't work well without it. use fvm::kernel::prelude::*; @@ -238,7 +237,7 @@ impl Kernel for TestKernel { } impl SyscallHandler for TestKernel { - fn bind_syscalls(linker: &mut Linker>) -> anyhow::Result<()> { + fn bind_syscalls(linker: &mut Linker) -> anyhow::Result<()> { InnerTestKernel::bind_syscalls(linker) } }