Skip to content

Commit

Permalink
Switch to simpler fuel APIs
Browse files Browse the repository at this point in the history
In an effort to simplify the many fuel related APIs, simplify the
interface here to a single counter with get and set methods.
Additionally the async yield is reduced to an interval of the total fuel
instead of injecting fuel, so it's easy to still reason about how much
fuel is left even with yielding turned on.

Internally this works by keeping two counters - one the VM uses to
increment towards 0 for fuel, the other to track how much is in
"reserve". Then when we're out of gas, we pull from the reserve to
refuel and continue. We use the reserve in two cases: one for overflow
of the fuel (which is an i64 and the API expresses fuel as u64) and the
other for async yieling, which then the yield interval acts as a cap to
how much we can refuel with.

This also means that `get_fuel` can return the full range of `u64`
before this change it could only return up to `i64::MAX`. This is
important because this PR is removing the functionality to track fuel
consumption, and this makes the API less error prone for embedders to
track consumption themselves.

Careful to note that the VM counter that is stored as `i64` can be
positive if an instruction "costs" multiple units of fuel when the fuel
ran out.

prtest:full

Signed-off-by: Tyler Rockwood <rockwood@redpanda.com>
  • Loading branch information
rockwotj committed Oct 23, 2023
1 parent f24abd2 commit eac17cd
Show file tree
Hide file tree
Showing 19 changed files with 355 additions and 499 deletions.
2 changes: 1 addition & 1 deletion crates/bench-api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -522,7 +522,7 @@ impl BenchState {
store.set_epoch_deadline(1);
}
if let Some(fuel) = self.fuel {
store.add_fuel(fuel).unwrap();
store.set_fuel(fuel).unwrap();
}

let instance = self.linker.instantiate(&mut store, &module)?;
Expand Down
31 changes: 18 additions & 13 deletions crates/c-api/include/wasmtime/async.h
Original file line number Diff line number Diff line change
Expand Up @@ -71,19 +71,24 @@ WASMTIME_CONFIG_PROP(void, async_support, bool)
WASMTIME_CONFIG_PROP(void, async_stack_size, uint64_t)

/**
* \brief Configures a Store to yield execution of async WebAssembly code periodically.
* \brief Configures a Store to yield execution of async WebAssembly code
* periodically.
*
* When a Store is configured to consume fuel with `wasmtime_config_consume_fuel`
* this method will configure what happens when fuel runs out. Specifically executing
* WebAssembly will be suspended and control will be yielded back to the caller.
* When a Store is configured to consume fuel with
* `wasmtime_config_consume_fuel` this method will configure what happens when
* fuel runs out. Specifically executing WebAssembly will be suspended and
* control will be yielded back to the caller.
*
* This is only suitable with use of a store associated with an async config because
* only then are futures used and yields are possible.
* This is only suitable with use of a store associated with an async config
* because only then are futures used and yields are possible.
*
* \param context the context for the store to configure.
* \param interval the amount of fuel at which to yield. A value of 0 will
* disable yielding.
*/
WASM_API_EXTERN void wasmtime_context_out_of_fuel_async_yield(
wasmtime_context_t *context,
uint64_t injection_count,
uint64_t fuel_to_inject);
WASM_API_EXTERN void
wasmtime_context_fuel_async_yield_interval(wasmtime_context_t *context,
uint64_t interval);

