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

Expose webkitURL through the existing lazy Window-only alias registry so it shares the realm URL constructor and retains independent writable/configurable global bindings.

Make URL.parse construct through the realm intrinsic registry instead of reading the mutable public URL property. This prevents replaced constructors, getters, deletion, and argument-conversion side effects from changing the factory result or the realm of its prototype.

Add five Window regressions for laziness, descriptors, independent rebinding/deletion, named properties, argument-conversion side effects, and retained cross-realm constructors. Extend Dedicated/Shared/Service Worker checks to verify the alias is absent and parsing ignores a replaced public URL binding.

Validation: cargo fmt --all; cargo clippy --workspace --all-targets --all-features -- -D warnings; cargo nextest run --no-fail-fast (17637 passed, 13 skipped). All 27 targeted regressions pass.

CLI and CDP agree on all 54 fixed comparison cases: 51 to 52 passing cases, 7858 to 7859 passing subtests, no new failures. URL idlharness Window is now fully passing; the original 50-case URL/Location selection is 49/50, with only the existing srcdoc scrolling failure. The additional Web Audio historical failures are unchanged.

CDP smoke: 42/43 groups pass. Puppeteer cannot start because node is absent, also reproduced with the before binary. Artifacts: target/wpt-url-alias-20260908-ikStj8. Release SHA256: 1e6918fd6fc754f6aa5bebcd4582e904ecf113ae97165723c9b1b350225fc0bc.

References: https://webidl.spec.whatwg.org/#LegacyWindowAlias and https://url.spec.whatwg.org/#dom-url-parse.
This commit is contained in:
ldm0
2026-09-08 10:20:09 +08:00
parent 61c83e1cc0
commit 8ad5424480
5 changed files with 205 additions and 11 deletions
@@ -17,6 +17,7 @@ use crate::util::{
};
const LEGACY_WINDOW_INTERFACE_ALIASES: &[(&str, &str)] = &[
("webkitURL", "URL"),
("SVGPoint", "DOMPoint"),
("SVGRect", "DOMRect"),
("SVGMatrix", "DOMMatrix"),
@@ -329,6 +329,32 @@ 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!(
script
.run(scope)
.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 +363,7 @@ fn worker_realm_lazy_properties_follow_chromium_exposure_sets() {
"FileSystemSyncAccessHandle",
"URL",
"WorkerLocation",
"webkitURL",
]
.iter()
.map(|name| {
@@ -349,15 +376,15 @@ 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]
);
}
@@ -2,6 +2,7 @@ use super::helpers::{
can_parse_url_input, require_url_receiver, resolve_url_constructor_input, url_href_slot,
};
use super::*;
use crate::context_bootstrap::{ensure_intrinsic_interface_constructor, shared::throw_error};
use crate::util::get_private_value;
use crate::webidl;
use moli_webapi_declare::WebApiObject;
@@ -126,14 +127,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"
);
}