fix(webidl): honor NewTarget realm prototype fallback

This commit is contained in:
ldm0
2026-09-22 21:43:34 +08:00
parent d5b7d896bb
commit c3fcc36c32
4 changed files with 126 additions and 46 deletions
@@ -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,
@@ -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<v8::Local<'s, v8::Object>> {
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))
}
+6 -5
View File
@@ -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;
@@ -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,
'<html></html>',
'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";