Merge pull request from GHSA-v4cp-h94r-m7xf
Fix a use-after-free bug when passing `ExternRef`s to Wasm
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
|
||||
@@ -1083,9 +1144,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