diff --git a/Cargo.lock b/Cargo.lock index 8aea5737..a5066a70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2772,6 +2772,7 @@ dependencies = [ name = "jstz_engine" version = "0.1.0-alpha.0" dependencies = [ + "anyhow", "mozjs", ] diff --git a/Cargo.toml b/Cargo.toml index 914c7e7c..860a9352 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ members = [ "crates/jstz_sdk", "crates/jstz_wpt", "crates/jstzd", - "crates/octez" + "crates/octez", ] [workspace.package] @@ -112,7 +112,14 @@ wasm-bindgen = "0.2.92" [workspace.dependencies.tezos-smart-rollup] version = "0.2.2" default-features = false -features = ["std", "crypto", "panic-hook", "data-encoding", "storage", "proto-alpha"] +features = [ + "std", + "crypto", + "panic-hook", + "data-encoding", + "storage", + "proto-alpha", +] [workspace.dependencies.tezos-smart-rollup-host] version = "0.2.2" diff --git a/crates/jstz_engine/Cargo.toml b/crates/jstz_engine/Cargo.toml index f83f2811..6180ce22 100644 --- a/crates/jstz_engine/Cargo.toml +++ b/crates/jstz_engine/Cargo.toml @@ -13,4 +13,5 @@ description = "A memory-safe JavaScript engine API in Rust" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +anyhow.workspace = true mozjs.workspace = true diff --git a/crates/jstz_engine/src/context.rs b/crates/jstz_engine/src/context.rs index 75a10aab..cc25d4d4 100644 --- a/crates/jstz_engine/src/context.rs +++ b/crates/jstz_engine/src/context.rs @@ -21,15 +21,19 @@ //! //! For more details, refer to the [ECMAScript Specification on Contexts](https://tc39.es/ecma262/#sec-global-environment-records). -use std::{marker::PhantomData, ptr::NonNull}; +use std::{ffi::c_void, marker::PhantomData, pin::Pin, ptr::NonNull}; use mozjs::{ - jsapi::{JSContext, JS}, + jsapi::{JSContext, JS_AddExtraGCRootsTracer, JS_RemoveExtraGCRootsTracer, JS}, rust::Runtime, }; use crate::{ compartment::{self, Compartment}, + gc::{ + root::{unsafe_ffi_trace_context_roots, Root, ShadowStack}, + Trace, + }, realm::Realm, AsRawPtr, }; @@ -42,6 +46,7 @@ pub struct Context { // SAFETY: This is only `Some` if the state `S` is `Entered<'a, C, S>`. // In this case, the old realm is guaranteed to be alive for at least as long as `'a`. old_realm: Option<*mut JS::Realm>, + shadow_stack: Pin>, marker: PhantomData, } @@ -72,6 +77,20 @@ impl<'a, C: Compartment, S> CanAccess for Entered<'a, C, S> {} pub trait InCompartment {} impl<'a, C: Compartment, S> InCompartment for Entered<'a, C, S> {} +fn new_shadow_stack(raw_cx: NonNull) -> Pin> { + // Initialize the GC roots for the context. + let shadow_stack = Box::pin(ShadowStack::new()); + unsafe { + JS_AddExtraGCRootsTracer( + raw_cx.as_ptr(), + Some(unsafe_ffi_trace_context_roots), + &*shadow_stack as *const ShadowStack as *mut c_void, + ); + } + + shadow_stack +} + impl Context { pub fn from_runtime(rt: &Runtime) -> Self { // SAFETY: `rt.cx()` cannot be `NULL`. @@ -80,6 +99,7 @@ impl Context { Self { raw_cx, old_realm: None, + shadow_stack: new_shadow_stack(raw_cx), marker: PhantomData, } } @@ -89,7 +109,7 @@ impl Context { /// Enter an existing realm pub fn enter_realm<'a, 'b, C: Compartment>( &'a mut self, - realm: Realm<'b, C>, + realm: &Realm<'b, C>, ) -> Context> where S: CanAlloc + CanAccess, @@ -100,6 +120,7 @@ impl Context { Context { raw_cx: self.raw_cx, old_realm: Some(old_realm), + shadow_stack: new_shadow_stack(self.raw_cx), marker: PhantomData, } } @@ -111,22 +132,39 @@ impl Context { { let realm = Realm::new(self)?; - // TODO(https://linear.app/tezos/issue/JSTZ-196): - // Remove this `unsafe` block once rooting is implemented. - // - // SAFETY: We transmute the lifetime of the realm. This is equivalent to rooting the realm. + // SAFETY: + // We transmute the lifetime of the realm. This is equivalent to rooting the realm. // This safe because entering the realm immediately roots the realm. + // + // Unfortunately we cannot truly root the realm (using [`letroot!`]) since + // the lifetime of the realm is bound to the lifetime of [`self`]. This is + // a unique case where our approach to static rooting fails. unsafe { let rooted_realm: Realm<'_, compartment::Ref<'_>> = std::mem::transmute(realm); - Some(self.enter_realm(rooted_realm)) + Some(self.enter_realm(&rooted_realm)) } } + + /// Creates a new root + pub fn root(&self) -> Root { + Root::new(self.shadow_stack.as_ref()) + } } impl Drop for Context { fn drop(&mut self) { + // Unroot everything in the current realm + unsafe { + JS_RemoveExtraGCRootsTracer( + self.as_raw_ptr(), + Some(unsafe_ffi_trace_context_roots), + &*self.shadow_stack as *const ShadowStack as *mut c_void, + ); + } + + // Leave the current realm if let Some(old_realm) = self.old_realm { unsafe { JS::LeaveRealm(self.as_raw_ptr(), old_realm); diff --git a/crates/jstz_engine/src/gc/mod.rs b/crates/jstz_engine/src/gc/mod.rs index b72df5f8..7c5a3bff 100644 --- a/crates/jstz_engine/src/gc/mod.rs +++ b/crates/jstz_engine/src/gc/mod.rs @@ -19,7 +19,9 @@ //! //! For further details, see the [GC Implementation Guide](https://udn.realityripple.com/docs/Mozilla/Projects/SpiderMonkey/Internals/Garbage_collection). -mod ptr; -mod trace; +pub mod ptr; +pub mod root; +pub mod trace; +pub use root::Prolong; pub use trace::{Finalize, Trace, Tracer}; diff --git a/crates/jstz_engine/src/gc/ptr.rs b/crates/jstz_engine/src/gc/ptr.rs index f5d5612d..f849bcb1 100644 --- a/crates/jstz_engine/src/gc/ptr.rs +++ b/crates/jstz_engine/src/gc/ptr.rs @@ -48,6 +48,7 @@ pub unsafe trait WriteBarrieredPtr: Copy { /// /// [`GcPtr`] should only be used by values on the heap. Garbage collected pointers /// on the stack should be rooted. +#[derive(Debug)] pub struct GcPtr { // # Safety // diff --git a/crates/jstz_engine/src/gc/root.rs b/crates/jstz_engine/src/gc/root.rs new file mode 100644 index 00000000..04d14162 --- /dev/null +++ b/crates/jstz_engine/src/gc/root.rs @@ -0,0 +1,261 @@ +//! This module provides the mechanisms to manage roots in SpiderMonkey's garbage collection (GC) system. +//! Rooting ensures that specific objects are kept alive during GC cycles by marking them as reachable. +//! It is a critical component of GC, preventing unintended collection of active or important objects. +//! +//! In languages with native support for GC (such as JavaScript), rooting is supported by the compiler/interpreter, +//! which can provide metadata for each stack frame allowing it to be traced. However, as implementators of native +//! objects/functions for JavaScript, we have no such metadata. Instead rooting has to be performed explicitly. +//! +//! This explicit rooting is needed whenever an object is needed to outlive the borrow of the JS +//! context that produced it. For example, a function that compiles and then evaluates a script: +//! ```no_run rust +//! pub fn compile_and_evaluate( +//! path: &Path, +//! src: &str, +//! mut cx: &mut Context, +//! ) -> Option> where S: InCompartment + CanAlloc { +//! let script = Script::compile(path, src, &mut cx)?; +//! +//! script.evaluate(&mut cx) +//! } +//! ``` +//! This is the natural way to write such a function, however, it is in fact not safe since the script +//! is not rooted. If `evaluate` triggers a GC before the script is rooted during evaluation, then `script` +//! may be reclaimed, causing a later use-after-free error. +//! +//! Fortunately, our approach catches these safety problems as lifetime errors: +//! ```notrust +//! error[E0499]: cannot borrow `cx` as mutable more than once at a time +//! --> crates/jstz_engine/src/script.rs:111:25 +//! | +//! 109 | let script = Script::compile(path, src, &mut cx)?; +//! | ------- first mutable borrow occurs here +//! 110 | +//! 111 | script.evaluate(&mut cx) +//! | -------- ^^^^^^^ second mutable borrow occurs here +//! | | +//! | first borrow later used by call +//! +//! For more information about this error, try `rustc --explain E0499`. +//! ``` +//! The fix is to explicit root `script`. To do so, we use the `letroot!` macro implemented in +//! this module. +//! ```no_run rust +//! pub fn compile_and_evaluate( +//! path: &Path, +//! src: &str, +//! mut cx: &mut Context, +//! ) -> Option> where S: InCompartment + CanAlloc { +//! letroot!(script = Script::compile(path, src, &mut cx)?; [cx]); +//! +//! script.evaluate(&mut cx) +//! } +//! ``` +//! The declaration of a root allocates space on the stack for a new root. Note that it is just the reference +//! that is copied to the stack, the JS cell is still stored on the heap. +//! +//! Roots have type `Rooted<'a,T>` where `'a` is the lifetime of the root, and T is the +//! type being rooted. Once the local variables are rooted, the code typechecks, because +//! rooting changes the lifetime of the value. The rule is giving as follows: +//! +//! p: T<'a, C> r: Pin<&'b mut Root> +//! --------------------------------------------- 'a : 'b +//! r.init(p) : T<'b, C> +//! +//! where `T` is some JS type that contains the lifetime `'a` and is in the compartment `C`. +//! Note that `T<'b, C>` represents *substituting* the lifetime `'b` for the lifetime `'a` recursively on the +//! structure of `T`. +//! +//! Before rooting, the JS value had lifetime `'a`, which is usually the lifetime of the borrow of +//! the `Context` that created or accessed it. After rooting, the JS value has lifetime `'b`, which +//! is the lifetime of the root itself. Since roots are considered reachable by GC, the contents of a root +//! are guaranteed not to be GC’d during its lifetime, so this rule is sound. +//! +//! Note that this use of substitution `T<'b, C>` is being used to extend the lifetime of the JS value +//! since `'a : 'β`. + +use std::{ + cell::Cell, ffi::c_void, marker::PhantomPinned, mem, ops::Deref, pin::Pin, + ptr::NonNull, +}; + +use super::{Trace, Tracer}; + +/// Shadow stack implementation. This is a singly linked list of on stack rooted values +#[derive(Debug)] +pub(crate) struct ShadowStack { + head: Cell>>, +} + +impl ShadowStack { + /// Creates a new shadow stack + pub fn new() -> Self { + Self { + head: Cell::new(None), + } + } + + /// Trace all rooted values in the shadow stack. + /// + /// # Safety + /// + /// Calling this function outside the context of the garbage collector + /// can result in undefined behaviour + pub unsafe fn trace(&self, trc: *mut Tracer) { + let mut head = self.head.get(); + while let Some(some_head) = head { + let head_ref = some_head.as_ref(); + let next = head_ref.prev; + (*head_ref.value).trace(trc); + head = next; + } + } +} + +static DEFAULT_VALUE: &'static (dyn Trace + Sync) = &(); + +/// Entry in the GC shadow stack +/// +/// This type internally stores the shadow stack pointer, the previous pointer +/// and a pointer to the vtable and data that is to be rooted. +#[derive(Debug)] +pub(crate) struct ShadowStackEntry { + /// Shadowstack itself + stack: NonNull, + /// Previous rooted entry + prev: Option>, + /// Pointer to vtable and data to `Trace` the rooted value + value: *const dyn Trace, + // This removes the auto-implemented `Unpin` bound since this struct has + // some address-sensitive state + _marker: PhantomPinned, +} + +impl ShadowStackEntry { + /// Constructs internal shadow stack value. + pub fn new(stack: Pin<&ShadowStack>) -> Self { + Self { + stack: stack.get_ref().into(), + prev: None, + value: DEFAULT_VALUE as _, + _marker: PhantomPinned, + } + } + + pub fn link(&mut self) { + // SAFETY: self.stack is pinned on construction + self.prev = unsafe { self.stack.as_ref().head.get() } + } +} + +impl Drop for ShadowStackEntry { + fn drop(&mut self) { + // Drop current shadow stack entry and update shadow stack state. + unsafe { self.stack.as_mut().head.set(self.prev) } + } +} + +/// A stack cell for a rooted value. +#[derive(Debug)] +pub struct Root { + /// Shadow stack entry + stack_entry: ShadowStackEntry, + /// Value that is rooted. + /// [`None`] if no value has been rooted yet. See [`Root::init`]. + value: Option, +} + +impl Root { + /// Creates a new root. + pub(crate) fn new(stack: Pin<&ShadowStack>) -> Self { + Self { + stack_entry: ShadowStackEntry::new(stack), + value: None, + } + } + + /// Initialises the root by rooting the given value. + pub fn init<'a, U>(self: Pin<&'a mut Self>, value: U) -> Rooted<'a, T> + where + U: Prolong<'a, Aged = T>, + { + let inited_self = unsafe { + // SAFETY: we do not move out of self + Pin::map_unchecked_mut(self, |self_mut| { + // SAFETY: we can safely prolong `value`'s lifetime to the lifetime of the root + self_mut.value = Some(value.extend_lifetime()); + self_mut.stack_entry.link(); + + // SAFETY: We know the lifetime of `stack_entry.value` will not outlive `value`, so + // we can safely take a `dyn` pointer of it. Additionally we know this pointer will + // remain valid until the `Pin` (aka Root) is dropped, which is bound to the lifetime + // of `value` + self_mut.stack_entry.value = + mem::transmute((&mut self_mut.value) as &mut dyn Trace); + + self_mut + }) + }; + + Rooted { + pinned: inited_self, + } + } +} + +// Rooted value on the stack. This is non-copyable type that is used to hold GC thing on stack. +#[derive(Debug)] +pub struct Rooted<'a, T: Trace + 'a> { + pinned: Pin<&'a mut Root>, +} + +impl<'a, T: Trace> Deref for Rooted<'a, T> { + type Target = T; + fn deref(&self) -> &Self::Target { + self.pinned.value.as_ref().unwrap() + } +} + +pub(crate) unsafe extern "C" fn unsafe_ffi_trace_context_roots( + trc: *mut Tracer, + shadow_stack: *mut c_void, +) { + let shadow_stack = Box::from_raw(shadow_stack as *mut ShadowStack); + shadow_stack.trace(trc); + + // Don't free the box, this is done by `Context::drop` + mem::forget(shadow_stack) +} + +#[macro_export] +macro_rules! letroot { + ($var_name: ident = $value: expr; [$cx: expr]) => { + #[allow(unused_mut)] + let mut $var_name = core::pin::pin!($cx.root()); + + #[allow(unused_mut)] + let mut $var_name = $var_name.init($value); + }; +} + +/// A trait for extending or prolonging the lifetime of a value to `'a`. +/// +/// # Safety +/// +/// Usaged of `extend_lifetime` outside the context of the garbage collector is considered +/// undefined behaviour +pub unsafe trait Prolong<'a> { + type Aged; + + unsafe fn extend_lifetime(self) -> Self::Aged + where + Self: Sized, + { + // SAFETY: We `transmute_copy` the value to change the lifetime without + // causing the destructor to run. `forget`ting the value is safe + // because the value is still alive. + let result = std::mem::transmute_copy(&self); + std::mem::forget(self); + result + } +} diff --git a/crates/jstz_engine/src/realm.rs b/crates/jstz_engine/src/realm.rs index bba932af..96a26179 100644 --- a/crates/jstz_engine/src/realm.rs +++ b/crates/jstz_engine/src/realm.rs @@ -22,7 +22,7 @@ //! //! For more details, refer to the [ECMAScript Specification on Realms](https://tc39.es/ecma262/#sec-code-realms). -use std::{marker::PhantomData, ptr::NonNull}; +use std::{marker::PhantomData, pin::Pin, sync::Arc}; use mozjs::{ jsapi::{JSObject, JS_NewGlobalObject, OnNewGlobalHookOption, JS}, @@ -32,21 +32,25 @@ use mozjs::{ use crate::{ compartment::{self, Compartment}, context::{CanAccess, CanAlloc, Context, InCompartment}, + custom_trace, + gc::{ptr::GcPtr, Finalize, Prolong, Trace}, AsRawPtr, }; /// A JavaScript realm with lifetime of at least `'a` allocated in compartment `C`. /// A realm is a global object. +#[derive(Debug)] pub struct Realm<'a, C: Compartment> { - global_object: NonNull, + global_object: Pin>>, marker: PhantomData<(&'a (), C)>, } -impl<'a, C: Compartment> Copy for Realm<'a, C> {} - impl<'a, C: Compartment> Clone for Realm<'a, C> { fn clone(&self) -> Self { - *self + Self { + global_object: self.global_object.clone(), + marker: PhantomData, + } } } @@ -73,8 +77,12 @@ impl<'a> Realm<'a, compartment::Ref<'a>> { ) }; + if global_object.is_null() { + return None; + } + Some(Self { - global_object: NonNull::new(global_object)?, + global_object: GcPtr::pinned(global_object), marker: PhantomData, }) } @@ -87,8 +95,12 @@ impl<'a, C: Compartment> Realm<'a, C> { { let global_object = unsafe { JS::CurrentGlobalOrNull(cx.as_raw_ptr()) }; + if global_object.is_null() { + return None; + } + Some(Self { - global_object: NonNull::new(global_object)?, + global_object: GcPtr::pinned(global_object), marker: PhantomData, }) } @@ -98,6 +110,18 @@ impl<'a, C: Compartment> AsRawPtr for Realm<'a, C> { type Ptr = *mut JSObject; unsafe fn as_raw_ptr(&self) -> Self::Ptr { - self.global_object.as_ptr() + self.global_object.get() } } + +impl<'a, C: Compartment> Finalize for Realm<'a, C> {} + +unsafe impl<'a, C: Compartment> Trace for Realm<'a, C> { + custom_trace!(this, mark, { + mark(&this.global_object); + }); +} + +unsafe impl<'a, 'b, C: Compartment> Prolong<'a> for Realm<'b, C> { + type Aged = Realm<'a, C>; +} diff --git a/crates/jstz_engine/src/script.rs b/crates/jstz_engine/src/script.rs index cc281953..d021c41d 100644 --- a/crates/jstz_engine/src/script.rs +++ b/crates/jstz_engine/src/script.rs @@ -10,10 +10,10 @@ //! //! For more details, refer to the [ECMAScript Specification on Scripts and Modules](https://tc39.es/ecma262/#sec-scripts). -use std::{marker::PhantomData, path::Path, ptr::NonNull}; +use std::{marker::PhantomData, path::Path, pin::Pin, sync::Arc}; use mozjs::{ - jsapi::{Compile1, JSScript, JS_ExecuteScript}, + jsapi::{Compile1, Handle, JSScript, JS_ExecuteScript}, jsval::{JSVal, UndefinedValue}, rooted, rust::CompileOptionsWrapper, @@ -22,14 +22,26 @@ use mozjs::{ use crate::{ compartment::Compartment, context::{CanAlloc, Context, InCompartment}, - AsRawPtr, + custom_trace, + gc::{ptr::GcPtr, Finalize, Prolong, Trace}, + letroot, AsRawPtr, }; +#[derive(Debug)] pub struct Script<'a, C: Compartment> { - script: NonNull, + script: Pin>>, marker: PhantomData<(&'a (), C)>, } +impl<'a, C: Compartment> Clone for Script<'a, C> { + fn clone(&self) -> Self { + Self { + script: self.script.clone(), + marker: PhantomData, + } + } +} + impl<'a, C: Compartment> Script<'a, C> { /// Compiles a script with a given filename and returns the compiled script. /// Returns `None` if the script could not be compiled. @@ -47,8 +59,12 @@ impl<'a, C: Compartment> Script<'a, C> { let script = unsafe { Compile1(cx.as_raw_ptr(), options.ptr, &mut source) }; + if script.is_null() { + return None; + } + Some(Self { - script: NonNull::new(script)?, + script: GcPtr::pinned(script), marker: PhantomData, }) } @@ -67,12 +83,14 @@ impl<'a, C: Compartment> Script<'a, C> { // TODO(https://linear.app/tezos/issue/JSTZ-196): // Remove this once we have a proper way to root values rooted!(in(unsafe { cx.as_raw_ptr() }) let mut rval = UndefinedValue()); - rooted!(in(unsafe { cx.as_raw_ptr() }) let mut rooted_script = unsafe { self.as_raw_ptr() }); if unsafe { JS_ExecuteScript( cx.as_raw_ptr(), - rooted_script.handle_mut().into(), + Handle { + ptr: self.script.get_unsafe(), + _phantom_0: PhantomData, + }, rval.handle_mut().into(), ) } { @@ -81,16 +99,41 @@ impl<'a, C: Compartment> Script<'a, C> { None } } + + pub fn compile_and_evaluate( + path: &Path, + src: &str, + cx: &mut Context, + ) -> Option + where + S: InCompartment + CanAlloc, + { + letroot!(script = Script::compile(path, src, cx)?; [cx]); + + script.evaluate(cx) + } } impl<'a, C: Compartment> AsRawPtr for Script<'a, C> { type Ptr = *mut JSScript; unsafe fn as_raw_ptr(&self) -> Self::Ptr { - self.script.as_ptr() + self.script.get() } } +impl<'a, C: Compartment> Finalize for Script<'a, C> {} + +unsafe impl<'a, C: Compartment> Trace for Script<'a, C> { + custom_trace!(this, mark, { + mark(&this.script); + }); +} + +unsafe impl<'a, 'b, C: Compartment> Prolong<'a> for Script<'b, C> { + type Aged = Script<'a, C>; +} + #[cfg(test)] mod test { @@ -98,7 +141,7 @@ mod test { use mozjs::rust::{JSEngine, Runtime}; - use crate::{compartment, context::Context, script::Script}; + use crate::{context::Context, letroot, script::Script}; #[test] fn test_compile_and_evaluate() { @@ -115,17 +158,13 @@ mod test { let source: &'static str = "40 + 2"; // Compile the script - let script = Script::compile(&filename, source, &mut cx).unwrap(); + println!("Compiling script..."); + letroot!(script = Script::compile(&filename, source, &mut cx).unwrap(); [cx]); - // TODO(https://linear.app/tezos/issue/JSTZ-196): - // Remove once we have a proper way of rooting things. - // The script is rooted in the context in `eval`, but this doesn't work due to lifetimes. - // So we need to transmute it here. - let rooted_script: Script<'_, compartment::Ref<'_>> = - unsafe { std::mem::transmute(script) }; + println!("Script, {:?}", script); // Evaluate the script - let res = rooted_script.evaluate(&mut cx); + let res = script.evaluate(&mut cx); assert!(res.is_some());