mirror of
https://github.com/lexmount/moli.git
synced 2026-10-30 16:07:24 +00:00
fix(navigator): freeze and cache languages per realm
This commit is contained in:
@@ -1961,17 +1961,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;
|
||||
@@ -1986,6 +1990,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"));
|
||||
@@ -2025,7 +2030,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;
|
||||
@@ -2038,6 +2051,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!(""));
|
||||
|
||||
|
||||
@@ -964,10 +964,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>(
|
||||
@@ -1050,7 +1056,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(),
|
||||
|
||||
@@ -2504,6 +2504,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),
|
||||
@@ -2535,7 +2536,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"}"#
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3334,3 +3335,20 @@ async fn worker_does_not_expose_window_audio_node_interface() {
|
||||
.expect("channel closed");
|
||||
assert_eq!(expect_post_json(message), r#"{"AudioNode":false}"#);
|
||||
}
|
||||
|
||||
#[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""#);
|
||||
}
|
||||
|
||||
@@ -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