diff --git a/ext/webgpu/01_webgpu.js b/ext/webgpu/01_webgpu.js index e84db1a721ecec..25e9d1ec41033e 100644 --- a/ext/webgpu/01_webgpu.js +++ b/ext/webgpu/01_webgpu.js @@ -105,7 +105,13 @@ class GPUValidationError extends GPUError { this[_message] = message; } } -core.registerErrorClass("GPUValidationError", GPUValidationError); +// These errors rely on Web IDL brand and private message state established by +// their constructors. Keep them builder-only: registered classes intentionally +// bypass constructors during native op-error construction. +core.registerErrorBuilder( + "GPUValidationError", + (message) => new GPUValidationError(message), +); class GPUOutOfMemoryError extends GPUError { constructor(message) { @@ -117,7 +123,10 @@ class GPUOutOfMemoryError extends GPUError { this[_message] = message; } } -core.registerErrorClass("GPUOutOfMemoryError", GPUOutOfMemoryError); +core.registerErrorBuilder( + "GPUOutOfMemoryError", + (message) => new GPUOutOfMemoryError(message), +); class GPUInternalError extends GPUError { constructor() { @@ -125,7 +134,10 @@ class GPUInternalError extends GPUError { this[webidl.brand] = webidl.brand; } } -core.registerErrorClass("GPUInternalError", GPUInternalError); +core.registerErrorBuilder( + "GPUInternalError", + () => new GPUInternalError(), +); class GPUPipelineError extends DOMException { #reason; diff --git a/libs/core/00_infra.js b/libs/core/00_infra.js index 244d09ddf34fe2..74d2230000484c 100644 --- a/libs/core/00_infra.js +++ b/libs/core/00_infra.js @@ -42,6 +42,7 @@ let isLeakTracingEnabled = false; let submitLeakTrace; + let registerErrorClassNative; // Exposed for testing promise id wraparound behavior. function __setNextPromiseId(promiseId) { @@ -56,8 +57,12 @@ return isLeakTracingEnabled; } - function __initializeCoreMethods(submitLeakTrace_) { + function __initializeCoreMethods( + submitLeakTrace_, + registerErrorClassNative_, + ) { submitLeakTrace = submitLeakTrace_; + registerErrorClassNative = registerErrorClassNative_; } const build = { @@ -84,11 +89,9 @@ const errorMap = {}; // Maps a registered error class name to its constructor. Unlike `errorMap` - // (which stores builder closures), this keeps a reference to the class itself - // so the Rust side can restore the exact prototype when building an exception - // natively (e.g. from inside a V8 fast call, where re-entering JS to call - // `buildCustomError` is forbidden). Null prototype so class names can't - // collide with `Object.prototype` members. + // (which stores builder closures), this lets native error construction + // restore the exact prototype. Null prototype and immutable entries keep + // lookups independent of inherited properties or later map mutations. const errorConstructors = { __proto__: null }; // Builtin v8 / JS errors registerErrorClass("Error", Error); @@ -132,7 +135,15 @@ function registerErrorClass(className, errorClass) { registerErrorBuilder(className, (msg) => new errorClass(msg)); - errorConstructors[className] = errorClass; + ObjectDefineProperty(errorConstructors, className, { + value: errorClass, + writable: false, + enumerable: true, + configurable: false, + }); + // Op errors bypass registered constructors in both slow and fast dispatch. + // Classes that depend on constructor-created state must use a builder. + registerErrorClassNative?.(className, errorClass); } function registerErrorBuilder(className, errorBuilder) { diff --git a/libs/core/01_core.js b/libs/core/01_core.js index 65a4ac859b2392..abddc9bbd9ed18 100755 --- a/libs/core/01_core.js +++ b/libs/core/01_core.js @@ -67,6 +67,7 @@ op_op_names, op_print, op_ref_op, + op_register_error_class, op_resources, op_run_microtasks, op_serialize, @@ -123,6 +124,7 @@ __initializeCoreMethods( submitLeakTrace, + op_register_error_class, ); function submitLeakTrace(id) { diff --git a/libs/core/error.rs b/libs/core/error.rs index d812ab26e37ada..7eb345c271ae51 100644 --- a/libs/core/error.rs +++ b/libs/core/error.rs @@ -1,6 +1,7 @@ // Copyright 2018-2026 the Deno authors. MIT license. use std::borrow::Cow; +use std::collections::HashMap; use std::collections::HashSet; use std::error::Error; use std::fmt; @@ -268,29 +269,162 @@ pub fn throw_js_error_class( scope: &mut v8::PinScope, error: &dyn JsErrorClass, ) { - let exception = build_js_error_class_exception(scope, error); + if try_throw_js_error_class(scope, error) { + return; + } + let class = error.get_class(); + let exception = build_js_error_class_exception(scope, error, &class, None); scope.throw_exception(exception); } -/// Builds a JS exception for `error` using only native V8 APIs, without -/// re-entering JavaScript. +/// Throws an op error through native registered-class construction when +/// available, falling back to its JavaScript builder otherwise. +pub fn throw_op_error(scope: &mut v8::PinScope, error: &dyn JsErrorClass) { + if try_throw_js_error_class(scope, error) { + return; + } + let exception = to_v8_error(scope, error); + scope.throw_exception(exception); +} + +/// Throws `error` using its registered class when native construction is +/// available. /// -/// This mirrors what the JS `Deno.core.buildCustomError` callback does -/// (restoring the registered error class so `instanceof` holds, and attaching -/// the additional properties), but never invokes user-reachable JS. That makes -/// it safe to call from inside a V8 fast call, where re-entering JS is -/// forbidden: the callback could run an attacker-controlled prototype setter -/// that detaches an `ArrayBuffer` argument out from under the JIT, a -/// use-after-free (GHSA-p4r3-6jgx-4cj5). +/// Returns `false` when the class cannot be reconstructed natively, including +/// classes that only have a custom JavaScript builder. Callers that support +/// custom builders may then fall back to [`to_v8_error`]. +pub fn try_throw_js_error_class( + scope: &mut v8::PinScope, + error: &dyn JsErrorClass, +) -> bool { + let class = error.get_class(); + let Some(prototype) = registered_error_class_prototype(scope, &class) else { + return false; + }; + let exception = + build_js_error_class_exception(scope, error, &class, Some(prototype)); + scope.throw_exception(exception); + true +} + +pub(crate) fn register_error_class<'s, 'i>( + scope: &mut v8::PinScope<'s, 'i>, + class: &str, + constructor: v8::Local<'s, v8::Value>, +) { + let Some(prototype) = error_class_prototype(scope, constructor) else { + return; + }; + let state = JsRealm::exception_state_from_scope(scope); + let mut prototypes = state.registered_error_class_prototypes.borrow_mut(); + if !prototypes.contains_key(class) { + prototypes.insert(class.to_owned(), v8::Global::new(scope, prototype)); + } +} + +/// Rebuild the native prototype cache after loading a snapshot. Registrations +/// performed later notify Rust directly through `op_register_error_class`. +pub(crate) fn snapshot_registered_error_classes<'s, 'i>( + scope: &mut v8::PinScope<'s, 'i>, + constructors: v8::Local<'s, v8::Object>, +) { + let Some(class_names) = constructors.get_own_property_names( + scope, + v8::GetPropertyNamesArgsBuilder::new() + .mode(v8::KeyCollectionMode::OwnOnly) + .property_filter(v8::PropertyFilter::SKIP_SYMBOLS) + .key_conversion(v8::KeyConversionMode::ConvertToString) + .build(), + ) else { + return; + }; + + let mut prototypes = HashMap::with_capacity(class_names.length() as usize); + for index in 0..class_names.length() { + let Some(class_key) = class_names.get_index(scope, index) else { + continue; + }; + let Ok(class_key) = v8::Local::::try_from(class_key) else { + continue; + }; + let Some(constructor) = + own_data_property(scope, constructors, class_key.into()) + else { + continue; + }; + let Some(prototype) = error_class_prototype(scope, constructor) else { + continue; + }; + prototypes.insert( + class_key.to_rust_string_lossy(scope), + v8::Global::new(scope, prototype), + ); + } + + *JsRealm::exception_state_from_scope(scope) + .registered_error_class_prototypes + .borrow_mut() = prototypes; +} + +fn registered_error_class_prototype<'s, 'i>( + scope: &mut v8::PinScope<'s, 'i>, + class: &str, +) -> Option> { + let prototype = JsRealm::exception_state_from_scope(scope) + .registered_error_class_prototypes + .borrow() + .get(class) + .cloned()?; + Some(v8::Local::new(scope, prototype)) +} + +fn error_class_prototype<'s, 'i>( + scope: &mut v8::PinScope<'s, 'i>, + constructor: v8::Local<'s, v8::Value>, +) -> Option> { + // Proxy constructors and non-standard constructor objects retain their + // custom-builder behavior. For ordinary functions, inspect the own property + // descriptor once at registration so later mutation cannot affect throws. + if !constructor.is_function() || constructor.is_proxy() { + return None; + } + let constructor = + TryInto::>::try_into(constructor).ok()?; + let prototype_key = v8::String::new(scope, "prototype")?; + let prototype = own_data_property(scope, constructor, prototype_key.into())?; + prototype.is_object().then_some(prototype) +} + +fn own_data_property<'s, 'i>( + scope: &mut v8::PinScope<'s, 'i>, + object: v8::Local<'s, v8::Object>, + key: v8::Local<'s, v8::Name>, +) -> Option> { + let descriptor = object.get_own_property_descriptor(scope, key)?; + let descriptor = + TryInto::>::try_into(descriptor).ok()?; + // A data descriptor has its own `value`; an accessor descriptor does not. + let value_key = v8::String::new(scope, "value")?; + if descriptor.has_own_property(scope, value_key.into()) != Some(true) { + return None; + } + descriptor.get(scope, value_key.into()) +} + +/// Builds a JS exception for `error` using native V8 APIs. /// -/// Reading data properties off the cached `errorConstructors` map and calling -/// `SetPrototype` / `CreateDataProperty` / `DefineOwnProperty` never execute -/// user JS, so the fast-call contract is upheld. +/// This restores the registered prototype, defines the class name and +/// additional properties directly on the exception, and records the +/// additional-property keys used when converting the exception back to Rust. +/// Unlike `buildCustomError`, native construction intentionally defines an own +/// data property even when the prototype has a property with the same name. +/// This avoids invoking inherited accessors while an op callback is active. fn build_js_error_class_exception<'s, 'i>( scope: &mut v8::PinScope<'s, 'i>, error: &dyn JsErrorClass, + class: &str, + prototype: Option>, ) -> v8::Local<'s, v8::Value> { - let class = error.get_class(); let message = v8::String::new(scope, &error.get_message()).unwrap(); let exception = v8::Exception::error(scope, message); @@ -304,23 +438,8 @@ fn build_js_error_class_exception<'s, 'i>( // `Deno.errors.NotFound`) by re-parenting the prototype, so that // `err instanceof Deno.errors.NotFound` holds. We never call the class // constructor (that would run JS); we only borrow its `.prototype`. - let constructors = JsRealm::exception_state_from_scope(scope) - .js_error_constructors - .borrow() - .clone(); - if let Some(constructors) = constructors { - let constructors = v8::Local::new(scope, constructors); - let class_key = v8::String::new(scope, &class).unwrap(); - if let Some(ctor) = constructors.get(scope, class_key.into()) - && let Ok(ctor) = TryInto::>::try_into(ctor) - { - let prototype_key = v8::String::new(scope, "prototype").unwrap(); - if let Some(prototype) = ctor.get(scope, prototype_key.into()) - && prototype.is_object() - { - exception_obj.set_prototype(scope, prototype); - } - } + if let Some(prototype) = prototype { + exception_obj.set_prototype(scope, prototype); } // Set `name` explicitly. The registered classes assign `this.name` in their @@ -328,11 +447,12 @@ fn build_js_error_class_exception<'s, 'i>( // reproduce since we never run the constructor. The registration key matches // that assigned name, so using the class here mirrors `new ErrorClass(...)`. let name_key = v8::String::new(scope, "name").unwrap(); - let class_value = v8::String::new(scope, &class).unwrap(); - exception_obj.create_data_property( + let class_value = v8::String::new(scope, class).unwrap(); + exception_obj.define_own_property( scope, name_key.into(), class_value.into(), + v8::PropertyAttribute::NONE, ); // Copy the additional properties (e.g. `code`) and record their keys under @@ -341,8 +461,9 @@ fn build_js_error_class_exception<'s, 'i>( let mut added_keys = vec![]; for (key, value) in error.get_additional_properties() { let key = v8::String::new(scope, &key).unwrap(); - // Match `buildCustomError`: don't clobber a property the class defines. - if exception_obj.has(scope, key.into()) == Some(true) { + // Don't clobber properties already present on the new exception. Checking + // only the exception itself keeps inherited accessors out of construction. + if exception_obj.has_own_property(scope, key.into()) == Some(true) { continue; } let value = match value { @@ -351,7 +472,12 @@ fn build_js_error_class_exception<'s, 'i>( } PropertyValue::Number(value) => v8::Number::new(scope, value).into(), }; - exception_obj.create_data_property(scope, key.into(), value); + exception_obj.define_own_property( + scope, + key.into(), + value, + v8::PropertyAttribute::NONE, + ); added_keys.push(v8::Local::::from(key)); } if !added_keys.is_empty() { diff --git a/libs/core/ops_builtin.rs b/libs/core/ops_builtin.rs index 401f9272723b21..664ab00fdb52b8 100644 --- a/libs/core/ops_builtin.rs +++ b/libs/core/ops_builtin.rs @@ -104,6 +104,7 @@ builtin_ops! { ops_builtin_types::op_is_weak_map, ops_builtin_types::op_is_weak_set, ops_builtin_v8::op_add_main_module_handler, + ops_builtin_v8::op_register_error_class, ops_builtin_v8::op_set_handled_promise_rejection_handler, ops_builtin_v8::op_timer_schedule, ops_builtin_v8::op_timer_track, diff --git a/libs/core/ops_builtin_v8.rs b/libs/core/ops_builtin_v8.rs index 8a43f17763ee31..235d2cefadc22a 100644 --- a/libs/core/ops_builtin_v8.rs +++ b/libs/core/ops_builtin_v8.rs @@ -41,6 +41,15 @@ pub fn op_add_main_module_handler( .push(f); } +#[op2(fast)] +pub fn op_register_error_class<'s, 'i>( + scope: &mut v8::PinScope<'s, 'i>, + #[string] class_name: &str, + constructor: v8::Local<'s, v8::Value>, +) { + error::register_error_class(scope, class_name, constructor); +} + #[op2(fast)] pub fn op_set_handled_promise_rejection_handler( scope: &mut v8::PinScope, diff --git a/libs/core/runtime/exception_state.rs b/libs/core/runtime/exception_state.rs index 440ad83ddea12c..c0fb9ffdfdec34 100644 --- a/libs/core/runtime/exception_state.rs +++ b/libs/core/runtime/exception_state.rs @@ -2,6 +2,7 @@ use std::cell::Cell; use std::cell::RefCell; +use std::collections::HashMap; use std::collections::VecDeque; use crate::error::JsError; @@ -25,11 +26,11 @@ pub(crate) struct ExceptionState { RefCell, v8::Global)>>, pub(crate) js_build_custom_error_cb: RefCell>>, - /// Map of registered error class name to its constructor (the JS - /// `Deno.core.errorConstructors` object). Used to natively rebuild an - /// exception with the correct prototype without re-entering JS, e.g. from a - /// V8 fast call. See [`crate::error::throw_js_error_class`]. - pub(crate) js_error_constructors: RefCell>>, + /// Prototypes captured when error classes are registered. Looking them up + /// here keeps op-error construction native and independent of later + /// constructor mutations. + pub(crate) registered_error_class_prototypes: + RefCell>>, pub(crate) js_handled_promise_rejection_cb: RefCell>>, pub(crate) js_format_exception_cb: RefCell>>, @@ -44,7 +45,7 @@ impl ExceptionState { pub(crate) fn prepare_to_destroy(&self) { // TODO(mmastrac): we can probably move this to Drop eventually self.js_build_custom_error_cb.borrow_mut().take(); - self.js_error_constructors.borrow_mut().take(); + self.registered_error_class_prototypes.borrow_mut().clear(); self.js_handled_promise_rejection_cb.borrow_mut().take(); self.js_format_exception_cb.borrow_mut().take(); self.pending_promise_rejections.borrow_mut().clear(); diff --git a/libs/core/runtime/jsruntime.rs b/libs/core/runtime/jsruntime.rs index 5499ab3cb737eb..d529f81db1c6a5 100644 --- a/libs/core/runtime/jsruntime.rs +++ b/libs/core/runtime/jsruntime.rs @@ -1691,7 +1691,6 @@ impl JsRuntime { drain_next_tick_and_macrotasks_cb, handle_rejections_cb, build_custom_error_cb, - error_constructors, run_immediate_callbacks_cb, wasm_instance_fn, wasm_instances_map, @@ -1742,6 +1741,10 @@ impl JsRuntime { ERROR_CONSTRUCTORS, "Deno.core.errorConstructors", ); + crate::error::snapshot_registered_error_classes( + scope, + error_constructors, + ); let run_immediate_callbacks_cb: v8::Local = bindings::get( scope, core_obj, @@ -1883,7 +1886,6 @@ impl JsRuntime { v8::Global::new(scope, drain_next_tick_and_macrotasks_cb), v8::Global::new(scope, handle_rejections_cb), v8::Global::new(scope, build_custom_error_cb), - v8::Global::new(scope, error_constructors), v8::Global::new(scope, run_immediate_callbacks_cb), wasm_instance_fn.map(|f| v8::Global::new(scope, f)), wasm_instances_map.map(|m| v8::Global::new(scope, m)), @@ -1913,11 +1915,6 @@ impl JsRuntime { .js_build_custom_error_cb .borrow_mut() .replace(build_custom_error_cb); - state_rc - .exception_state - .js_error_constructors - .borrow_mut() - .replace(error_constructors); state_rc .run_immediate_callbacks_cb .borrow_mut() diff --git a/libs/core/runtime/tests/error.rs b/libs/core/runtime/tests/error.rs index a55ab8be03a37a..107724887c65af 100644 --- a/libs/core/runtime/tests/error.rs +++ b/libs/core/runtime/tests/error.rs @@ -16,7 +16,16 @@ async fn test_error_builder() { Err(JsErrorBox::new("DOMExceptionOperationError", "abc")) } - deno_core::extension!(test_ext, ops = [op_err]); + #[op2(fast)] + fn op_registered_err(#[buffer] buffer: &[u8]) -> Result<(), JsErrorBox> { + if buffer.first() == Some(&1) { + Err(JsErrorBox::new("RegisteredError", "registered message")) + } else { + Ok(()) + } + } + + deno_core::extension!(test_ext, ops = [op_err, op_registered_err]); let mut runtime = JsRuntime::new(RuntimeOptions { extensions: vec![test_ext::init()], ..Default::default() @@ -46,13 +55,11 @@ fn syntax_error() { assert_eq!(frame.column_number, Some(12)); } -// `throw_js_error_class` is what the op2 fast-call error path uses. Unlike the -// slow path it cannot re-enter JS to call `buildCustomError`, so it rebuilds -// the exception natively. This verifies that the native rebuild still restores -// the registered error class (`instanceof`), name, message, the `code` -// additional property, and the round-trip key symbol. +// Native construction is shared by op dispatch paths for registered classes. +// Verify that it restores the class and defines the expected own properties +// without consulting inherited accessors. #[test] -fn fast_call_error_preserves_class() { +fn native_registered_error_preserves_class() { use std::borrow::Cow; use deno_error::JsErrorClass; @@ -102,6 +109,20 @@ fn fast_call_error_preserves_class() { } }; Deno.core.registerErrorClass("MyError", globalThis.MyError); + globalThis.nameSetterCalls = 0; + globalThis.codeSetterCalls = 0; + Object.defineProperty(globalThis.MyError.prototype, "name", { + configurable: true, + set() { + globalThis.nameSetterCalls++; + }, + }); + Object.defineProperty(globalThis.MyError.prototype, "code", { + configurable: true, + set() { + globalThis.codeSetterCalls++; + }, + }); "#, ) .unwrap(); @@ -135,12 +156,24 @@ fn fast_call_error_preserves_class() { if (!(e instanceof globalThis.MyError)) { throw new Error("expected instanceof MyError, got " + e.name); } + if (globalThis.nameSetterCalls !== 0) { + throw new Error("name setter ran during native error construction"); + } + if (globalThis.codeSetterCalls !== 0) { + throw new Error("code setter ran during native error construction"); + } + if (!Object.hasOwn(e, "name")) { + throw new Error("expected own name property"); + } if (e.name !== "MyError") { throw new Error("expected name 'MyError', got " + e.name); } if (e.message !== "custom message") { throw new Error("expected message 'custom message', got " + e.message); } + if (!Object.hasOwn(e, "code")) { + throw new Error("expected own code property"); + } if (e.code !== "E_CUSTOM") { throw new Error("expected code 'E_CUSTOM', got " + e.code); } diff --git a/libs/core/runtime/tests/error_builder_test.js b/libs/core/runtime/tests/error_builder_test.js index f0c18de32ecc34..1e5ee18388f779 100644 --- a/libs/core/runtime/tests/error_builder_test.js +++ b/libs/core/runtime/tests/error_builder_test.js @@ -2,11 +2,21 @@ const { core } = Deno; const { ops } = core; +const domExceptionBrand = Symbol("DOMException brand"); +const domExceptionMessage = Symbol("DOMException message"); class DOMException { constructor(message, code) { - this.msg = message; + this[domExceptionBrand] = true; + this[domExceptionMessage] = message; this.code = code; } + + get msg() { + if (this[domExceptionBrand] !== true) { + throw new TypeError("Illegal invocation"); + } + return this[domExceptionMessage]; + } } core.registerErrorBuilder( @@ -16,17 +26,123 @@ core.registerErrorBuilder( }, ); -try { - ops.op_err(); - throw new Error("op_err didn't throw!"); -} catch (err) { - if (!(err instanceof DOMException)) { - throw new Error("err not DOMException"); - } - if (err.msg !== "abc") { - throw new Error("err.message is incorrect"); - } - if (err.code !== "OperationError") { - throw new Error("err.code is incorrect"); +let registeredConstructorCalls = 0; +let registeredNameSetterCalls = 0; +function RegisteredError(message) { + registeredConstructorCalls++; + const error = new Error(message); + Object.setPrototypeOf(error, RegisteredError.prototype); + return error; +} +RegisteredError.prototype = Object.create(Error.prototype); +const registeredPrototype = RegisteredError.prototype; +core.registerErrorClass("RegisteredError", RegisteredError); +const registeredDescriptor = Object.getOwnPropertyDescriptor( + core.errorConstructors, + "RegisteredError", +); +if (registeredDescriptor.writable || registeredDescriptor.configurable) { + throw new Error("registered constructor entry is mutable"); +} +let inheritedConstructorLookupCalls = 0; +Object.setPrototypeOf(core.errorConstructors, { + get DOMExceptionOperationError() { + inheritedConstructorLookupCalls++; + return RegisteredError; + }, +}); +let ownConstructorLookupCalls = 0; +Object.defineProperty( + core.errorConstructors, + "DOMExceptionOperationError", + { + configurable: true, + get() { + ownConstructorLookupCalls++; + return RegisteredError; + }, + }, +); +Object.defineProperty(RegisteredError.prototype, "name", { + configurable: true, + set() { + registeredNameSetterCalls++; + }, +}); + +function assertBuilderOnlyError() { + try { + ops.op_err(); + throw new Error("op_err didn't throw!"); + } catch (err) { + if (!(err instanceof DOMException)) { + throw new Error("err not DOMException"); + } + if (err.msg !== "abc") { + throw new Error("err.message is incorrect"); + } + if (err.code !== "OperationError") { + throw new Error("err.code is incorrect"); + } } } + +assertBuilderOnlyError(); +if (ownConstructorLookupCalls !== 0) { + throw new Error("own constructor accessor was consulted"); +} +delete core.errorConstructors.DOMExceptionOperationError; +assertBuilderOnlyError(); +if (inheritedConstructorLookupCalls !== 0) { + throw new Error("inherited constructor lookup was consulted"); +} + +function callRegisteredError(buffer) { + return ops.op_registered_err(buffer); +} + +const fastBuffer = new Uint8Array(1); +// Keep one call site hot enough for V8 to dispatch through the generated fast +// callback, then make that same callback return an error. +for (let i = 0; i < 6000; i++) { + callRegisteredError(fastBuffer); +} +fastBuffer[0] = 1; +let err; +try { + callRegisteredError(fastBuffer); + throw new Error("op_registered_err didn't throw!"); +} catch (error) { + err = error; +} +if (!(err instanceof RegisteredError)) { + throw new Error("err not RegisteredError"); +} +if (registeredConstructorCalls !== 0) { + throw new Error("registered constructor was called"); +} +if (registeredNameSetterCalls !== 0) { + throw new Error("registered name setter was called"); +} +if (!Object.hasOwn(err, "name") || err.name !== "RegisteredError") { + throw new Error("err.name is incorrect"); +} +if (err.message !== "registered message") { + throw new Error("err.message is incorrect"); +} +if (fastBuffer.byteLength !== 1) { + throw new Error("fast op buffer was detached"); +} + +// Replacing a writable function prototype after registration must not change +// the prototype captured by native error construction. +RegisteredError.prototype = Object.create(Error.prototype); +try { + callRegisteredError(fastBuffer); + throw new Error("op_registered_err didn't throw!"); +} catch (error) { + err = error; +} +if (Object.getPrototypeOf(err) !== registeredPrototype) { + throw new Error("registered prototype was not snapshotted"); +} diff --git a/libs/ops/op2/dispatch_fast.rs b/libs/ops/op2/dispatch_fast.rs index 62a16186e4dabe..708d070cc94c5d 100644 --- a/libs/ops/op2/dispatch_fast.rs +++ b/libs/ops/op2/dispatch_fast.rs @@ -395,15 +395,9 @@ pub(crate) fn generate_fast_result_early_exit( Err(err) => { let scope = ::std::pin::pin!(#create_scope); let mut scope = scope.init(); - // Build and throw the exception using only native V8 APIs. Calling - // `to_v8_error` here would synchronously invoke the JS - // `Deno.core.buildCustomError` callback, which can execute - // attacker-controlled JS (e.g. a setter on an error prototype's - // `name`) *inside* the fast call. That JS can detach an `ArrayBuffer` - // argument, leaving the JIT to write through a now-stale backing-store - // pointer once the fast call returns (use-after-free, - // GHSA-p4r3-6jgx-4cj5). The V8 fast-call contract forbids re-entering - // JS, so the error must be constructed without it. + // Keep fast-call error construction on the native registered-class + // path. This preserves the expected error shape without invoking a + // class constructor or custom builder from the callback. deno_core::error::throw_js_error_class(&mut scope, &err); // SAFETY: All fast return types have zero as a valid value return unsafe { std::mem::zeroed() }; diff --git a/libs/ops/op2/dispatch_slow.rs b/libs/ops/op2/dispatch_slow.rs index ed8a9c7c3df138..b0a9baabd0fe85 100644 --- a/libs/ops/op2/dispatch_slow.rs +++ b/libs/ops/op2/dispatch_slow.rs @@ -1258,27 +1258,9 @@ pub(crate) fn throw_exception( with_scope(generator_state) }; - let maybe_opctx = if generator_state.needs_opctx { - quote!() - } else { - with_opctx(generator_state) - }; - - let maybe_args = if generator_state.needs_args { - quote!() - } else { - with_fn_args(generator_state) - }; - gs_quote!(generator_state(scope) => { #maybe_scope - #maybe_args - #maybe_opctx - let exception = deno_core::error::to_v8_error( - &mut #scope, - &err, - ); - #scope.throw_exception(exception); + deno_core::error::throw_op_error(&mut #scope, &err); return 1; }) } diff --git a/libs/ops/op2/test_cases/async/async_arg_return_result.out b/libs/ops/op2/test_cases/async/async_arg_return_result.out index dad79ca7abd2fc..3cacedb7461dbe 100644 --- a/libs/ops/op2/test_cases/async/async_arg_return_result.out +++ b/libs/ops/op2/test_cases/async/async_arg_return_result.out @@ -76,8 +76,7 @@ pub const fn op_async() -> ::deno_core::_ops::OpDecl { unsafe { deno_core::v8::CallbackScope::new(info) } ); let mut scope = scope.init(); - let exception = deno_core::error::to_v8_error(&mut scope, &err); - scope.throw_exception(exception); + deno_core::error::throw_op_error(&mut scope, &err); return 1; } }; diff --git a/libs/ops/op2/test_cases/async/async_opstate.out b/libs/ops/op2/test_cases/async/async_opstate.out index 528a2e8b9aca0e..0545b381ddbd9c 100644 --- a/libs/ops/op2/test_cases/async/async_opstate.out +++ b/libs/ops/op2/test_cases/async/async_opstate.out @@ -72,8 +72,7 @@ pub const fn op_async_opstate() -> ::deno_core::_ops::OpDecl { unsafe { deno_core::v8::CallbackScope::new(info) } ); let mut scope = scope.init(); - let exception = deno_core::error::to_v8_error(&mut scope, &err); - scope.throw_exception(exception); + deno_core::error::throw_op_error(&mut scope, &err); return 1; } }; diff --git a/libs/ops/op2/test_cases/async/async_precise_capture.out b/libs/ops/op2/test_cases/async/async_precise_capture.out index cec3f73c7fc3b3..49035acd201d34 100644 --- a/libs/ops/op2/test_cases/async/async_precise_capture.out +++ b/libs/ops/op2/test_cases/async/async_precise_capture.out @@ -76,8 +76,7 @@ pub const fn op_async_impl_use() -> ::deno_core::_ops::OpDecl { unsafe { deno_core::v8::CallbackScope::new(info) } ); let mut scope = scope.init(); - let exception = deno_core::error::to_v8_error(&mut scope, &err); - scope.throw_exception(exception); + deno_core::error::throw_op_error(&mut scope, &err); return 1; } }; diff --git a/libs/ops/op2/test_cases/async/async_result.out b/libs/ops/op2/test_cases/async/async_result.out index 92ec7f3b93010b..3f8165d6eacd12 100644 --- a/libs/ops/op2/test_cases/async/async_result.out +++ b/libs/ops/op2/test_cases/async/async_result.out @@ -68,8 +68,7 @@ pub const fn op_async() -> ::deno_core::_ops::OpDecl { unsafe { deno_core::v8::CallbackScope::new(info) } ); let mut scope = scope.init(); - let exception = deno_core::error::to_v8_error(&mut scope, &err); - scope.throw_exception(exception); + deno_core::error::throw_op_error(&mut scope, &err); return 1; } }; diff --git a/libs/ops/op2/test_cases/async/async_result_impl.out b/libs/ops/op2/test_cases/async/async_result_impl.out index 4ba2bbd2fcd59f..51c4aa436210d7 100644 --- a/libs/ops/op2/test_cases/async/async_result_impl.out +++ b/libs/ops/op2/test_cases/async/async_result_impl.out @@ -64,8 +64,7 @@ pub const fn op_async_result_impl() -> ::deno_core::_ops::OpDecl { unsafe { deno_core::v8::CallbackScope::new(info) } ); let mut scope = scope.init(); - let exception = deno_core::error::to_v8_error(&mut scope, &err); - scope.throw_exception(exception); + deno_core::error::throw_op_error(&mut scope, &err); return 1; } }; @@ -88,8 +87,7 @@ pub const fn op_async_result_impl() -> ::deno_core::_ops::OpDecl { unsafe { deno_core::v8::CallbackScope::new(info) } ); let mut scope = scope.init(); - let exception = deno_core::error::to_v8_error(&mut scope, &err); - scope.throw_exception(exception); + deno_core::error::throw_op_error(&mut scope, &err); return 1; } }; diff --git a/libs/ops/op2/test_cases/async/async_result_smi.out b/libs/ops/op2/test_cases/async/async_result_smi.out index 3be0542da75a2c..be70b782f4cf70 100644 --- a/libs/ops/op2/test_cases/async/async_result_smi.out +++ b/libs/ops/op2/test_cases/async/async_result_smi.out @@ -92,8 +92,7 @@ pub const fn op_async() -> ::deno_core::_ops::OpDecl { unsafe { deno_core::v8::CallbackScope::new(info) } ); let mut scope = scope.init(); - let exception = deno_core::error::to_v8_error(&mut scope, &err); - scope.throw_exception(exception); + deno_core::error::throw_op_error(&mut scope, &err); return 1; } }; diff --git a/libs/ops/op2/test_cases/sync/bool_result.out b/libs/ops/op2/test_cases/sync/bool_result.out index dbf8701f84cb69..78ee42ec52bf0b 100644 --- a/libs/ops/op2/test_cases/sync/bool_result.out +++ b/libs/ops/op2/test_cases/sync/bool_result.out @@ -151,14 +151,7 @@ pub const fn op_bool() -> ::deno_core::_ops::OpDecl { unsafe { deno_core::v8::CallbackScope::new(info) } ); let mut scope = scope.init(); - let opctx: &'s _ = unsafe { - &*(deno_core::v8::Local::< - deno_core::v8::External, - >::cast_unchecked(args.data()) - .value() as *const deno_core::_ops::OpCtx) - }; - let exception = deno_core::error::to_v8_error(&mut scope, &err); - scope.throw_exception(exception); + deno_core::error::throw_op_error(&mut scope, &err); return 1; } }; diff --git a/libs/ops/op2/test_cases/sync/object_wrap.out b/libs/ops/op2/test_cases/sync/object_wrap.out index 707f7806e2fe72..67e24225a0e2b2 100644 --- a/libs/ops/op2/test_cases/sync/object_wrap.out +++ b/libs/ops/op2/test_cases/sync/object_wrap.out @@ -979,14 +979,7 @@ impl Foo { &parts, ); if let Err(err) = f(&mut scope, &args) { - let opctx: &'s _ = unsafe { - &*(deno_core::v8::Local::< - deno_core::v8::External, - >::cast_unchecked(args.data()) - .value() as *const deno_core::_ops::OpCtx) - }; - let exception = deno_core::error::to_v8_error(&mut scope, &err); - scope.throw_exception(exception); + deno_core::error::throw_op_error(&mut scope, &err); return 1; } let Some(self_) = deno_core::_ops::try_unwrap_cppgc_object::< diff --git a/libs/ops/op2/test_cases/sync/result_external.out b/libs/ops/op2/test_cases/sync/result_external.out index a1df064b529ef9..cf3f74b0b30f36 100644 --- a/libs/ops/op2/test_cases/sync/result_external.out +++ b/libs/ops/op2/test_cases/sync/result_external.out @@ -126,24 +126,13 @@ pub const fn op_external_with_result() -> ::deno_core::_ops::OpDecl { ); let mut scope = scope.init(); let mut rv = parts.return_value; - let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info_parts( - info, - &parts, - ); let result = { Self::call() }; match result { Ok(result) => { rv.set(deno_core::_ops::RustToV8::to_v8(result, &mut scope)) } Err(err) => { - let opctx: &'s _ = unsafe { - &*(deno_core::v8::Local::< - deno_core::v8::External, - >::cast_unchecked(args.data()) - .value() as *const deno_core::_ops::OpCtx) - }; - let exception = deno_core::error::to_v8_error(&mut scope, &err); - scope.throw_exception(exception); + deno_core::error::throw_op_error(&mut scope, &err); return 1; } }; diff --git a/libs/ops/op2/test_cases/sync/result_primitive.out b/libs/ops/op2/test_cases/sync/result_primitive.out index f757da9e2ccf7c..1fad61c51bf9bd 100644 --- a/libs/ops/op2/test_cases/sync/result_primitive.out +++ b/libs/ops/op2/test_cases/sync/result_primitive.out @@ -128,10 +128,6 @@ pub const fn op_u32_with_result() -> ::deno_core::_ops::OpDecl { ); let parts: deno_core::v8::FunctionCallbackInfoParts<'s> = info.get_parts(); let mut rv = parts.return_value; - let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info_parts( - info, - &parts, - ); let result = { Self::call() }; match result { Ok(result) => deno_core::_ops::RustToV8RetVal::to_v8_rv(result, &mut rv), @@ -140,14 +136,7 @@ pub const fn op_u32_with_result() -> ::deno_core::_ops::OpDecl { unsafe { deno_core::v8::CallbackScope::new(info) } ); let mut scope = scope.init(); - let opctx: &'s _ = unsafe { - &*(deno_core::v8::Local::< - deno_core::v8::External, - >::cast_unchecked(args.data()) - .value() as *const deno_core::_ops::OpCtx) - }; - let exception = deno_core::error::to_v8_error(&mut scope, &err); - scope.throw_exception(exception); + deno_core::error::throw_op_error(&mut scope, &err); return 1; } }; diff --git a/libs/ops/op2/test_cases/sync/result_scope.out b/libs/ops/op2/test_cases/sync/result_scope.out index f99e447fc299e8..706fbe9147611c 100644 --- a/libs/ops/op2/test_cases/sync/result_scope.out +++ b/libs/ops/op2/test_cases/sync/result_scope.out @@ -142,14 +142,7 @@ pub const fn op_void_with_result() -> ::deno_core::_ops::OpDecl { match result { Ok(result) => deno_core::_ops::RustToV8RetVal::to_v8_rv(result, &mut rv), Err(err) => { - let opctx: &'s _ = unsafe { - &*(deno_core::v8::Local::< - deno_core::v8::External, - >::cast_unchecked(args.data()) - .value() as *const deno_core::_ops::OpCtx) - }; - let exception = deno_core::error::to_v8_error(&mut scope, &err); - scope.throw_exception(exception); + deno_core::error::throw_op_error(&mut scope, &err); return 1; } }; diff --git a/libs/ops/op2/test_cases/sync/result_void.out b/libs/ops/op2/test_cases/sync/result_void.out index b7c807482c3885..82545bb9a63d04 100644 --- a/libs/ops/op2/test_cases/sync/result_void.out +++ b/libs/ops/op2/test_cases/sync/result_void.out @@ -122,10 +122,6 @@ pub const fn op_void_with_result() -> ::deno_core::_ops::OpDecl { ); let parts: deno_core::v8::FunctionCallbackInfoParts<'s> = info.get_parts(); let mut rv = parts.return_value; - let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info_parts( - info, - &parts, - ); let result = { Self::call() }; match result { Ok(result) => deno_core::_ops::RustToV8RetVal::to_v8_rv(result, &mut rv), @@ -134,14 +130,7 @@ pub const fn op_void_with_result() -> ::deno_core::_ops::OpDecl { unsafe { deno_core::v8::CallbackScope::new(info) } ); let mut scope = scope.init(); - let opctx: &'s _ = unsafe { - &*(deno_core::v8::Local::< - deno_core::v8::External, - >::cast_unchecked(args.data()) - .value() as *const deno_core::_ops::OpCtx) - }; - let exception = deno_core::error::to_v8_error(&mut scope, &err); - scope.throw_exception(exception); + deno_core::error::throw_op_error(&mut scope, &err); return 1; } }; diff --git a/tests/unit/webgpu_test.ts b/tests/unit/webgpu_test.ts index 24c91f5f58e896..b1a6917ed57591 100644 --- a/tests/unit/webgpu_test.ts +++ b/tests/unit/webgpu_test.ts @@ -53,6 +53,44 @@ function checkWindowingSystem(): boolean { } } +Deno.test(function webgpuErrorBuildersPreserveClassState() { + // Accessing the lazy global loads WebGPU and registers its error builders. + void GPUValidationError; + // @ts-ignore: Deno[Deno.internal].core is available to internal tests. + const core = Deno[Deno.internal].core; + const messageGetter = Object.getOwnPropertyDescriptor( + GPUError.prototype, + "message", + )!.get!; + + const validation = core.buildCustomError( + "GPUValidationError", + "validation message", + ); + assert(validation instanceof GPUValidationError); + assertEquals(messageGetter.call(validation), "validation message"); + assert(!Object.hasOwn(validation, "message")); + assert(!("name" in validation)); + + const outOfMemory = core.buildCustomError( + "GPUOutOfMemoryError", + "out of memory message", + ); + assert(outOfMemory instanceof GPUOutOfMemoryError); + assertEquals(messageGetter.call(outOfMemory), "out of memory message"); + assert(!Object.hasOwn(outOfMemory, "message")); + assert(!("name" in outOfMemory)); + + const internal = core.buildCustomError( + "GPUInternalError", + "ignored message", + ); + assert(internal instanceof GPUInternalError); + assertEquals(messageGetter.call(internal), undefined); + assert(!Object.hasOwn(internal, "message")); + assert(!("name" in internal)); +}); + Deno.test({ permissions: { read: true, env: true }, ignore: isWsl || isCIWithoutGPU,