fix(webidl): defer constructor prototype selection until after conversion

This commit is contained in:
ldm0
2026-10-22 22:22:27 +08:00
parent c56c1e27b4
commit cf7650f744
22 changed files with 741 additions and 145 deletions
@@ -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!(
@@ -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::<ConstructorArgs>(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());
}
}
@@ -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);
}
@@ -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(
@@ -278,6 +278,33 @@ fn finish_materialized_interface<'s>(
set_interface_prototype_constructor(scope, constructor_prototype, proxy.into())?;
v8::Local::<v8::Object>::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
@@ -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::<VideoColorSpaceConstructorArgs>(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());
}
@@ -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);
@@ -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());
}
@@ -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::<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());
}
@@ -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());
}
+6 -3
View File
@@ -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;
@@ -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;
})()
@@ -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 = '<iframe id=child></iframe>'")
.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"
);
}
@@ -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;
+1 -52
View File
@@ -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<v8::Local<'s, v8::Context>> {
// 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::<v8::Proxy>::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::<v8::Function>::try_from(callable) {
let target = function.get_bound_function(scope);
if !target.is_undefined() {
callable = target;
continue;
}
}
return v8::Local::<v8::Object>::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,
+30
View File
@@ -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<v8::Local<'s, v8::Context>> {
loop {
if let Ok(proxy) = v8::Local::<v8::Proxy>::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::<v8::Function>::try_from(callable) {
let target = function.get_bound_function(scope);
if !target.is_undefined() {
callable = target;
continue;
}
}
return v8::Local::<v8::Object>::try_from(callable)
.ok()?
.get_creation_context(scope);
}
}
+2
View File
@@ -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::*;
+7
View File
@@ -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;
+188
View File
@@ -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::<v8::Object>::try_from(reflect).ok()?;
let construct = reflect.get(scope, v8str(scope, "construct").into())?;
let construct = v8::Local::<v8::Function>::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<v8::Local<'s, v8::Proxy>> {
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::<v8::Function>::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::<v8::Object>::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::<v8::Object>::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)
}
+5
View File
@@ -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;
+231
View File
@@ -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::<v8::Object>::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()
);
}
+13 -1
View File
@@ -385,6 +385,7 @@ pub struct FunctionCallbackArguments<'s> {
info: &'s FunctionCallbackInfo,
data: Option<Local<'s, Value>>,
length: Option<int>,
new_target: Option<Local<'s, Value>>,
}
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