summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDivy Srivastava <dj.srivastava23@gmail.com>2024-04-09 21:24:25 +0530
committerGitHub <noreply@github.com>2024-04-09 21:24:25 +0530
commitfad12b7c2ebd87a2a11f63998f4c2549fd405eff (patch)
tree38c43f48365d2436dff47d2f91c7e13930932626
parenta4f3e7436e81cd229f85657f2fee2caaa0e3b05e (diff)
fix(ext/node): `node:vm` contexts (#23202)
Implement contextified objects in `node:vm` Fixes https://github.com/denoland/deno/issues/23186 Fixes https://github.com/denoland/deno/issues/22395 Fixes https://github.com/denoland/deno/issues/20607 Fixes https://github.com/denoland/deno/issues/18299 Fixes https://github.com/denoland/deno/issues/19395 Fixes https://github.com/denoland/deno/issues/18315 Fixes https://github.com/denoland/deno/issues/18319 Fixes https://github.com/denoland/deno/issues/23183
-rw-r--r--ext/node/lib.rs6
-rw-r--r--ext/node/ops/mod.rs2
-rw-r--r--ext/node/ops/vm.rs138
-rw-r--r--ext/node/ops/vm_internal.rs599
-rw-r--r--ext/node/polyfills/vm.ts59
-rw-r--r--tests/unit_node/vm_test.ts82
6 files changed, 849 insertions, 37 deletions
diff --git a/ext/node/lib.rs b/ext/node/lib.rs
index 85abe49e4..f91ed0b1a 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 8aed274bc..c14b63bf4 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 000000000..f18038f8f
--- /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<v8::String>,
+ ) -> Result<Self, AnyError> {
+ Ok(Self {
+ inner: i::ContextifyScript::new(scope, source)?,
+ })
+ }
+
+ fn run_in_this_context<'s>(
+ &self,
+ scope: &'s mut v8::HandleScope,
+ ) -> Result<v8::Local<'s, v8::Value>, 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<v8::Local<'s, v8::Value>, 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<v8::Local<'a, v8::Object>, 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<v8::Local<'a, v8::Value>, 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<v8::Local<'a, v8::Value>, AnyError> {
+ script.run_in_this_context(scope)
+}
+
+#[op2]
+pub fn op_vm_create_context(
+ scope: &mut v8::HandleScope,
+ sandbox_obj: v8::Local<v8::Object>,
+) {
+ // 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<v8::Value>,
+) -> 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 000000000..274fac91a
--- /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<v8::UnboundScript>,
+}
+
+impl ContextifyScript {
+ pub fn new(
+ scope: &mut v8::HandleScope,
+ source_str: v8::Local<v8::String>,
+ ) -> Result<Self, AnyError> {
+ 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<v8::Context>,
+ ) -> Option<v8::Local<'s, v8::Value>> {
+ 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<v8::Context>,
+ sandbox: v8::Global<v8::Object>,
+}
+
+impl ContextifyContext {
+ pub fn attach(
+ scope: &mut v8::HandleScope,
+ sandbox_obj: v8::Local<v8::Object>,
+ ) {
+ 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<v8::Context>,
+ sandbox_obj: v8::Local<v8::Object>,
+ ) {
+ 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::<Self>(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<v8::Object>,
+ ) -> 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::<Self>(wrapper)
+ })
+ }
+
+ pub fn is_contextify_context(
+ scope: &mut v8::HandleScope,
+ object: v8::Local<v8::Object>,
+ ) -> 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<v8::ObjectTemplate>,
+ 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<v8::ObjectTemplate>);
+
+fn init_global_template<'a>(
+ scope: &mut v8::HandleScope<'a>,
+) -> v8::Local<'a, v8::ObjectTemplate> {
+ let mut maybe_object_template_slot =
+ scope.get_slot::<SlotContextifyGlobalTemplate>();
+
+ if maybe_object_template_slot.is_none() {
+ init_global_template_inner(scope);
+ maybe_object_template_slot =
+ scope.get_slot::<SlotContextifyGlobalTemplate>();
+ }
+ 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<v8::Object> = 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::<v8::Uint32>::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 10000b08c..3378e3886 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 f8bc11b82..b557350ad 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",
@@ -11,15 +18,75 @@ 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);
+
+ 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);
+ },
+});
- runInNewContext("a = 2");
- assertEquals(a, 1);
+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() {