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:
@@ -489,7 +489,7 @@ type TableElem = UnsafeCell<Option<VMExternRef>>;
|
||||
///
|
||||
/// Under the covers, this is a simple bump allocator that allows duplicate
|
||||
/// 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 {
|
||||
/// Structures used to perform fast bump allocation of storage of externref
|
||||
/// values.
|
||||
@@ -521,9 +521,14 @@ pub struct VMExternRefActivationsTable {
|
||||
/// inside-a-Wasm-frame roots, and doing a GC could lead to freeing one of
|
||||
/// those missed roots, and use after free.
|
||||
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 {
|
||||
/// Bump-allocation finger within the `chunk`.
|
||||
///
|
||||
@@ -573,6 +578,8 @@ impl VMExternRefActivationsTable {
|
||||
over_approximated_stack_roots: HashSet::with_capacity(Self::CHUNK_SIZE),
|
||||
precise_stack_roots: HashSet::with_capacity(Self::CHUNK_SIZE),
|
||||
stack_canary: None,
|
||||
#[cfg(debug_assertions)]
|
||||
gc_okay: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -581,6 +588,14 @@ impl VMExternRefActivationsTable {
|
||||
(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.
|
||||
///
|
||||
/// This is a fast path that only succeeds when the bump chunk has the
|
||||
@@ -624,6 +639,9 @@ impl VMExternRefActivationsTable {
|
||||
externref: VMExternRef,
|
||||
module_info_lookup: &dyn ModuleInfoLookup,
|
||||
) {
|
||||
#[cfg(debug_assertions)]
|
||||
assert!(self.gc_okay);
|
||||
|
||||
if let Err(externref) = self.try_insert(externref) {
|
||||
self.gc_and_insert_slow(externref, module_info_lookup);
|
||||
}
|
||||
@@ -644,6 +662,20 @@ impl VMExternRefActivationsTable {
|
||||
.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 {
|
||||
let next = unsafe { *self.alloc.next.get() };
|
||||
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>) {
|
||||
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
|
||||
@@ -807,6 +857,9 @@ pub unsafe fn gc(
|
||||
) {
|
||||
log::debug!("start GC");
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
assert!(externref_activations_table.gc_okay);
|
||||
|
||||
debug_assert!({
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user