From c3fcc36c32ea4802e1502a0438bce15e4def4bca Mon Sep 17 00:00:00 2001 From: ldm0 Date: Mon, 31 Aug 2026 08:45:24 +0800 Subject: [PATCH] fix(webidl): honor NewTarget realm prototype fallback --- .../src/custom_elements/html_constructor.rs | 11 +- .../html_constructor_prototype.rs | 49 +++------ moli-renderer-v8/src/dom_parser.rs | 11 +- .../src/script_vm/tests/dom_xhr/dom.rs | 101 ++++++++++++++++++ 4 files changed, 126 insertions(+), 46 deletions(-) diff --git a/moli-renderer-v8/src/custom_elements/html_constructor.rs b/moli-renderer-v8/src/custom_elements/html_constructor.rs index 3cf6d0621f..b269ddb064 100644 --- a/moli-renderer-v8/src/custom_elements/html_constructor.rs +++ b/moli-renderer-v8/src/custom_elements/html_constructor.rs @@ -4,12 +4,15 @@ use super::element_state::{ set_dom_custom_element_state, }; use super::html_constructor_prototype::{ - receiver_prototype_chain_contains_constructor_prototype, - receiver_uses_new_target_realm_object_fallback, set_wrapper_html_constructor_prototype, + receiver_prototype_chain_contains_constructor_prototype, set_wrapper_html_constructor_prototype, }; use crate::dom::{native::CustomElementState, native::html_element_interface_name}; -use super::super::{document_runtime::DomHandle, native_bridge::JsContextHost, util::v8_string}; +use super::super::{ + document_runtime::DomHandle, + native_bridge::JsContextHost, + util::{receiver_uses_new_target_realm_object_fallback, v8_string}, +}; pub(crate) fn create_element_from_registered_constructor<'s>( scope: &mut v8::PinScope<'s, '_>, @@ -30,7 +33,7 @@ pub(crate) fn create_element_from_registered_constructor<'s>( return None; } let receiver_uses_object_fallback = - receiver_uses_new_target_realm_object_fallback(scope, receiver, constructor); + receiver_uses_new_target_realm_object_fallback(scope, receiver, constructor.into()); let receiver_inherits_active_interface = receiver_prototype_chain_contains_constructor_prototype( scope, diff --git a/moli-renderer-v8/src/custom_elements/html_constructor_prototype.rs b/moli-renderer-v8/src/custom_elements/html_constructor_prototype.rs index bfea17c1b0..bd7365a832 100644 --- a/moli-renderer-v8/src/custom_elements/html_constructor_prototype.rs +++ b/moli-renderer-v8/src/custom_elements/html_constructor_prototype.rs @@ -2,8 +2,8 @@ use super::super::{ dom_parser::DOM_PARSER_FOREIGN_NODE_SLOT, native_bridge::JsContextHost, util::{ - callable_relevant_context, constructor_prototype, get_private_object, - global_constructor_prototype, + constructor_prototype, get_private_object, global_constructor_prototype, + new_target_realm_constructor_prototype, receiver_uses_new_target_realm_object_fallback, }, }; use super::CustomElementRegistryKey; @@ -51,22 +51,6 @@ fn registry_constructor_prototype<'s>( } } -pub(super) fn receiver_uses_new_target_realm_object_fallback<'s>( - scope: &mut v8::PinScope<'s, '_>, - receiver: v8::Local<'s, v8::Object>, - new_target: v8::Local<'s, v8::Function>, -) -> bool { - let Some(receiver_prototype) = receiver.get_prototype(scope) else { - return false; - }; - let Some(object_prototype) = - new_target_realm_constructor_prototype(scope, new_target, "Object") - else { - return false; - }; - receiver_prototype.strict_equals(object_prototype.into()) -} - pub(super) fn set_wrapper_html_constructor_prototype<'s>( scope: &mut v8::PinScope<'s, '_>, wrapper: v8::Local<'s, v8::Object>, @@ -82,29 +66,20 @@ pub(super) fn set_wrapper_html_constructor_prototype<'s>( // getters twice. A non-object value is represented by the Object // prototype from NewTarget's relevant Realm; replace only that fallback // with the active HTML interface prototype from the same Realm. - let prototype = if receiver_uses_new_target_realm_object_fallback(scope, receiver, new_target) { - new_target_realm_constructor_prototype(scope, new_target, active_constructor_name) + let prototype = + if receiver_uses_new_target_realm_object_fallback(scope, receiver, new_target.into()) { + new_target_realm_constructor_prototype( + scope, + new_target.into(), + active_constructor_name, + ) .map(Into::into) .unwrap_or(prototype) - } else { - prototype - }; + } else { + prototype + }; let _ = wrapper.set_prototype(scope, prototype); if let Some(foreign) = get_private_object(scope, wrapper, DOM_PARSER_FOREIGN_NODE_SLOT) { let _ = foreign.set_prototype(scope, prototype); } } - -fn new_target_realm_constructor_prototype<'s>( - scope: &mut v8::PinScope<'s, '_>, - new_target: v8::Local<'s, v8::Function>, - constructor_name: &str, -) -> Option> { - let context = callable_relevant_context(scope, new_target.into())?; - let prototype = { - let context_scope = &mut v8::ContextScope::new(scope, context); - let prototype = global_constructor_prototype(context_scope, constructor_name)?; - v8::Global::new(context_scope, prototype) - }; - Some(v8::Local::new(scope, &prototype)) -} diff --git a/moli-renderer-v8/src/dom_parser.rs b/moli-renderer-v8/src/dom_parser.rs index 786789a1e9..88c712d8fb 100644 --- a/moli-renderer-v8/src/dom_parser.rs +++ b/moli-renderer-v8/src/dom_parser.rs @@ -20,8 +20,8 @@ use super::{ }, }, util::{ - context_host_ptr_from_global_bridge, get_private_object, get_private_value, - set_private_value, throw_type_error, + apply_webidl_constructor_prototype_fallback, context_host_ptr_from_global_bridge, + get_private_object, get_private_value, set_private_value, throw_type_error, }, }; @@ -118,9 +118,9 @@ struct DomParserPrototypeMethodsDeclaration { parse_from_string: (), } -pub(super) fn dom_parser_constructor_callback( - scope: &mut v8::PinScope<'_, '_>, - args: v8::FunctionCallbackArguments<'_>, +pub(super) fn dom_parser_constructor_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { if !args.is_construct_call() { @@ -128,6 +128,7 @@ pub(super) fn dom_parser_constructor_callback( return; } let parser = args.this(); + apply_webidl_constructor_prototype_fallback(scope, parser, args.new_target(), "DOMParser"); 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/dom_xhr/dom.rs b/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom.rs index 180022f115..7e1cdd08f4 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_xhr/dom.rs @@ -9490,6 +9490,107 @@ fn dom_parser_uses_its_constructor_realm_associated_document() { ); } +#[test] +fn dom_parser_non_object_new_target_prototype_uses_new_target_realm_default() { + let mut vm = new_storage_test_vm("https://dom-parser-new-target.test/top.html"); + + let result = vm + .eval( + r#" +(() => { + const frame = document.createElement('iframe'); + (document.body || document.documentElement || document).appendChild(frame); + const child = frame.contentWindow; + child.history.replaceState(null, '', '/child.html'); + + const TopBad = new Function(); + TopBad.prototype = 7; + const ChildBad = new child.Function(); + ChildBad.prototype = 7; + + const BoundChild = Function.prototype.bind.call(new child.Function()); + BoundChild.prototype = 7; + const BoundTop = child.Function.prototype.bind.call(new Function()); + BoundTop.prototype = 7; + + const ProxyChild = new Proxy(new child.Function(), {}); + ProxyChild.prototype = 7; + const ProxyTop = new child.Proxy(new Function(), {}); + ProxyTop.prototype = 7; + + let getterCount = 0; + const GetterProxyChild = new Proxy(new child.Function(), { + get(target, property, receiver) { + if (property === 'prototype') { + getterCount += 1; + return 7; + } + return Reflect.get(target, property, receiver); + } + }); + + const parseUrl = parser => + DOMParser.prototype.parseFromString.call( + parser, + '', + 'text/html' + ).URL; + const check = (parser, expectedPrototype, expectedUrl) => [ + Object.getPrototypeOf(parser) === expectedPrototype, + parseUrl(parser) === expectedUrl + ]; + const topUrl = location.href; + const childUrl = child.location.href; + + return JSON.stringify({ + directTop: check( + Reflect.construct(child.DOMParser, [], TopBad), + DOMParser.prototype, + childUrl + ), + directChild: check( + Reflect.construct(DOMParser, [], ChildBad), + child.DOMParser.prototype, + topUrl + ), + boundChild: check( + Reflect.construct(DOMParser, [], BoundChild), + child.DOMParser.prototype, + topUrl + ), + boundTop: check( + Reflect.construct(child.DOMParser, [], BoundTop), + DOMParser.prototype, + childUrl + ), + proxyChild: check( + Reflect.construct(DOMParser, [], ProxyChild), + child.DOMParser.prototype, + topUrl + ), + proxyTop: check( + Reflect.construct(child.DOMParser, [], ProxyTop), + DOMParser.prototype, + childUrl + ), + getterProxyChild: check( + Reflect.construct(DOMParser, [], GetterProxyChild), + child.DOMParser.prototype, + topUrl + ), + getterCount + }); +})() +"#, + ) + .expect("DOMParser NewTarget realm prototype fallback probe should evaluate"); + + assert_eq!( + result, + r#"{"directTop":[true,true],"directChild":[true,true],"boundChild":[true,true],"boundTop":[true,true],"proxyChild":[true,true],"proxyTop":[true,true],"getterProxyChild":[true,true],"getterCount":1}"# + ); +} + #[tokio::test(flavor = "current_thread")] async fn dom_parser_retained_across_child_navigation_uses_original_document() { const HOST: &str = "dom-parser-retained.test";