Fix a use-after-free bug when passing ExternRefs to Wasm

We _must not_ trigger a GC when moving refs from host code into
Wasm (e.g. returned from a host function or passed as arguments to a Wasm
function). After insertion into the table, this reference is no longer
rooted. If multiple references are being sent from the host into Wasm and we
allowed GCs during insertion, then the following events could happen:

* Reference A is inserted into the activations table. This does not trigger a
  GC, but does fill the table to capacity.

* The caller's reference to A is removed. Now the only reference to A is from
  the activations table.

* Reference B is inserted into the activations table. Because the table is at
  capacity, a GC is triggered.

* A is reclaimed because the only reference keeping it alive was the activation
  table's reference (it isn't inside any Wasm frames on the stack yet, so stack
  scanning and stack maps don't increment its reference count).

* We transfer control to Wasm, giving it A and B. Wasm uses A. That's a use
  after free.

To prevent uses after free, we cannot GC when moving refs into the
`VMExternRefActivationsTable` because we are passing them from the host to Wasm.

On the other hand, when we are *cloning* -- as opposed to moving -- refs from
the host to Wasm, then it is fine to GC while inserting into the activations
table, because the original referent that we are cloning from is still alive and
rooting the ref.
This commit is contained in:
Nick Fitzgerald
2021-08-31 14:06:02 -07:00
parent 4b256ab968
commit d2ce1ac753
14 changed files with 665 additions and 106 deletions

View File

@@ -688,8 +688,8 @@ impl<'data> ModuleEnvironment<'data> for DummyEnvironment {
WasmType::FuncRef | WasmType::ExternRef | WasmType::ExnRef => reference_type, WasmType::FuncRef | WasmType::ExternRef | WasmType::ExnRef => reference_type,
}) })
}; };
sig.params.extend(wasm.params.iter().map(&mut cvt)); sig.params.extend(wasm.params().iter().map(&mut cvt));
sig.returns.extend(wasm.returns.iter().map(&mut cvt)); sig.returns.extend(wasm.returns().iter().map(&mut cvt));
self.info.signatures.push(sig); self.info.signatures.push(sig);
Ok(()) Ok(())
} }

View File

