Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Js integrity checks #4176

Merged
merged 3 commits into from
Aug 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/api/src/js/externals/table.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ fn get_function(store: &mut impl AsStoreMut, val: Value) -> Result<Function, Run
return Err(RuntimeError::new("cannot pass Value across contexts"));
}
match val {
Value::FuncRef(Some(ref func)) => Ok(func.0.handle.function.clone().into()),
Value::FuncRef(Some(ref func)) => Ok(func.0.handle.function.clone().into_inner()),
// Only funcrefs is supported by the spec atm
_ => unimplemented!("The {val:?} is not yet supported"),
}
Expand Down
10 changes: 7 additions & 3 deletions lib/api/src/js/instance.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
use crate::errors::InstantiationError;
use crate::exports::Exports;
use crate::imports::Imports;
use crate::js::as_js::AsJs;
use crate::js::vm::VMInstance;
use crate::module::Module;
use crate::store::AsStoreMut;
use crate::Extern;
use crate::{errors::InstantiationError, js::js_handle::JsHandle};
use js_sys::WebAssembly;

#[derive(Clone, PartialEq, Eq)]
pub struct Instance {
pub(crate) _handle: VMInstance,
pub(crate) _handle: JsHandle<VMInstance>,
}

// Instance can't be Send in js because it dosen't support `structuredClone`
Expand Down Expand Up @@ -67,6 +67,10 @@ impl Instance {
})
.collect::<Result<Exports, InstantiationError>>()?;

Ok((Self { _handle: instance }, exports))
let instance = Instance {
_handle: JsHandle::new(instance),
};

Ok((instance, exports))
}
}
217 changes: 217 additions & 0 deletions lib/api/src/js/js_handle.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
use std::ops::{Deref, DerefMut};

use wasm_bindgen::JsValue;

use self::integrity_check::IntegrityCheck;

/// A handle that lets you detect thread-safety issues when passing a
/// [`JsValue`] (or derived type) around.
#[derive(Debug, Clone)]
pub(crate) struct JsHandle<T> {
value: T,
integrity: IntegrityCheck,
}

impl<T> JsHandle<T> {
#[track_caller]
pub fn new(value: T) -> Self {
JsHandle {
value,
integrity: IntegrityCheck::new(std::any::type_name::<T>()),
}
}

#[track_caller]
pub fn into_inner(self) -> T {
self.integrity.check();
self.value
}
}

impl<T: PartialEq> PartialEq for JsHandle<T> {
fn eq(&self, other: &Self) -> bool {
let JsHandle {
value,
integrity: _,
} = self;

*value == other.value
}
}

impl<T: Eq> Eq for JsHandle<T> {}

impl<T> From<T> for JsHandle<T> {
#[track_caller]
fn from(value: T) -> Self {
JsHandle::new(value)
}
}

impl<T: Into<JsValue>> From<JsHandle<T>> for JsValue {
fn from(value: JsHandle<T>) -> Self {
value.into_inner().into()
}
}

impl<A, T> AsRef<A> for JsHandle<T>
where
T: AsRef<A>,
{
#[track_caller]
fn as_ref(&self) -> &A {
self.integrity.check();
self.value.as_ref()
}
}

impl<T> Deref for JsHandle<T> {
type Target = T;

#[track_caller]
fn deref(&self) -> &Self::Target {
self.integrity.check();
&self.value
}
}

impl<T> DerefMut for JsHandle<T> {
#[track_caller]
fn deref_mut(&mut self) -> &mut Self::Target {
self.integrity.check();
&mut self.value
}
}

#[cfg(not(debug_assertions))]
mod integrity_check {

#[derive(Debug, Clone, PartialEq)]
pub(crate) struct IntegrityCheck;

impl IntegrityCheck {
#[track_caller]
pub(crate) fn new(_type_name: &'static str) -> Self {
IntegrityCheck
}

pub(crate) fn check(&self) {}
}
}

