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:
@@ -774,6 +774,21 @@ impl Func {
|
||||
|
||||
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`.
|
||||
let param_tys = ty.params();
|
||||
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");
|
||||
}
|
||||
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);
|
||||
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
|
||||
// values produced are correct. There could be a bug in `func` that
|
||||
// produces the wrong number, wrong types, or wrong stores of
|
||||
@@ -887,7 +913,7 @@ impl Func {
|
||||
));
|
||||
}
|
||||
unsafe {
|
||||
ret.write_value_to(caller.store.0, values_vec.add(i));
|
||||
ret.write_value_without_gc(caller.store.0, values_vec.add(i));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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 anyhow::{bail, Result};
|
||||
use std::marker;
|
||||
@@ -115,15 +115,33 @@ where
|
||||
store: &mut StoreContextMut<'_, T>,
|
||||
params: Params,
|
||||
) -> 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
|
||||
// belong within this store, otherwise it would be unsafe for store
|
||||
// values to cross each other.
|
||||
let params = match params.into_abi(store.0) {
|
||||
Some(abi) => abi,
|
||||
None => {
|
||||
return Err(Trap::new(
|
||||
"attempt to pass cross-`Store` value to Wasm as function argument",
|
||||
))
|
||||
|
||||
let params = {
|
||||
// GC is not safe here, since we move refs into the activations
|
||||
// table but don't hold a strong reference onto them until we enter
|
||||
// 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)]
|
||||
fn compatible_with_store(&self, store: &StoreOpaque) -> bool;
|
||||
#[doc(hidden)]
|
||||
fn is_externref(&self) -> bool;
|
||||
#[doc(hidden)]
|
||||
fn into_abi(self, store: &mut StoreOpaque) -> Self::Abi;
|
||||
#[doc(hidden)]
|
||||
unsafe fn from_abi(abi: Self::Abi, store: &mut StoreOpaque) -> Self;
|
||||
@@ -201,6 +221,10 @@ macro_rules! primitives {
|
||||
true
|
||||
}
|
||||
#[inline]
|
||||
fn is_externref(&self) -> bool {
|
||||
false
|
||||
}
|
||||
#[inline]
|
||||
fn into_abi(self, _store: &mut StoreOpaque) -> Self::Abi {
|
||||
self
|
||||
}
|
||||
@@ -234,12 +258,46 @@ unsafe impl WasmTy for Option<ExternRef> {
|
||||
true
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_externref(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn into_abi(self, store: &mut StoreOpaque) -> Self::Abi {
|
||||
if let Some(x) = self {
|
||||
let abi = x.inner.as_raw();
|
||||
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
|
||||
} else {
|
||||
@@ -276,6 +334,11 @@ unsafe impl WasmTy for Option<Func> {
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn is_externref(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn into_abi(self, store: &mut StoreOpaque) -> Self::Abi {
|
||||
if let Some(f) = self {
|
||||
@@ -299,10 +362,16 @@ unsafe impl WasmTy for Option<Func> {
|
||||
pub unsafe trait WasmParams: Send {
|
||||
#[doc(hidden)]
|
||||
type Abi: Copy;
|
||||
|
||||
#[doc(hidden)]
|
||||
fn typecheck(params: impl ExactSizeIterator<Item = crate::ValType>) -> Result<()>;
|
||||
|
||||
#[doc(hidden)]
|
||||
fn externrefs_count(&self) -> usize;
|
||||
|
||||
#[doc(hidden)]
|
||||
fn into_abi(self, store: &mut StoreOpaque) -> Option<Self::Abi>;
|
||||
|
||||
#[doc(hidden)]
|
||||
unsafe fn invoke<R: WasmResults>(
|
||||
func: *const VMFunctionBody,
|
||||
@@ -323,10 +392,17 @@ where
|
||||
fn typecheck(params: impl ExactSizeIterator<Item = crate::ValType>) -> Result<()> {
|
||||
<(T,) as WasmParams>::typecheck(params)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn externrefs_count(&self) -> usize {
|
||||
T::is_externref(self) as usize
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn into_abi(self, store: &mut StoreOpaque) -> Option<Self::Abi> {
|
||||
<(T,) as WasmParams>::into_abi((self,), store)
|
||||
}
|
||||
|
||||
unsafe fn invoke<R: WasmResults>(
|
||||
func: *const VMFunctionBody,
|
||||
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]
|
||||
fn into_abi(self, _store: &mut StoreOpaque) -> Option<Self::Abi> {
|
||||
let ($($t,)*) = self;
|
||||
|
||||
@@ -290,6 +290,67 @@ unsafe impl Send for AsyncState {}
|
||||
#[cfg(feature = "async")]
|
||||
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.
|
||||
///
|
||||
/// 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
|
||||
}
|
||||
|
||||
pub unsafe fn insert_vmexternref(&mut self, r: VMExternRef) {
|
||||
self.externref_activations_table
|
||||
.insert_with_gc(r, &self.modules)
|
||||
pub unsafe fn insert_vmexternref_without_gc(&mut self, r: VMExternRef) {
|
||||
self.externref_activations_table.insert_without_gc(r);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
|
||||
@@ -230,21 +230,21 @@ impl FuncType {
|
||||
results: impl IntoIterator<Item = ValType>,
|
||||
) -> FuncType {
|
||||
FuncType {
|
||||
sig: WasmFuncType {
|
||||
params: params.into_iter().map(|t| t.to_wasm_type()).collect(),
|
||||
returns: results.into_iter().map(|t| t.to_wasm_type()).collect(),
|
||||
},
|
||||
sig: WasmFuncType::new(
|
||||
params.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.
|
||||
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.
|
||||
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 {
|
||||
|
||||
@@ -93,17 +93,17 @@ impl Val {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) unsafe fn write_value_to(&self, store: &mut StoreOpaque, p: *mut u128) {
|
||||
match self {
|
||||
Val::I32(i) => ptr::write(p as *mut i32, *i),
|
||||
Val::I64(i) => ptr::write(p as *mut i64, *i),
|
||||
Val::F32(u) => ptr::write(p as *mut u32, *u),
|
||||
Val::F64(u) => ptr::write(p as *mut u64, *u),
|
||||
Val::V128(b) => ptr::write(p as *mut u128, *b),
|
||||
pub(crate) unsafe fn write_value_without_gc(&self, store: &mut StoreOpaque, p: *mut u128) {
|
||||
match *self {
|
||||
Val::I32(i) => ptr::write(p as *mut i32, i),
|
||||
Val::I64(i) => ptr::write(p as *mut i64, i),
|
||||
Val::F32(u) => ptr::write(p as *mut u32, u),
|
||||
Val::F64(u) => ptr::write(p as *mut u64, u),
|
||||
Val::V128(b) => ptr::write(p as *mut u128, b),
|
||||
Val::ExternRef(None) => ptr::write(p, 0),
|
||||
Val::ExternRef(Some(x)) => {
|
||||
Val::ExternRef(Some(ref x)) => {
|
||||
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)
|
||||
}
|
||||
Val::FuncRef(f) => ptr::write(
|
||||
|
||||
Reference in New Issue
Block a user