From 660b00a0498a4b6bc492fcccb4b774f181641459 Mon Sep 17 00:00:00 2001 From: Nathan Whitaker Date: Wed, 22 Jul 2026 13:14:21 -0700 Subject: [PATCH] fix(core): construct registered op errors natively --- ext/webgpu/01_webgpu.js | 15 ++- libs/core/00_infra.js | 15 ++- libs/core/error.rs | 119 ++++++++++++------ libs/core/runtime/exception_state.rs | 4 +- libs/core/runtime/tests/error.rs | 46 +++++-- libs/core/runtime/tests/error_builder_test.js | 103 +++++++++++++-- libs/ops/op2/dispatch_fast.rs | 12 +- libs/ops/op2/dispatch_slow.rs | 26 ++-- .../async/async_arg_return_result.out | 12 +- .../op2/test_cases/async/async_opstate.out | 12 +- .../async/async_precise_capture.out | 12 +- .../ops/op2/test_cases/async/async_result.out | 12 +- .../test_cases/async/async_result_impl.out | 18 ++- .../op2/test_cases/async/async_result_smi.out | 12 +- libs/ops/op2/test_cases/sync/bool_result.out | 12 +- libs/ops/op2/test_cases/sync/object_wrap.out | 12 +- .../op2/test_cases/sync/result_external.out | 16 +-- .../op2/test_cases/sync/result_primitive.out | 16 +-- libs/ops/op2/test_cases/sync/result_scope.out | 12 +- libs/ops/op2/test_cases/sync/result_void.out | 16 +-- tests/unit/webgpu_test.ts | 38 ++++++ 21 files changed, 375 insertions(+), 165 deletions(-) diff --git a/ext/webgpu/01_webgpu.js b/ext/webgpu/01_webgpu.js index e84db1a721ecec..ee7c7f403d5752 100644 --- a/ext/webgpu/01_webgpu.js +++ b/ext/webgpu/01_webgpu.js @@ -105,7 +105,10 @@ class GPUValidationError extends GPUError { this[_message] = message; } } -core.registerErrorClass("GPUValidationError", GPUValidationError); +core.registerErrorBuilder( + "GPUValidationError", + (message) => new GPUValidationError(message), +); class GPUOutOfMemoryError extends GPUError { constructor(message) { @@ -117,7 +120,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 +131,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..41fdd313e7245c 100644 --- a/libs/core/00_infra.js +++ b/libs/core/00_infra.js @@ -84,11 +84,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 +130,12 @@ function registerErrorClass(className, errorClass) { registerErrorBuilder(className, (msg) => new errorClass(msg)); - errorConstructors[className] = errorClass; + ObjectDefineProperty(errorConstructors, className, { + value: errorClass, + writable: false, + enumerable: true, + configurable: false, + }); } function registerErrorBuilder(className, errorBuilder) { diff --git a/libs/core/error.rs b/libs/core/error.rs index d812ab26e37ada..66229ef6be9d4f 100644 --- a/libs/core/error.rs +++ b/libs/core/error.rs @@ -268,29 +268,84 @@ pub fn throw_js_error_class( scope: &mut v8::PinScope, error: &dyn JsErrorClass, ) { - let exception = build_js_error_class_exception(scope, error); + let class = error.get_class(); + let prototype = registered_error_class_prototype(scope, &class); + let exception = + build_js_error_class_exception(scope, error, &class, prototype); scope.throw_exception(exception); } -/// Builds a JS exception for `error` using only native V8 APIs, without -/// re-entering JavaScript. +/// 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 +} + +fn registered_error_class_prototype<'s, 'i>( + scope: &mut v8::PinScope<'s, 'i>, + class: &str, +) -> Option> { + let constructors = JsRealm::exception_state_from_scope(scope) + .js_error_constructors + .borrow() + .clone()?; + let constructors = v8::Local::new(scope, constructors); + let class_key = v8::String::new(scope, class)?; + let descriptor = + constructors.get_own_property_descriptor(scope, class_key.into())?; + let descriptor = + TryInto::>::try_into(descriptor).ok()?; + let value_key = v8::String::new(scope, "value")?; + if descriptor.has_own_property(scope, value_key.into()) != Some(true) { + return None; + } + let ctor = descriptor.get(scope, value_key.into())?; + + // Proxy constructors and non-standard constructor objects retain their + // custom-builder behavior. For ordinary functions, inspect the own property + // descriptor so an unusual accessor cannot run during error construction. + if !ctor.is_function() || ctor.is_proxy() { + return None; + } + let ctor = TryInto::>::try_into(ctor).ok()?; + let prototype_key = v8::String::new(scope, "prototype")?; + let descriptor = + ctor.get_own_property_descriptor(scope, prototype_key.into())?; + let descriptor = + TryInto::>::try_into(descriptor).ok()?; + if descriptor.has_own_property(scope, value_key.into()) != Some(true) { + return None; + } + let prototype = descriptor.get(scope, value_key.into())?; + prototype.is_object().then_some(prototype) +} + +/// 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 mirrors the observable shape produced by `buildCustomError` for a +/// registered class: it 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. 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 +359,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 +368,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 +382,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 +393,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/runtime/exception_state.rs b/libs/core/runtime/exception_state.rs index 440ad83ddea12c..fd8dca3c3a811a 100644 --- a/libs/core/runtime/exception_state.rs +++ b/libs/core/runtime/exception_state.rs @@ -27,8 +27,8 @@ pub(crate) struct ExceptionState { 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`]. + /// exception with the correct prototype across op dispatch paths. See + /// [`crate::error::throw_js_error_class`]. pub(crate) js_error_constructors: RefCell>>, pub(crate) js_handled_promise_rejection_cb: RefCell>>, diff --git a/libs/core/runtime/tests/error.rs b/libs/core/runtime/tests/error.rs index a55ab8be03a37a..417c7d82e709a5 100644 --- a/libs/core/runtime/tests/error.rs +++ b/libs/core/runtime/tests/error.rs @@ -5,6 +5,7 @@ use std::task::Poll; use deno_error::JsErrorBox; +use crate::JsBuffer; use crate::JsRuntime; use crate::RuntimeOptions; use crate::op2; @@ -16,7 +17,14 @@ async fn test_error_builder() { Err(JsErrorBox::new("DOMExceptionOperationError", "abc")) } - deno_core::extension!(test_ext, ops = [op_err]); + #[op2] + fn op_registered_err( + #[buffer(detach)] _buffer: JsBuffer, + ) -> Result<(), JsErrorBox> { + Err(JsErrorBox::new("RegisteredError", "registered message")) + } + + 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 +54,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 +108,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 +155,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..44eb067364b97b 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,92 @@ core.registerErrorBuilder( }, ); +let registeredConstructorCalls = 0; +let registeredNameSetterCalls = 0; +class RegisteredError extends Error { + constructor(message) { + super(message); + registeredConstructorCalls++; + } +} +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"); +} + try { - ops.op_err(); - throw new Error("op_err didn't throw!"); + ops.op_registered_err(new Uint8Array()); + throw new Error("op_registered_err didn't throw!"); } catch (err) { - if (!(err instanceof DOMException)) { - throw new Error("err not DOMException"); + if (!(err instanceof RegisteredError)) { + throw new Error("err not RegisteredError"); } - if (err.msg !== "abc") { - throw new Error("err.message is incorrect"); + if (registeredConstructorCalls !== 0) { + throw new Error("registered constructor was called"); + } + if (registeredNameSetterCalls !== 0) { + throw new Error("registered name setter was called"); } - if (err.code !== "OperationError") { - throw new Error("err.code is incorrect"); + 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"); } } 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..c8afccb181044c 100644 --- a/libs/ops/op2/dispatch_slow.rs +++ b/libs/ops/op2/dispatch_slow.rs @@ -1258,27 +1258,15 @@ 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); + if !deno_core::error::try_throw_js_error_class(&mut #scope, &err) { + let exception = deno_core::error::to_v8_error( + &mut #scope, + &err, + ); + #scope.throw_exception(exception); + } 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..17f832d3fb900a 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,16 @@ 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); + if !deno_core::error::try_throw_js_error_class( + &mut scope, + &err, + ) { + let exception = deno_core::error::to_v8_error( + &mut scope, + &err, + ); + scope.throw_exception(exception); + } 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..e8ef746a6779ae 100644 --- a/libs/ops/op2/test_cases/async/async_opstate.out +++ b/libs/ops/op2/test_cases/async/async_opstate.out @@ -72,8 +72,16 @@ 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); + if !deno_core::error::try_throw_js_error_class( + &mut scope, + &err, + ) { + let exception = deno_core::error::to_v8_error( + &mut scope, + &err, + ); + scope.throw_exception(exception); + } 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..623f9238b435bd 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,16 @@ 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); + if !deno_core::error::try_throw_js_error_class( + &mut scope, + &err, + ) { + let exception = deno_core::error::to_v8_error( + &mut scope, + &err, + ); + scope.throw_exception(exception); + } 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..0fa3fd87fa5520 100644 --- a/libs/ops/op2/test_cases/async/async_result.out +++ b/libs/ops/op2/test_cases/async/async_result.out @@ -68,8 +68,16 @@ 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); + if !deno_core::error::try_throw_js_error_class( + &mut scope, + &err, + ) { + let exception = deno_core::error::to_v8_error( + &mut scope, + &err, + ); + scope.throw_exception(exception); + } 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..4cb40b001f7bf3 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,10 @@ 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); + if !deno_core::error::try_throw_js_error_class(&mut scope, &err) { + let exception = deno_core::error::to_v8_error(&mut scope, &err); + scope.throw_exception(exception); + } return 1; } }; @@ -88,8 +90,16 @@ 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); + if !deno_core::error::try_throw_js_error_class( + &mut scope, + &err, + ) { + let exception = deno_core::error::to_v8_error( + &mut scope, + &err, + ); + scope.throw_exception(exception); + } 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..bad02ae236b62f 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,16 @@ 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); + if !deno_core::error::try_throw_js_error_class( + &mut scope, + &err, + ) { + let exception = deno_core::error::to_v8_error( + &mut scope, + &err, + ); + scope.throw_exception(exception); + } 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..6af5c0ebde65c5 100644 --- a/libs/ops/op2/test_cases/sync/bool_result.out +++ b/libs/ops/op2/test_cases/sync/bool_result.out @@ -151,14 +151,10 @@ 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); + if !deno_core::error::try_throw_js_error_class(&mut scope, &err) { + let exception = deno_core::error::to_v8_error(&mut scope, &err); + scope.throw_exception(exception); + } 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..47b5c989b7bd1b 100644 --- a/libs/ops/op2/test_cases/sync/object_wrap.out +++ b/libs/ops/op2/test_cases/sync/object_wrap.out @@ -979,14 +979,10 @@ 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); + if !deno_core::error::try_throw_js_error_class(&mut scope, &err) { + let exception = deno_core::error::to_v8_error(&mut scope, &err); + scope.throw_exception(exception); + } 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..34039dea93818e 100644 --- a/libs/ops/op2/test_cases/sync/result_external.out +++ b/libs/ops/op2/test_cases/sync/result_external.out @@ -126,24 +126,16 @@ 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); + if !deno_core::error::try_throw_js_error_class(&mut scope, &err) { + let exception = deno_core::error::to_v8_error(&mut scope, &err); + scope.throw_exception(exception); + } 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..dd99a426df5ffc 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,10 @@ 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); + if !deno_core::error::try_throw_js_error_class(&mut scope, &err) { + let exception = deno_core::error::to_v8_error(&mut scope, &err); + scope.throw_exception(exception); + } 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..fb5fe0c3074591 100644 --- a/libs/ops/op2/test_cases/sync/result_scope.out +++ b/libs/ops/op2/test_cases/sync/result_scope.out @@ -142,14 +142,10 @@ 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); + if !deno_core::error::try_throw_js_error_class(&mut scope, &err) { + let exception = deno_core::error::to_v8_error(&mut scope, &err); + scope.throw_exception(exception); + } 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..77308d0053a1c1 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,10 @@ 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); + if !deno_core::error::try_throw_js_error_class(&mut scope, &err) { + let exception = deno_core::error::to_v8_error(&mut scope, &err); + scope.throw_exception(exception); + } 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,