diff --git a/ext/node/lib.rs b/ext/node/lib.rs index 85abe49e49..f91ed0b1a3 100644 --- a/ext/node/lib.rs +++ b/ext/node/lib.rs @@ -260,7 +260,11 @@ deno_core::extension!(deno_node, ops::winerror::op_node_sys_to_uv_error, ops::v8::op_v8_cached_data_version_tag, ops::v8::op_v8_get_heap_statistics, - ops::v8::op_vm_run_in_new_context, + ops::vm::op_vm_create_script, + ops::vm::op_vm_create_context, + ops::vm::op_vm_script_run_in_context, + ops::vm::op_vm_script_run_in_this_context, + ops::vm::op_vm_is_context, ops::idna::op_node_idna_domain_to_ascii, ops::idna::op_node_idna_domain_to_unicode, ops::idna::op_node_idna_punycode_to_ascii, diff --git a/ext/node/ops/mod.rs b/ext/node/ops/mod.rs index 8aed274bc2..c14b63bf4d 100644 --- a/ext/node/ops/mod.rs +++ b/ext/node/ops/mod.rs @@ -10,6 +10,8 @@ pub mod os; pub mod require; pub mod util; pub mod v8; +pub mod vm; +mod vm_internal; pub mod winerror; pub mod worker_threads; pub mod zlib; diff --git a/ext/node/ops/vm.rs b/ext/node/ops/vm.rs new file mode 100644 index 0000000000..f18038f8f0 --- /dev/null +++ b/ext/node/ops/vm.rs @@ -0,0 +1,138 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use deno_core::error::type_error; +use deno_core::error::AnyError; +use deno_core::op2; +use deno_core::v8; + +use super::vm_internal as i; + +pub struct Script { + inner: i::ContextifyScript, +} + +impl Script { + fn new( + scope: &mut v8::HandleScope, + source: v8::Local, + ) -> Result { + Ok(Self { + inner: i::ContextifyScript::new(scope, source)?, + }) + } + + fn run_in_this_context<'s>( + &self, + scope: &'s mut v8::HandleScope, + ) -> Result, AnyError> { + let context = scope.get_current_context(); + + let context_scope = &mut v8::ContextScope::new(scope, context); + let mut scope = v8::EscapableHandleScope::new(context_scope); + let result = self + .inner + .eval_machine(&mut scope, context) + .unwrap_or_else(|| v8::undefined(&mut scope).into()); + Ok(scope.escape(result)) + } + + fn run_in_context<'s>( + &self, + scope: &mut v8::HandleScope<'s>, + sandbox: v8::Local<'s, v8::Value>, + ) -> Result, AnyError> { + let context = if let Ok(sandbox_obj) = sandbox.try_into() { + let context = i::ContextifyContext::from_sandbox_obj(scope, sandbox_obj) + .ok_or_else(|| type_error("Invalid sandbox object"))?; + context.context(scope) + } else { + scope.get_current_context() + }; + + let context_scope = &mut v8::ContextScope::new(scope, context); + let mut scope = v8::EscapableHandleScope::new(context_scope); + let result = self + .inner + .eval_machine(&mut scope, context) + .unwrap_or_else(|| v8::undefined(&mut scope).into()); + Ok(scope.escape(result)) + } +} + +#[op2] +pub fn op_vm_create_script<'a>( + scope: &mut v8::HandleScope<'a>, + source: v8::Local<'a, v8::String>, +) -> Result, AnyError> { + let script = Script::new(scope, source)?; + Ok(deno_core::cppgc::make_cppgc_object(scope, script)) +} + +#[op2(reentrant)] +pub fn op_vm_script_run_in_context<'a>( + scope: &mut v8::HandleScope<'a>, + #[cppgc] script: &Script, + sandbox: v8::Local<'a, v8::Value>, +) -> Result, AnyError> { + script.run_in_context(scope, sandbox) +} + +#[op2(reentrant)] +pub fn op_vm_script_run_in_this_context<'a>( + scope: &'a mut v8::HandleScope, + #[cppgc] script: &Script, +) -> Result, AnyError> { + script.run_in_this_context(scope) +} + +#[op2] +pub fn op_vm_create_context( + scope: &mut v8::HandleScope, + sandbox_obj: v8::Local, +) { + // Don't allow contextifying a sandbox multiple times. + assert!(!i::ContextifyContext::is_contextify_context( + scope, + sandbox_obj + )); + + i::ContextifyContext::attach(scope, sandbox_obj); +} + +#[op2] +pub fn op_vm_is_context( + scope: &mut v8::HandleScope, + sandbox_obj: v8::Local, +) -> bool { + sandbox_obj + .try_into() + .map(|sandbox_obj| { + i::ContextifyContext::is_contextify_context(scope, sandbox_obj) + }) + .unwrap_or(false) +} + +#[cfg(test)] +mod tests { + use super::*; + use deno_core::v8; + + #[test] + fn test_run_in_this_context() { + let platform = v8::new_default_platform(0, false).make_shared(); + v8::V8::initialize_platform(platform); + v8::V8::initialize(); + + let isolate = &mut v8::Isolate::new(Default::default()); + + let scope = &mut v8::HandleScope::new(isolate); + let context = v8::Context::new(scope); + let scope = &mut v8::ContextScope::new(scope, context); + + let source = v8::String::new(scope, "1 + 2").unwrap(); + let script = Script::new(scope, source).unwrap(); + + let result = script.run_in_this_context(scope).unwrap(); + assert!(result.is_number()); + } +} diff --git a/ext/node/ops/vm_internal.rs b/ext/node/ops/vm_internal.rs new file mode 100644 index 0000000000..274fac91a4 --- /dev/null +++ b/ext/node/ops/vm_internal.rs @@ -0,0 +1,599 @@ +// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. + +use deno_core::error::type_error; +use deno_core::error::AnyError; +use deno_core::v8; +use deno_core::v8::MapFnTo; + +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. +#[derive(Debug)] +pub struct ContextifyScript { + script: v8::Global, +} + +impl ContextifyScript { + pub fn new( + scope: &mut v8::HandleScope, + source_str: v8::Local, + ) -> Result { + let source = v8::script_compiler::Source::new(source_str, None); + + let unbound_script = v8::script_compiler::compile_unbound_script( + scope, + source, + v8::script_compiler::CompileOptions::NoCompileOptions, + v8::script_compiler::NoCacheReason::NoReason, + ) + .ok_or_else(|| type_error("Failed to compile script"))?; + let script = v8::Global::new(scope, unbound_script); + Ok(Self { script }) + } + + // TODO(littledivy): Support `options` + pub fn eval_machine<'s>( + &self, + scope: &mut v8::HandleScope<'s>, + _context: v8::Local, + ) -> Option> { + let tc_scope = &mut v8::TryCatch::new(scope); + + let unbound_script = v8::Local::new(tc_scope, self.script.clone()); + let script = unbound_script.bind_to_current_context(tc_scope); + + let result = script.run(tc_scope); + + if tc_scope.has_caught() { + // If there was an exception thrown during script execution, re-throw it. + if !tc_scope.has_terminated() { + tc_scope.rethrow(); + } + + return None; + } + + Some(result.unwrap()) + } +} + +#[derive(Debug, Clone)] +pub struct ContextifyContext { + context: v8::Global, + sandbox: v8::Global, +} + +impl ContextifyContext { + pub fn attach( + scope: &mut v8::HandleScope, + sandbox_obj: v8::Local, + ) { + let tmp = init_global_template(scope); + + let context = create_v8_context(scope, tmp, None); + Self::from_context(scope, context, sandbox_obj); + } + + pub fn from_context( + scope: &mut v8::HandleScope, + v8_context: v8::Local, + sandbox_obj: v8::Local, + ) { + let main_context = scope.get_current_context(); + v8_context.set_security_token(main_context.get_security_token(scope)); + + let context = v8::Global::new(scope, v8_context); + let sandbox = v8::Global::new(scope, sandbox_obj); + let wrapper = + deno_core::cppgc::make_cppgc_object(scope, Self { context, sandbox }); + let ptr = deno_core::cppgc::try_unwrap_cppgc_object::(wrapper.into()) + .unwrap(); + + // 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 { + v8_context.set_aligned_pointer_in_embedder_data( + 0, + ptr 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, + 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::(wrapper) + }) + } + + 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> { + v8::Local::new(scope, &self.context) + } + + 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> { + v8::Local::new(scope, &self.sandbox) + } + + fn get<'a, 'c>( + scope: &mut v8::HandleScope<'a>, + object: v8::Local<'a, v8::Object>, + ) -> Option<&'c ContextifyContext> { + let Some(context) = object.get_creation_context(scope) else { + return None; + }; + + let context_ptr = context.get_aligned_pointer_from_embedder_data(0); + // 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; + +fn create_v8_context<'a>( + scope: &mut v8::HandleScope<'a>, + object_template: v8::Local, + snapshot_data: Option<&'static [u8]>, +) -> v8::Local<'a, v8::Context> { + let scope = &mut v8::EscapableHandleScope::new(scope); + + let context = if let Some(_snapshot_data) = snapshot_data { + v8::Context::from_snapshot(scope, VM_CONTEXT_INDEX).unwrap() + } else { + v8::Context::new_from_template(scope, object_template) + }; + + scope.escape(context) +} + +#[derive(Debug, Clone)] +struct SlotContextifyGlobalTemplate(v8::Global); + +fn init_global_template<'a>( + scope: &mut v8::HandleScope<'a>, +) -> v8::Local<'a, v8::ObjectTemplate> { + let mut maybe_object_template_slot = + scope.get_slot::(); + + if maybe_object_template_slot.is_none() { + init_global_template_inner(scope); + maybe_object_template_slot = + scope.get_slot::(); + } + let object_template_slot = maybe_object_template_slot + .expect("ContextifyGlobalTemplate slot should be already populated.") + .clone(); + v8::Local::new(scope, object_template_slot.0) +} + +extern "C" fn c_noop(_: *const v8::FunctionCallbackInfo) {} + +// Using thread_local! to get around compiler bug. +// +// See NOTE in ext/node/global.rs#L12 +thread_local! { + pub static GETTER_MAP_FN: v8::GenericNamedPropertyGetterCallback<'static> = property_getter.map_fn_to(); + pub static SETTER_MAP_FN: v8::GenericNamedPropertySetterCallback<'static> = property_setter.map_fn_to(); + pub static DELETER_MAP_FN: v8::GenericNamedPropertyGetterCallback<'static> = property_deleter.map_fn_to(); + pub static ENUMERATOR_MAP_FN: v8::GenericNamedPropertyEnumeratorCallback<'static> = property_enumerator.map_fn_to(); + pub static DEFINER_MAP_FN: v8::GenericNamedPropertyDefinerCallback<'static> = property_definer.map_fn_to(); + pub static DESCRIPTOR_MAP_FN: v8::GenericNamedPropertyGetterCallback<'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::IndexedPropertyGetterCallback<'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::IndexedPropertyGetterCallback<'static> = indexed_property_descriptor.map_fn_to(); +} + +fn init_global_template_inner(scope: &mut v8::HandleScope) { + let global_func_template = + v8::FunctionTemplate::builder_raw(c_noop).build(scope); + let global_object_template = global_func_template.instance_template(scope); + + 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 = 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_DELETER_MAP_FN.with(|deleter| config.deleter_raw(*deleter)); + config = + 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); + let contextify_global_template_slot = SlotContextifyGlobalTemplate( + v8::Global::new(scope, global_object_template), + ); + scope.set_slot(contextify_global_template_slot); +} + +fn property_getter<'s>( + scope: &mut v8::HandleScope<'s>, + key: v8::Local<'s, v8::Name>, + args: v8::PropertyCallbackArguments<'s>, + mut ret: v8::ReturnValue, +) { + let ctx = ContextifyContext::get(scope, args.this()).unwrap(); + + 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); + } +} + +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>, + mut rv: v8::ReturnValue, +) { + let ctx = ContextifyContext::get(scope, args.this()).unwrap(); + + 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; + } + + // 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; + } + + if !is_declared && key.is_symbol() { + return; + }; + + if ctx.sandbox(scope).set(scope, key.into(), value).is_none() { + return; + } + + 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"get").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) + { + rv.set(value); + } + } + } + } +} + +fn property_deleter<'s>( + scope: &mut v8::HandleScope<'s>, + key: v8::Local<'s, v8::Name>, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue, +) { + let ctx = ContextifyContext::get(scope, args.this()).unwrap(); + + 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; + } + + rv.set_bool(false); +} + +fn property_enumerator<'s>( + scope: &mut v8::HandleScope<'s>, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue, +) { + let ctx = ContextifyContext::get(scope, args.this()).unwrap(); + + 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.into()); +} + +fn property_definer<'s>( + scope: &mut v8::HandleScope<'s>, + key: v8::Local<'s, v8::Name>, + desc: &v8::PropertyDescriptor, + args: v8::PropertyCallbackArguments<'s>, + _: v8::ReturnValue, +) { + let ctx = ContextifyContext::get(scope, args.this()).unwrap(); + + let context = ctx.context(scope); + 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; + } + + let sandbox = ctx.sandbox(scope); + let scope = &mut v8::ContextScope::new(scope, context); + + 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); + } + } +} + +fn property_descriptor<'s>( + scope: &mut v8::HandleScope<'s>, + key: v8::Local<'s, v8::Name>, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue, +) { + let ctx = ContextifyContext::get(scope, args.this()).unwrap(); + + 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); + } + } +} + +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_getter<'s>( + scope: &mut v8::HandleScope<'s>, + index: u32, + args: v8::PropertyCallbackArguments<'s>, + rv: v8::ReturnValue, +) { + 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, +) { + let key = uint32_to_name(scope, index); + property_setter(scope, key, value, args, rv); +} + +fn indexed_property_deleter<'s>( + scope: &mut v8::HandleScope<'s>, + index: u32, + args: v8::PropertyCallbackArguments<'s>, + mut rv: v8::ReturnValue, +) { + let ctx = ContextifyContext::get(scope, args.this()).unwrap(); + + 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; + } + + // Delete failed on the sandbox, intercept and do not delete on + // the global object. + rv.set_bool(false); +} + +fn indexed_property_definer<'s>( + scope: &mut v8::HandleScope<'s>, + index: u32, + descriptor: &v8::PropertyDescriptor, + args: v8::PropertyCallbackArguments<'s>, + rv: v8::ReturnValue, +) { + let key = uint32_to_name(scope, index); + property_definer(scope, key, descriptor, args, rv); +} + +fn indexed_property_descriptor<'s>( + scope: &mut v8::HandleScope<'s>, + index: u32, + args: v8::PropertyCallbackArguments<'s>, + rv: v8::ReturnValue, +) { + let key = uint32_to_name(scope, index); + property_descriptor(scope, key, args, rv); +} diff --git a/ext/node/polyfills/vm.ts b/ext/node/polyfills/vm.ts index 10000b08c7..3378e38862 100644 --- a/ext/node/polyfills/vm.ts +++ b/ext/node/polyfills/vm.ts @@ -1,46 +1,48 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -// TODO(petamoriken): enable prefer-primordials for node polyfills -// deno-lint-ignore-file no-explicit-any prefer-primordials +// deno-lint-ignore-file no-explicit-any -import { core } from "ext:core/mod.js"; import { notImplemented } from "ext:deno_node/_utils.ts"; -import { op_vm_run_in_new_context } from "ext:core/ops"; +import { + op_vm_create_context, + op_vm_create_script, + op_vm_is_context, + op_vm_script_run_in_context, + op_vm_script_run_in_this_context, +} from "ext:core/ops"; export class Script { - code: string; + #inner; + constructor(code: string, _options = {}) { - this.code = `${code}`; + this.#inner = op_vm_create_script(code); } runInThisContext(_options: any) { - const [result, error] = core.evalContext(this.code, "data:"); - if (error) { - throw error.thrown; - } - return result; + return op_vm_script_run_in_this_context(this.#inner); } - runInContext(_contextifiedObject: any, _options: any) { - notImplemented("Script.prototype.runInContext"); + runInContext(contextifiedObject: any, _options: any) { + return op_vm_script_run_in_context(this.#inner, contextifiedObject); } runInNewContext(contextObject: any, options: any) { - if (options) { - console.warn( - "Script.runInNewContext options are currently not supported", - ); - } - return op_vm_run_in_new_context(this.code, contextObject); + const context = createContext(contextObject); + return this.runInContext(context, options); } createCachedData() { - notImplemented("Script.prototyp.createCachedData"); + notImplemented("Script.prototype.createCachedData"); } } -export function createContext(_contextObject: any, _options: any) { - notImplemented("createContext"); +export function createContext(contextObject: any = {}, _options: any) { + if (isContext(contextObject)) { + return contextObject; + } + + op_vm_create_context(contextObject); + return contextObject; } export function createScript(code: string, options: any) { @@ -48,11 +50,11 @@ export function createScript(code: string, options: any) { } export function runInContext( - _code: string, - _contextifiedObject: any, + code: string, + contextifiedObject: any, _options: any, ) { - notImplemented("runInContext"); + return createScript(code).runInContext(contextifiedObject); } export function runInNewContext( @@ -63,7 +65,7 @@ export function runInNewContext( if (options) { console.warn("vm.runInNewContext options are currently not supported"); } - return op_vm_run_in_new_context(code, contextObject); + return createScript(code).runInNewContext(contextObject); } export function runInThisContext( @@ -73,9 +75,8 @@ export function runInThisContext( return createScript(code, options).runInThisContext(options); } -export function isContext(_maybeContext: any) { - // TODO(@littledivy): Currently we do not expose contexts so this is always false. - return false; +export function isContext(maybeContext: any) { + return op_vm_is_context(maybeContext); } export function compileFunction(_code: string, _params: any, _options: any) { diff --git a/tests/unit_node/vm_test.ts b/tests/unit_node/vm_test.ts index f8bc11b823..b557350ad5 100644 --- a/tests/unit_node/vm_test.ts +++ b/tests/unit_node/vm_test.ts @@ -1,6 +1,13 @@ // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. -import { isContext, runInNewContext } from "node:vm"; import { assertEquals, assertThrows } from "@std/assert/mod.ts"; +import { + createContext, + isContext, + runInContext, + runInNewContext, + runInThisContext, + Script, +} from "node:vm"; Deno.test({ name: "vm runInNewContext", @@ -10,16 +17,76 @@ Deno.test({ }, }); +Deno.test({ + name: "vm new Script()", + fn() { + const script = new Script(` +function add(a, b) { + return a + b; +} +const x = add(1, 2); +x +`); + + const value = script.runInThisContext(); + assertEquals(value, 3); + }, +}); + +// https://github.com/denoland/deno/issues/23186 Deno.test({ name: "vm runInNewContext sandbox", fn() { - assertThrows(() => runInNewContext("Deno")); - // deno-lint-ignore no-var - var a = 1; - assertThrows(() => runInNewContext("a + 1")); + const sandbox = { fromAnotherRealm: false }; + runInNewContext("fromAnotherRealm = {}", sandbox); - runInNewContext("a = 2"); - assertEquals(a, 1); + assertEquals(typeof sandbox.fromAnotherRealm, "object"); + }, +}); + +// https://github.com/denoland/deno/issues/22395 +Deno.test({ + name: "vm runInewContext with context object", + fn() { + const context = { a: 1, b: 2 }; + const result = runInNewContext("a + b", context); + assertEquals(result, 3); + }, +}); + +// https://github.com/denoland/deno/issues/18299 +Deno.test({ + name: "vm createContext and runInContext", + fn() { + // @ts-expect-error implicit any + globalThis.globalVar = 3; + + const context = { globalVar: 1 }; + createContext(context); + runInContext("globalVar *= 2", context); + assertEquals(context.globalVar, 2); + // @ts-expect-error implicit any + assertEquals(globalThis.globalVar, 3); + }, +}); + +Deno.test({ + name: "vm runInThisContext Error rethrow", + fn() { + assertThrows( + () => { + runInThisContext("throw new Error('error')"); + }, + Error, + "error", + ); + assertThrows( + () => { + runInThisContext("throw new TypeError('type error')"); + }, + TypeError, + "type error", + ); }, }); @@ -53,6 +120,7 @@ Deno.test({ }, }); +// https://github.com/denoland/deno/issues/18315 Deno.test({ name: "vm isContext", fn() {