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:
Nick Fitzgerald
2021-09-17 10:27:29 -07:00
committed by GitHub
14 changed files with 665 additions and 106 deletions

View File

@@ -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));
}
}

View File

@@ -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;

View File

@@ -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]

View File

@@ -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 {

View File

@@ -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(