@@ -465,7 +465,7 @@ impl Compiler {
// Compute the size of the values vector. The vmctx and caller vmctx are passed separately. // Compute the size of the values vector. The vmctx and caller vmctx are passed separately.
let value_size = mem::size_of::<u128>(); let value_size = mem::size_of::<u128>();
let values_vec_len = (value_size * cmp::max(ty.params.len(), ty.returns.len())) as u32; let values_vec_len = (value_size * cmp::max(ty.params().len(), ty.returns().len())) as u32;
let mut context = Context::new(); let mut context = Context::new();
context.func = context.func =
@@ -486,7 +486,7 @@ impl Compiler {
let values_vec_ptr_val = builder.ins().stack_addr(pointer_type, ss, 0); let values_vec_ptr_val = builder.ins().stack_addr(pointer_type, ss, 0);
let mflags = MemFlags::trusted(); let mflags = MemFlags::trusted();
for i in 0..ty.params.len() { for i in 0..ty.params().len() {
let val = builder.func.dfg.block_params(block0)[i + 2]; let val = builder.func.dfg.block_params(block0)[i + 2];
builder builder
.ins() .ins()
@@ -508,7 +508,7 @@ impl Compiler {
let mflags = MemFlags::trusted(); let mflags = MemFlags::trusted();
let mut results = Vec::new(); let mut results = Vec::new();
for (i, r) in ty.returns.iter().enumerate() { for (i, r) in ty.returns().iter().enumerate() {
let load = builder.ins().load( let load = builder.ins().load(
value_type(isa, *r), value_type(isa, *r),
mflags, mflags,

View File

@@ -220,8 +220,8 @@ fn wasmtime_call_conv(isa: &dyn TargetIsa) -> CallConv {
/// above. /// above.
fn push_types(isa: &dyn TargetIsa, sig: &mut ir::Signature, wasm: &WasmFuncType) { fn push_types(isa: &dyn TargetIsa, sig: &mut ir::Signature, wasm: &WasmFuncType) {
let cvt = |ty: &WasmType| ir::AbiParam::new(value_type(isa, *ty)); let cvt = |ty: &WasmType| ir::AbiParam::new(value_type(isa, *ty));
sig.params.extend(wasm.params.iter().map(&cvt)); sig.params.extend(wasm.params().iter().map(&cvt));
sig.returns.extend(wasm.returns.iter().map(&cvt)); sig.returns.extend(wasm.returns().iter().map(&cvt));
} }
/// Returns the corresponding cranelift type for the provided wasm type. /// Returns the corresponding cranelift type for the provided wasm type.

View File

@@ -608,7 +608,7 @@ impl<'data> ModuleEnvironment<'data> {
.funcs .funcs
.push(FunctionMetadata { .push(FunctionMetadata {
locals: locals.into_boxed_slice(), locals: locals.into_boxed_slice(),
params: sig.params.iter().cloned().map(|i| i.into()).collect(), params: sig.params().iter().cloned().map(|i| i.into()).collect(),
}); });
} }
body.allow_memarg64(self.features.memory64); body.allow_memarg64(self.features.memory64);

View File

@@ -18,7 +18,7 @@ pub struct TableOps {
const NUM_PARAMS_RANGE: Range<u8> = 1..10; const NUM_PARAMS_RANGE: Range<u8> = 1..10;
const TABLE_SIZE_RANGE: Range<u32> = 1..100; const TABLE_SIZE_RANGE: Range<u32> = 1..100;
const MAX_OPS: usize = 1000; const MAX_OPS: usize = 100;
impl TableOps { impl TableOps {
/// Get the number of parameters this module's "run" function takes. /// Get the number of parameters this module's "run" function takes.
@@ -49,9 +49,46 @@ impl TableOps {
pub fn to_wasm_binary(&self) -> Vec<u8> { pub fn to_wasm_binary(&self) -> Vec<u8> {
let mut module = Module::new(); let mut module = Module::new();
// Encode the types for all functions that we are using.
let mut types = TypeSection::new();
// 0: "gc"
types.function(
vec![],
// Return a bunch of stuff from `gc` so that we exercise GCing when
// there is return pointer space allocated on the stack. This is
// especially important because the x64 backend currently
// dynamically adjusts the stack pointer for each call that uses
// return pointers rather than statically allocating space in the
// stack frame.
vec![ValType::ExternRef, ValType::ExternRef, ValType::ExternRef],
);
// 1: "run"
let mut params: Vec<ValType> = Vec::with_capacity(self.num_params() as usize);
for _i in 0..self.num_params() {
params.push(ValType::ExternRef);
}
let results = vec![];
types.function(params, results);
// 2: `take_refs`
types.function(
vec![ValType::ExternRef, ValType::ExternRef, ValType::ExternRef],
vec![],
);
// 3: `make_refs`
types.function(
vec![],
vec![ValType::ExternRef, ValType::ExternRef, ValType::ExternRef],
);
// Import the GC function. // Import the GC function.
let mut imports = ImportSection::new(); let mut imports = ImportSection::new();
imports.import("", Some("gc"), EntityType::Function(0)); imports.import("", Some("gc"), EntityType::Function(0));
imports.import("", Some("take_refs"), EntityType::Function(2));
imports.import("", Some("make_refs"), EntityType::Function(3));
// Define our table. // Define our table.
let mut tables = TableSection::new(); let mut tables = TableSection::new();
@@ -61,32 +98,24 @@ impl TableOps {
maximum: None, maximum: None,
}); });
// Encode the types for all functions that we are using.
let mut types = TypeSection::new();
types.function(vec![], vec![]); // 0: "gc"
let mut params: Vec<ValType> = Vec::with_capacity(self.num_params() as usize);
for _i in 0..self.num_params() {
params.push(ValType::ExternRef);
}
let results = vec![];
types.function(params, results); // 1: "run"
// Define the "run" function export. // Define the "run" function export.
let mut functions = FunctionSection::new(); let mut functions = FunctionSection::new();
functions.function(1); functions.function(1);
let mut exports = ExportSection::new(); let mut exports = ExportSection::new();
exports.export("run", Export::Function(1)); exports.export("run", Export::Function(3));
let mut params: Vec<(u32, ValType)> = Vec::with_capacity(self.num_params() as usize); // Give ourselves one scratch local that we can use in various `TableOp`
for _i in 0..self.num_params() { // implementations.
params.push((0, ValType::ExternRef)); let mut func = Function::new(vec![(1, ValType::ExternRef)]);
}
let mut func = Function::new(params);
func.instruction(Instruction::Loop(wasm_encoder::BlockType::Empty));
for op in self.ops.iter().take(MAX_OPS) { for op in self.ops.iter().take(MAX_OPS) {
op.insert(&mut func); op.insert(&mut func, self.num_params() as u32, self.table_size());
} }
func.instruction(Instruction::Br(0));
func.instruction(Instruction::End);
func.instruction(Instruction::End);
let mut code = CodeSection::new(); let mut code = CodeSection::new();
code.function(&func); code.function(&func);
@@ -105,7 +134,7 @@ impl TableOps {
#[derive(Arbitrary, Debug)] #[derive(Arbitrary, Debug)]
pub(crate) enum TableOp { pub(crate) enum TableOp {
// `(call 0)` // `call $gc; drop; drop; drop;`
Gc, Gc,
// `(drop (table.get x))` // `(drop (table.get x))`
Get(i32), Get(i32),
@@ -113,30 +142,102 @@ pub(crate) enum TableOp {
SetFromParam(i32, u32), SetFromParam(i32, u32),
// `(table.set x (table.get y))` // `(table.set x (table.get y))`
SetFromGet(i32, i32), SetFromGet(i32, i32),
// `call $make_refs; table.set x; table.set y; table.set z`
SetFromMake(i32, i32, i32),
// `call $make_refs; drop; drop; drop;`
Make,
// `local.get x; local.get y; local.get z; call $take_refs`
TakeFromParams(u32, u32, u32),
// `table.get x; table.get y; table.get z; call $take_refs`
TakeFromGet(i32, i32, i32),
// `call $make_refs; call $take_refs`
TakeFromMake,
// `call $gc; call $take_refs`
TakeFromGc,
} }
impl TableOp { impl TableOp {
fn insert(&self, func: &mut Function) { fn insert(&self, func: &mut Function, num_params: u32, table_size: u32) {
assert!(num_params > 0);
assert!(table_size > 0);
// Add one to make sure that out of bounds table accesses are possible,
// but still rare.
let table_mod = table_size as i32 + 1;
match self { match self {
Self::Gc => { Self::Gc => {
func.instruction(Instruction::Call(0)); func.instruction(Instruction::Call(0));
func.instruction(Instruction::Drop);
func.instruction(Instruction::Drop);
func.instruction(Instruction::Drop);
} }
Self::Get(x) => { Self::Get(x) => {
func.instruction(Instruction::I32Const(*x)); func.instruction(Instruction::I32Const(*x % table_mod));
func.instruction(Instruction::TableGet { table: 0 }); func.instruction(Instruction::TableGet { table: 0 });
func.instruction(Instruction::Drop); func.instruction(Instruction::Drop);
} }
Self::SetFromParam(x, y) => { Self::SetFromParam(x, y) => {
func.instruction(Instruction::I32Const(*x)); func.instruction(Instruction::I32Const(*x % table_mod));
func.instruction(Instruction::LocalGet(*y)); func.instruction(Instruction::LocalGet(*y % num_params));
func.instruction(Instruction::TableSet { table: 0 }); func.instruction(Instruction::TableSet { table: 0 });
} }
Self::SetFromGet(x, y) => { Self::SetFromGet(x, y) => {
func.instruction(Instruction::I32Const(*x)); func.instruction(Instruction::I32Const(*x % table_mod));
func.instruction(Instruction::I32Const(*y)); func.instruction(Instruction::I32Const(*y % table_mod));
func.instruction(Instruction::TableGet { table: 0 }); func.instruction(Instruction::TableGet { table: 0 });
func.instruction(Instruction::TableSet { table: 0 }); func.instruction(Instruction::TableSet { table: 0 });
} }
Self::SetFromMake(x, y, z) => {
func.instruction(Instruction::Call(2));
func.instruction(Instruction::LocalSet(num_params));
func.instruction(Instruction::I32Const(*x % table_mod));
func.instruction(Instruction::LocalGet(num_params));
func.instruction(Instruction::TableSet { table: 0 });
func.instruction(Instruction::LocalSet(num_params));
func.instruction(Instruction::I32Const(*y % table_mod));
func.instruction(Instruction::LocalGet(num_params));
func.instruction(Instruction::TableSet { table: 0 });
func.instruction(Instruction::LocalSet(num_params));
func.instruction(Instruction::I32Const(*z % table_mod));
func.instruction(Instruction::LocalGet(num_params));
func.instruction(Instruction::TableSet { table: 0 });
}
TableOp::Make => {
func.instruction(Instruction::Call(2));
func.instruction(Instruction::Drop);
func.instruction(Instruction::Drop);
func.instruction(Instruction::Drop);
}
TableOp::TakeFromParams(x, y, z) => {
func.instruction(Instruction::LocalGet(x % num_params));
func.instruction(Instruction::LocalGet(y % num_params));
func.instruction(Instruction::LocalGet(z % num_params));
func.instruction(Instruction::Call(1));
}
TableOp::TakeFromGet(x, y, z) => {
func.instruction(Instruction::I32Const(*x % table_mod));
func.instruction(Instruction::TableGet { table: 0 });
func.instruction(Instruction::I32Const(*y % table_mod));
func.instruction(Instruction::TableGet { table: 0 });
func.instruction(Instruction::I32Const(*z % table_mod));
func.instruction(Instruction::TableGet { table: 0 });
func.instruction(Instruction::Call(1));
}
TableOp::TakeFromMake => {
func.instruction(Instruction::Call(2));
func.instruction(Instruction::Call(1));
}
Self::TakeFromGc => {
func.instruction(Instruction::Call(0));
func.instruction(Instruction::Call(1));
}
} }
} }
} }
@@ -148,38 +249,95 @@ mod tests {
#[test] #[test]
fn test_wat_string() { fn test_wat_string() {
let ops = TableOps { let ops = TableOps {
num_params: 2, num_params: 5,
table_size: 10, table_size: 20,
ops: vec![ ops: vec![
TableOp::Gc, TableOp::Gc,
TableOp::Get(0), TableOp::Get(0),
TableOp::SetFromParam(1, 2), TableOp::SetFromParam(1, 2),
TableOp::SetFromGet(3, 4), TableOp::SetFromGet(3, 4),
TableOp::SetFromMake(5, 6, 7),
TableOp::Make,
TableOp::TakeFromParams(8, 9, 10),
TableOp::TakeFromGet(11, 12, 13),
TableOp::TakeFromMake,
], ],
}; };
let expected = r#" let expected = r#"
(module (module
(type (;0;) (func)) (type (;0;) (func))
(type (;1;) (func (param externref externref))) (type (;1;) (func (param externref externref externref externref externref)))
(type (;2;) (func (param externref externref externref)))
(type (;3;) (func (result externref externref externref)))
(import "" "gc" (func (;0;) (type 0))) (import "" "gc" (func (;0;) (type 0)))
(func (;1;) (type 1) (param externref externref) (import "" "take_refs" (func (;1;) (type 2)))
call 0 (import "" "make_refs" (func (;2;) (type 3)))
i32.const 0 (func (;3;) (type 1) (param externref externref externref externref externref)
table.get 0 (local externref i32)
drop i32.const 100
i32.const 1 local.set 6
local.get 2 loop ;; label = @1
table.set 0 call 0
i32.const 3 i32.const 0
i32.const 4 table.get 0
table.get 0 drop
table.set 0) i32.const 1
(table (;0;) 10 externref) local.get 2
(export "run" (func 1))) table.set 0
i32.const 3
i32.const 4
table.get 0
table.set 0
call 2
local.set 5
i32.const 5
local.get 5
table.set 0
local.set 5
i32.const 6
local.get 5
table.set 0
local.set 5
i32.const 7
local.get 5
table.set 0
call 2
drop
drop
drop
local.get 3
local.get 4
local.get 0
call 1
i32.const 11
table.get 0
i32.const 12
table.get 0
i32.const 13
table.get 0
call 1
call 2
call 1
local.get 6
i32.const -1
i32.add
local.tee 6
br_if 0 (;@1;)
end)
(table (;0;) 20 externref)
(export "run" (func 3)))
"#; "#;
eprintln!("expected WAT = {}", expected);
let actual = ops.to_wasm_binary(); let actual = ops.to_wasm_binary();
if let Err(e) = wasmparser::validate(&actual) {
panic!("TableOps should generate valid Wasm; got error: {}", e);
}
let actual = wasmprinter::print_bytes(&actual).unwrap(); let actual = wasmprinter::print_bytes(&actual).unwrap();
eprintln!("actual WAT = {}", actual);
assert_eq!(actual.trim(), expected.trim()); assert_eq!(actual.trim(), expected.trim());
} }
} }

View File

@@ -514,16 +514,17 @@ pub fn table_ops(
) { ) {
let _ = env_logger::try_init(); let _ = env_logger::try_init();
let expected_drops = Arc::new(AtomicUsize::new(ops.num_params() as usize));
let num_dropped = Arc::new(AtomicUsize::new(0)); let num_dropped = Arc::new(AtomicUsize::new(0));
{ {
let mut config = fuzz_config.to_wasmtime(); let mut config = fuzz_config.to_wasmtime();
config.wasm_reference_types(true); config.wasm_reference_types(true);
config.consume_fuel(true);
let engine = Engine::new(&config).unwrap(); let engine = Engine::new(&config).unwrap();
let mut store = create_store(&engine); let mut store = create_store(&engine);
if fuzz_config.consume_fuel { store.add_fuel(100).unwrap();
store.add_fuel(u64::max_value()).unwrap();
}
let wasm = ops.to_wasm_binary(); let wasm = ops.to_wasm_binary();
log_wasm(&wasm); log_wasm(&wasm);
@@ -532,18 +533,104 @@ pub fn table_ops(
Err(_) => return, Err(_) => return,
}; };
let mut linker = Linker::new(&engine);
// To avoid timeouts, limit the number of explicit GCs we perform per // To avoid timeouts, limit the number of explicit GCs we perform per
// test case. // test case.
const MAX_GCS: usize = 5; const MAX_GCS: usize = 5;
let num_gcs = AtomicUsize::new(0); let num_gcs = AtomicUsize::new(0);
let gc = Func::wrap(&mut store, move |mut caller: Caller<'_, StoreLimits>| { linker
if num_gcs.fetch_add(1, SeqCst) < MAX_GCS { .define(
caller.gc(); "",
} "gc",
}); // NB: use `Func::new` so that this can still compile on the old x86
// backend, where `IntoFunc` isn't implemented for multi-value
// returns.
Func::new(
&mut store,
FuncType::new(
vec![],
vec![ValType::ExternRef, ValType::ExternRef, ValType::ExternRef],
),
{
let num_dropped = num_dropped.clone();
let expected_drops = expected_drops.clone();
move |mut caller: Caller<'_, StoreLimits>, _params, results| {
if num_gcs.fetch_add(1, SeqCst) < MAX_GCS {
caller.gc();
}
let instance = Instance::new(&mut store, &module, &[gc.into()]).unwrap(); expected_drops.fetch_add(3, SeqCst);
results[0] =
Some(ExternRef::new(CountDrops(num_dropped.clone()))).into();
results[1] =
Some(ExternRef::new(CountDrops(num_dropped.clone()))).into();
results[2] =
Some(ExternRef::new(CountDrops(num_dropped.clone()))).into();
Ok(())
}
},
),
)
.unwrap();
linker
.func_wrap("", "take_refs", {
let expected_drops = expected_drops.clone();
move |a: Option<ExternRef>, b: Option<ExternRef>, c: Option<ExternRef>| {
// Do the assertion on each ref's inner data, even though it
// all points to the same atomic, so that if we happen to
// run into a use-after-free bug with one of these refs we
// are more likely to trigger a segfault.
if let Some(a) = a {
let a = a.data().downcast_ref::<CountDrops>().unwrap();
assert!(a.0.load(SeqCst) <= expected_drops.load(SeqCst));
}
if let Some(b) = b {
let b = b.data().downcast_ref::<CountDrops>().unwrap();
assert!(b.0.load(SeqCst) <= expected_drops.load(SeqCst));
}
if let Some(c) = c {
let c = c.data().downcast_ref::<CountDrops>().unwrap();
assert!(c.0.load(SeqCst) <= expected_drops.load(SeqCst));
}
}
})
.unwrap();
linker
.define(
"",
"make_refs",
// NB: use `Func::new` so that this can still compile on the old
// x86 backend, where `IntoFunc` isn't implemented for
// multi-value returns.
Func::new(
&mut store,
FuncType::new(
vec![],
vec![ValType::ExternRef, ValType::ExternRef, ValType::ExternRef],
),
{
let num_dropped = num_dropped.clone();
let expected_drops = expected_drops.clone();
move |_caller, _params, results| {
expected_drops.fetch_add(3, SeqCst);
results[0] =
Some(ExternRef::new(CountDrops(num_dropped.clone()))).into();
results[1] =
Some(ExternRef::new(CountDrops(num_dropped.clone()))).into();
results[2] =
Some(ExternRef::new(CountDrops(num_dropped.clone()))).into();
Ok(())
}
},
),
)
.unwrap();
let instance = linker.instantiate(&mut store, &module).unwrap();
let run = instance.get_func(&mut store, "run").unwrap(); let run = instance.get_func(&mut store, "run").unwrap();
let args: Vec<_> = (0..ops.num_params()) let args: Vec<_> = (0..ops.num_params())
@@ -552,7 +639,7 @@ pub fn table_ops(
let _ = run.call(&mut store, &args); let _ = run.call(&mut store, &args);
} }
assert_eq!(num_dropped.load(SeqCst), ops.num_params() as usize); assert_eq!(num_dropped.load(SeqCst), expected_drops.load(SeqCst));
return; return;
struct CountDrops(Arc<AtomicUsize>); struct CountDrops(Arc<AtomicUsize>);

View File

@@ -489,7 +489,7 @@ type TableElem = UnsafeCell<Option<VMExternRef>>;
/// ///
/// Under the covers, this is a simple bump allocator that allows duplicate /// Under the covers, this is a simple bump allocator that allows duplicate
/// entries. Deduplication happens at GC time. /// entries. Deduplication happens at GC time.
#[repr(C)] // `alloc` must be the first member, it's accessed from JIT code #[repr(C)] // `alloc` must be the first member, it's accessed from JIT code.
pub struct VMExternRefActivationsTable { pub struct VMExternRefActivationsTable {
/// Structures used to perform fast bump allocation of storage of externref /// Structures used to perform fast bump allocation of storage of externref
/// values. /// values.
@@ -521,9 +521,14 @@ pub struct VMExternRefActivationsTable {
/// inside-a-Wasm-frame roots, and doing a GC could lead to freeing one of /// inside-a-Wasm-frame roots, and doing a GC could lead to freeing one of
/// those missed roots, and use after free. /// those missed roots, and use after free.
stack_canary: Option<usize>, stack_canary: Option<usize>,
/// A debug-only field for asserting that we are in a region of code where
/// GC is okay to preform.
#[cfg(debug_assertions)]
gc_okay: bool,
} }
#[repr(C)] // this is accessed from JTI code #[repr(C)] // This is accessed from JIT code.
struct VMExternRefTableAlloc { struct VMExternRefTableAlloc {
/// Bump-allocation finger within the `chunk`. /// Bump-allocation finger within the `chunk`.
/// ///
@@ -573,6 +578,8 @@ impl VMExternRefActivationsTable {
over_approximated_stack_roots: HashSet::with_capacity(Self::CHUNK_SIZE), over_approximated_stack_roots: HashSet::with_capacity(Self::CHUNK_SIZE),
precise_stack_roots: HashSet::with_capacity(Self::CHUNK_SIZE), precise_stack_roots: HashSet::with_capacity(Self::CHUNK_SIZE),
stack_canary: None, stack_canary: None,
#[cfg(debug_assertions)]
gc_okay: true,
} }
} }
@@ -581,6 +588,14 @@ impl VMExternRefActivationsTable {
(0..size).map(|_| UnsafeCell::new(None)).collect() (0..size).map(|_| UnsafeCell::new(None)).collect()
} }
/// Get the available capacity in the bump allocation chunk.
#[inline]
pub fn bump_capacity_remaining(&self) -> usize {
let end = self.alloc.end.as_ptr() as usize;
let next = unsafe { *self.alloc.next.get() };
end - next.as_ptr() as usize
}
/// Try and insert a `VMExternRef` into this table. /// Try and insert a `VMExternRef` into this table.
/// ///
/// This is a fast path that only succeeds when the bump chunk has the /// This is a fast path that only succeeds when the bump chunk has the
@@ -624,6 +639,9 @@ impl VMExternRefActivationsTable {
externref: VMExternRef, externref: VMExternRef,
module_info_lookup: &dyn ModuleInfoLookup, module_info_lookup: &dyn ModuleInfoLookup,
) { ) {
#[cfg(debug_assertions)]
assert!(self.gc_okay);
if let Err(externref) = self.try_insert(externref) { if let Err(externref) = self.try_insert(externref) {
self.gc_and_insert_slow(externref, module_info_lookup); self.gc_and_insert_slow(externref, module_info_lookup);
} }
@@ -644,6 +662,20 @@ impl VMExternRefActivationsTable {
.insert(VMExternRefWithTraits(externref)); .insert(VMExternRefWithTraits(externref));
} }
/// Insert a reference into the table, without ever performing GC.
#[inline]
pub fn insert_without_gc(&mut self, externref: VMExternRef) {
if let Err(externref) = self.try_insert(externref) {
self.insert_slow_without_gc(externref);
}
}
#[inline(never)]
fn insert_slow_without_gc(&mut self, externref: VMExternRef) {
self.over_approximated_stack_roots
.insert(VMExternRefWithTraits(externref));
}
fn num_filled_in_bump_chunk(&self) -> usize { fn num_filled_in_bump_chunk(&self) -> usize {
let next = unsafe { *self.alloc.next.get() }; let next = unsafe { *self.alloc.next.get() };
let bytes_unused = (self.alloc.end.as_ptr() as usize) - (next.as_ptr() as usize); let bytes_unused = (self.alloc.end.as_ptr() as usize) - (next.as_ptr() as usize);
@@ -742,6 +774,24 @@ impl VMExternRefActivationsTable {
pub fn set_stack_canary(&mut self, canary: Option<usize>) { pub fn set_stack_canary(&mut self, canary: Option<usize>) {
self.stack_canary = canary; self.stack_canary = canary;
} }
/// Set whether it is okay to GC or not right now.
///
/// This is provided as a helper for enabling various debug-only assertions
/// and checking places where the `wasmtime-runtime` user expects there not
/// to be any GCs.
#[inline]
pub fn set_gc_okay(&mut self, okay: bool) -> bool {
#[cfg(debug_assertions)]
{
return std::mem::replace(&mut self.gc_okay, okay);
}
#[cfg(not(debug_assertions))]
{
let _ = okay;
return true;
}
}
} }
/// Used by the runtime to lookup information about a module given a /// Used by the runtime to lookup information about a module given a
@@ -807,6 +857,9 @@ pub unsafe fn gc(
) { ) {
log::debug!("start GC"); log::debug!("start GC");
#[cfg(debug_assertions)]
assert!(externref_activations_table.gc_okay);
debug_assert!({ debug_assert!({
// This set is only non-empty within this function. It is built up when // This set is only non-empty within this function. It is built up when
// walking the stack and interpreting stack maps, and then drained back // walking the stack and interpreting stack maps, and then drained back

View File

@@ -71,29 +71,66 @@ impl From<WasmType> for wasmparser::Type {
/// WebAssembly function type -- equivalent of `wasmparser`'s FuncType. /// WebAssembly function type -- equivalent of `wasmparser`'s FuncType.
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct WasmFuncType { pub struct WasmFuncType {
params: Box<[WasmType]>,
externref_params_count: usize,
returns: Box<[WasmType]>,
externref_returns_count: usize,
}
impl WasmFuncType {
#[inline]
pub fn new(params: Box<[WasmType]>, returns: Box<[WasmType]>) -> Self {
let externref_params_count = params.iter().filter(|p| **p == WasmType::ExternRef).count();
let externref_returns_count = params.iter().filter(|r| **r == WasmType::ExternRef).count();
WasmFuncType {
params,
externref_params_count,
returns,
externref_returns_count,
}
}
/// Function params types. /// Function params types.
pub params: Box<[WasmType]>, #[inline]
pub fn params(&self) -> &[WasmType] {
&self.params
}
/// How many `externref`s are in this function's params?
#[inline]
pub fn externref_params_count(&self) -> usize {
self.externref_params_count
}
/// Returns params types. /// Returns params types.
pub returns: Box<[WasmType]>, #[inline]
pub fn returns(&self) -> &[WasmType] {
&self.returns
}
/// How many `externref`s are in this function's returns?
#[inline]
pub fn externref_returns_count(&self) -> usize {
self.externref_returns_count
}
} }
impl TryFrom<wasmparser::FuncType> for WasmFuncType { impl TryFrom<wasmparser::FuncType> for WasmFuncType {
type Error = WasmError; type Error = WasmError;
fn try_from(ty: wasmparser::FuncType) -> Result<Self, Self::Error> { fn try_from(ty: wasmparser::FuncType) -> Result<Self, Self::Error> {
Ok(Self { let params = ty
params: ty .params
.params .into_vec()
.into_vec() .into_iter()
.into_iter() .map(WasmType::try_from)
.map(WasmType::try_from) .collect::<Result<_, Self::Error>>()?;
.collect::<Result<_, Self::Error>>()?, let returns = ty
returns: ty .returns
.returns .into_vec()
.into_vec() .into_iter()
.into_iter() .map(WasmType::try_from)
.map(WasmType::try_from) .collect::<Result<_, Self::Error>>()?;
.collect::<Result<_, Self::Error>>()?, Ok(Self::new(params, returns))
})
} }
} }

View File

@@ -774,6 +774,21 @@ impl Func {
let mut values_vec = vec![0; max(params.len(), ty.results().len())]; let mut values_vec = vec![0; max(params.len(), ty.results().len())];
// Whenever we pass `externref`s from host code to Wasm code, they
// go into the `VMExternRefActivationsTable`. But the table might be
// at capacity already, so check for that. If it is at capacity
// (unlikely) then do a GC to free up space. This is necessary
// because otherwise we would either keep filling up the bump chunk
// and making it larger and larger or we would always take the slow
// path when inserting references into the table.
if ty.as_wasm_func_type().externref_params_count()
> store
.externref_activations_table()
.bump_capacity_remaining()
{
store.gc();
}
// Store the argument values into `values_vec`. // Store the argument values into `values_vec`.
let param_tys = ty.params(); let param_tys = ty.params();
for ((arg, slot), ty) in params.iter().cloned().zip(&mut values_vec).zip(param_tys) { for ((arg, slot), ty) in params.iter().cloned().zip(&mut values_vec).zip(param_tys) {
@@ -788,7 +803,7 @@ impl Func {
bail!("cross-`Store` values are not currently supported"); bail!("cross-`Store` values are not currently supported");
} }
unsafe { unsafe {
arg.write_value_to(store, slot); arg.write_value_without_gc(store, slot);
} }
} }
@@ -871,6 +886,17 @@ impl Func {
let (params, results) = val_vec.split_at_mut(nparams); let (params, results) = val_vec.split_at_mut(nparams);
func(caller.sub_caller(), params, results)?; func(caller.sub_caller(), params, results)?;
// See the comment in `Func::call_impl`'s `write_params` function.
if ty.as_wasm_func_type().externref_returns_count()
> caller
.store
.0
.externref_activations_table()
.bump_capacity_remaining()
{
caller.store.gc();
}
// Unlike our arguments we need to dynamically check that the return // Unlike our arguments we need to dynamically check that the return
// values produced are correct. There could be a bug in `func` that // values produced are correct. There could be a bug in `func` that
// produces the wrong number, wrong types, or wrong stores of // produces the wrong number, wrong types, or wrong stores of
@@ -887,7 +913,7 @@ impl Func {
)); ));
} }
unsafe { unsafe {
ret.write_value_to(caller.store.0, values_vec.add(i)); ret.write_value_without_gc(caller.store.0, values_vec.add(i));
} }
} }

View File

@@ -1,5 +1,5 @@
use super::{invoke_wasm_and_catch_traps, HostAbi}; use super::{invoke_wasm_and_catch_traps, HostAbi};
use crate::store::StoreOpaque; use crate::store::{AutoAssertNoGc, StoreOpaque};
use crate::{AsContextMut, ExternRef, Func, StoreContextMut, Trap, ValType}; use crate::{AsContextMut, ExternRef, Func, StoreContextMut, Trap, ValType};
use anyhow::{bail, Result}; use anyhow::{bail, Result};
use std::marker; use std::marker;
@@ -115,15 +115,33 @@ where
store: &mut StoreContextMut<'_, T>, store: &mut StoreContextMut<'_, T>,
params: Params, params: Params,
) -> Result<Results, Trap> { ) -> Result<Results, Trap> {
// See the comment in `Func::call_impl`'s `write_params` function.
if params.externrefs_count()
> store
.0
.externref_activations_table()
.bump_capacity_remaining()
{
store.gc();
}
// Validate that all runtime values flowing into this store indeed // Validate that all runtime values flowing into this store indeed
// belong within this store, otherwise it would be unsafe for store // belong within this store, otherwise it would be unsafe for store
// values to cross each other. // values to cross each other.
let params = match params.into_abi(store.0) {
Some(abi) => abi, let params = {
None => { // GC is not safe here, since we move refs into the activations
return Err(Trap::new( // table but don't hold a strong reference onto them until we enter
"attempt to pass cross-`Store` value to Wasm as function argument", // the Wasm frame and they get referenced from the stack maps.
)) let mut store = AutoAssertNoGc::new(&mut **store.as_context_mut().0);
match params.into_abi(&mut store) {
Some(abi) => abi,
None => {
return Err(Trap::new(
"attempt to pass cross-`Store` value to Wasm as function argument",
))
}
} }
}; };
@@ -183,6 +201,8 @@ pub unsafe trait WasmTy: Send {
#[doc(hidden)] #[doc(hidden)]
fn compatible_with_store(&self, store: &StoreOpaque) -> bool; fn compatible_with_store(&self, store: &StoreOpaque) -> bool;
#[doc(hidden)] #[doc(hidden)]
fn is_externref(&self) -> bool;
#[doc(hidden)]
fn into_abi(self, store: &mut StoreOpaque) -> Self::Abi; fn into_abi(self, store: &mut StoreOpaque) -> Self::Abi;
#[doc(hidden)] #[doc(hidden)]
unsafe fn from_abi(abi: Self::Abi, store: &mut StoreOpaque) -> Self; unsafe fn from_abi(abi: Self::Abi, store: &mut StoreOpaque) -> Self;
@@ -201,6 +221,10 @@ macro_rules! primitives {
true true
} }
#[inline] #[inline]
fn is_externref(&self) -> bool {
false
}
#[inline]
fn into_abi(self, _store: &mut StoreOpaque) -> Self::Abi { fn into_abi(self, _store: &mut StoreOpaque) -> Self::Abi {
self self
} }
@@ -234,12 +258,46 @@ unsafe impl WasmTy for Option<ExternRef> {
true true
} }
#[inline]
fn is_externref(&self) -> bool {
true
}
#[inline] #[inline]
fn into_abi(self, store: &mut StoreOpaque) -> Self::Abi { fn into_abi(self, store: &mut StoreOpaque) -> Self::Abi {
if let Some(x) = self { if let Some(x) = self {
let abi = x.inner.as_raw(); let abi = x.inner.as_raw();
unsafe { unsafe {
store.insert_vmexternref(x.inner); // NB: We _must not_ trigger a GC when passing refs from host
// code into Wasm (e.g. returned from a host function or passed
// as arguments to a Wasm function). After insertion into the
// table, this reference is no longer rooted. If multiple
// references are being sent from the host into Wasm and we
// allowed GCs during insertion, then the following events could
// happen:
//
// * Reference A is inserted into the activations
// table. This does not trigger a GC, but does fill the table
// to capacity.
//
// * The caller's reference to A is removed. Now the only
// reference to A is from the activations table.
//
// * Reference B is inserted into the activations table. Because
// the table is at capacity, a GC is triggered.
//
// * A is reclaimed because the only reference keeping it alive
// was the activation table's reference (it isn't inside any
// Wasm frames on the stack yet, so stack scanning and stack
// maps don't increment its reference count).
//
// * We transfer control to Wasm, giving it A and B. Wasm uses
// A. That's a use after free.
//
// In conclusion, to prevent uses after free, we cannot GC
// during this insertion.
let mut store = AutoAssertNoGc::new(store);
store.insert_vmexternref_without_gc(x.inner);
} }
abi abi
} else { } else {
@@ -276,6 +334,11 @@ unsafe impl WasmTy for Option<Func> {
} }
} }
#[inline]
fn is_externref(&self) -> bool {
false
}
#[inline] #[inline]
fn into_abi(self, store: &mut StoreOpaque) -> Self::Abi { fn into_abi(self, store: &mut StoreOpaque) -> Self::Abi {
if let Some(f) = self { if let Some(f) = self {
@@ -299,10 +362,16 @@ unsafe impl WasmTy for Option<Func> {
pub unsafe trait WasmParams: Send { pub unsafe trait WasmParams: Send {
#[doc(hidden)] #[doc(hidden)]
type Abi: Copy; type Abi: Copy;
#[doc(hidden)] #[doc(hidden)]
fn typecheck(params: impl ExactSizeIterator<Item = crate::ValType>) -> Result<()>; fn typecheck(params: impl ExactSizeIterator<Item = crate::ValType>) -> Result<()>;
#[doc(hidden)]
fn externrefs_count(&self) -> usize;
#[doc(hidden)] #[doc(hidden)]
fn into_abi(self, store: &mut StoreOpaque) -> Option<Self::Abi>; fn into_abi(self, store: &mut StoreOpaque) -> Option<Self::Abi>;
#[doc(hidden)] #[doc(hidden)]
unsafe fn invoke<R: WasmResults>( unsafe fn invoke<R: WasmResults>(
func: *const VMFunctionBody, func: *const VMFunctionBody,
@@ -323,10 +392,17 @@ where
fn typecheck(params: impl ExactSizeIterator<Item = crate::ValType>) -> Result<()> { fn typecheck(params: impl ExactSizeIterator<Item = crate::ValType>) -> Result<()> {
<(T,) as WasmParams>::typecheck(params) <(T,) as WasmParams>::typecheck(params)
} }
#[inline]
fn externrefs_count(&self) -> usize {
T::is_externref(self) as usize
}
#[inline] #[inline]
fn into_abi(self, store: &mut StoreOpaque) -> Option<Self::Abi> { fn into_abi(self, store: &mut StoreOpaque) -> Option<Self::Abi> {
<(T,) as WasmParams>::into_abi((self,), store) <(T,) as WasmParams>::into_abi((self,), store)
} }
unsafe fn invoke<R: WasmResults>( unsafe fn invoke<R: WasmResults>(
func: *const VMFunctionBody, func: *const VMFunctionBody,
vmctx1: *mut VMContext, vmctx1: *mut VMContext,
@@ -365,6 +441,15 @@ macro_rules! impl_wasm_params {
} }
} }
#[inline]
fn externrefs_count(&self) -> usize {
let ($(ref $t,)*) = self;
0 $(
+ $t.is_externref() as usize
)*
}
#[inline] #[inline]
fn into_abi(self, _store: &mut StoreOpaque) -> Option<Self::Abi> { fn into_abi(self, _store: &mut StoreOpaque) -> Option<Self::Abi> {
let ($($t,)*) = self; let ($($t,)*) = self;

View File

@@ -290,6 +290,67 @@ unsafe impl Send for AsyncState {}
#[cfg(feature = "async")] #[cfg(feature = "async")]
unsafe impl Sync for AsyncState {} unsafe impl Sync for AsyncState {}
/// An RAII type to automatically mark a region of code as unsafe for GC.
pub(crate) struct AutoAssertNoGc<T>
where
T: std::ops::DerefMut<Target = StoreOpaque>,
{
#[cfg(debug_assertions)]
prev_okay: bool,
store: T,
}
impl<T> AutoAssertNoGc<T>
where
T: std::ops::DerefMut<Target = StoreOpaque>,
{
pub fn new(mut store: T) -> Self {
#[cfg(debug_assertions)]
{
let prev_okay = store.externref_activations_table.set_gc_okay(false);
return AutoAssertNoGc { store, prev_okay };
}
#[cfg(not(debug_assertions))]
{
return AutoAssertNoGc { store };
}
}
}
impl<T> std::ops::Deref for AutoAssertNoGc<T>
where
T: std::ops::DerefMut<Target = StoreOpaque>,
{
type Target = T;
fn deref(&self) -> &Self::Target {
&self.store
}
}
impl<T> std::ops::DerefMut for AutoAssertNoGc<T>
where
T: std::ops::DerefMut<Target = StoreOpaque>,
{
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.store
}
}
impl<T> Drop for AutoAssertNoGc<T>
where
T: std::ops::DerefMut<Target = StoreOpaque>,
{
fn drop(&mut self) {
#[cfg(debug_assertions)]
{
self.store
.externref_activations_table
.set_gc_okay(self.prev_okay);
}
}
}
/// Used to associate instances with the store. /// Used to associate instances with the store.
/// ///
/// This is needed to track if the instance was allocated explicitly with the on-demand /// This is needed to track if the instance was allocated explicitly with the on-demand
@@ -1039,9 +1100,8 @@ impl StoreOpaque {
&*self.interrupts as *const VMInterrupts as *mut VMInterrupts &*self.interrupts as *const VMInterrupts as *mut VMInterrupts
} }
pub unsafe fn insert_vmexternref(&mut self, r: VMExternRef) { pub unsafe fn insert_vmexternref_without_gc(&mut self, r: VMExternRef) {
self.externref_activations_table self.externref_activations_table.insert_without_gc(r);
.insert_with_gc(r, &self.modules)
} }
#[inline] #[inline]

View File

@@ -230,21 +230,21 @@ impl FuncType {
results: impl IntoIterator<Item = ValType>, results: impl IntoIterator<Item = ValType>,
) -> FuncType { ) -> FuncType {
FuncType { FuncType {
sig: WasmFuncType { sig: WasmFuncType::new(
params: params.into_iter().map(|t| t.to_wasm_type()).collect(), params.into_iter().map(|t| t.to_wasm_type()).collect(),
returns: results.into_iter().map(|t| t.to_wasm_type()).collect(), results.into_iter().map(|t| t.to_wasm_type()).collect(),
}, ),
} }
} }
/// Returns the list of parameter types for this function. /// Returns the list of parameter types for this function.
pub fn params(&self) -> impl ExactSizeIterator<Item = ValType> + '_ { pub fn params(&self) -> impl ExactSizeIterator<Item = ValType> + '_ {
self.sig.params.iter().map(ValType::from_wasm_type) self.sig.params().iter().map(ValType::from_wasm_type)
} }
/// Returns the list of result types for this function. /// Returns the list of result types for this function.
pub fn results(&self) -> impl ExactSizeIterator<Item = ValType> + '_ { pub fn results(&self) -> impl ExactSizeIterator<Item = ValType> + '_ {
self.sig.returns.iter().map(ValType::from_wasm_type) self.sig.returns().iter().map(ValType::from_wasm_type)
} }
pub(crate) fn as_wasm_func_type(&self) -> &WasmFuncType { pub(crate) fn as_wasm_func_type(&self) -> &WasmFuncType {

View File

@@ -93,17 +93,17 @@ impl Val {
} }
} }
pub(crate) unsafe fn write_value_to(&self, store: &mut StoreOpaque, p: *mut u128) { pub(crate) unsafe fn write_value_without_gc(&self, store: &mut StoreOpaque, p: *mut u128) {
match self { match *self {
Val::I32(i) => ptr::write(p as *mut i32, *i), Val::I32(i) => ptr::write(p as *mut i32, i),
Val::I64(i) => ptr::write(p as *mut i64, *i), Val::I64(i) => ptr::write(p as *mut i64, i),
Val::F32(u) => ptr::write(p as *mut u32, *u), Val::F32(u) => ptr::write(p as *mut u32, u),
Val::F64(u) => ptr::write(p as *mut u64, *u), Val::F64(u) => ptr::write(p as *mut u64, u),
Val::V128(b) => ptr::write(p as *mut u128, *b), Val::V128(b) => ptr::write(p as *mut u128, b),
Val::ExternRef(None) => ptr::write(p, 0), Val::ExternRef(None) => ptr::write(p, 0),
Val::ExternRef(Some(x)) => { Val::ExternRef(Some(ref x)) => {
let externref_ptr = x.inner.as_raw(); let externref_ptr = x.inner.as_raw();
store.insert_vmexternref(x.inner.clone()); store.insert_vmexternref_without_gc(x.clone().inner);
ptr::write(p as *mut *mut u8, externref_ptr) ptr::write(p as *mut *mut u8, externref_ptr)
} }
Val::FuncRef(f) => ptr::write( Val::FuncRef(f) => ptr::write(

View File

@@ -424,3 +424,56 @@ fn global_init_no_leak() -> anyhow::Result<()> {
Ok(()) Ok(())
} }
#[test]
fn no_gc_middle_of_args() -> anyhow::Result<()> {
let (mut store, module) = ref_types_module(
r#"
(module
(import "" "return_some" (func $return (result externref externref externref)))
(import "" "take_some" (func $take (param externref externref externref)))
(func (export "run")
(local i32)
i32.const 1000
local.set 0
loop
call $return
call $take
local.get 0
i32.const -1
i32.add
local.tee 0
br_if 0
end
)
)
"#,
)?;
let mut linker = Linker::new(store.engine());
linker.func_wrap("", "return_some", || {
(
Some(ExternRef::new("a".to_string())),
Some(ExternRef::new("b".to_string())),
Some(ExternRef::new("c".to_string())),
)
})?;
linker.func_wrap(
"",
"take_some",
|a: Option<ExternRef>, b: Option<ExternRef>, c: Option<ExternRef>| {
let a = a.unwrap();
let b = b.unwrap();
let c = c.unwrap();
assert_eq!(a.data().downcast_ref::<String>().unwrap(), "a");
assert_eq!(b.data().downcast_ref::<String>().unwrap(), "b");
assert_eq!(c.data().downcast_ref::<String>().unwrap(), "c");
},
)?;
let instance = linker.instantiate(&mut store, &module)?;
let func = instance.get_typed_func::<(), (), _>(&mut store, "run")?;
func.call(&mut store, ())?;
Ok(())
}