// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. use crate::create_host_defined_options; use deno_core::op2; use deno_core::serde_v8; use deno_core::v8; use deno_core::v8::MapFnTo; use deno_core::JsBuffer; use std::sync::atomic::AtomicBool; use std::sync::atomic::Ordering; use std::time::Duration; pub const PRIVATE_SYMBOL_NAME: v8::OneByteConst = v8::String::create_external_onebyte_const(b"node:contextify:context"); /// An unbounded script that can be run in a context. pub struct ContextifyScript { script: v8::TracedReference, } impl v8::cppgc::GarbageCollected for ContextifyScript { fn trace(&self, visitor: &v8::cppgc::Visitor) { visitor.trace(&self.script); } } impl ContextifyScript { #[allow(clippy::too_many_arguments)] fn create<'s>( scope: &mut v8::HandleScope<'s>, source: v8::Local<'s, v8::String>, filename: v8::Local<'s, v8::Value>, line_offset: i32, column_offset: i32, cached_data: Option, produce_cached_data: bool, parsing_context: Option>, ) -> Option> { let context = if let Some(parsing_context) = parsing_context { let Some(context) = ContextifyContext::from_sandbox_obj(scope, parsing_context) else { let message = v8::String::new_external_onebyte_static( scope, b"Invalid sandbox object", ) .unwrap(); let exception = v8::Exception::type_error(scope, message); scope.throw_exception(exception); return None; }; context.context(scope) } else { scope.get_current_context() }; let scope = &mut v8::ContextScope::new(scope, context); let host_defined_options = create_host_defined_options(scope); let origin = v8::ScriptOrigin::new( scope, filename, line_offset, column_offset, true, -1, None, false, false, false, Some(host_defined_options), ); let mut source = if let Some(cached_data) = cached_data { let cached_data = v8::script_compiler::CachedData::new(&cached_data); v8::script_compiler::Source::new_with_cached_data( source, Some(&origin), cached_data, ) } else { v8::script_compiler::Source::new(source, Some(&origin)) }; let options = if source.get_cached_data().is_some() { v8::script_compiler::CompileOptions::ConsumeCodeCache } else { v8::script_compiler::CompileOptions::NoCompileOptions }; let scope = &mut v8::TryCatch::new(scope); let Some(unbound_script) = v8::script_compiler::compile_unbound_script( scope, &mut source, options, v8::script_compiler::NoCacheReason::NoReason, ) else { if !scope.has_terminated() { scope.rethrow(); } return None; }; let cached_data = if produce_cached_data { unbound_script.create_code_cache() } else { None }; let script = v8::TracedReference::new(scope, unbound_script); let this = deno_core::cppgc::make_cppgc_object(scope, Self { script }); Some(CompileResult { value: serde_v8::Value { v8_value: this.into(), }, cached_data: cached_data.as_ref().map(|c| { let backing_store = v8::ArrayBuffer::new_backing_store_from_vec(c.to_vec()); v8::ArrayBuffer::with_backing_store(scope, &backing_store.make_shared()) .into() }), cached_data_rejected: source .get_cached_data() .map(|c| c.rejected()) .unwrap_or(false), cached_data_produced: cached_data.is_some(), }) } fn run_in_context<'s>( &self, scope: &mut v8::HandleScope<'s>, sandbox: Option>, timeout: i64, display_errors: bool, break_on_sigint: bool, ) -> Option> { let (context, microtask_queue) = if let Some(sandbox) = sandbox { let Some(context) = ContextifyContext::from_sandbox_obj(scope, sandbox) else { let message = v8::String::new_external_onebyte_static( scope, b"Invalid sandbox object", ) .unwrap(); let exception = v8::Exception::type_error(scope, message); scope.throw_exception(exception); return None; }; (context.context(scope), context.microtask_queue()) } else { (scope.get_current_context(), None) }; self.eval_machine( scope, context, timeout, display_errors, break_on_sigint, microtask_queue, ) } pub fn eval_machine<'s>( &self, scope: &mut v8::HandleScope<'s>, context: v8::Local, timeout: i64, _display_errors: bool, _break_on_sigint: bool, microtask_queue: Option<&v8::MicrotaskQueue>, ) -> Option> { let context_scope = &mut v8::ContextScope::new(scope, context); let scope = &mut v8::EscapableHandleScope::new(context_scope); let scope = &mut v8::TryCatch::new(scope); let unbound_script = self.script.get(scope).unwrap(); let script = unbound_script.bind_to_current_context(scope); let handle = scope.thread_safe_handle(); let mut run = || { let r = script.run(scope); if r.is_some() { if let Some(mtask_queue) = microtask_queue { mtask_queue.perform_checkpoint(scope); } } r }; #[allow(clippy::disallowed_types)] let timed_out = std::sync::Arc::new(AtomicBool::new(false)); let result = if timeout != -1 { let timed_out = timed_out.clone(); let (tx, rx) = std::sync::mpsc::channel(); deno_core::unsync::spawn_blocking(move || { if rx .recv_timeout(Duration::from_millis(timeout as _)) .is_err() { timed_out.store(true, Ordering::Relaxed); handle.terminate_execution(); } }); let r = run(); let _ = tx.send(()); r } else { run() }; if timed_out.load(Ordering::Relaxed) { if scope.has_terminated() { scope.cancel_terminate_execution(); } let message = v8::String::new( scope, &format!("Script execution timed out after {timeout}ms"), ) .unwrap(); let exception = v8::Exception::error(scope, message); let code_str = v8::String::new_external_onebyte_static(scope, b"code").unwrap(); let code = v8::String::new_external_onebyte_static( scope, b"ERR_SCRIPT_EXECUTION_TIMEOUT", ) .unwrap(); exception .cast::() .set(scope, code_str.into(), code.into()); scope.throw_exception(exception); } if scope.has_caught() { // If there was an exception thrown during script execution, re-throw it. if !scope.has_terminated() { scope.rethrow(); } return None; } Some(scope.escape(result?)) } } pub struct ContextifyContext { microtask_queue: *mut v8::MicrotaskQueue, context: v8::TracedReference, sandbox: v8::TracedReference, } impl deno_core::GarbageCollected for ContextifyContext { fn trace(&self, visitor: &v8::cppgc::Visitor) { visitor.trace(&self.context); visitor.trace(&self.sandbox); } } impl Drop for ContextifyContext { fn drop(&mut self) { if !self.microtask_queue.is_null() { // SAFETY: If this isn't null, it is a valid MicrotaskQueue. unsafe { std::ptr::drop_in_place(self.microtask_queue); } } } } struct AllowCodeGenWasm(bool); extern "C" fn allow_wasm_code_gen( context: v8::Local, _source: v8::Local, ) -> bool { match context.get_slot::() { Some(b) => b.0, None => true, } } impl ContextifyContext { pub fn attach( scope: &mut v8::HandleScope, sandbox_obj: v8::Local, _name: String, _origin: String, allow_code_gen_strings: bool, allow_code_gen_wasm: bool, own_microtask_queue: bool, ) { let main_context = scope.get_current_context(); let tmp = init_global_template(scope, ContextInitMode::UseSnapshot); let microtask_queue = if own_microtask_queue { v8::MicrotaskQueue::new(scope, v8::MicrotasksPolicy::Explicit).into_raw() } else { std::ptr::null_mut() }; let context = create_v8_context( scope, tmp, ContextInitMode::UseSnapshot, microtask_queue, ); let context_state = main_context.get_aligned_pointer_from_embedder_data( deno_core::CONTEXT_STATE_SLOT_INDEX, ); let module_map = main_context .get_aligned_pointer_from_embedder_data(deno_core::MODULE_MAP_SLOT_INDEX); context.set_security_token(main_context.get_security_token(scope)); // SAFETY: set embedder data from the creation context unsafe { context.set_aligned_pointer_in_embedder_data( deno_core::CONTEXT_STATE_SLOT_INDEX, context_state, ); context.set_aligned_pointer_in_embedder_data( deno_core::MODULE_MAP_SLOT_INDEX, module_map, ); } scope.set_allow_wasm_code_generation_callback(allow_wasm_code_gen); context.set_allow_generation_from_strings(allow_code_gen_strings); context.set_slot(AllowCodeGenWasm(allow_code_gen_wasm)); let wrapper = { let context = v8::TracedReference::new(scope, context); let sandbox = v8::TracedReference::new(scope, sandbox_obj); deno_core::cppgc::make_cppgc_object( scope, Self { context, sandbox, microtask_queue, }, ) }; let ptr = deno_core::cppgc::try_unwrap_cppgc_object::(scope, wrapper.into()); // SAFETY: We are storing a pointer to the ContextifyContext // in the embedder data of the v8::Context. The contextified wrapper // lives longer than the execution context, so this should be safe. unsafe { context.set_aligned_pointer_in_embedder_data( 3, &*ptr.unwrap() as *const ContextifyContext as _, ); } let private_str = v8::String::new_from_onebyte_const(scope, &PRIVATE_SYMBOL_NAME); let private_symbol = v8::Private::for_api(scope, private_str); sandbox_obj.set_private(scope, private_symbol, wrapper.into()); } pub fn from_sandbox_obj<'a>( scope: &mut v8::HandleScope<'a>, sandbox_obj: v8::Local, ) -> Option<&'a Self> { let private_str = v8::String::new_from_onebyte_const(scope, &PRIVATE_SYMBOL_NAME); let private_symbol = v8::Private::for_api(scope, private_str); sandbox_obj .get_private(scope, private_symbol) .and_then(|wrapper| { deno_core::cppgc::try_unwrap_cppgc_object::(scope, wrapper) // SAFETY: the lifetime of the scope does not actually bind to // the lifetime of this reference at all, but the object we read // it from does, so it will be alive at least that long. .map(|r| unsafe { &*(&*r as *const _) }) }) } pub fn is_contextify_context( scope: &mut v8::HandleScope, object: v8::Local, ) -> bool { Self::from_sandbox_obj(scope, object).is_some() } pub fn context<'a>( &self, scope: &mut v8::HandleScope<'a>, ) -> v8::Local<'a, v8::Context> { self.context.get(scope).unwrap() } fn global_proxy<'s>( &self, scope: &mut v8::HandleScope<'s>, ) -> v8::Local<'s, v8::Object> { let ctx = self.context(scope); ctx.global(scope) } fn sandbox<'a>( &self, scope: &mut v8::HandleScope<'a>, ) -> v8::Local<'a, v8::Object> { self.sandbox.get(scope).unwrap() } fn microtask_queue(&self) -> Option<&v8::MicrotaskQueue> { if self.microtask_queue.is_null() { None } else { // SAFETY: If this isn't null, it is a valid MicrotaskQueue. Some(unsafe { &*self.microtask_queue }) } } fn get<'a, 'c>( scope: &mut v8::HandleScope<'a>, object: v8::Local<'a, v8::Object>, ) -> Option<&'c ContextifyContext> { let context = object.get_creation_context(scope)?; let context_ptr = context.get_aligned_pointer_from_embedder_data(3); if context_ptr.is_null() { return None; } // SAFETY: We are storing a pointer to the ContextifyContext // in the embedder data of the v8::Context during creation. Some(unsafe { &*(context_ptr as *const ContextifyContext) }) } } pub const VM_CONTEXT_INDEX: usize = 0; #[derive(PartialEq)] pub enum ContextInitMode { ForSnapshot, UseSnapshot, } pub fn create_v8_context<'a>( scope: &mut v8::HandleScope<'a, ()>, object_template: v8::Local, mode: ContextInitMode, microtask_queue: *mut v8::MicrotaskQueue, ) -> v8::Local<'a, v8::Context> { let scope = &mut v8::EscapableHandleScope::new(scope); let context = if mode == ContextInitMode::UseSnapshot { v8::Context::from_snapshot( scope, VM_CONTEXT_INDEX, v8::ContextOptions { microtask_queue: Some(microtask_queue), ..Default::default() }, ) .unwrap() } else { let ctx = v8::Context::new( scope, v8::ContextOptions { global_template: Some(object_template), microtask_queue: Some(microtask_queue), ..Default::default() }, ); // SAFETY: ContextifyContexts will update this to a pointer to the native object unsafe { ctx.set_aligned_pointer_in_embedder_data(1, std::ptr::null_mut()); ctx.set_aligned_pointer_in_embedder_data(2, std::ptr::null_mut()); ctx.set_aligned_pointer_in_embedder_data(3, std::ptr::null_mut()); ctx.clear_all_slots(); }; ctx }; scope.escape(context) } #[derive(Debug, Clone)] struct SlotContextifyGlobalTemplate(v8::Global); pub fn init_global_template<'a>( scope: &mut v8::HandleScope<'a, ()>, mode: ContextInitMode, ) -> v8::Local<'a, v8::ObjectTemplate> { let maybe_object_template_slot = scope.get_slot::(); if maybe_object_template_slot.is_none() { let global_object_template = init_global_template_inner(scope); if mode == ContextInitMode::UseSnapshot { let contextify_global_template_slot = SlotContextifyGlobalTemplate( v8::Global::new(scope, global_object_template), ); scope.set_slot(contextify_global_template_slot); } global_object_template } else { let object_template_slot = maybe_object_template_slot .expect("ContextifyGlobalTemplate slot should be already populated.") .clone(); v8::Local::new(scope, object_template_slot.0) } } // Using thread_local! to get around compiler bug. // // See NOTE in ext/node/global.rs#L12 thread_local! { pub static QUERY_MAP_FN: v8::NamedPropertyQueryCallback<'static> = property_query.map_fn_to(); pub static GETTER_MAP_FN: v8::NamedPropertyGetterCallback<'static> = property_getter.map_fn_to(); pub static SETTER_MAP_FN: v8::NamedPropertySetterCallback<'static> = property_setter.map_fn_to(); pub static DELETER_MAP_FN: v8::NamedPropertyDeleterCallback<'static> = property_deleter.map_fn_to(); pub static ENUMERATOR_MAP_FN: v8::NamedPropertyEnumeratorCallback<'static> = property_enumerator.map_fn_to(); pub static DEFINER_MAP_FN: v8::NamedPropertyDefinerCallback<'static> = property_definer.map_fn_to(); pub static DESCRIPTOR_MAP_FN: v8::NamedPropertyDescriptorCallback<'static> = property_descriptor.map_fn_to(); } thread_local! { pub static INDEXED_GETTER_MAP_FN: v8::IndexedPropertyGetterCallback<'static> = indexed_property_getter.map_fn_to(); pub static INDEXED_SETTER_MAP_FN: v8::IndexedPropertySetterCallback<'static> = indexed_property_setter.map_fn_to(); pub static INDEXED_DELETER_MAP_FN: v8::IndexedPropertyDeleterCallback<'static> = indexed_property_deleter.map_fn_to(); pub static INDEXED_DEFINER_MAP_FN: v8::IndexedPropertyDefinerCallback<'static> = indexed_property_definer.map_fn_to(); pub static INDEXED_DESCRIPTOR_MAP_FN: v8::IndexedPropertyDescriptorCallback<'static> = indexed_property_descriptor.map_fn_to(); pub static INDEXED_ENUMERATOR_MAP_FN: v8::IndexedPropertyEnumeratorCallback<'static> = indexed_property_enumerator.map_fn_to(); pub static INDEXED_QUERY_MAP_FN: v8::IndexedPropertyQueryCallback<'static> = indexed_property_query.map_fn_to(); } pub fn init_global_template_inner<'a>( scope: &mut v8::HandleScope<'a, ()>, ) -> v8::Local<'a, v8::ObjectTemplate> { let global_object_template = v8::ObjectTemplate::new(scope); global_object_template.set_internal_field_count(3); let named_property_handler_config = { let mut config = v8::NamedPropertyHandlerConfiguration::new() .flags(v8::PropertyHandlerFlags::HAS_NO_SIDE_EFFECT); config = GETTER_MAP_FN.with(|getter| config.getter_raw(*getter)); config = SETTER_MAP_FN.with(|setter| config.setter_raw(*setter)); config = QUERY_MAP_FN.with(|query| config.query_raw(*query)); config = DELETER_MAP_FN.with(|deleter| config.deleter_raw(*deleter)); config = ENUMERATOR_MAP_FN.with(|enumerator| config.enumerator_raw(*enumerator)); config = DEFINER_MAP_FN.with(|definer| config.definer_raw(*definer)); config = DESCRIPTOR_MAP_FN.with(|descriptor| config.descriptor_raw(*descriptor)); config }; let indexed_property_handler_config = { let mut config = v8::IndexedPropertyHandlerConfiguration::new() .flags(v8::PropertyHandlerFlags::HAS_NO_SIDE_EFFECT); config = INDEXED_GETTER_MAP_FN.with(|getter| config.getter_raw(*getter)); config = INDEXED_SETTER_MAP_FN.with(|setter| config.setter_raw(*setter)); config = INDEXED_QUERY_MAP_FN.with(|query| config.query_raw(*query)); config = INDEXED_DELETER_MAP_FN.with(|deleter| config.deleter_raw(*deleter)); config = INDEXED_ENUMERATOR_MAP_FN .with(|enumerator| config.enumerator_raw(*enumerator)); config = INDEXED_DEFINER_MAP_FN.with(|definer| config.definer_raw(*definer)); config = INDEXED_DESCRIPTOR_MAP_FN .with(|descriptor| config.descriptor_raw(*descriptor)); config }; global_object_template .set_named_property_handler(named_property_handler_config); global_object_template .set_indexed_property_handler(indexed_property_handler_config); global_object_template } fn property_query<'s>( scope: &mut v8::HandleScope<'s>, property: v8::Local<'s, v8::Name>, args: v8::PropertyCallbackArguments<'s>, mut rv: v8::ReturnValue, ) -> v8::Intercepted { let Some(ctx) = ContextifyContext::get(scope, args.this()) else { return v8::Intercepted::No; }; let context = ctx.context(scope); let scope = &mut v8::ContextScope::new(scope, context); let sandbox = ctx.sandbox(scope); match sandbox.has_real_named_property(scope, property) { None => v8::Intercepted::No, Some(true) => { let Some(attr) = sandbox.get_real_named_property_attributes(scope, property) else { return v8::Intercepted::No; }; rv.set_uint32(attr.as_u32()); v8::Intercepted::Yes } Some(false) => { match ctx .global_proxy(scope) .has_real_named_property(scope, property) { None => v8::Intercepted::No, Some(true) => { let Some(attr) = ctx .global_proxy(scope) .get_real_named_property_attributes(scope, property) else { return v8::Intercepted::No; }; rv.set_uint32(attr.as_u32()); v8::Intercepted::Yes } Some(false) => v8::Intercepted::No, } } } } fn property_getter<'s>( scope: &mut v8::HandleScope<'s>, key: v8::Local<'s, v8::Name>, args: v8::PropertyCallbackArguments<'s>, mut ret: v8::ReturnValue, ) -> v8::Intercepted { let Some(ctx) = ContextifyContext::get(scope, args.this()) else { return v8::Intercepted::No; }; let sandbox = ctx.sandbox(scope); let tc_scope = &mut v8::TryCatch::new(scope); let maybe_rv = sandbox.get_real_named_property(tc_scope, key).or_else(|| { ctx .global_proxy(tc_scope) .get_real_named_property(tc_scope, key) }); if let Some(mut rv) = maybe_rv { if tc_scope.has_caught() && !tc_scope.has_terminated() { tc_scope.rethrow(); } if rv == sandbox { rv = ctx.global_proxy(tc_scope).into(); } ret.set(rv); return v8::Intercepted::Yes; } v8::Intercepted::No } fn property_setter<'s>( scope: &mut v8::HandleScope<'s>, key: v8::Local<'s, v8::Name>, value: v8::Local<'s, v8::Value>, args: v8::PropertyCallbackArguments<'s>, _rv: v8::ReturnValue<()>, ) -> v8::Intercepted { let Some(ctx) = ContextifyContext::get(scope, args.this()) else { return v8::Intercepted::No; }; let (attributes, is_declared_on_global_proxy) = match ctx .global_proxy(scope) .get_real_named_property_attributes(scope, key) { Some(attr) => (attr, true), None => (v8::PropertyAttribute::NONE, false), }; let mut read_only = attributes.is_read_only(); let (attributes, is_declared_on_sandbox) = match ctx .sandbox(scope) .get_real_named_property_attributes(scope, key) { Some(attr) => (attr, true), None => (v8::PropertyAttribute::NONE, false), }; read_only |= attributes.is_read_only(); if read_only { return v8::Intercepted::No; } // true for x = 5 // false for this.x = 5 // false for Object.defineProperty(this, 'foo', ...) // false for vmResult.x = 5 where vmResult = vm.runInContext(); let is_contextual_store = ctx.global_proxy(scope) != args.this(); // Indicator to not return before setting (undeclared) function declarations // on the sandbox in strict mode, i.e. args.ShouldThrowOnError() = true. // True for 'function f() {}', 'this.f = function() {}', // 'var f = function()'. // In effect only for 'function f() {}' because // var f = function(), is_declared = true // this.f = function() {}, is_contextual_store = false. let is_function = value.is_function(); let is_declared = is_declared_on_global_proxy || is_declared_on_sandbox; if !is_declared && args.should_throw_on_error() && is_contextual_store && !is_function { return v8::Intercepted::No; } if !is_declared && key.is_symbol() { return v8::Intercepted::No; }; if ctx.sandbox(scope).set(scope, key.into(), value).is_none() { return v8::Intercepted::No; } if is_declared_on_sandbox { if let Some(desc) = ctx.sandbox(scope).get_own_property_descriptor(scope, key) { if !desc.is_undefined() { let desc_obj: v8::Local = desc.try_into().unwrap(); // We have to specify the return value for any contextual or get/set // property let get_key = v8::String::new_external_onebyte_static(scope, b"get").unwrap(); let set_key = v8::String::new_external_onebyte_static(scope, b"set").unwrap(); if desc_obj .has_own_property(scope, get_key.into()) .unwrap_or(false) || desc_obj .has_own_property(scope, set_key.into()) .unwrap_or(false) { return v8::Intercepted::Yes; } } } } v8::Intercepted::No } fn property_descriptor<'s>( scope: &mut v8::HandleScope<'s>, key: v8::Local<'s, v8::Name>, args: v8::PropertyCallbackArguments<'s>, mut rv: v8::ReturnValue, ) -> v8::Intercepted { let Some(ctx) = ContextifyContext::get(scope, args.this()) else { return v8::Intercepted::No; }; let context = ctx.context(scope); let sandbox = ctx.sandbox(scope); let scope = &mut v8::ContextScope::new(scope, context); if sandbox.has_own_property(scope, key).unwrap_or(false) { if let Some(desc) = sandbox.get_own_property_descriptor(scope, key) { rv.set(desc); return v8::Intercepted::Yes; } } v8::Intercepted::No } fn property_definer<'s>( scope: &mut v8::HandleScope<'s>, key: v8::Local<'s, v8::Name>, desc: &v8::PropertyDescriptor, args: v8::PropertyCallbackArguments<'s>, _: v8::ReturnValue<()>, ) -> v8::Intercepted { let Some(ctx) = ContextifyContext::get(scope, args.this()) else { return v8::Intercepted::No; }; let context = ctx.context(scope); let scope = &mut v8::ContextScope::new(scope, context); let (attributes, is_declared) = match ctx .global_proxy(scope) .get_real_named_property_attributes(scope, key) { Some(attr) => (attr, true), None => (v8::PropertyAttribute::NONE, false), }; let read_only = attributes.is_read_only(); let dont_delete = attributes.is_dont_delete(); // If the property is set on the global as read_only, don't change it on // the global or sandbox. if is_declared && read_only && dont_delete { return v8::Intercepted::No; } let sandbox = ctx.sandbox(scope); let define_prop_on_sandbox = |scope: &mut v8::HandleScope, desc_for_sandbox: &mut v8::PropertyDescriptor| { if desc.has_enumerable() { desc_for_sandbox.set_enumerable(desc.enumerable()); } if desc.has_configurable() { desc_for_sandbox.set_configurable(desc.configurable()); } sandbox.define_property(scope, key, desc_for_sandbox); }; if desc.has_get() || desc.has_set() { let mut desc_for_sandbox = v8::PropertyDescriptor::new_from_get_set( if desc.has_get() { desc.get() } else { v8::undefined(scope).into() }, if desc.has_set() { desc.set() } else { v8::undefined(scope).into() }, ); define_prop_on_sandbox(scope, &mut desc_for_sandbox); } else { let value = if desc.has_value() { desc.value() } else { v8::undefined(scope).into() }; if desc.has_writable() { let mut desc_for_sandbox = v8::PropertyDescriptor::new_from_value_writable(value, desc.writable()); define_prop_on_sandbox(scope, &mut desc_for_sandbox); } else { let mut desc_for_sandbox = v8::PropertyDescriptor::new_from_value(value); define_prop_on_sandbox(scope, &mut desc_for_sandbox); } } v8::Intercepted::Yes } fn property_deleter<'s>( scope: &mut v8::HandleScope<'s>, key: v8::Local<'s, v8::Name>, args: v8::PropertyCallbackArguments<'s>, mut rv: v8::ReturnValue, ) -> v8::Intercepted { let Some(ctx) = ContextifyContext::get(scope, args.this()) else { return v8::Intercepted::No; }; let context = ctx.context(scope); let sandbox = ctx.sandbox(scope); let context_scope = &mut v8::ContextScope::new(scope, context); if sandbox.delete(context_scope, key.into()).unwrap_or(false) { return v8::Intercepted::No; } rv.set_bool(false); v8::Intercepted::Yes } fn property_enumerator<'s>( scope: &mut v8::HandleScope<'s>, args: v8::PropertyCallbackArguments<'s>, mut rv: v8::ReturnValue, ) { let Some(ctx) = ContextifyContext::get(scope, args.this()) else { return; }; let context = ctx.context(scope); let sandbox = ctx.sandbox(scope); let context_scope = &mut v8::ContextScope::new(scope, context); let Some(properties) = sandbox .get_property_names(context_scope, v8::GetPropertyNamesArgs::default()) else { return; }; rv.set(properties); } fn indexed_property_enumerator<'s>( scope: &mut v8::HandleScope<'s>, args: v8::PropertyCallbackArguments<'s>, mut rv: v8::ReturnValue, ) { let Some(ctx) = ContextifyContext::get(scope, args.this()) else { return; }; let context = ctx.context(scope); let scope = &mut v8::ContextScope::new(scope, context); // By default, GetPropertyNames returns string and number property names, and // doesn't convert the numbers to strings. let Some(properties) = ctx .sandbox(scope) .get_property_names(scope, v8::GetPropertyNamesArgs::default()) else { return; }; let Ok(properties_vec) = serde_v8::from_v8::>(scope, properties.into()) else { return; }; let mut indices = vec![]; for prop in properties_vec { if prop.v8_value.is_number() { indices.push(prop.v8_value); } } rv.set(v8::Array::new_with_elements(scope, &indices)); } fn uint32_to_name<'s>( scope: &mut v8::HandleScope<'s>, index: u32, ) -> v8::Local<'s, v8::Name> { let int = v8::Integer::new_from_unsigned(scope, index); let u32 = v8::Local::::try_from(int).unwrap(); u32.to_string(scope).unwrap().into() } fn indexed_property_query<'s>( scope: &mut v8::HandleScope<'s>, index: u32, args: v8::PropertyCallbackArguments<'s>, rv: v8::ReturnValue, ) -> v8::Intercepted { let name = uint32_to_name(scope, index); property_query(scope, name, args, rv) } fn indexed_property_getter<'s>( scope: &mut v8::HandleScope<'s>, index: u32, args: v8::PropertyCallbackArguments<'s>, rv: v8::ReturnValue, ) -> v8::Intercepted { let key = uint32_to_name(scope, index); property_getter(scope, key, args, rv) } fn indexed_property_setter<'s>( scope: &mut v8::HandleScope<'s>, index: u32, value: v8::Local<'s, v8::Value>, args: v8::PropertyCallbackArguments<'s>, rv: v8::ReturnValue<()>, ) -> v8::Intercepted { let key = uint32_to_name(scope, index); property_setter(scope, key, value, args, rv) } fn indexed_property_descriptor<'s>( scope: &mut v8::HandleScope<'s>, index: u32, args: v8::PropertyCallbackArguments<'s>, rv: v8::ReturnValue, ) -> v8::Intercepted { let key = uint32_to_name(scope, index); property_descriptor(scope, key, args, rv) } fn indexed_property_definer<'s>( scope: &mut v8::HandleScope<'s>, index: u32, descriptor: &v8::PropertyDescriptor, args: v8::PropertyCallbackArguments<'s>, rv: v8::ReturnValue<()>, ) -> v8::Intercepted { let key = uint32_to_name(scope, index); property_definer(scope, key, descriptor, args, rv) } fn indexed_property_deleter<'s>( scope: &mut v8::HandleScope<'s>, index: u32, args: v8::PropertyCallbackArguments<'s>, mut rv: v8::ReturnValue, ) -> v8::Intercepted { let Some(ctx) = ContextifyContext::get(scope, args.this()) else { return v8::Intercepted::No; }; let context = ctx.context(scope); let sandbox = ctx.sandbox(scope); let context_scope = &mut v8::ContextScope::new(scope, context); if !sandbox.delete_index(context_scope, index).unwrap_or(false) { return v8::Intercepted::No; } // Delete failed on the sandbox, intercept and do not delete on // the global object. rv.set_bool(false); v8::Intercepted::No } #[allow(clippy::too_many_arguments)] #[op2] #[serde] pub fn op_vm_create_script<'a>( scope: &mut v8::HandleScope<'a>, source: v8::Local<'a, v8::String>, filename: v8::Local<'a, v8::Value>, line_offset: i32, column_offset: i32, #[buffer] cached_data: Option, produce_cached_data: bool, parsing_context: Option>, ) -> Option> { ContextifyScript::create( scope, source, filename, line_offset, column_offset, cached_data, produce_cached_data, parsing_context, ) } #[op2(reentrant)] pub fn op_vm_script_run_in_context<'a>( scope: &mut v8::HandleScope<'a>, #[cppgc] script: &ContextifyScript, sandbox: Option>, #[serde] timeout: i64, display_errors: bool, break_on_sigint: bool, ) -> Option> { script.run_in_context( scope, sandbox, timeout, display_errors, break_on_sigint, ) } #[op2(fast)] pub fn op_vm_create_context( scope: &mut v8::HandleScope, sandbox_obj: v8::Local, #[string] name: String, #[string] origin: String, allow_code_gen_strings: bool, allow_code_gen_wasm: bool, own_microtask_queue: bool, ) { // Don't allow contextifying a sandbox multiple times. assert!(!ContextifyContext::is_contextify_context( scope, sandbox_obj )); ContextifyContext::attach( scope, sandbox_obj, name, origin, allow_code_gen_strings, allow_code_gen_wasm, own_microtask_queue, ); } #[op2(fast)] pub fn op_vm_is_context( scope: &mut v8::HandleScope, sandbox_obj: v8::Local, ) -> bool { sandbox_obj .try_into() .map(|sandbox_obj| { ContextifyContext::is_contextify_context(scope, sandbox_obj) }) .unwrap_or(false) } #[derive(serde::Serialize)] struct CompileResult<'s> { value: serde_v8::Value<'s>, cached_data: Option>, cached_data_rejected: bool, cached_data_produced: bool, } #[allow(clippy::too_many_arguments)] #[op2] #[serde] pub fn op_vm_compile_function<'s>( scope: &mut v8::HandleScope<'s>, source: v8::Local<'s, v8::String>, filename: v8::Local<'s, v8::Value>, line_offset: i32, column_offset: i32, #[buffer] cached_data: Option, produce_cached_data: bool, parsing_context: Option>, context_extensions: Option>, params: Option>, ) -> Option> { let context = if let Some(parsing_context) = parsing_context { let Some(context) = ContextifyContext::from_sandbox_obj(scope, parsing_context) else { let message = v8::String::new(scope, "Invalid sandbox object").unwrap(); let exception = v8::Exception::type_error(scope, message); scope.throw_exception(exception); return None; }; context.context(scope) } else { scope.get_current_context() }; let scope = &mut v8::ContextScope::new(scope, context); let host_defined_options = create_host_defined_options(scope); let origin = v8::ScriptOrigin::new( scope, filename, line_offset, column_offset, true, -1, None, false, false, false, Some(host_defined_options), ); let mut source = if let Some(cached_data) = cached_data { let cached_data = v8::script_compiler::CachedData::new(&cached_data); v8::script_compiler::Source::new_with_cached_data( source, Some(&origin), cached_data, ) } else { v8::script_compiler::Source::new(source, Some(&origin)) }; let context_extensions = if let Some(context_extensions) = context_extensions { let mut exts = Vec::with_capacity(context_extensions.length() as _); for i in 0..context_extensions.length() { let ext = context_extensions.get_index(scope, i)?.try_into().ok()?; exts.push(ext); } exts } else { vec![] }; let params = if let Some(params) = params { let mut exts = Vec::with_capacity(params.length() as _); for i in 0..params.length() { let ext = params.get_index(scope, i)?.try_into().ok()?; exts.push(ext); } exts } else { vec![] }; let options = if source.get_cached_data().is_some() { v8::script_compiler::CompileOptions::ConsumeCodeCache } else { v8::script_compiler::CompileOptions::NoCompileOptions }; let scope = &mut v8::TryCatch::new(scope); let Some(function) = v8::script_compiler::compile_function( scope, &mut source, ¶ms, &context_extensions, options, v8::script_compiler::NoCacheReason::NoReason, ) else { if scope.has_caught() && !scope.has_terminated() { scope.rethrow(); } return None; }; let cached_data = if produce_cached_data { function.create_code_cache() } else { None }; Some(CompileResult { value: serde_v8::Value { v8_value: function.into(), }, cached_data: cached_data.as_ref().map(|c| { let backing_store = v8::ArrayBuffer::new_backing_store_from_vec(c.to_vec()); v8::ArrayBuffer::with_backing_store(scope, &backing_store.make_shared()) .into() }), cached_data_rejected: source .get_cached_data() .map(|c| c.rejected()) .unwrap_or(false), cached_data_produced: cached_data.is_some(), }) } #[op2] pub fn op_vm_script_get_source_map_url<'s>( scope: &mut v8::HandleScope<'s>, #[cppgc] script: &ContextifyScript, ) -> v8::Local<'s, v8::Value> { let unbound_script = script.script.get(scope).unwrap(); unbound_script.get_source_mapping_url(scope) } #[op2] pub fn op_vm_script_create_cached_data<'s>( scope: &mut v8::HandleScope<'s>, #[cppgc] script: &ContextifyScript, ) -> v8::Local<'s, v8::Value> { let unbound_script = script.script.get(scope).unwrap(); let data = match unbound_script.create_code_cache() { Some(c) => c.to_vec(), None => vec![], }; let backing_store = v8::ArrayBuffer::new_backing_store_from_vec(data); v8::ArrayBuffer::with_backing_store(scope, &backing_store.make_shared()) .into() }