Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
575 changes: 131 additions & 444 deletions crates/cranelift/src/func_environ.rs

Large diffs are not rendered by default.

22 changes: 16 additions & 6 deletions crates/cranelift/src/func_environ/gc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -771,9 +771,15 @@ pub fn translate_array_new(
array_type_index: TypeIndex,
elem: ir::Value,
len: ir::Value,
cost_per_element: u8,
) -> WasmResult<ir::Value> {
log::trace!("translate_array_new({array_type_index:?}, {elem:?}, {len:?})");
let cost = func_env
.tunables
.operator_cost
.variable()
.array_new_per_element;
func_env.pre_translate_bulk_op(builder, len, cost)?;

let result =
gc_compiler(func_env)?.alloc_uninit_array(func_env, builder, array_type_index, len)?;
let zero = builder.ins().iconst(ir::types::I32, 0);
Expand All @@ -788,7 +794,6 @@ pub fn translate_array_new(
zero,
elem,
len,
cost_per_element,
)?;
log::trace!("translate_array_new(..) -> {result:?}");
Ok(result)
Expand All @@ -799,9 +804,14 @@ pub fn translate_array_new_default(
builder: &mut FunctionBuilder,
array_type_index: TypeIndex,
len: ir::Value,
cost_per_element: u8,
) -> WasmResult<ir::Value> {
log::trace!("translate_array_new_default({array_type_index:?}, {len:?})");
let cost = func_env
.tunables
.operator_cost
.variable()
.array_new_default_per_element;
func_env.pre_translate_bulk_op(builder, len, cost)?;

let interned_ty = func_env.module.types[array_type_index].unwrap_module_type_index();
let array_ty = func_env.types.unwrap_array(interned_ty)?;
Expand All @@ -820,7 +830,6 @@ pub fn translate_array_new_default(
zero,
elem,
len,
cost_per_element,
)?;
Ok(result)
}
Expand Down Expand Up @@ -1724,8 +1733,10 @@ pub fn translate_array_new_entity(
entity: CheckedEntity,
entity_offset: ir::Value,
len: ir::Value,
cost_per_element: u8,
cost_per_unit: u8,
) -> WasmResult<ir::Value> {
env.pre_translate_bulk_op(builder, len, cost_per_unit)?;

// Before actually allocating this array first do a bounds-check on the
// passive entity itself.
let interned_type_index = env.module.types[array_type_index].unwrap_module_type_index();
Expand All @@ -1744,7 +1755,6 @@ pub fn translate_array_new_entity(
dst,
entity_offset,
len,
cost_per_element,
)?;

Ok(array)
Expand Down
25 changes: 21 additions & 4 deletions crates/wasmtime/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -690,10 +690,6 @@ impl Config {
/// signal handler), then we can ensure that all async code will
/// yield to the executor within a bounded time.
///
/// The deadline check cannot be avoided by malicious wasm code. It is safe
/// to use epoch deadlines to limit the execution time of untrusted
/// code.
///
/// The [`Store`](crate::Store) tracks the deadline, and controls
/// what happens when the deadline is reached during
/// execution. Several behaviors are possible:
Expand Down Expand Up @@ -739,6 +735,27 @@ impl Config {
/// computation and have the desired effect of cancelling a blocking
/// operation when a timeout expires.
///
/// ## Limitations with malicious guests
///
/// Epochs are designed to handle malicious WebAssembly guests -- the
/// deadline check cannot be avoided by WebAssembly code. It is safe to use
/// epoch deadlines to limit the execution time of untrusted code.
///
/// Note, though, that a current limitation to this is that
/// bulk-data-transfer instructions, such as `memory.copy`, only check the
/// epoch once at the start of the operation. These operations can take a
/// variable amount of time to complete based on how many bytes are being
/// copied. This means that the maximal time slice a guest might take is
/// the maximum of the epoch interval and the largest
/// memory-copy-style-instruction executed. The size of a copy is bounded
/// on the size of linear memory or GC heap size. In the limit, however, a
/// guest using a 64-bit linear memory with a 128GiB size could issue a
/// 128GiB `memory.copy` which would have no preemption within the
/// instruction itself. Hosts which need strict time limits for guests right
/// now are recommended to ensure that the store's allocated heap size
/// (linear memory + GC heap) are bounded with a
/// [`ResourceLimiter`](crate::ResourceLimiter).
///
/// ## When to use fuel vs. epochs
///
/// In general, epoch-based interruption results in faster
Expand Down
4 changes: 4 additions & 0 deletions crates/wasmtime/src/runtime/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1041,6 +1041,10 @@ impl<T> Store<T> {
/// The `interval` parameter indicates how much fuel should be
/// consumed between yields of an async future. When fuel runs out wasm will trap.
///
/// For limitations related to consumption of fuel and when yield points are
/// injected, see the discussion in
/// [`Config::epoch_interruption`](crate::Config::epoch_interruption).
///
/// # Error
///
/// This method will error if fuel is not enabled or `interval` is
Expand Down
127 changes: 127 additions & 0 deletions tests/all/epoch_interruption.rs
Original file line number Diff line number Diff line change
Expand Up @@ -496,3 +496,130 @@ async fn drop_future_on_epoch_yield(config: &mut Config) -> Result<()> {
assert_eq!(true, alive_flag.load(Ordering::Acquire));
Ok(())
}

#[test]
fn memory_grow_in_epoch_callback() -> Result<()> {
let mut config = Config::new();
config.epoch_interruption(true);
config.memory_reservation(0);
config.memory_reservation_for_growth(0);
config.memory_guard_size(0);
config.memory_may_move(true);
config.memory_init_cow(false);
let engine = Engine::new(&config)?;
let module = Module::new(
&engine,
r#"
(module
(memory (export "mem") 1)
(func (export "go")
(memory.fill (i32.const 0) (i32.const 0x41) (i32.const 65536))))
"#,
)?;

let mut store: Store<Option<Memory>> = Store::new(&engine, None);
store.set_epoch_deadline(1);
store.epoch_deadline_callback(move |mut cx| {
if let Some(mem) = *cx.data() {
mem.grow(&mut cx, 5)?;
}
Ok(UpdateDeadline::Continue(0))
});

let instance = Instance::new(&mut store, &module, &[])?;
let mem = instance.get_memory(&mut store, "mem").unwrap();
*store.data_mut() = Some(mem);
engine.increment_epoch();

instance
.get_typed_func::<(), ()>(&mut store, "go")?
.call(&mut store, ())?;

let data = mem.data(&store);
assert_eq!(data[0], 0x41);
assert_eq!(data[65535], 0x41);
Ok(())
}

#[test]
fn table_grow_in_epoch_callback() -> Result<()> {
let mut config = Config::new();
config.epoch_interruption(true);
let engine = Engine::new(&config)?;
let module = Module::new(
&engine,
r#"
(module
(table $t (export "t") 1 funcref)
(func (export "go")
(table.fill $t (i32.const 0) (ref.null func) (i32.const 1))))
"#,
)?;

let mut store: Store<Option<Table>> = Store::new(&engine, None);
store.set_epoch_deadline(1);
store.epoch_deadline_callback(move |mut cx| {
if let Some(t) = *cx.data() {
t.grow(&mut cx, 5, Ref::Func(None))?;
}
Ok(UpdateDeadline::Continue(0))
});

let instance = Instance::new(&mut store, &module, &[])?;
let t = instance.get_table(&mut store, "t").unwrap();
*store.data_mut() = Some(t);
engine.increment_epoch();

instance
.get_typed_func::<(), ()>(&mut store, "go")?
.call(&mut store, ())?;
Ok(())
}

#[test]
fn gc_during_epoch_callback() -> Result<()> {
let mut config = Config::new();
config.epoch_interruption(true);
let engine = Engine::new(&config)?;
let module = Module::new(
&engine,
r#"
(module
(type $box (struct (field i32)))
(type $arr (array (mut (ref null $box))))
(global $sink (mut (ref null $arr)) (ref.null $arr))
(func $mk (param $n i32) (result (ref $arr))
(array.new_default $arr (local.get $n)))
(func (export "run") (param $n i32) (result i32)
(local $i i32)
(block $done
(loop $l
(br_if $done (i32.ge_u (local.get $i) (i32.const 40)))
(array.fill $arr (call $mk (local.get $n)) (i32.const 0)
(struct.new $box (i32.const 7)) (local.get $n))
(global.set $sink (call $mk (i32.const 8)))
(local.set $i (i32.add (local.get $i) (i32.const 1)))
(br $l)
)
)
(i32.mul (local.get $n) (i32.const 7))))
"#,
)?;

let mut store = Store::new(&engine, ());
store.set_epoch_deadline(1);
store.epoch_deadline_callback(|mut caller| {
caller.gc(None)?;
Ok(UpdateDeadline::Continue(0))
});
engine.increment_epoch();

let instance = Instance::new(&mut store, &module, &[])?;
let run = instance.get_typed_func::<u32, u32>(&mut store, "run")?;
let n = 200;
for i in 0..5 {
let got = run.call(&mut store, n)?;
assert_eq!(got, 7 * n, "iteration {i} read back {got}");
}
Ok(())
}
82 changes: 82 additions & 0 deletions tests/all/fuel.rs
Original file line number Diff line number Diff line change
Expand Up @@ -966,6 +966,88 @@ fn table64_variable_operator_cost_saturates(config: &mut Config) -> Result<()> {
// i64::MAX * 2 must saturate at i64::MAX rather than wrap to -2.
let error = grow.call(&mut store, i64::MAX).unwrap_err();
assert_eq!(error.downcast::<Trap>().unwrap(), Trap::OutOfFuel);
Ok(())
}

#[test]
#[cfg_attr(miri, ignore)]
fn huge_table64_grow_cannot_mint_fuel() -> Result<()> {
huge_table64_grow_cannot_mint_fuel_impl(
r#"
(module
(table $t i64 0 0x10000 (ref null func))
(func (export "run") (param $delta i64)
(loop $l
(drop (table.grow $t (ref.null func) (local.get $delta)))
(br $l))))
"#,
)
}

#[test]
#[cfg_attr(miri, ignore)]
fn huge_table64_grow_cannot_mint_fuel_const() -> Result<()> {
huge_table64_grow_cannot_mint_fuel_impl(
r#"
(module
(table $t i64 0 0x10000 (ref null func))
(func (export "run") (param $delta i64)
(loop $l
(drop (table.grow $t (ref.null func) (i64.const -500)))
(br $l))))
"#,
)
}

fn huge_table64_grow_cannot_mint_fuel_impl(wat: &str) -> Result<()> {
let mut config = Config::new();
config.consume_fuel(true);
let engine = Engine::new(&config)?;
let module = Module::new(&engine, wat)?;

let mut store = Store::new(&engine, ());
store.set_fuel(100_000)?;
let instance = Instance::new(&mut store, &module, &[])?;
let run = instance.get_typed_func::<i64, ()>(&mut store, "run")?;

let trap = run.call(&mut store, -500).unwrap_err().downcast::<Trap>()?;
assert_eq!(trap, Trap::OutOfFuel);
assert_eq!(store.get_fuel()?, 0);
Ok(())
}

#[test]
fn fuel_around_table_grow() -> Result<()> {
let mut config = Config::new();
config.consume_fuel(true);
let engine = Engine::new(&config)?;
let module = Module::new(
&engine,
r#"
(module
(type $ft (func))
(func $f (type $ft))
(table $t 1 10000000 (ref $ft) (ref.func $f))
(func (export "grow") (result i32)
(table.grow $t (ref.func $f) (i32.const 9999999)))
(func (export "call") (param i32)
(call_indirect $t (type $ft) (local.get 0))))
"#,
)?;

let mut store = Store::new(&engine, ());
store.set_fuel(2)?;
let instance = Instance::new(&mut store, &module, &[])?;
let grow = instance.get_typed_func::<(), i32>(&mut store, "grow")?;
let trap = grow.call(&mut store, ()).unwrap_err().downcast::<Trap>()?;
assert_eq!(trap, Trap::OutOfFuel);

store.set_fuel(u64::MAX)?;
let call = instance.get_typed_func::<i32, ()>(&mut store, "call")?;
let trap = call
.call(&mut store, 9999999)
.unwrap_err()
.downcast::<Trap>()?;
assert_eq!(trap, Trap::TableOutOfBounds);
Ok(())
}
Loading
Loading