fix(url): preserve intrinsic identity through the Window alias

This commit is contained in:
ldm0
2026-09-22 12:23:10 +08:00
committed by Donough Liu
parent 8bb43a1ccb
commit 2295eaf608
5 changed files with 204 additions and 11 deletions
@@ -17,6 +17,7 @@ use crate::util::{
};
const LEGACY_WINDOW_INTERFACE_ALIASES: &[(&str, &str)] = &[
("webkitURL", "URL"),
("webkitAudioContext", "AudioContext"),
("WebKitCSSMatrix", "DOMMatrix"),
];
@@ -329,6 +329,31 @@ fn worker_realm_lazy_properties_follow_chromium_exposure_sets() {
assert!(first_url.strict_equals(second_url));
assert_eq!(registry.build_count(url_id), 1);
let source = v8str(
scope,
r#"(() => {
const Original = URL;
const parse = Original.parse;
globalThis.URL = null;
try {
const value = parse('https://example.test/?q=value');
return value instanceof Original &&
Object.getPrototypeOf(value) === Original.prototype &&
value.searchParams.get('q') === 'value' && globalThis.URL === null;
} finally {
globalThis.URL = Original;
}
})()"#,
);
let script = v8::Script::compile(scope, source, None).expect("worker URL factory script");
assert!(
crate::script_execution::execute_compiled_script(scope, script)
.expect("worker URL factory evaluation")
.is_true(),
"worker URL factories must not follow the public constructor binding"
);
assert_eq!(registry.build_count(url_id), 1);
[
"Worker",
"XMLHttpRequest",
@@ -337,6 +362,7 @@ fn worker_realm_lazy_properties_follow_chromium_exposure_sets() {
"FileSystemSyncAccessHandle",
"URL",
"WorkerLocation",
"webkitURL",
]
.iter()
.map(|name| {
@@ -349,14 +375,14 @@ fn worker_realm_lazy_properties_follow_chromium_exposure_sets() {
assert_eq!(
own_properties(super::RealmKind::DedicatedWorker),
vec![true, true, true, false, true, true, false]
vec![true, true, true, false, true, true, false, false]
);
assert_eq!(
own_properties(super::RealmKind::SharedWorker),
vec![true, true, true, false, false, true, false]
vec![true, true, true, false, false, true, false, false]
);
assert_eq!(
own_properties(super::RealmKind::ServiceWorker),
vec![false, false, false, false, false, true, false]
vec![false, false, false, false, false, true, false, false]
);
}
@@ -3,6 +3,7 @@ use super::helpers::{
url_href_slot,
};
use super::*;
use crate::context_bootstrap::{ensure_intrinsic_interface_constructor, shared::throw_error};
use crate::util::get_private_value;
use crate::web_api_interfaces;
use crate::webidl;
@@ -128,14 +129,14 @@ pub(super) fn url_parse_callback<'s>(
rv.set(v8::null(scope).into());
return;
};
let Some(constructor) = scope
.get_current_context()
.global(scope)
.get(scope, v8str(scope, "URL").into())
.and_then(|value| v8::Local::<v8::Function>::try_from(value).ok())
else {
rv.set(v8::null(scope).into());
return;
// The factory creates a URL in its own realm, regardless of author changes
// to the public URL binding (including during argument conversion).
let constructor = match ensure_intrinsic_interface_constructor(scope, "URL") {
Ok(constructor) => constructor,
Err(error) => {
throw_error(scope, &format!("Failed to create URL: {error}"));
return;
}
};
let Some(object) = constructor.new_instance(scope, &[url_value.into()]) else {
rv.set(v8::null(scope).into());
@@ -3,6 +3,7 @@ use super::*;
mod eval;
mod rebind;
mod seeds;
mod url_alias;
fn current_surface_state(scope: &mut v8::PinScope<'_, '_>) -> (bool, bool, bool) {
let diagnostics = crate::context_bootstrap::window_lazy_surface_diagnostics(scope);
@@ -0,0 +1,164 @@
use super::*;
#[test]
fn legacy_url_alias_is_lazy_and_shares_the_realm_constructor() {
for scheme in ["http", "https"] {
for first in [
"webkitURL",
"URL",
"Object.getOwnPropertyDescriptor(globalThis, 'webkitURL').value",
] {
let mut vm = new_storage_test_vm(&format!("{scheme}://url-alias.test/"));
assert_eq!(constructor_materialization_count(&mut vm, "URL"), 0);
assert_eq!(
vm.eval("'webkitURL' in globalThis && Object.hasOwn(globalThis, 'webkitURL')")
.unwrap(),
"true"
);
assert_eq!(constructor_materialization_count(&mut vm, "URL"), 0);
vm.eval(&format!("void ({first})")).unwrap();
assert_eq!(constructor_materialization_count(&mut vm, "URL"), 1);
assert_eq!(
vm.eval(r#"(() => {
const alias = webkitURL;
const value = new alias('next?q=value', 'https://example.test/base/');
const descriptor = Object.getOwnPropertyDescriptor(globalThis, 'webkitURL');
return alias === URL && alias.name === 'URL' && alias.length === 1 &&
value instanceof URL && Object.getPrototypeOf(value) === URL.prototype &&
value.href === 'https://example.test/base/next?q=value' &&
value.searchParams.get('q') === 'value' &&
alias.parse('/next', 'https://example.test/').href === 'https://example.test/next' &&
alias.canParse('https://example.test/') && !alias.canParse('relative') &&
alias.createObjectURL === URL.createObjectURL &&
alias.revokeObjectURL === URL.revokeObjectURL &&
descriptor.value === alias && descriptor.writable && descriptor.configurable &&
!descriptor.enumerable && !('get' in descriptor) && !('set' in descriptor);
})()"#).unwrap(),
"true",
"{scheme}: first access through {first}"
);
assert_eq!(constructor_materialization_count(&mut vm, "URL"), 1);
}
}
}
#[test]
fn legacy_url_alias_bindings_can_be_replaced_or_deleted_independently_before_first_read() {
for binding in ["URL", "webkitURL"] {
for mode in ["assignment", "deletion", "getter", "readonly"] {
let mut vm = new_storage_test_vm("https://url-alias-rebinding.test/");
vm.eval(&format!(
"globalThis.__binding = {binding:?}; globalThis.__mode = {mode:?};"
))
.unwrap();
assert_eq!(
vm.eval(r#"(() => {
const other = __binding === 'URL' ? 'webkitURL' : 'URL';
const sentinel = {};
let getterCalls = 0;
const getter = () => { getterCalls++; throw new Error('must not read replaced binding'); };
if (__mode === 'assignment') globalThis[__binding] = sentinel;
if (__mode === 'deletion' && !Reflect.deleteProperty(globalThis, __binding)) return false;
if (__mode === 'getter') Object.defineProperty(globalThis, __binding, {get: getter, configurable: true});
if (__mode === 'readonly') Object.defineProperty(globalThis, __binding,
{value: sentinel, writable: false, configurable: false});
const constructor = globalThis[other];
if (typeof constructor !== 'function' || constructor.name !== 'URL') return false;
const url = new constructor('https://example.test/?q=value');
const parsed = constructor.parse('/parsed', 'https://example.test/');
if (url.href !== 'https://example.test/?q=value' || url.searchParams.get('q') !== 'value' ||
Object.getPrototypeOf(url) !== constructor.prototype ||
parsed.href !== 'https://example.test/parsed' || getterCalls !== 0) return false;
const descriptor = Object.getOwnPropertyDescriptor(globalThis, __binding);
if (__mode === 'deletion') return descriptor === undefined && !(__binding in globalThis);
if (__mode === 'getter') return descriptor.get === getter;
return descriptor.value === sentinel &&
(__mode !== 'readonly' || (!descriptor.writable && !descriptor.configurable));
})()"#).unwrap(),
"true",
"{mode} of {binding} must not change or be undone by the other binding"
);
assert_eq!(constructor_materialization_count(&mut vm, "URL"), 1);
}
}
}
#[test]
fn url_parse_ignores_a_public_constructor_replaced_during_argument_conversion() {
let mut vm = new_storage_test_vm("https://url-parse-intrinsic.test/");
assert_eq!(
vm.eval(
r#"(() => {
const Original = URL;
const parse = Original.parse;
let conversions = 0;
let constructorCalls = 0;
const input = {toString() {
conversions++;
globalThis.URL = function Replacement() { constructorCalls++; return {forged: true}; };
return '/parsed';
}};
const result = parse.call({ignored: true}, input, 'https://example.test/base');
return conversions === 1 && constructorCalls === 0 &&
result.href === 'https://example.test/parsed' &&
Object.getPrototypeOf(result) === Original.prototype && result instanceof Original &&
globalThis.URL !== Original;
})()"#
)
.unwrap(),
"true"
);
}
#[test]
fn legacy_url_alias_materializes_in_its_own_window_realm() {
let mut vm = new_parsed_test_vm(
"https://url-alias-realms.test/",
"<!doctype html><html><body></body></html>",
);
assert_eq!(
vm.eval(
r#"(() => {
const frame = document.body.appendChild(document.createElement('iframe'));
const child = frame.contentWindow;
const alias = child.webkitURL;
const url = new alias('https://example.test/');
const valid = alias === child.URL && alias !== webkitURL && webkitURL === URL &&
Object.getPrototypeOf(alias) === child.Function.prototype &&
Object.getPrototypeOf(url) === child.URL.prototype &&
url instanceof child.URL && !(url instanceof URL);
child.URL = {};
const parsed = alias.parse.call(URL, 'https://example.test/parsed');
frame.remove();
return valid && Object.getPrototypeOf(parsed) === alias.prototype &&
parsed instanceof alias && !(parsed instanceof URL) &&
new alias('/retained', 'https://example.test/').href ===
'https://example.test/retained';
})()"#
)
.unwrap(),
"true"
);
}
#[test]
fn legacy_url_alias_takes_precedence_over_window_named_properties() {
let mut vm = new_parsed_test_vm(
"https://url-alias-named.test/",
"<!doctype html><html><body><div id=webkitURL></div></body></html>",
);
assert_eq!(constructor_materialization_count(&mut vm, "URL"), 0);
assert_eq!(
vm.eval(
r#"(() => {
const named = document.getElementById('webkitURL');
const alias = window.webkitURL;
if (typeof alias !== 'function' || alias !== URL || alias === named) return false;
delete window.webkitURL;
return window.webkitURL === named && typeof URL === 'function';
})()"#
)
.unwrap(),
"true"
);
}