#[cfg(debug_assertions)]
mod integrity_check {
use std::{
fmt::Write as _,
panic::Location,
sync::atomic::{AtomicU32, Ordering},
};

use js_sys::{JsString, Symbol};
use wasm_bindgen::JsValue;

#[derive(Debug, Clone, PartialEq)]
pub(crate) struct IntegrityCheck {
original_thread: u32,
created: &'static Location<'static>,
type_name: &'static str,
backtrace: Option<String>,
}

impl IntegrityCheck {
#[track_caller]
pub(crate) fn new(type_name: &'static str) -> Self {
IntegrityCheck {
original_thread: current_thread_id(),
created: Location::caller(),
type_name,
backtrace: record_backtrace(),
}
}

#[track_caller]
pub(crate) fn check(&self) {
let current_thread = current_thread_id();

if current_thread != self.original_thread {
let IntegrityCheck {
original_thread,
created,
type_name,
backtrace,
} = self;
let mut error_message = String::new();

writeln!(
error_message,
"Thread-safety integrity check for {type_name} failed."
)
.unwrap();

writeln!(
error_message,
"Created at {created} on thread #{original_thread}"
)
.unwrap();

if let Some(bt) = backtrace {
writeln!(error_message, "{bt}").unwrap();
writeln!(error_message).unwrap();
}

let caller = Location::caller();

writeln!(
error_message,
"Accessed from {caller} on thread #{current_thread}"
)
.unwrap();

if let Some(bt) = record_backtrace() {
writeln!(error_message, "{bt}").unwrap();
writeln!(error_message).unwrap();
}

panic!("{error_message}");
}
}
}

/// Get a unique ID for the current "thread" (i.e. web worker or the main
/// thread).
///
/// This works by creating a `$WASMER_THREAD_ID` symbol and setting it on
/// the global object.
fn current_thread_id() -> u32 {
static NEXT_ID: AtomicU32 = AtomicU32::new(0);

let global = js_sys::global();
let thread_id_symbol = Symbol::for_("$WASMER_THREAD_ID");

if let Some(v) = js_sys::Reflect::get(&global, &thread_id_symbol)
.ok()
.and_then(|v| v.as_f64())
{
// Note: we use a symbol so we know for sure that nobody else created
// this field.
return v as u32;
}

// Looks like we haven't set the thread ID yet.
let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);

js_sys::Reflect::set(&global, &thread_id_symbol, &JsValue::from(id))
.expect("Setting a field on the global object should never fail");

id
}

fn record_backtrace() -> Option<String> {
let err = js_sys::Error::new("");
let stack = JsString::from(wasm_bindgen::intern("stack"));

js_sys::Reflect::get(&err, &stack)
.ok()
.and_then(|v| v.as_string())
}
}
1 change: 1 addition & 0 deletions lib/api/src/js/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ pub(crate) mod errors;
pub(crate) mod extern_ref;
pub(crate) mod externals;
pub(crate) mod instance;
mod js_handle;
pub(crate) mod mem_access;
pub(crate) mod module;
#[cfg(feature = "wasm-types-polyfill")]
Expand Down
13 changes: 7 additions & 6 deletions lib/api/src/js/module.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use crate::errors::InstantiationError;
use crate::errors::RuntimeError;
use crate::imports::Imports;
use crate::js::AsJs;
use crate::store::AsStoreMut;
use crate::vm::VMInstance;
use crate::Extern;
use crate::IntoBytes;
use crate::{errors::InstantiationError, js::js_handle::JsHandle};
use crate::{AsEngineRef, ExportType, ImportType};
use bytes::Bytes;
use js_sys::{Reflect, Uint8Array, WebAssembly};
Expand Down Expand Up @@ -37,7 +37,7 @@ pub struct ModuleTypeHints {

#[derive(Clone, PartialEq, Eq)]
pub struct Module {
module: WebAssembly::Module,
module: JsHandle<WebAssembly::Module>,
name: Option<String>,
// WebAssembly type hints
type_hints: Option<ModuleTypeHints>,
Expand Down Expand Up @@ -81,12 +81,12 @@ impl Module {
}

/// Creates a new WebAssembly module skipping any kind of validation from a javascript module
///
pub(crate) unsafe fn from_js_module(
module: WebAssembly::Module,
binary: impl IntoBytes,
) -> Self {
let binary = binary.into_bytes();

// The module is now validated, so we can safely parse it's types
#[cfg(feature = "wasm-types-polyfill")]
let (type_hints, name) = {
Expand All @@ -112,11 +112,11 @@ impl Module {
let (type_hints, name) = (None, None);

Self {
module,
module: JsHandle::new(module),
type_hints,
name,
#[cfg(feature = "js-serializable-module")]
raw_bytes: Some(binary.into_bytes()),
raw_bytes: Some(binary),
}
}

Expand Down Expand Up @@ -451,9 +451,10 @@ impl Module {
}

impl From<WebAssembly::Module> for Module {
#[track_caller]
fn from(module: WebAssembly::Module) -> Module {
Module {
module,
module: JsHandle::new(module),
name: None,
type_hints: None,
#[cfg(feature = "js-serializable-module")]
Expand Down
4 changes: 3 additions & 1 deletion lib/api/src/js/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@ pub(crate) use objects::{InternalStoreHandle, StoreObject};
pub use objects::{StoreHandle, StoreObjects};

mod objects {
use std::{fmt, marker::PhantomData, num::NonZeroUsize};

use wasm_bindgen::JsValue;

use crate::js::vm::{VMFunctionEnvironment, VMGlobal};
use std::{fmt, marker::PhantomData, num::NonZeroUsize};

pub use wasmer_types::StoreId;

/// Trait to represent an object managed by a context. This is implemented on
Expand Down
Loading