mirror of
https://github.com/lexmount/moli.git
synced 2026-10-01 08:00:38 +00:00
fix(navigator): freeze language arrays in every realm
Freeze the native language snapshot before caching it, and retain frozen snapshots across identity overrides. Cover Window, Worker, shared worker, borrowed realm getters and live CDP profile changes.
This commit is contained in:
@@ -1936,17 +1936,21 @@ async fn set_user_agent_override_applies_complete_chromium_identity_profile() {
|
||||
"params": {
|
||||
"awaitPromise": true,
|
||||
"returnByValue": true,
|
||||
"expression": r#"(async () => JSON.stringify({
|
||||
"expression": r#"(async () => {
|
||||
globalThis.__identityLanguages = navigator.languages;
|
||||
return JSON.stringify({
|
||||
userAgent: navigator.userAgent,
|
||||
platform: navigator.platform,
|
||||
language: navigator.language,
|
||||
languages: navigator.languages,
|
||||
languagesFrozen: Object.isFrozen(navigator.languages),
|
||||
base: navigator.userAgentData.toJSON(),
|
||||
high: await navigator.userAgentData.getHighEntropyValues([
|
||||
'architecture', 'bitness', 'formFactors', 'fullVersionList',
|
||||
'platformVersion', 'uaFullVersion', 'wow64'
|
||||
])
|
||||
}))()"#
|
||||
});
|
||||
})()"#
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
@@ -1961,6 +1965,7 @@ async fn set_user_agent_override_applies_complete_chromium_identity_profile() {
|
||||
assert_eq!(identity["platform"], json!("Linux x86_64"));
|
||||
assert_eq!(identity["language"], json!("fr-CA"));
|
||||
assert_eq!(identity["languages"], json!(["fr-CA", "fr;q=0.9"]));
|
||||
assert_eq!(identity["languagesFrozen"], json!(true));
|
||||
assert_eq!(identity["base"]["platform"], json!("Linux"));
|
||||
assert_eq!(identity["base"]["brands"][0]["brand"], json!("Chromium"));
|
||||
assert_eq!(identity["high"]["architecture"], json!("x86"));
|
||||
@@ -2000,7 +2005,15 @@ async fn set_user_agent_override_applies_complete_chromium_identity_profile() {
|
||||
"sessionId": "SID-1",
|
||||
"params": {
|
||||
"returnByValue": true,
|
||||
"expression": "JSON.stringify({ platform: navigator.platform, languages: navigator.languages, uaData: navigator.userAgentData.toJSON() })"
|
||||
"expression": r#"JSON.stringify({
|
||||
platform: navigator.platform,
|
||||
languages: navigator.languages,
|
||||
languagesFrozen: Object.isFrozen(navigator.languages),
|
||||
oldLanguages: __identityLanguages,
|
||||
oldLanguagesFrozen: Object.isFrozen(__identityLanguages),
|
||||
languagesReplaced: __identityLanguages !== navigator.languages,
|
||||
uaData: navigator.userAgentData.toJSON()
|
||||
})"#
|
||||
}
|
||||
}))
|
||||
.await;
|
||||
@@ -2013,6 +2026,10 @@ async fn set_user_agent_override_applies_complete_chromium_identity_profile() {
|
||||
.expect("identity result should parse");
|
||||
assert_eq!(identity["platform"], json!("Win32"));
|
||||
assert_eq!(identity["languages"], json!(["en-US", "en"]));
|
||||
assert_eq!(identity["languagesFrozen"], json!(true));
|
||||
assert_eq!(identity["oldLanguages"], json!(["fr-CA", "fr;q=0.9"]));
|
||||
assert_eq!(identity["oldLanguagesFrozen"], json!(true));
|
||||
assert_eq!(identity["languagesReplaced"], json!(true));
|
||||
assert_eq!(identity["uaData"]["brands"], json!([]));
|
||||
assert_eq!(identity["uaData"]["platform"], json!(""));
|
||||
|
||||
|
||||
@@ -970,10 +970,16 @@ fn delete_object_property(
|
||||
fn build_languages_array<'s>(
|
||||
scope: &mut v8::PinScope<'s, '_>,
|
||||
languages: &[String],
|
||||
) -> v8::Local<'s, v8::Array> {
|
||||
serialize_v8_array(scope, languages)
|
||||
) -> Result<v8::Local<'s, v8::Array>> {
|
||||
let array = serialize_v8_array(scope, languages)
|
||||
.or_else(|| serialize_v8_array(scope, [""]))
|
||||
.unwrap_or_else(|| v8::Array::new(scope, 0))
|
||||
.unwrap_or_else(|| v8::Array::new(scope, 0));
|
||||
// NavigatorLanguage returns a FrozenArray in both Window and Worker realms.
|
||||
// Freeze the native snapshot before it can enter the lazy subobject cache.
|
||||
if array.set_integrity_level(scope, v8::IntegrityLevel::Frozen) != Some(true) {
|
||||
return Err(anyhow!("failed to freeze Navigator.languages"));
|
||||
}
|
||||
Ok(array)
|
||||
}
|
||||
|
||||
fn build_navigator_connection<'s>(
|
||||
@@ -1056,7 +1062,7 @@ pub(super) fn build_lazy_navigator_subobject_in_current_realm<'s>(
|
||||
let value: v8::Local<'s, v8::Value> = match subobject {
|
||||
NavigatorSubobject::Languages => {
|
||||
let languages = effective_navigator_languages(scope, backing);
|
||||
build_languages_array(scope, &languages).into()
|
||||
build_languages_array(scope, &languages)?.into()
|
||||
}
|
||||
NavigatorSubobject::MimeTypes | NavigatorSubobject::Plugins => {
|
||||
build_navigator_plugin_collection_subobject(scope, backing, subobject)?
|
||||
|
||||
@@ -691,7 +691,7 @@ fn navigator_scalar_access_does_not_materialize_unused_same_object_children() {
|
||||
"#,
|
||||
)
|
||||
.expect("Navigator lazy subobjects should materialize"),
|
||||
r#"{"same":true,"languagesFrozen":false,"permissionsRealm":true,"mediaDevicesRealm":true,"storageRealm":true,"clipboardRealm":true,"geolocationRealm":true,"mediaCapabilitiesRealm":true}"#
|
||||
r#"{"same":true,"languagesFrozen":true,"permissionsRealm":true,"mediaDevicesRealm":true,"storageRealm":true,"clipboardRealm":true,"geolocationRealm":true,"mediaCapabilitiesRealm":true}"#
|
||||
);
|
||||
assert_eq!(
|
||||
default_navigator_subobjects(&mut vm),
|
||||
@@ -732,6 +732,18 @@ fn navigator_scalar_access_does_not_materialize_unused_same_object_children() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn navigator_languages_are_frozen() {
|
||||
let mut vm = new_storage_test_vm("https://navigator-languages.test/");
|
||||
assert_eq!(
|
||||
vm.eval(include_str!(
|
||||
"../../../tests/fixtures/navigator-languages.js"
|
||||
))
|
||||
.expect("Navigator.languages must satisfy the FrozenArray contract"),
|
||||
"ok"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detached_child_screen_keeps_identity_and_zeros_geometry() {
|
||||
let mut vm = new_storage_test_vm("https://detached-child-screen.test/");
|
||||
@@ -807,6 +819,7 @@ fn borrowed_navigator_getters_materialize_subobjects_in_the_receiver_realm() {
|
||||
const permissions = permissionsGetter.call(child.navigator);
|
||||
return JSON.stringify({
|
||||
languagesSame: languages === child.navigator.languages,
|
||||
languagesFrozen: Object.isFrozen(languages),
|
||||
languagesRealm:
|
||||
Object.getPrototypeOf(languages) === child.Array.prototype,
|
||||
permissionsSame: permissions === child.navigator.permissions,
|
||||
@@ -817,7 +830,7 @@ fn borrowed_navigator_getters_materialize_subobjects_in_the_receiver_realm() {
|
||||
"#,
|
||||
)
|
||||
.expect("borrowed Navigator getters should use the receiver realm"),
|
||||
r#"{"languagesSame":true,"languagesRealm":true,"permissionsSame":true,"permissionsRealm":true}"#
|
||||
r#"{"languagesSame":true,"languagesFrozen":true,"languagesRealm":true,"permissionsSame":true,"permissionsRealm":true}"#
|
||||
);
|
||||
assert!(
|
||||
default_navigator_subobjects(&mut vm).is_empty(),
|
||||
|
||||
@@ -2082,6 +2082,23 @@ async fn worker_offscreen_canvas_exposes_webgl_identity_consistently() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn worker_navigator_languages_are_frozen() {
|
||||
ensure_v8();
|
||||
let mut handle = spawn_worker(
|
||||
format!(
|
||||
"try {{ postMessage({}); }} catch (error) {{ postMessage(String(error)); }} close();",
|
||||
include_str!("../../../../tests/fixtures/navigator-languages.js")
|
||||
),
|
||||
"https://navigator-languages.test/worker.js".into(),
|
||||
);
|
||||
let message = timeout(TIMEOUT, handle.recv())
|
||||
.await
|
||||
.expect("worker language contract must settle")
|
||||
.expect("worker must return a language contract result");
|
||||
assert_eq!(expect_post_json(message), r#""ok""#);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn worker_media_capabilities_matches_the_software_chromium_profile() {
|
||||
ensure_v8();
|
||||
@@ -2240,6 +2257,7 @@ async fn shared_worker_navigator_exposes_canonical_user_agent_data() {
|
||||
sameObject: uaData === navigator.userAgentData,
|
||||
userAgent: navigator.userAgent,
|
||||
languages: Array.from(navigator.languages),
|
||||
languagesFrozen: Object.isFrozen(navigator.languages),
|
||||
brands: uaData.brands,
|
||||
emptyKeys: Object.keys(empty),
|
||||
selectedKeys: Object.keys(selected),
|
||||
@@ -2271,7 +2289,7 @@ async fn shared_worker_navigator_exposes_canonical_user_agent_data() {
|
||||
"JSON.stringify(__wire)",
|
||||
)
|
||||
.await,
|
||||
r#"{"constructorType":"function","dataType":"object","instance":true,"sameObject":false,"userAgent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.1.2.3 Safari/537.36","languages":["de-DE","de"],"brands":[{"brand":"Chromium","version":"146"},{"brand":"Not-A.Brand","version":"24"},{"brand":"Google Chrome","version":"146"}],"emptyKeys":["brands","mobile","platform"],"selectedKeys":["architecture","brands","mobile","platform"],"architecture":"x86"}"#
|
||||
r#"{"constructorType":"function","dataType":"object","instance":true,"sameObject":false,"userAgent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.1.2.3 Safari/537.36","languages":["de-DE","de"],"languagesFrozen":true,"brands":[{"brand":"Chromium","version":"146"},{"brand":"Not-A.Brand","version":"24"},{"brand":"Google Chrome","version":"146"}],"emptyKeys":["brands","mobile","platform"],"selectedKeys":["architecture","brands","mobile","platform"],"architecture":"x86"}"#
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
// NavigatorLanguage.languages is a FrozenArray<DOMString> in its own realm.
|
||||
(() => {
|
||||
const languages = navigator.languages;
|
||||
const check = (value, message) => { if (!value) throw new Error(message); };
|
||||
check(Array.isArray(languages), 'languages must be an Array');
|
||||
check(Object.getPrototypeOf(languages) === Array.prototype, 'languages array realm');
|
||||
check(languages.every(value => typeof value === 'string'), 'language values');
|
||||
check(Object.isFrozen(languages), 'languages must be frozen');
|
||||
check(!Object.isExtensible(languages), 'languages must not be extensible');
|
||||
const length = Object.getOwnPropertyDescriptor(languages, 'length');
|
||||
check(!length.writable && !length.configurable && !length.enumerable, 'length descriptor');
|
||||
for (let index = 0; index < languages.length; index++) {
|
||||
const item = Object.getOwnPropertyDescriptor(languages, index);
|
||||
check(!item.writable && !item.configurable && item.enumerable, 'item descriptor');
|
||||
}
|
||||
const before = JSON.stringify(languages);
|
||||
check(!Reflect.set(languages, '0', 'changed'), 'Reflect.set must fail');
|
||||
check(!Reflect.defineProperty(languages, 'extra', {value: true}), 'extra property must fail');
|
||||
if (languages.length) check(!Reflect.deleteProperty(languages, '0'), 'delete must fail');
|
||||
for (const mutate of [
|
||||
() => { 'use strict'; languages[0] = 'changed'; },
|
||||
() => languages.push('changed'),
|
||||
() => { 'use strict'; languages.length = 0; }
|
||||
]) {
|
||||
let threw = false;
|
||||
try { mutate(); } catch (error) { threw = error instanceof TypeError; }
|
||||
check(threw, 'strict mutation must throw TypeError');
|
||||
}
|
||||
check(JSON.stringify(languages) === before, 'cached language values must not mutate');
|
||||
check(JSON.stringify(navigator.languages) === before, 'later reads must remain unchanged');
|
||||
return 'ok';
|
||||
})()
|
||||
Reference in New Issue
Block a user