From cf7650f74483ad9754ab6fd75cd3cc5b02f09674 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Mon, 5 Oct 2026 16:17:30 +0800 Subject: [PATCH] fix(webidl): defer constructor prototype selection until after conversion --- .../assets/constructor_templates.rs | 18 +- .../constructors/dom_exception/rtc_error.rs | 11 +- .../constructors/elements.rs | 26 +- .../exposed_interfaces/install.rs | 2 + .../exposed_interfaces/materialize.rs | 27 ++ .../context_bootstrap/video_color_space.rs | 17 +- .../src/context_bootstrap/webrtc/events.rs | 30 +-- .../context_bootstrap/webrtc/ice_candidate.rs | 18 +- .../webrtc/ice_error_event.rs | 20 +- .../webrtc/session_description.rs | 16 +- moli-renderer-v8/src/dom_parser.rs | 9 +- .../src/script_vm/tests/constructor_entry.js | 118 +++++++++ .../src/script_vm/tests/constructor_entry.rs | 43 ++++ moli-renderer-v8/src/script_vm/tests/mod.rs | 1 + moli-renderer-v8/src/util.rs | 53 +--- moli-v8-util/src/callables.rs | 30 +++ moli-v8-util/src/lib.rs | 2 + moli-webapi-declare/src/callback.rs | 7 + moli-webapi-declare/src/constructor.rs | 188 ++++++++++++++ moli-webapi-declare/src/lib.rs | 5 + moli-webapi-declare/tests/constructor.rs | 231 ++++++++++++++++++ vendor/v8-152.2.0/src/function.rs | 14 +- 22 files changed, 741 insertions(+), 145 deletions(-) create mode 100644 moli-renderer-v8/src/script_vm/tests/constructor_entry.js create mode 100644 moli-renderer-v8/src/script_vm/tests/constructor_entry.rs create mode 100644 moli-v8-util/src/callables.rs create mode 100644 moli-webapi-declare/src/constructor.rs create mode 100644 moli-webapi-declare/tests/constructor.rs diff --git a/moli-renderer-v8/src/context_bootstrap/assets/constructor_templates.rs b/moli-renderer-v8/src/context_bootstrap/assets/constructor_templates.rs index f4a4f63f2f..dbb594b533 100644 --- a/moli-renderer-v8/src/context_bootstrap/assets/constructor_templates.rs +++ b/moli-renderer-v8/src/context_bootstrap/assets/constructor_templates.rs @@ -754,9 +754,12 @@ pub(in crate::context_bootstrap) fn build_constructor_template_for_profile<'s>( .build(scope) } ConstructorKind::RtcPeerConnectionIceEvent => { - v8::FunctionTemplate::builder(rtc_peer_connection_ice_event_constructor_callback) - .length(1) - .build(scope) + v8::FunctionTemplate::builder(moli_webapi_declare::web_api_constructor!( + web_api_interfaces::RTCPeerConnectionIceEvent, + rtc_peer_connection_ice_event_constructor_callback + )) + .length(1) + .build(scope) } ConstructorKind::RtcPeerConnectionIceErrorEvent => { v8::FunctionTemplate::builder(moli_webapi_declare::web_api_constructor!( @@ -767,9 +770,12 @@ pub(in crate::context_bootstrap) fn build_constructor_template_for_profile<'s>( .build(scope) } ConstructorKind::RtcDataChannelEvent => { - v8::FunctionTemplate::builder(rtc_data_channel_event_constructor_callback) - .length(2) - .build(scope) + v8::FunctionTemplate::builder(moli_webapi_declare::web_api_constructor!( + web_api_interfaces::RTCDataChannelEvent, + rtc_data_channel_event_constructor_callback + )) + .length(2) + .build(scope) } ConstructorKind::RtcPeerConnectionIceErrorEvent => { v8::FunctionTemplate::builder(moli_webapi_declare::web_api_constructor!( diff --git a/moli-renderer-v8/src/context_bootstrap/constructors/dom_exception/rtc_error.rs b/moli-renderer-v8/src/context_bootstrap/constructors/dom_exception/rtc_error.rs index b2d9fa4c09..7512b942ad 100644 --- a/moli-renderer-v8/src/context_bootstrap/constructors/dom_exception/rtc_error.rs +++ b/moli-renderer-v8/src/context_bootstrap/constructors/dom_exception/rtc_error.rs @@ -1,5 +1,5 @@ use super::*; -use crate::util::apply_webidl_constructor_prototype_fallback; +use moli_webapi_declare::initialize_web_api_constructor_receiver; use webidl::WebIdlDictionarySerialize; const ERROR_DETAIL: &str = "__moliRtcErrorDetail"; @@ -91,6 +91,9 @@ pub(crate) fn rtc_error_constructor_callback<'s>( let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; + if !initialize_web_api_constructor_receiver(scope, args.this(), "RTCError") { + return; + } let Some(message) = v8::String::new_from_two_byte(scope, &parsed.message.0, v8::NewStringType::Normal) else { @@ -104,12 +107,6 @@ pub(crate) fn rtc_error_constructor_callback<'s>( && declaration.initialize(scope, args.this()).is_ok() { capture_dom_exception_stack(scope, args.this()); - apply_webidl_constructor_prototype_fallback( - scope, - args.this(), - args.new_target(), - "RTCError", - ); rv.set(args.this().into()); } } diff --git a/moli-renderer-v8/src/context_bootstrap/constructors/elements.rs b/moli-renderer-v8/src/context_bootstrap/constructors/elements.rs index 2160dd67d2..c941fe1fb1 100644 --- a/moli-renderer-v8/src/context_bootstrap/constructors/elements.rs +++ b/moli-renderer-v8/src/context_bootstrap/constructors/elements.rs @@ -1,6 +1,6 @@ use super::*; use crate::webidl; -use moli_webapi_declare::WebApiObject; +use moli_webapi_declare::{WebApiObject, initialize_web_api_constructor_receiver}; #[derive(WebApiObject)] #[webapi(plain)] @@ -48,6 +48,9 @@ pub(in crate::context_bootstrap) fn image_constructor_callback<'s>( rv.set_undefined(); return; }; + if !initialize_web_api_constructor_receiver(scope, args.this(), "HTMLImageElement") { + return; + } let global = scope.get_current_context().global(scope); let Some(document) = global.get(scope, v8str(scope, "document").into()) else { rv.set_undefined(); @@ -85,7 +88,7 @@ pub(in crate::context_bootstrap) fn image_constructor_callback<'s>( let value = v8::Integer::new_from_unsigned(scope, height); let _ = image.set(scope, v8str(scope, "height").into(), value.into()); } - apply_legacy_factory_new_target_prototype(scope, &args, image, "HTMLImageElement"); + copy_legacy_factory_receiver_prototype(scope, &args, image); rv.set(image.into()); } @@ -102,6 +105,9 @@ pub(in crate::context_bootstrap) fn audio_constructor_callback<'s>( rv.set_undefined(); return; }; + if !initialize_web_api_constructor_receiver(scope, args.this(), "HTMLAudioElement") { + return; + } let global = scope.get_current_context().global(scope); let Some(document) = global.get(scope, v8str(scope, "document").into()) else { rv.set_undefined(); @@ -155,7 +161,7 @@ pub(in crate::context_bootstrap) fn audio_constructor_callback<'s>( ); } } - apply_legacy_factory_new_target_prototype(scope, &args, audio, "HTMLAudioElement"); + copy_legacy_factory_receiver_prototype(scope, &args, audio); rv.set(audio.into()); } @@ -172,6 +178,9 @@ pub(in crate::context_bootstrap) fn option_constructor_callback<'s>( rv.set_undefined(); return; }; + if !initialize_web_api_constructor_receiver(scope, args.this(), "HTMLOptionElement") { + return; + } let global = scope.get_current_context().global(scope); let Some(document) = global.get(scope, v8str(scope, "document").into()) else { rv.set_undefined(); @@ -246,23 +255,16 @@ pub(in crate::context_bootstrap) fn option_constructor_callback<'s>( false, ); } - apply_legacy_factory_new_target_prototype(scope, &args, option, "HTMLOptionElement"); + copy_legacy_factory_receiver_prototype(scope, &args, option); rv.set(option.into()); } -fn apply_legacy_factory_new_target_prototype<'s>( +fn copy_legacy_factory_receiver_prototype<'s>( scope: &mut v8::PinScope<'s, '_>, args: &v8::FunctionCallbackArguments<'s>, result: v8::Local<'s, v8::Object>, - default_constructor_name: &str, ) { let receiver = args.this(); - crate::util::apply_webidl_constructor_prototype_fallback( - scope, - receiver, - args.new_target(), - default_constructor_name, - ); if let Some(prototype) = receiver.get_prototype(scope) { let _ = result.set_prototype(scope, prototype); } diff --git a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/install.rs b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/install.rs index d50f538010..4e0983226f 100644 --- a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/install.rs +++ b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/install.rs @@ -211,6 +211,8 @@ pub(crate) fn capture_eager_intrinsic_interfaces<'s>( let registry = ExposedInterfaceTemplateRegistry::current(scope) .ok_or_else(|| anyhow!("exposed interface template registry is unavailable"))?; initialize_ecmascript_intrinsic_registry(scope, global); + moli_webapi_declare::capture_web_api_constructor_intrinsics(scope) + .ok_or_else(|| anyhow!("native Reflect.construct is unavailable during realm bootstrap"))?; capture_ecmascript_intrinsic(scope, global, "Error")?; capture_ecmascript_intrinsic(scope, global, "Promise")?; let realm = IntrinsicInterfaceRegistry::initialize_for_current_context( diff --git a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/materialize.rs b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/materialize.rs index 0890e179cd..a7bfc491c6 100644 --- a/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/materialize.rs +++ b/moli-renderer-v8/src/context_bootstrap/exposed_interfaces/materialize.rs @@ -278,6 +278,33 @@ fn finish_materialized_interface<'s>( set_interface_prototype_constructor(scope, constructor_prototype, proxy.into())?; v8::Local::::from(proxy) } + ConstructorKind::DomParser + | ConstructorKind::VideoColorSpace + | ConstructorKind::RtcIceCandidate + | ConstructorKind::RtcSessionDescription + | ConstructorKind::RtcError + | ConstructorKind::RtcErrorEvent + | ConstructorKind::RtcDataChannelEvent + | ConstructorKind::RtcPeerConnectionIceEvent + | ConstructorKind::RtcPeerConnectionIceErrorEvent + | ConstructorKind::Image + | ConstructorKind::Audio + | ConstructorKind::Option => { + let proxy = moli_webapi_declare::web_api_constructor_with_deferred_prototype( + scope, + constructor, + ) + .ok_or_else(|| { + anyhow!( + "failed to create WebIDL constructor entry for `{}`", + metadata.name + ) + })?; + if metadata.prototype_property == ResolvedPrototypeProperty::TemplateReadOnly { + set_interface_prototype_constructor(scope, constructor_prototype, proxy.into())?; + } + proxy.into() + } _ => constructor.into(), }; // Like Blink's ConstructorForTypeSlowCase, finish every fallible binding diff --git a/moli-renderer-v8/src/context_bootstrap/video_color_space.rs b/moli-renderer-v8/src/context_bootstrap/video_color_space.rs index 5b1c10bf51..1e6dbc9bd5 100644 --- a/moli-renderer-v8/src/context_bootstrap/video_color_space.rs +++ b/moli-renderer-v8/src/context_bootstrap/video_color_space.rs @@ -1,12 +1,12 @@ //! Immutable color metadata, independent of video decoding and rendering. use crate::{ - util::{ - apply_webidl_constructor_prototype_fallback, get_private_value, throw_type_error, v8str, - }, + util::{get_private_value, throw_type_error, v8str}, web_api_interfaces, webidl, }; -use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject}; +use moli_webapi_declare::{ + WebApiFunctionTemplate, WebApiObject, initialize_web_api_constructor_receiver, +}; const PRIMARIES_SLOT: &str = "__moliVideoColorSpacePrimaries"; const TRANSFER_SLOT: &str = "__moliVideoColorSpaceTransfer"; @@ -185,6 +185,9 @@ pub(in crate::context_bootstrap) fn video_color_space_constructor_callback<'s>( let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; + if !initialize_web_api_constructor_receiver(scope, args.this(), "VideoColorSpace") { + return; + } let init = parsed.init; let primaries = nullable_string(scope, init.primaries.map(VideoColorPrimaries::label)); let transfer = nullable_string( @@ -199,12 +202,6 @@ pub(in crate::context_bootstrap) fn video_color_space_constructor_callback<'s>( VideoColorSpaceObjectDeclaration::new(primaries, transfer, matrix, full_range) .initialize(scope, args.this()) .expect("VideoColorSpace slots initialize"); - apply_webidl_constructor_prototype_fallback( - scope, - args.this(), - args.new_target(), - "VideoColorSpace", - ); rv.set(args.this().into()); } diff --git a/moli-renderer-v8/src/context_bootstrap/webrtc/events.rs b/moli-renderer-v8/src/context_bootstrap/webrtc/events.rs index be96305332..039550890b 100644 --- a/moli-renderer-v8/src/context_bootstrap/webrtc/events.rs +++ b/moli-renderer-v8/src/context_bootstrap/webrtc/events.rs @@ -4,13 +4,12 @@ use crate::{ EventInit, event_private_value, initialize_event_object_with_type, initialize_event_wrapper, new_event_state, }, - util::{ - apply_webidl_constructor_prototype_fallback, callback_data_index_value, callback_data_item, - throw_type_error, - }, + util::{callback_data_index_value, callback_data_item, throw_type_error}, webidl::{self, WebIdlDictionarySerialize}, }; -use moli_webapi_declare::{WebApiFunctionTemplate, WebApiInterfaceDescriptor}; +use moli_webapi_declare::{ + WebApiFunctionTemplate, WebApiInterfaceDescriptor, initialize_web_api_constructor_receiver, +}; const ICE_EVENT_CANDIDATE_SLOT: &str = "__moliRtcIceEventCandidate"; const ICE_EVENT_URL_SLOT: &str = "__moliRtcIceEventUrl"; @@ -148,12 +147,6 @@ pub(in crate::context_bootstrap) fn rtc_peer_connection_ice_event_constructor_ca ) .is_some() { - apply_webidl_constructor_prototype_fallback( - scope, - args.this(), - args.new_target(), - "RTCPeerConnectionIceEvent", - ); rv.set(args.this().into()); } } @@ -180,12 +173,6 @@ pub(in crate::context_bootstrap) fn rtc_data_channel_event_constructor_callback< ) .is_some() { - apply_webidl_constructor_prototype_fallback( - scope, - args.this(), - args.new_target(), - "RTCDataChannelEvent", - ); rv.set(args.this().into()); } } @@ -212,12 +199,6 @@ pub(in crate::context_bootstrap) fn rtc_error_event_constructor_callback<'s>( ) .is_some() { - apply_webidl_constructor_prototype_fallback( - scope, - args.this(), - args.new_target(), - "RTCErrorEvent", - ); rv.set(args.this().into()); } } @@ -230,6 +211,9 @@ fn initialize_event<'s, T: WebIdlDictionarySerialize<'s>>( payload: &T, interface: WebApiInterfaceDescriptor, ) -> Option<()> { + if !initialize_web_api_constructor_receiver(scope, wrapper, interface.name()) { + return None; + } let event_type = v8::String::new_from_two_byte(scope, &event_type.0, v8::NewStringType::Normal)?; let state = new_event_state(scope); diff --git a/moli-renderer-v8/src/context_bootstrap/webrtc/ice_candidate.rs b/moli-renderer-v8/src/context_bootstrap/webrtc/ice_candidate.rs index d1272160ba..cdd200787e 100644 --- a/moli-renderer-v8/src/context_bootstrap/webrtc/ice_candidate.rs +++ b/moli-renderer-v8/src/context_bootstrap/webrtc/ice_candidate.rs @@ -1,13 +1,12 @@ use super::ice_candidate_parser::parse_ice_candidate; use crate::web_api_interfaces; use crate::{ - util::{ - apply_webidl_constructor_prototype_fallback, callback_data_index_value, get_private_value, - throw_type_error, v8str, - }, + util::{callback_data_index_value, get_private_value, throw_type_error, v8str}, webidl::{self, WebIdlConverter}, }; -use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject}; +use moli_webapi_declare::{ + WebApiFunctionTemplate, WebApiObject, initialize_web_api_constructor_receiver, +}; const CANDIDATE_VALUES_SLOT: &str = "__moliRtcIceCandidateValues"; @@ -98,6 +97,9 @@ pub(in crate::context_bootstrap) fn rtc_ice_candidate_constructor_callback<'s>( let Some(mut values) = parse_candidate_init(scope, dictionary) else { return; }; + if !initialize_web_api_constructor_receiver(scope, args.this(), "RTCIceCandidate") { + return; + } if values[1].is_null() && values[2].is_null() { throw_type_error(scope, "RTCIceCandidate requires sdpMid or sdpMLineIndex."); return; @@ -130,12 +132,6 @@ pub(in crate::context_bootstrap) fn rtc_ice_candidate_constructor_callback<'s>( { return; } - apply_webidl_constructor_prototype_fallback( - scope, - args.this(), - args.new_target(), - "RTCIceCandidate", - ); rv.set(args.this().into()); } diff --git a/moli-renderer-v8/src/context_bootstrap/webrtc/ice_error_event.rs b/moli-renderer-v8/src/context_bootstrap/webrtc/ice_error_event.rs index 39b14d2b1e..d1a0ebae3b 100644 --- a/moli-renderer-v8/src/context_bootstrap/webrtc/ice_error_event.rs +++ b/moli-renderer-v8/src/context_bootstrap/webrtc/ice_error_event.rs @@ -3,13 +3,10 @@ use crate::{ EventInit, event_private_value, initialize_event_object_with_type, initialize_event_wrapper, new_event_state, }, - util::{ - apply_webidl_constructor_prototype_fallback, callback_data_index_value, callback_data_item, - throw_type_error, - }, + util::{callback_data_index_value, callback_data_item, throw_type_error}, web_api_interfaces, webidl, }; -use moli_webapi_declare::WebApiFunctionTemplate; +use moli_webapi_declare::{WebApiFunctionTemplate, initialize_web_api_constructor_receiver}; use webidl::WebIdlDictionarySerialize; const ADDRESS_SLOT: &str = "__moliRtcIceErrorAddress"; @@ -91,6 +88,13 @@ pub(in crate::context_bootstrap) fn rtc_peer_connection_ice_error_event_construc let Some(parsed) = webidl::parse_args::(scope, &args) else { return; }; + if !initialize_web_api_constructor_receiver( + scope, + args.this(), + "RTCPeerConnectionIceErrorEvent", + ) { + return; + } let Some(event_type) = v8::String::new_from_two_byte(scope, &parsed.event_type.0, v8::NewStringType::Normal) else { @@ -110,12 +114,6 @@ pub(in crate::context_bootstrap) fn rtc_peer_connection_ice_error_event_construc { return; } - apply_webidl_constructor_prototype_fallback( - scope, - args.this(), - args.new_target(), - "RTCPeerConnectionIceErrorEvent", - ); rv.set(args.this().into()); } diff --git a/moli-renderer-v8/src/context_bootstrap/webrtc/session_description.rs b/moli-renderer-v8/src/context_bootstrap/webrtc/session_description.rs index f3c046e0d0..72aa25965a 100644 --- a/moli-renderer-v8/src/context_bootstrap/webrtc/session_description.rs +++ b/moli-renderer-v8/src/context_bootstrap/webrtc/session_description.rs @@ -2,12 +2,13 @@ use super::RtcSessionDescriptionInitDeclaration; use crate::web_api_interfaces; use crate::{ util::{ - apply_webidl_constructor_prototype_fallback, callback_data_index_value, callback_data_item, - get_private_value, throw_type_error, v8str, + callback_data_index_value, callback_data_item, get_private_value, throw_type_error, v8str, }, webidl, }; -use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject}; +use moli_webapi_declare::{ + WebApiFunctionTemplate, WebApiObject, initialize_web_api_constructor_receiver, +}; const DESCRIPTION_TYPE_SLOT: &str = "__moliRtcSessionDescriptionType"; const DESCRIPTION_SDP_SLOT: &str = "__moliRtcSessionDescriptionSdp"; @@ -105,18 +106,15 @@ pub(in crate::context_bootstrap) fn rtc_session_description_constructor_callback throw_type_error(scope, "Invalid RTCSdpType."); return; } + if !initialize_web_api_constructor_receiver(scope, args.this(), "RTCSessionDescription") { + return; + } if SessionDescriptionObjectDeclaration::new(kind, sdp) .initialize(scope, args.this()) .is_err() { return; } - apply_webidl_constructor_prototype_fallback( - scope, - args.this(), - args.new_target(), - "RTCSessionDescription", - ); rv.set(args.this().into()); } diff --git a/moli-renderer-v8/src/dom_parser.rs b/moli-renderer-v8/src/dom_parser.rs index 97d7e9ff4a..39b0baf418 100644 --- a/moli-renderer-v8/src/dom_parser.rs +++ b/moli-renderer-v8/src/dom_parser.rs @@ -1,6 +1,7 @@ use crate::web_api_interfaces; use moli_web_mime::{is_dom_parser_xml_mime, is_html_document_mime, is_xml_document_mime}; use moli_webapi_declare::WebApiFunctionTemplate; +use moli_webapi_declare::initialize_web_api_constructor_receiver; use url::Url; use crate::{ @@ -19,8 +20,8 @@ use super::{ }, }, util::{ - apply_webidl_constructor_prototype_fallback, context_host_ptr_from_global_bridge, - get_private_object, get_private_value, set_private_value, throw_type_error, + context_host_ptr_from_global_bridge, get_private_object, get_private_value, + set_private_value, throw_type_error, }, }; @@ -132,7 +133,9 @@ pub(super) fn dom_parser_constructor_callback<'s>( return; } let parser = args.this(); - apply_webidl_constructor_prototype_fallback(scope, parser, args.new_target(), "DOMParser"); + if !initialize_web_api_constructor_receiver(scope, parser, "DOMParser") { + return; + } let Some(host_ptr) = context_host_ptr_from_global_bridge(scope) else { throw_type_error(scope, "DOMParser constructor has no associated Document"); return; diff --git a/moli-renderer-v8/src/script_vm/tests/constructor_entry.js b/moli-renderer-v8/src/script_vm/tests/constructor_entry.js new file mode 100644 index 0000000000..4bd8ff6e07 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/constructor_entry.js @@ -0,0 +1,118 @@ +(async () => { + const rows = []; + const check = (name, ok, detail = null) => rows.push({name, ok: !!ok, detail}); + const child = document.querySelector('iframe'); + await new Promise(resolve => { + if (child.contentDocument.readyState === 'complete') resolve(); + else child.addEventListener('load', resolve, {once: true}); + }); + const other = child.contentWindow; + const definitions = [ + ['DOMParser', () => [], 'DOMParser'], + ['VideoColorSpace', () => [{matrix: 'bt709'}], 'VideoColorSpace'], + ['RTCIceCandidate', () => [{candidate: '', sdpMid: '0'}], 'RTCIceCandidate'], + ['RTCSessionDescription', () => [{type: 'offer', sdp: ''}], 'RTCSessionDescription'], + ['RTCError', () => [{errorDetail: 'sdp-syntax-error'}, 'reason'], 'RTCError'], + ['RTCDataChannelEvent', w => ['x', {channel: channels.get(w)}], 'RTCDataChannelEvent'], + ['RTCErrorEvent', w => ['x', {error: new w.RTCError({errorDetail: 'sdp-syntax-error'})}], 'RTCErrorEvent'], + ['RTCPeerConnectionIceEvent', () => ['x', {candidate: null}], 'RTCPeerConnectionIceEvent'], + ['RTCPeerConnectionIceErrorEvent', () => ['x', {errorCode: 701}], 'RTCPeerConnectionIceErrorEvent'], + ['Image', () => [1, 2], 'HTMLImageElement'], + ['Audio', () => [''], 'HTMLAudioElement'], + ['Option', () => ['label', 'value', true, true], 'HTMLOptionElement'] + ]; + const connections = []; + const channels = new Map(); + try { + for (const w of [window, other]) { + const realm = w === window ? 'main' : 'child'; + const peer = new w.RTCPeerConnection(); connections.push(peer); + channels.set(w, peer.createDataChannel('constructor-test')); + for (const [name, args, defaultName] of definitions) { + try { + const C = w[name], defaults = w[defaultName].prototype; + check(`${realm}/${name}/metadata`, typeof C === 'function' && C.name === name && C.prototype === defaults); + const explicit = [w.Object.prototype, Object.create(null), new Proxy({}, {})]; + for (const [i, prototype] of explicit.entries()) { + const N = new Proxy(w.Function(), {get(target, key, receiver) { + if (key === 'prototype') return prototype; + return Reflect.get(target, key, receiver); + }}); + const value = Reflect.construct(C, args(w), N); + check(`${realm}/${name}/explicit-${i}`, Object.getPrototypeOf(value) === prototype); + } + for (const nw of [window, other]) { + let reads = 0; + const N = new Proxy(nw.Function(), {get(target, key, receiver) { + if (key === 'prototype') { reads++; return 42; } + return Reflect.get(target, key, receiver); + }}); + const value = Reflect.construct(C, args(w), N); + check(`${realm}/${name}/fallback-${nw === window ? 'main' : 'child'}`, Object.getPrototypeOf(value) === nw[defaultName].prototype && reads === 1, {reads}); + } + const prototype = Object.create(null); + let reads = 0, nested = null; + const N = new Proxy(w.Function(), {get(target, key, receiver) { + if (key === 'prototype') { + reads++; + if (reads === 1) nested = Reflect.construct(C, args(w), N); + return prototype; + } + return Reflect.get(target, key, receiver); + }}); + const outer = Reflect.construct(C, args(w), N); + check(`${realm}/${name}/reentry`, reads === 2 && Object.getPrototypeOf(outer) === prototype && Object.getPrototypeOf(nested) === prototype, {reads}); + const sentinel = {}; + const throwing = new Proxy(w.Function(), {get(target, key, receiver) { + if (key === 'prototype') throw sentinel; + return Reflect.get(target, key, receiver); + }}); + let thrown; + try { Reflect.construct(C, args(w), throwing); } catch (error) { thrown = error; } + check(`${realm}/${name}/prototype-exception`, thrown === sentinel); + } catch (error) { check(`${realm}/${name}/setup`, false, String(error)); } + } + for (const kind of ['object', 'primitive']) { + let revoke, reads = 0; + const prototype = Object.create(null); + const pair = Proxy.revocable(w.Function(), {get(target, key, receiver) { + if (key === 'prototype') { reads++; revoke(); return kind === 'object' ? prototype : 7; } + return Reflect.get(target, key, receiver); + }}); + revoke = pair.revoke; + let value, error; + try { value = Reflect.construct(w.VideoColorSpace, [], pair.proxy); } catch (e) { error = e; } + check(`${realm}/revoked-after-get-${kind}`, reads === 1 && (kind === 'object' ? value && Object.getPrototypeOf(value) === prototype : error instanceof w.TypeError), {reads, error: String(error)}); + } + const log = []; + const N = new Proxy(w.Function(), {get(target, key, receiver) { + if (key === 'prototype') { log.push('prototype'); return w.Object.prototype; } + return Reflect.get(target, key, receiver); + }}); + const init = {get fullRange() { log.push('fullRange'); return true; }, get matrix() { log.push('matrix'); return 'bt709'; }, get primaries() { log.push('primaries'); return 'bt709'; }, get transfer() { log.push('transfer'); return 'bt709'; }}; + Reflect.construct(w.VideoColorSpace, [init], N); + check(`${realm}/conversion-before-prototype`, log.join(',') === 'fullRange,matrix,primaries,transfer,prototype', log); + log.length = 0; + const sentinel = {}; + try { Reflect.construct(w.VideoColorSpace, [{get matrix() { log.push('matrix'); throw sentinel; }}], N); } catch (error) { check(`${realm}/conversion-exception-identity`, error === sentinel); } + check(`${realm}/failed-conversion-no-prototype`, log.join(',') === 'matrix', log); + } + // A lazily materialized constructor must use captured native construction, + // even when an author has replaced Reflect.construct and polluted traps. + const intrinsicConstruct = Reflect.construct; + const prototypeGet = Object.prototype.get; + const inheritedConstruct = Object.prototype.construct; + try { + Reflect.construct = () => { throw Error('author Reflect.construct'); }; + Object.prototype.get = () => { throw Error('inherited get trap'); }; + Object.prototype.construct = () => { throw Error('inherited construct trap'); }; + check('intrinsic-capture-and-handler-prototype', Object.getPrototypeOf(new VideoColorSpace()) === VideoColorSpace.prototype); + } finally { + Reflect.construct = intrinsicConstruct; + if (prototypeGet === undefined) delete Object.prototype.get; else Object.prototype.get = prototypeGet; + if (inheritedConstruct === undefined) delete Object.prototype.construct; else Object.prototype.construct = inheritedConstruct; + } + } finally { for (const connection of connections) connection.close(); } + globalThis.__uiEventResults = {rows, total: rows.length, passed: rows.filter(row => row.ok).length, failed: rows.filter(row => !row.ok)}; + return true; +})() diff --git a/moli-renderer-v8/src/script_vm/tests/constructor_entry.rs b/moli-renderer-v8/src/script_vm/tests/constructor_entry.rs new file mode 100644 index 0000000000..50b817c7d9 --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/constructor_entry.rs @@ -0,0 +1,43 @@ +use super::*; + +#[test] +fn webidl_constructors_preserve_explicit_prototypes_and_conversion_order_across_realms() { + let mut vm = new_storage_page_task_executor_test_vm("https://constructor-entry.test/"); + vm.eval("document.body.innerHTML = ''") + .unwrap(); + vm.eval(include_str!("constructor_entry.js")).unwrap(); + assert_eq!( + vm.eval_after_selected_page_tasks("JSON.stringify(__uiEventResults.failed)") + .unwrap(), + "[]" + ); + assert_eq!(vm.eval("__uiEventResults.total").unwrap(), "203"); +} + +#[test] +fn lazy_constructor_materialization_keeps_captured_intrinsics_and_public_metadata() { + let mut vm = new_storage_page_task_executor_test_vm("https://constructor-intrinsic.test/"); + assert_eq!( + vm.eval( + r#"(() => { + const reflectConstruct = Reflect.construct; + const functionToString = Function.prototype.toString; + Reflect.construct = () => { throw Error('author Reflect.construct'); }; + Reflect = null; + Object.prototype.get = () => { throw Error('inherited get trap'); }; + Object.prototype.construct = () => { throw Error('inherited construct trap'); }; + const C = VideoColorSpace; + const value = new C({matrix: 'rgb'}); + const descriptor = Object.getOwnPropertyDescriptor(C, 'prototype'); + return C.prototype.constructor === C && Object.getPrototypeOf(value) === C.prototype + && value.matrix === 'rgb' && C.length === 0 && C.name === 'VideoColorSpace' + && !descriptor.writable && !descriptor.enumerable && !descriptor.configurable + && JSON.stringify(Object.getOwnPropertyNames(C)) === '["length","name","prototype"]' + && functionToString.call(C).includes('[native code]') + && Object.getPrototypeOf(reflectConstruct(C, [])) === C.prototype; + })()"# + ) + .unwrap(), + "true" + ); +} diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index db817b5b66..7dc4e00636 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -11,6 +11,7 @@ mod visual_viewport_receiver; mod writable_stream_native_receivers; mod dom_rect_factory_descriptors; mod offline_audio_float; +mod constructor_entry; mod webtransport_stream_interface_exposure; use super::post_parse::dynamic_script_execute_is_runnable_before_dom_content_loaded; diff --git a/moli-renderer-v8/src/util.rs b/moli-renderer-v8/src/util.rs index bf5483c18f..c56280803c 100644 --- a/moli-renderer-v8/src/util.rs +++ b/moli-renderer-v8/src/util.rs @@ -1,7 +1,7 @@ use super::document_runtime::DomHandle; use super::native_bridge::JsContextHost; pub use moli_v8_util::{ - array_contains_strict, array_push_value, call_object_method, callback_data_index_value, + callable_relevant_context, array_contains_strict, array_push_value, call_object_method, callback_data_index_value, callback_data_item, constructor_object, constructor_prototype, constructor_prototype_object, define_non_enumerable_static_bool_property, define_non_enumerable_static_number_property, define_non_enumerable_static_property, define_non_enumerable_static_string_property, @@ -346,35 +346,6 @@ where serialize_v8_array(scope, values.as_slice()) } -pub(crate) fn callable_relevant_context<'s>( - scope: &mut v8::PinScope<'s, '_>, - mut callable: v8::Local<'s, v8::Value>, -) -> Option> { - // GetFunctionRealm follows both bound targets and Proxy targets. A bound - // Proxy's creation context need not be the underlying function's realm. - // Read internal targets without invoking author-defined property traps. - loop { - if let Ok(proxy) = v8::Local::::try_from(callable) { - if proxy.is_revoked() { - throw_type_error(scope, "Cannot determine the realm of a revoked Proxy"); - return None; - } - callable = proxy.get_target(scope); - continue; - } - if let Ok(function) = v8::Local::::try_from(callable) { - let target = function.get_bound_function(scope); - if !target.is_undefined() { - callable = target; - continue; - } - } - return v8::Local::::try_from(callable) - .ok()? - .get_creation_context(scope); - } -} - pub(crate) fn new_target_realm_constructor_prototype<'s>( scope: &mut v8::PinScope<'s, '_>, new_target: v8::Local<'s, v8::Value>, @@ -405,28 +376,6 @@ pub(crate) fn receiver_uses_new_target_realm_object_fallback<'s>( receiver_prototype.strict_equals(object_prototype.into()) } -pub(crate) fn apply_webidl_constructor_prototype_fallback<'s>( - scope: &mut v8::PinScope<'s, '_>, - receiver: v8::Local<'s, v8::Object>, - new_target: v8::Local<'s, v8::Value>, - default_constructor_name: &str, -) { - // V8 has already read `NewTarget.prototype` while preparing the native - // constructor receiver. Do not read it again: a Proxy getter must run only - // once. V8 represents a non-object result with NewTarget's realm-local - // Object prototype, which lets us replace precisely that fallback with - // the WebIDL interface's default prototype from the same realm. - if !receiver_uses_new_target_realm_object_fallback(scope, receiver, new_target) { - return; - } - let Some(prototype) = - new_target_realm_constructor_prototype(scope, new_target, default_constructor_name) - else { - return; - }; - let _ = receiver.set_prototype(scope, prototype.into()); -} - pub(super) fn create_script_origin_with_base_url<'s>( scope: &mut v8::PinScope<'s, '_>, resource_name: &str, diff --git a/moli-v8-util/src/callables.rs b/moli-v8-util/src/callables.rs new file mode 100644 index 0000000000..eecbfc0d66 --- /dev/null +++ b/moli-v8-util/src/callables.rs @@ -0,0 +1,30 @@ +use crate::throw_type_error; + +/// ECMAScript GetFunctionRealm follows bound and Proxy targets without calling +/// author property traps. In particular, a bound Proxy's creation context need +/// not be the underlying function's realm. +pub fn callable_relevant_context<'s>( + scope: &mut v8::PinScope<'s, '_>, + mut callable: v8::Local<'s, v8::Value>, +) -> Option> { + loop { + if let Ok(proxy) = v8::Local::::try_from(callable) { + if proxy.is_revoked() { + throw_type_error(scope, "Cannot determine the realm of a revoked Proxy"); + return None; + } + callable = proxy.get_target(scope); + continue; + } + if let Ok(function) = v8::Local::::try_from(callable) { + let target = function.get_bound_function(scope); + if !target.is_undefined() { + callable = target; + continue; + } + } + return v8::Local::::try_from(callable) + .ok()? + .get_creation_context(scope); + } +} diff --git a/moli-v8-util/src/lib.rs b/moli-v8-util/src/lib.rs index 230b0790a4..d31c81fba8 100644 --- a/moli-v8-util/src/lib.rs +++ b/moli-v8-util/src/lib.rs @@ -1,3 +1,4 @@ +mod callables; mod callbacks; mod constructors; mod exceptions; @@ -9,6 +10,7 @@ mod properties; mod strings; mod symbols; +pub use callables::*; pub use callbacks::*; pub use constructors::*; pub use exceptions::*; diff --git a/moli-webapi-declare/src/callback.rs b/moli-webapi-declare/src/callback.rs index c7965e3612..5d4f0c20ef 100644 --- a/moli-webapi-declare/src/callback.rs +++ b/moli-webapi-declare/src/callback.rs @@ -20,7 +20,14 @@ pub fn invoke_web_api_constructor<'s>( let receiver = args.this(); let construct = args.is_construct_call(); v8::tc_scope!(let scope, scope); + let Some((args, deferred)) = crate::constructor::restore_constructor_new_target(scope, args) + else { + return; + }; callback(scope, args, rv); + if deferred && scope.can_continue() { + crate::constructor::clear_constructor_invocation(scope, receiver); + } if scope.has_caught() { scope.rethrow(); return; diff --git a/moli-webapi-declare/src/constructor.rs b/moli-webapi-declare/src/constructor.rs new file mode 100644 index 0000000000..58368f2e13 --- /dev/null +++ b/moli-webapi-declare/src/constructor.rs @@ -0,0 +1,188 @@ +//! WebIDL argument conversion precedes the single Get(NewTarget, "prototype"). +//! V8 allocates API constructor receivers before entering their callbacks. A +//! private NewTarget lets the native callback perform that Get at the specified +//! point, without guessing from V8's already selected Object.prototype fallback. + +use moli_v8_util::{ + callable_relevant_context, get_private_object, get_private_value, global_constructor_prototype, + new_null_prototype_object, private_key, throw_type_error, v8str, +}; + +const REFLECT_CONSTRUCT: &str = "moli.webidl.reflectConstruct"; +const INVOCATION: &str = "moli.webidl.constructorInvocation"; +const NEW_TARGET: &str = "moli.webidl.originalNewTarget"; +const SELECTED_PROTOTYPE: &str = "moli.webidl.selectedConstructorPrototype"; + +/// Capture before author script runs, including before lazy interface access. +pub fn capture_web_api_constructor_intrinsics<'s>(scope: &mut v8::PinScope<'s, '_>) -> Option<()> { + let global = scope.get_current_context().global(scope); + if get_private_value(scope, global, REFLECT_CONSTRUCT).is_some() { + return Some(()); + } + let reflect = global.get(scope, v8str(scope, "Reflect").into())?; + let reflect = v8::Local::::try_from(reflect).ok()?; + let construct = reflect.get(scope, v8str(scope, "construct").into())?; + let construct = v8::Local::::try_from(construct).ok()?; + let key = private_key(scope, REFLECT_CONSTRUCT)?; + global + .set_private(scope, key, construct.into())? + .then_some(()) +} + +/// Keep the native function as the intrinsic constructor; publish this entry +/// for JavaScript construction. Native callback data and instance templates are +/// preserved by invoking the original function with a private NewTarget. +pub fn web_api_constructor_with_deferred_prototype<'s>( + scope: &mut v8::PinScope<'s, '_>, + constructor: v8::Local<'s, v8::Function>, +) -> Option> { + let global = scope.get_current_context().global(scope); + let construct = get_private_value(scope, global, REFLECT_CONSTRUCT)?; + let trap = v8::Function::builder(construct_trap) + .data(construct) + .length(3) + .build(scope)?; + let handler = new_null_prototype_object(scope); + handler + .define_own_property( + scope, + v8str(scope, "construct").into(), + trap.into(), + v8::PropertyAttribute::NONE, + )? + .then_some(())?; + v8::Proxy::new(scope, constructor.into(), handler) +} + +fn construct_trap<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'s>, +) { + let construct = + v8::Local::::try_from(args.data()).expect("captured Reflect.construct"); + let Some(hidden_new_target) = v8::Function::new(scope, private_constructor) else { + return; + }; + let prototype = new_null_prototype_object(scope); + if !hidden_new_target + .define_own_property( + scope, + v8str(scope, "prototype").into(), + prototype.into(), + v8::PropertyAttribute::READ_ONLY + | v8::PropertyAttribute::DONT_ENUM + | v8::PropertyAttribute::DONT_DELETE, + ) + .unwrap_or(false) + { + return; + } + let invocation = new_null_prototype_object(scope); + let Some(new_target_key) = private_key(scope, NEW_TARGET) else { + return; + }; + let Some(invocation_key) = private_key(scope, INVOCATION) else { + return; + }; + if !invocation + .set_private(scope, new_target_key, args.get(2)) + .unwrap_or(false) + || !hidden_new_target + .set_private(scope, invocation_key, invocation.into()) + .unwrap_or(false) + { + return; + } + if let Some(result) = construct.call( + scope, + v8::undefined(scope).into(), + &[args.get(0), args.get(1), hidden_new_target.into()], + ) { + rv.set(result); + } +} + +fn private_constructor( + _scope: &mut v8::PinScope<'_, '_>, + _args: v8::FunctionCallbackArguments<'_>, + _rv: v8::ReturnValue<'_>, +) { +} + +pub(crate) fn restore_constructor_new_target<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, +) -> Option<(v8::FunctionCallbackArguments<'s>, bool)> { + if !args.is_construct_call() || args.new_target().is_proxy() { + return Some((args, false)); + } + let Ok(new_target) = v8::Local::::try_from(args.new_target()) else { + return Some((args, false)); + }; + let Some(invocation) = get_private_object(scope, new_target, INVOCATION) else { + return Some((args, false)); + }; + let original = get_private_value(scope, invocation, NEW_TARGET)?; + let key = private_key(scope, INVOCATION)?; + args.this() + .set_private(scope, key, invocation.into())? + .then_some((args.with_new_target(original), true)) +} + +pub(crate) fn clear_constructor_invocation( + scope: &mut v8::PinScope<'_, '_>, + receiver: v8::Local<'_, v8::Object>, +) { + if let Some(key) = private_key(scope, INVOCATION) { + let _ = receiver.delete_private(scope, key); + } +} + +/// Call after overload resolution and all WebIDL argument conversion, before +/// constructor initialization. Internal construction through the cached native +/// function retains its V8 receiver and does not inspect author properties. +pub fn initialize_web_api_constructor_receiver<'s>( + scope: &mut v8::PinScope<'s, '_>, + receiver: v8::Local<'s, v8::Object>, + default_interface_name: &str, +) -> bool { + let Some(invocation) = get_private_object(scope, receiver, INVOCATION) else { + return true; + }; + if get_private_value(scope, invocation, SELECTED_PROTOTYPE).is_some() { + return true; + } + let Some(new_target) = get_private_object(scope, invocation, NEW_TARGET) else { + return false; + }; + let Some(prototype) = new_target.get(scope, v8str(scope, "prototype").into()) else { + return false; + }; + let prototype = if let Ok(prototype) = v8::Local::::try_from(prototype) { + prototype + } else { + let Some(context) = callable_relevant_context(scope, new_target.into()) else { + return false; + }; + let prototype = { + let scope = &mut v8::ContextScope::new(scope, context); + let Some(prototype) = global_constructor_prototype(scope, default_interface_name) + else { + throw_type_error(scope, "WebIDL constructor prototype is unavailable"); + return false; + }; + v8::Global::new(scope, prototype) + }; + v8::Local::new(scope, &prototype) + }; + let Some(key) = private_key(scope, SELECTED_PROTOTYPE) else { + return false; + }; + receiver + .set_prototype(scope, prototype.into()) + .unwrap_or(false) + && invocation + .set_private(scope, key, prototype.into()) + .unwrap_or(false) +} diff --git a/moli-webapi-declare/src/lib.rs b/moli-webapi-declare/src/lib.rs index 45cf50e827..8920463f4a 100644 --- a/moli-webapi-declare/src/lib.rs +++ b/moli-webapi-declare/src/lib.rs @@ -257,6 +257,7 @@ extern crate self as moli_webapi_declare; mod brand; mod callback; mod cross_origin; +mod constructor; mod declaration; mod error; mod interface; @@ -266,6 +267,10 @@ mod value; pub mod __private; +pub use constructor::{ + capture_web_api_constructor_intrinsics, initialize_web_api_constructor_receiver, + web_api_constructor_with_deferred_prototype, +}; pub use interface::WebApiInterfaceDescriptor; pub use moli_webapi_declare_derive::{WebApiCrossOrigin, WebApiFunctionTemplate, WebApiObject}; pub use v8; diff --git a/moli-webapi-declare/tests/constructor.rs b/moli-webapi-declare/tests/constructor.rs new file mode 100644 index 0000000000..fd900aae65 --- /dev/null +++ b/moli-webapi-declare/tests/constructor.rs @@ -0,0 +1,231 @@ +use std::pin::pin; + +use moli_v8_test_util::ensure_v8; +use moli_v8_util::{ + WebApiIntrinsicKind, WebApiIntrinsicLookup, get_private_object, + install_web_api_intrinsic_resolver, set_private_value, v8str, +}; +use moli_webapi_declare::{ + capture_web_api_constructor_intrinsics, initialize_web_api_constructor_receiver, + web_api_constructor_with_deferred_prototype, +}; + +moli_webapi_declare::declare_web_api_interfaces! { + TestThing; +} + +fn intrinsic<'s>( + scope: &mut v8::PinScope<'s, '_>, + name: &str, + kind: WebApiIntrinsicKind, +) -> WebApiIntrinsicLookup<'s> { + if name != "TestThing" { + return WebApiIntrinsicLookup::Unmanaged; + } + let global = scope.get_current_context().global(scope); + WebApiIntrinsicLookup::Managed(get_private_object( + scope, + global, + match kind { + WebApiIntrinsicKind::Constructor => "test.constructor", + WebApiIntrinsicKind::Prototype => "test.prototype", + }, + )) +} + +fn install<'s>(scope: &mut v8::PinScope<'s, '_>) { + capture_web_api_constructor_intrinsics(scope).unwrap(); + let global = scope.get_current_context().global(scope); + let template = v8::FunctionTemplate::builder(moli_webapi_declare::web_api_constructor!( + TestThing, + constructor + )) + .data(v8str(scope, "native callback data").into()) + .length(2) + .build(scope); + template.set_class_name(v8str(scope, "TestThing")); + let native = template.get_function(scope).unwrap(); + let prototype = native.get(scope, v8str(scope, "prototype").into()).unwrap(); + let prototype = v8::Local::::try_from(prototype).unwrap(); + set_private_value(scope, global, "test.constructor", native.into()); + set_private_value(scope, global, "test.prototype", prototype.into()); + install_web_api_intrinsic_resolver(scope, intrinsic); + let public = web_api_constructor_with_deferred_prototype(scope, native).unwrap(); + prototype + .set(scope, v8str(scope, "constructor").into(), public.into()) + .unwrap(); + global + .set(scope, v8str(scope, "TestThing").into(), public.into()) + .unwrap(); +} + +fn constructor<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'s>, +) { + assert!(args.is_construct_call()); + let Some(converted) = args.get(0).to_string(scope) else { + return; + }; + if !initialize_web_api_constructor_receiver(scope, args.this(), "TestThing") { + return; + } + // A second native initialization must not invoke the author's getter twice. + assert!(initialize_web_api_constructor_receiver( + scope, + args.this(), + "TestThing" + )); + for (name, value) in [ + ("converted", converted.into()), + ("originalNewTarget", args.new_target()), + ("callbackData", args.data()), + ( + "argumentCount", + v8::Integer::new(scope, args.length()).into(), + ), + ] { + args.this() + .define_own_property( + scope, + v8str(scope, name).into(), + value, + v8::PropertyAttribute::NONE, + ) + .unwrap(); + } + rv.set(args.this().into()); +} + +fn eval<'s>(scope: &mut v8::PinScope<'s, '_>, source: &str) -> v8::Local<'s, v8::Value> { + let source = v8::String::new(scope, source).unwrap(); + v8::Script::compile(scope, source, None) + .unwrap() + .run(scope) + .unwrap() +} + +#[test] +fn constructor_defers_prototype_get_and_preserves_native_callback_information() { + ensure_v8(); + let mut isolate = v8::Isolate::new(Default::default()); + let scope = pin!(v8::HandleScope::new(&mut isolate)); + let scope = &mut scope.init(); + let context = v8::Context::new(scope, Default::default()); + let scope = &mut v8::ContextScope::new(scope, context); + install(scope); + assert!( + eval( + scope, + r#"(() => { + const log = []; + const prototype = Object.prototype; + const N = new Proxy(function() {}, {get(target, key, receiver) { + if (key === 'prototype') { log.push('prototype'); return prototype; } + return Reflect.get(target, key, receiver); + }}); + const argument = {toString() { log.push('conversion'); return 'value'; }}; + const value = Reflect.construct(TestThing, [argument, 1, 2], N); + if (log.join(',') !== 'conversion,prototype' + || Object.getPrototypeOf(value) !== prototype + || value.converted !== 'value' || value.originalNewTarget !== N + || value.callbackData !== 'native callback data' || value.argumentCount !== 3 + || TestThing.length !== 2 || TestThing.name !== 'TestThing' + || TestThing.prototype.constructor !== TestThing) return false; + log.length = 0; + const sentinel = {}; + try { Reflect.construct(TestThing, [{toString() { throw sentinel; }}], N); } + catch (error) { if (error !== sentinel) return false; } + if (log.length !== 0) return false; + const badN = new Proxy(function() {}, {get(target, key) { + if (key === 'prototype') throw sentinel; + return Reflect.get(target, key); + }}); + try { Reflect.construct(TestThing, [argument], badN); return false; } + catch (error) { if (error !== sentinel) return false; } + const nativeConstruct = Reflect.construct; + Reflect.construct = () => { throw Error('public Reflect'); }; + Object.prototype.get = () => { throw Error('inherited trap'); }; + Object.prototype.construct = () => { throw Error('inherited construct'); }; + const C = TestThing; + TestThing = {}; + const ordinary = new C('normal'); + return Object.getPrototypeOf(ordinary) === C.prototype + && ordinary.originalNewTarget === C + && nativeConstruct(C, ['again']).converted === 'again'; + })()"#, + ) + .is_true() + ); +} + +#[test] +fn constructor_uses_new_target_realm_and_keeps_reentrant_invocations_independent() { + ensure_v8(); + let mut isolate = v8::Isolate::new(Default::default()); + let scope = pin!(v8::HandleScope::new(&mut isolate)); + let scope = &mut scope.init(); + let first = v8::Context::new(scope, Default::default()); + let second = v8::Context::new(scope, Default::default()); + let token = v8str(scope, "same-origin constructor test"); + for context in [first, second] { + context.set_security_token(token.into()); + install(&mut v8::ContextScope::new(scope, context)); + } + let foreign = second.global(scope); + let scope = &mut v8::ContextScope::new(scope, first); + first + .global(scope) + .set(scope, v8str(scope, "other").into(), foreign.into()) + .unwrap(); + assert!( + eval( + scope, + r#"(() => { + const foreignPrototype = other.TestThing.prototype; + const foreignTarget = other.Function(); + foreignTarget.prototype = 7; + const bound = foreignTarget.bind(null); + const proxiedBound = new Proxy(bound, {}); + const boundProxy = new Proxy(foreignTarget, {}).bind(null); + for (const N of [foreignTarget, bound, proxiedBound, boundProxy]) { + const value = Reflect.construct(TestThing, ['x'], N); + if (Object.getPrototypeOf(value) !== foreignPrototype + || value.originalNewTarget !== N) return false; + } + const C = TestThing; + other.TestThing = {prototype: {poisoned: true}}; + if (Object.getPrototypeOf(Reflect.construct(C, ['x'], foreignTarget)) + !== foreignPrototype) return false; + let reads = 0, nested; + const prototype = Object.create(null); + const N = new Proxy(function() {}, {get(target, key, receiver) { + if (key === 'prototype') { + if (++reads === 1) nested = Reflect.construct(C, ['nested'], N); + return prototype; + } + return Reflect.get(target, key, receiver); + }}); + const value = Reflect.construct(C, ['outer'], N); + if (reads !== 2 || Object.getPrototypeOf(value) !== prototype + || Object.getPrototypeOf(nested) !== prototype + || value.converted !== 'outer' || nested.converted !== 'nested') return false; + for (const primitive of [true, false]) { + let revoke; + const pair = Proxy.revocable(function() {}, {get(target, key, receiver) { + if (key === 'prototype') { revoke(); return primitive ? 1 : prototype; } + return Reflect.get(target, key, receiver); + }}); + revoke = pair.revoke; + try { + const instance = Reflect.construct(C, ['x'], pair.proxy); + if (primitive || Object.getPrototypeOf(instance) !== prototype) return false; + } catch (error) { if (!primitive || !(error instanceof TypeError)) return false; } + } + return true; + })()"#, + ) + .is_true() + ); +} diff --git a/vendor/v8-152.2.0/src/function.rs b/vendor/v8-152.2.0/src/function.rs index bb78a9a436..acc815fd60 100644 --- a/vendor/v8-152.2.0/src/function.rs +++ b/vendor/v8-152.2.0/src/function.rs @@ -385,6 +385,7 @@ pub struct FunctionCallbackArguments<'s> { info: &'s FunctionCallbackInfo, data: Option>, length: Option, + new_target: Option>, } impl<'s> FunctionCallbackArguments<'s> { @@ -394,6 +395,7 @@ impl<'s> FunctionCallbackArguments<'s> { info, data: None, length: None, + new_target: None, } } @@ -408,6 +410,7 @@ impl<'s> FunctionCallbackArguments<'s> { info, data: Some(parts.data), length: Some(parts.length), + new_target: None, } } @@ -425,7 +428,16 @@ impl<'s> FunctionCallbackArguments<'s> { /// For construct calls, this returns the "new.target" value. #[inline(always)] pub fn new_target(&self) -> Local<'s, Value> { - self.info.new_target() + self.new_target.unwrap_or_else(|| self.info.new_target()) + } + + /// Restores the caller's NewTarget when a native binding deferred receiver + /// allocation through an internal constructor. Other callback information, + /// including whether this is a construct call, is preserved. + #[inline(always)] + pub fn with_new_target(mut self, new_target: Local<'s, Value>) -> Self { + self.new_target = Some(new_target); + self } /// Returns true if this is a construct call, i.e., if the function was