/**
* \brief Configures epoch-deadline expiration to yield to the async caller and the update the deadline.
Expand All @@ -94,7 +99,7 @@ WASM_API_EXTERN void wasmtime_context_out_of_fuel_async_yield(
* See the Rust documentation for more:
* https://docs.wasmtime.dev/api/wasmtime/struct.Store.html#method.epoch_deadline_async_yield_and_update
*/
WASM_API_EXTERN void wasmtime_context_epoch_deadline_async_yield_and_update(
WASM_API_EXTERN wasmtime_error_t* wasmtime_context_epoch_deadline_async_yield_and_update(
wasmtime_context_t *context,
uint64_t delta);

Expand Down Expand Up @@ -170,8 +175,8 @@ typedef struct wasmtime_call_future wasmtime_call_future_t;
* called again for a given future.
*
* This function returns false if execution has yielded either due to being out of fuel
* (see wasmtime_store_out_of_fuel_async_yield), or the epoch has been incremented enough
* (see wasmtime_store_epoch_deadline_async_yield_and_update). The function may also return false if
* (see wasmtime_context_fuel_async_yield_interval), or the epoch has been incremented enough
* (see wasmtime_context_epoch_deadline_async_yield_and_update). The function may also return false if
* asynchronous host functions have been called, which then calling this function will call the
* continuation from the async host function.
*
Expand Down
62 changes: 6 additions & 56 deletions crates/c-api/include/wasmtime/store.h
Original file line number Diff line number Diff line change
Expand Up @@ -147,25 +147,6 @@ WASM_API_EXTERN void wasmtime_context_set_data(wasmtime_context_t* context, void
*/
WASM_API_EXTERN void wasmtime_context_gc(wasmtime_context_t* context);

/**
* \brief Adds fuel to this context's store for wasm to consume while executing.
*
* For this method to work fuel consumption must be enabled via
* #wasmtime_config_consume_fuel_set. By default a store starts with 0 fuel
* for wasm to execute with (meaning it will immediately trap).
* This function must be called for the store to have
* some fuel to allow WebAssembly to execute.
*
* Note by default when fuel is entirely consumed it will cause
* wasm to trap. If async_support is enabled, you can use
* #wasmtime_context_out_of_fuel_async_yield and the async APIs to yield
* execution instead.
*
* If fuel is not enabled within this store then an error is returned. If fuel
* is successfully added then NULL is returned.
*/
WASM_API_EXTERN wasmtime_error_t *wasmtime_context_add_fuel(wasmtime_context_t *store, uint64_t fuel);

/**
* \brief Set fuel to this context's store for wasm to consume while executing.
*
Expand All @@ -175,55 +156,24 @@ WASM_API_EXTERN wasmtime_error_t *wasmtime_context_add_fuel(wasmtime_context_t *
* This function must be called for the store to have
* some fuel to allow WebAssembly to execute.
*
* Note by default when fuel is entirely consumed it will cause
* wasm to trap. If async_support is enabled, you can use
* #wasmtime_context_out_of_fuel_async_yield and the async APIs to yield
* execution instead.
* Note that when fuel is entirely consumed it will cause wasm to trap.
*
* If fuel is not enabled within this store then an error is returned. If fuel
* is successfully added then NULL is returned.
*/
WASM_API_EXTERN wasmtime_error_t *wasmtime_context_reset_fuel(wasmtime_context_t *store, uint64_t fuel);
WASM_API_EXTERN wasmtime_error_t *wasmtime_context_set_fuel(wasmtime_context_t *store, uint64_t fuel);

/**
* \brief Returns the amount of fuel consumed by this context's store execution
* so far.
* \brief Returns the amount of fuel remaining in this context's store.
*
* If fuel consumption is not enabled via #wasmtime_config_consume_fuel_set
* then this function will return false. Otherwise true is returned and the
* then this function will return an error. Otherwise `NULL` is returned and the
* fuel parameter is filled in with fuel consumed so far.
*
* Also note that fuel, if enabled, must be originally configured via
* #wasmtime_context_add_fuel.
*/
WASM_API_EXTERN bool wasmtime_context_fuel_consumed(const wasmtime_context_t *context, uint64_t *fuel);

/**
* \brief Returns the amount of fuel remaining in this context's store execution
* before engine traps execution.
*
* If fuel consumption is not enabled via #wasmtime_config_consume_fuel_set
* then this function will return false. Otherwise true is returned and the
* fuel parameter is filled in with remaining fuel.
*
* Also note that fuel, if enabled, must be originally configured via
* #wasmtime_context_add_fuel.
*/
WASM_API_EXTERN bool wasmtime_context_fuel_remaining(const wasmtime_context_t *context, uint64_t *fuel);

/**
* \brief Attempt to manually consume fuel from the store.
*
* If fuel consumption is not enabled via #wasmtime_config_consume_fuel_set then
* this function will return an error. Otherwise this will attempt to consume
* the specified amount of `fuel` from the store. If successful the remaining
* amount of fuel is stored into `remaining`. If `fuel` couldn't be consumed
* then an error is returned.
*
* Also note that fuel, if enabled, must be originally configured via
* #wasmtime_context_add_fuel.
* #wasmtime_context_set_fuel.
*/
WASM_API_EXTERN wasmtime_error_t *wasmtime_context_consume_fuel(wasmtime_context_t *context, uint64_t fuel, uint64_t *remaining);
WASM_API_EXTERN wasmtime_error_t* wasmtime_context_get_fuel(const wasmtime_context_t *context, uint64_t *fuel);

/**
* \brief Configures WASI state within the specified store.
Expand Down
13 changes: 8 additions & 5 deletions crates/c-api/src/async.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::ffi::c_void;
use std::future::Future;
use std::mem::{self, MaybeUninit};
use std::num::NonZeroU64;
use std::ops::Range;
use std::pin::Pin;
use std::sync::Arc;
Expand Down Expand Up @@ -36,12 +37,14 @@ pub extern "C" fn wasmtime_context_epoch_deadline_async_yield_and_update(
}

#[no_mangle]
pub extern "C" fn wasmtime_context_out_of_fuel_async_yield(
pub extern "C" fn wasmtime_context_fuel_async_yield_interval(
mut store: CStoreContextMut<'_>,
injection_count: u64,
fuel_to_inject: u64,
) {
store.out_of_fuel_async_yield(injection_count, fuel_to_inject);
interval: Option<NonZeroU64>,
) -> Option<Box<wasmtime_error_t>> {
handle_result(
store.fuel_async_yield_interval(interval.map(|n| n.get())),
|()| {},
)
}

pub type wasmtime_func_async_callback_t = extern "C" fn(
Expand Down
46 changes: 6 additions & 40 deletions crates/c-api/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,54 +209,20 @@ pub extern "C" fn wasmtime_context_gc(mut context: CStoreContextMut<'_>) {
}

#[no_mangle]
pub extern "C" fn wasmtime_context_add_fuel(
pub extern "C" fn wasmtime_context_set_fuel(
mut store: CStoreContextMut<'_>,
fuel: u64,
) -> Option<Box<wasmtime_error_t>> {
crate::handle_result(store.add_fuel(fuel), |()| {})
crate::handle_result(store.set_fuel(fuel), |()| {})
}

#[no_mangle]
pub extern "C" fn wasmtime_context_reset_fuel(
mut store: CStoreContextMut<'_>,
fuel: u64,
) -> Option<Box<wasmtime_error_t>> {
crate::handle_result(store.reset_fuel(fuel), |()| {})
}

#[no_mangle]
pub extern "C" fn wasmtime_context_fuel_consumed(store: CStoreContext<'_>, fuel: &mut u64) -> bool {
match store.fuel_consumed() {
Some(amt) => {
*fuel = amt;
true
}
None => false,
}
}

#[no_mangle]
pub extern "C" fn wasmtime_context_fuel_remaining(
store: CStoreContextMut<'_>,
pub extern "C" fn wasmtime_context_get_fuel(
store: CStoreContext<'_>,
fuel: &mut u64,
) -> bool {
match store.fuel_remaining() {
Some(remaining) => {
*fuel = remaining;
true
}
None => false,
}
}

#[no_mangle]
pub extern "C" fn wasmtime_context_consume_fuel(
mut store: CStoreContextMut<'_>,
fuel: u64,
remaining_fuel: &mut u64,
) -> Option<Box<wasmtime_error_t>> {
crate::handle_result(store.consume_fuel(fuel), |remaining| {
*remaining_fuel = remaining;
crate::handle_result(store.get_fuel(), |amt| {
*fuel = amt;
})
}

Expand Down
2 changes: 1 addition & 1 deletion crates/fuzzing/src/generators/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ impl Config {
pub fn configure_store(&self, store: &mut Store<StoreLimits>) {
store.limiter(|s| s as &mut dyn wasmtime::ResourceLimiter);
if self.wasmtime.consume_fuel {
store.add_fuel(u64::max_value()).unwrap();
store.set_fuel(u64::MAX).unwrap();
}
if self.wasmtime.epoch_interruption {
// Without fuzzing of async execution, we can't test the
Expand Down
18 changes: 2 additions & 16 deletions crates/fuzzing/src/oracles.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ pub fn instantiate(wasm: &[u8], known_valid: bool, config: &generators::Config,

let mut timeout_state = SignalOnDrop::default();
match timeout {
Timeout::Fuel(fuel) => set_fuel(&mut store, fuel),
Timeout::Fuel(fuel) => store.set_fuel(fuel).unwrap(),

// If a timeout is requested then we spawn a helper thread to wait for
// the requested time and then send us a signal to get interrupted. We
Expand Down Expand Up @@ -599,7 +599,7 @@ pub fn table_ops(
{
fuzz_config.wasmtime.consume_fuel = true;
let mut store = fuzz_config.to_store();
set_fuel(&mut store, 1_000);
store.set_fuel(1_000).unwrap();

let wasm = ops.to_wasm_binary();
log_wasm(&wasm);
Expand Down Expand Up @@ -837,20 +837,6 @@ impl Drop for SignalOnDrop {
}
}

/// Set the amount of fuel in a store to a given value
pub fn set_fuel<T>(store: &mut Store<T>, fuel: u64) {
// Determine the amount of fuel already within the store, if any, and
// add/consume as appropriate to set the remaining amount to` fuel`.
let remaining = store.consume_fuel(0).unwrap();
if fuel > remaining {
store.add_fuel(fuel - remaining).unwrap();
} else {
store.consume_fuel(remaining - fuel).unwrap();
}
// double-check that the store has the expected amount of fuel remaining
assert_eq!(store.consume_fuel(0).unwrap(), fuel);
}

/// Generate and execute a `crate::generators::component_types::TestCase` using the specified `input` to create
/// arbitrary types and values.
pub fn dynamic_component_api_target(input: &mut arbitrary::Unstructured) -> arbitrary::Result<()> {
Expand Down
7 changes: 4 additions & 3 deletions crates/wasmtime/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ impl Config {
///
/// * Alternatively you can enable the
/// [`Config::consume_fuel`](crate::Config::consume_fuel) method as well
/// as [`crate::Store::out_of_fuel_async_yield`] When doing so this will
/// as [`crate::Store::fuel_async_yield_interval`] When doing so this will
/// configure Wasmtime futures to yield periodically while they're
/// executing WebAssembly code. After consuming the specified amount of
/// fuel wasm futures will return `Poll::Pending` from their `poll`
Expand Down Expand Up @@ -456,8 +456,9 @@ impl Config {
///
/// This can be used to deterministically prevent infinitely-executing
/// WebAssembly code by instrumenting generated code to consume fuel as it
/// executes. When fuel runs out the behavior is defined by configuration
/// within a [`Store`], and by default a trap is raised.
/// executes. When fuel runs out a trap is raised, however [`Store`] can be
/// configured to yield execution periodically via
/// [`crate::Store::fuel_async_yield_interval`].
///
/// Note that a [`Store`] starts with no fuel, so if you enable this option
/// you'll have to be sure to pour some fuel into [`Store`] before
Expand Down
46 changes: 11 additions & 35 deletions crates/wasmtime/src/func.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1895,50 +1895,26 @@ impl<T> Caller<'_, T> {
self.store.gc()
}

/// Returns the fuel consumed by this store.
///
/// For more information see [`Store::fuel_consumed`](crate::Store::fuel_consumed)
pub fn fuel_consumed(&self) -> Option<u64> {
self.store.fuel_consumed()
}

/// Returns the remaining fuel in the store.
///
/// For more information see [`Store::fuel_remaining`](crate::Store::fuel_remaining)
pub fn fuel_remaining(&self) -> Option<u64> {
self.store.fuel_remaining()
}

/// Inject more fuel into this store to be consumed when executing wasm code.
///
/// For more information see [`Store::add_fuel`](crate::Store::add_fuel)
pub fn add_fuel(&mut self, fuel: u64) -> Result<()> {
self.store.add_fuel(fuel)
/// For more information see [`Store::get_fuel`](crate::Store::get_fuel)
pub fn get_fuel(&self) -> Result<u64> {
self.store.get_fuel()
}

/// Synthetically consumes fuel from the store.
/// Set the amount of fuel in this store to be consumed when executing wasm code.
///
/// For more information see [`Store::consume_fuel`](crate::Store::consume_fuel)
pub fn consume_fuel(&mut self, fuel: u64) -> Result<u64> {
self.store.consume_fuel(fuel)
}

/// Configures this `Store` to trap whenever fuel runs out.
///
/// For more information see
/// [`Store::out_of_fuel_trap`](crate::Store::out_of_fuel_trap)
pub fn out_of_fuel_trap(&mut self) {
self.store.out_of_fuel_trap()
/// For more information see [`Store::set_fuel`](crate::Store::set_fuel)
pub fn set_fuel(&mut self, fuel: u64) -> Result<()> {
self.store.set_fuel(fuel)
}

/// Configures this `Store` to yield while executing futures whenever fuel
/// runs out.
/// Configures this `Store` to yield while executing futures every N units of fuel.
///
/// For more information see
/// [`Store::out_of_fuel_async_yield`](crate::Store::out_of_fuel_async_yield)
pub fn out_of_fuel_async_yield(&mut self, injection_count: u64, fuel_to_inject: u64) {
self.store
.out_of_fuel_async_yield(injection_count, fuel_to_inject)
/// [`Store::fuel_async_yield_interval`](crate::Store::fuel_async_yield_interval)
pub fn fuel_async_yield_interval(&mut self, interval: Option<u64>) -> Result<()> {
self.store.fuel_async_yield_interval(interval)
}
}

Expand Down
Loading

0 comments on commit eac17cd

Please sign in to comment.