From fab598520eb8fe0e1918dca32aac19ea0f652eb6 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Wed, 9 Sep 2026 01:12:29 +0800 Subject: [PATCH] fix(trusted-types): align WebIDL interfaces and global accessors --- .../src/context_bootstrap/trusted_types.rs | 88 ++++---- .../trusted_types/realm_state.rs | 190 ++++++++++-------- .../misc/extracted/trusted_types.rs | 20 +- .../tests/browser_api/trusted_types.rs | 81 ++++++++ .../script_vm/tests/lazy_window_surfaces.rs | 57 ++++++ .../src/worker/thread/tests/postmessage.rs | 17 ++ .../tests/fixtures/trusted-types-webidl.js | 108 ++++++++++ 7 files changed, 425 insertions(+), 136 deletions(-) create mode 100644 moli-renderer-v8/tests/fixtures/trusted-types-webidl.js diff --git a/moli-renderer-v8/src/context_bootstrap/trusted_types.rs b/moli-renderer-v8/src/context_bootstrap/trusted_types.rs index d246d0d732..62e651c55f 100644 --- a/moli-renderer-v8/src/context_bootstrap/trusted_types.rs +++ b/moli-renderer-v8/src/context_bootstrap/trusted_types.rs @@ -23,9 +23,6 @@ const TRUSTED_TYPE_SCRIPT_URL_PROTOTYPE_SLOT: &str = "__moliTrustedScriptURLProt const TRUSTED_TYPE_HTML_CONSTRUCTOR_SLOT: &str = "__moliTrustedHTMLConstructor"; const TRUSTED_TYPE_SCRIPT_CONSTRUCTOR_SLOT: &str = "__moliTrustedScriptConstructor"; const TRUSTED_TYPE_SCRIPT_URL_CONSTRUCTOR_SLOT: &str = "__moliTrustedScriptURLConstructor"; -const TRUSTED_TYPE_POLICY_CONSTRUCTOR_SLOT: &str = "__moliTrustedTypePolicyConstructor"; -const TRUSTED_TYPE_POLICY_FACTORY_CONSTRUCTOR_SLOT: &str = - "__moliTrustedTypePolicyFactoryConstructor"; const TRUSTED_TYPES_DEFAULT_POLICY_SLOT: &str = "__moliTrustedTypesDefaultPolicy"; const TRUSTED_TYPES_CREATE_HTML_SLOT: &str = "__moliTrustedTypesCreateHTML"; const TRUSTED_TYPES_CREATE_SCRIPT_SLOT: &str = "__moliTrustedTypesCreateScript"; @@ -51,27 +48,30 @@ struct TrustedTypePolicyFactoryInterfaceDeclaration { #[webapi( method, callback = trusted_types_create_policy_callback, - length = 2, + length = 1, enumerable )] create_policy: (), #[webapi( method = "isHTML", - callback = trusted_types_is_html_callback, + callback = trusted_types_is_type_callback, + data = crate::util::callback_data_index_value(scope, 0), length = 1, enumerable )] is_html: (), #[webapi( method, - callback = trusted_types_is_script_callback, + callback = trusted_types_is_type_callback, + data = crate::util::callback_data_index_value(scope, 1), length = 1, enumerable )] is_script: (), #[webapi( method = "isScriptURL", - callback = trusted_types_is_script_url_callback, + callback = trusted_types_is_type_callback, + data = crate::util::callback_data_index_value(scope, 2), length = 1, enumerable )] @@ -123,6 +123,13 @@ struct TrustedTypesFactoryObjectDeclaration<'scope> { created_policy_names: v8::Local<'scope, v8::Array>, } +#[derive(webidl::WebIdlArgs)] +#[webidl(prefix = "TrustedTypePolicyFactory")] +struct TrustedTypesPredicateArgs<'scope> { + #[webidl(required)] + value: v8::Local<'scope, v8::Value>, +} + #[derive(webidl::WebIdlArgs)] #[webidl(prefix = "TrustedTypePolicyFactory.getAttributeType")] struct TrustedTypesGetAttributeTypeArgs { @@ -154,15 +161,15 @@ struct TrustedTypeObjectDeclaration<'scope> { value: v8::Local<'scope, v8::String>, } -#[derive(Default, WebApiObject)] -#[webapi(plain)] -struct TrustedTypePrototypeDeclaration { - #[webapi(method = "toString", callback = trusted_type_to_string_callback, length = 0)] +#[derive(WebApiObject)] +#[webapi(fragment, enumerable)] +struct TrustedTypePrototypeDeclaration<'scope> { + kind: v8::Local<'scope, v8::String>, + + #[webapi(method = "toString", callback = trusted_type_to_string_callback, data = self.kind, length = 0)] to_string: (), - #[webapi(method = "toJSON", callback = trusted_type_to_string_callback, length = 0)] + #[webapi(method = "toJSON", callback = trusted_type_to_string_callback, data = self.kind, length = 0)] to_json: (), - #[webapi(method, callback = trusted_type_to_string_callback, length = 0)] - value_of: (), } #[derive(WebApiFunctionTemplate)] @@ -745,16 +752,17 @@ fn build_trusted_type_constructor<'s>( kind: TrustedTypeKind, ) -> Result> { let name = kind.constructor_name(); - let constructor = v8::Function::builder(trusted_type_illegal_constructor_callback) - .length(0) - .build(scope) + let template = v8::FunctionTemplate::new(scope, trusted_type_illegal_constructor_callback); + template.read_only_prototype(); + let constructor = template + .get_function(scope) .ok_or_else(|| anyhow!("failed to create {name} constructor"))?; constructor.set_name(v8str(scope, name)); let prototype = constructor .get(scope, v8str(scope, "prototype").into()) .and_then(|value| v8::Local::::try_from(value).ok()) .ok_or_else(|| anyhow!("{name}.prototype missing"))?; - TrustedTypePrototypeDeclaration::default() + TrustedTypePrototypeDeclaration::new(v8str(scope, kind.constructor_name())) .initialize(scope, prototype) .map_err(|error| anyhow!("failed to initialize {name}.prototype declaration: {error}"))?; prototype @@ -762,7 +770,7 @@ fn build_trusted_type_constructor<'s>( scope, v8::Symbol::get_to_string_tag(scope).into(), v8str(scope, name).into(), - v8::PropertyAttribute::DONT_ENUM, + v8::PropertyAttribute::DONT_ENUM | v8::PropertyAttribute::READ_ONLY, ) .unwrap_or(false) .then_some(()) @@ -787,7 +795,7 @@ fn install_trusted_script_code_like_constructor<'s>( .get(scope, v8str(scope, "prototype").into()) .and_then(|value| v8::Local::::try_from(value).ok()) .ok_or_else(|| anyhow!("TrustedScript code-like constructor prototype missing"))?; - TrustedTypePrototypeDeclaration::default() + TrustedTypePrototypeDeclaration::new(v8str(scope, TrustedTypeKind::Script.constructor_name())) .initialize(scope, prototype) .map_err(|error| { anyhow!("failed to initialize TrustedScript code-like carrier: {error}") @@ -865,6 +873,13 @@ fn trusted_type_to_string_callback<'s>( mut rv: v8::ReturnValue<'_, v8::Value>, ) { let this = args.this(); + if !trusted_type_kind(scope, this.into()).is_some_and(|kind| { + v8str(scope, kind.constructor_name()).strict_equals(args.data()) + }) + { + throw_type_error(scope, "Illegal invocation"); + return; + } let Some(value) = get_private_value(scope, this, TRUSTED_TYPE_VALUE_SLOT) else { throw_type_error(scope, "Illegal invocation"); return; @@ -1108,7 +1123,7 @@ fn trusted_type_policy_create_callback<'s>( rv.set(object.into()); } -fn trusted_types_is_html_callback<'s>( +fn trusted_types_is_type_callback<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, @@ -1116,29 +1131,18 @@ fn trusted_types_is_html_callback<'s>( if !trusted_types_factory_receiver_is_valid(scope, args.this()) { return; } - rv.set_bool(trusted_type_string(scope, args.get(0), TrustedTypeKind::Html).is_some()); -} - -fn trusted_types_is_script_callback<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: v8::FunctionCallbackArguments<'s>, - mut rv: v8::ReturnValue<'_, v8::Value>, -) { - if !trusted_types_factory_receiver_is_valid(scope, args.this()) { + let Some(parsed) = webidl::parse_args::>(scope, &args) else { return; - } - rv.set_bool(trusted_type_string(scope, args.get(0), TrustedTypeKind::Script).is_some()); -} - -fn trusted_types_is_script_url_callback<'s>( - scope: &mut v8::PinScope<'s, '_>, - args: v8::FunctionCallbackArguments<'s>, - mut rv: v8::ReturnValue<'_, v8::Value>, -) { - if !trusted_types_factory_receiver_is_valid(scope, args.this()) { + }; + let Some(kind) = crate::util::callback_data_item( + scope, + &args, + &TRUSTED_TYPE_KINDS, + "TrustedTypePolicyFactory type predicates", + ) else { return; - } - rv.set_bool(trusted_type_string(scope, args.get(0), TrustedTypeKind::ScriptUrl).is_some()); + }; + rv.set_bool(trusted_type_kind(scope, parsed.value) == Some(kind)); } fn trusted_types_get_attribute_type_callback<'s>( diff --git a/moli-renderer-v8/src/context_bootstrap/trusted_types/realm_state.rs b/moli-renderer-v8/src/context_bootstrap/trusted_types/realm_state.rs index 883e21433f..74d4056ee2 100644 --- a/moli-renderer-v8/src/context_bootstrap/trusted_types/realm_state.rs +++ b/moli-renderer-v8/src/context_bootstrap/trusted_types/realm_state.rs @@ -8,49 +8,18 @@ const TRUSTED_TYPES_LAZY_STATE_INSTALLED_SLOT: &str = "__moliTrustedTypesLazySta const TRUSTED_TYPES_FACTORY_SLOT: &str = "__moliTrustedTypesFactory"; const TRUSTED_TYPES_MATERIALIZING_SLOT: &str = "__moliTrustedTypesMaterializing"; -#[derive(Clone, Copy)] -enum TrustedTypesGlobalProperty { - Html, - Script, - ScriptUrl, - Factory, -} +#[derive(WebApiObject)] +#[webapi(fragment)] +struct TrustedTypesGlobalAccessorDeclaration { + window_receiver: bool, -impl TrustedTypesGlobalProperty { - const ALL: [(Self, &'static str); 4] = [ - (Self::Html, "TrustedHTML"), - (Self::Script, "TrustedScript"), - (Self::ScriptUrl, "TrustedScriptURL"), - (Self::Factory, "trustedTypes"), - ]; - - const fn callback_data(self) -> u32 { - match self { - Self::Html => 0, - Self::Script => 1, - Self::ScriptUrl => 2, - Self::Factory => 3, - } - } - - fn from_callback_data(value: u32) -> Option { - match value { - 0 => Some(Self::Html), - 1 => Some(Self::Script), - 2 => Some(Self::ScriptUrl), - 3 => Some(Self::Factory), - _ => None, - } - } - - const fn trusted_type_kind(self) -> Option { - match self { - Self::Html => Some(TrustedTypeKind::Html), - Self::Script => Some(TrustedTypeKind::Script), - Self::ScriptUrl => Some(TrustedTypeKind::ScriptUrl), - Self::Factory => None, - } - } + #[webapi( + accessor_property = "trustedTypes", + getter = trusted_types_global_getter, + data = self.window_receiver, + enumerable + )] + trusted_types: (), } pub(super) fn install_lazy_trusted_types_runtime_state<'s>( @@ -74,12 +43,6 @@ pub(super) fn install_lazy_trusted_types_runtime_state<'s>( .unwrap_or(false) .then_some(()) .ok_or_else(|| anyhow!("failed to publish TrustedTypePolicy interface"))?; - set_private_value( - scope, - global, - TRUSTED_TYPE_POLICY_CONSTRUCTOR_SLOT, - policy_constructor.into(), - ); let policy_factory_constructor = TrustedTypePolicyFactoryInterfaceDeclaration::build(scope) .get_function(scope) .ok_or_else(|| anyhow!("failed to build TrustedTypePolicyFactory interface"))?; @@ -93,28 +56,55 @@ pub(super) fn install_lazy_trusted_types_runtime_state<'s>( .unwrap_or(false) .then_some(()) .ok_or_else(|| anyhow!("failed to publish TrustedTypePolicyFactory interface"))?; - set_private_value( - scope, - global, - TRUSTED_TYPE_POLICY_FACTORY_CONSTRUCTOR_SLOT, - policy_factory_constructor.into(), - ); - install_trusted_script_code_like_constructor(scope, global)?; + // These eager interfaces are outside the exposed-interface template table. + // Register their intrinsic prototypes before author code can replace globals; + // WebApiObject binding must never use an author-supplied constructor. + for (name, constructor) in [ + ("TrustedTypePolicy", policy_constructor), + ("TrustedTypePolicyFactory", policy_factory_constructor), + ] { + let prototype = constructor + .get(scope, v8str(scope, "prototype").into()) + .and_then(|value| v8::Local::::try_from(value).ok()) + .ok_or_else(|| anyhow!("{name}.prototype missing during bootstrap"))?; + if !crate::util::register_intrinsic_interface( + scope, + global, + name, + constructor.into(), + prototype, + ) { + return Err(anyhow!("failed to register intrinsic {name} interface")); + } + } install_function_constructor_brand_guards(scope, global)?; - for (property, name) in TrustedTypesGlobalProperty::ALL { - let data = v8::Integer::new_from_unsigned(scope, property.callback_data()); + for (index, kind) in TRUSTED_TYPE_KINDS.into_iter().enumerate() { + let name = kind.constructor_name(); + let data = crate::util::callback_data_index_value(scope, index); global .set_lazy_data_property_with_configuration( scope, v8str(scope, name).into(), v8::LazyDataPropertyConfiguration::new(trusted_types_global_lazy_getter) - .data(data.into()) + .data(data) .property_attribute(v8::PropertyAttribute::DONT_ENUM), ) .unwrap_or(false) .then_some(()) .ok_or_else(|| anyhow!("failed to install lazy `{name}` global"))?; } + // Window owns its mixin attribute; workers inherit it from WorkerGlobalScope. + let is_worker = get_private_value(scope, global, crate::worker::WORKER_STATE_SLOT).is_some(); + let target = if is_worker { + crate::util::global_constructor_prototype(scope, "WorkerGlobalScope").ok_or_else(|| { + anyhow!("WorkerGlobalScope.prototype missing during Trusted Types bootstrap") + })? + } else { + global + }; + TrustedTypesGlobalAccessorDeclaration::new(!is_worker) + .initialize(scope, target) + .map_err(|error| anyhow!("failed to install trustedTypes accessor: {error}"))?; set_private_value( scope, global, @@ -130,10 +120,10 @@ fn trusted_types_global_lazy_getter<'s>( args: v8::PropertyCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { - let Some(property) = args + let Some(kind) = args .data() .uint32_value(scope) - .and_then(TrustedTypesGlobalProperty::from_callback_data) + .and_then(|index| TrustedTypeKind::from_callback_index(index as usize)) else { throw_error( scope, @@ -149,14 +139,64 @@ fn trusted_types_global_lazy_getter<'s>( return; }; let target_scope = &mut v8::ContextScope::new(scope, relevant_context); - match ensure_trusted_types_state(target_scope) - .and_then(|()| cached_public_value(target_scope, property)) - { - Ok(value) => rv.set(value), - Err(error) => throw_error( + if let Err(error) = ensure_trusted_types_state(target_scope) { + throw_error( target_scope, &format!("Failed to materialize Trusted Types: {error}"), - ), + ); + return; + } + let global = relevant_context.global(target_scope); + if let Some(value) = get_private_value(target_scope, global, kind.constructor_slot()) { + rv.set(value); + } else { + throw_error( + target_scope, + "Materialized Trusted Types constructor is missing.", + ); + } +} + +fn trusted_types_global_getter<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'_, v8::Value>, +) { + let receiver = args.this(); + // Reject in the getter's realm before entering a valid receiver's realm. + // Borrowing a getter changes neither its TypeError realm nor its brand check. + let valid_receiver = if args.data().is_true() { + super::super::window_receiver::is_window_receiver(scope, receiver) + } else { + let global = scope.get_current_context().global(scope); + receiver.strict_equals(global.into()) + }; + if !valid_receiver { + throw_type_error( + scope, + "trustedTypes getter called on incompatible receiver.", + ); + return; + } + let relevant_context = receiver + .get_creation_context(scope) + .unwrap_or_else(|| scope.get_current_context()); + let target_scope = &mut v8::ContextScope::new(scope, relevant_context); + if let Err(error) = ensure_trusted_types_state(target_scope) { + throw_error( + target_scope, + &format!("Failed to materialize Trusted Types: {error}"), + ); + return; + } + let global = relevant_context.global(target_scope); + if let Some(factory) = cached_object(target_scope, global, TRUSTED_TYPES_FACTORY_SLOT) { + rv.set(factory.into()); + } else { + throw_error( + target_scope, + "Materialized Trusted Types factory is missing.", + ); } } @@ -222,24 +262,6 @@ fn build_and_cache_trusted_types_state<'s>( Ok(()) } -fn cached_public_value<'s>( - scope: &mut v8::PinScope<'s, '_>, - property: TrustedTypesGlobalProperty, -) -> Result> { - let global = scope.get_current_context().global(scope); - let slot = match property { - TrustedTypesGlobalProperty::Html - | TrustedTypesGlobalProperty::Script - | TrustedTypesGlobalProperty::ScriptUrl => property - .trusted_type_kind() - .expect("trusted type constructor property must have a kind") - .constructor_slot(), - TrustedTypesGlobalProperty::Factory => TRUSTED_TYPES_FACTORY_SLOT, - }; - get_private_value(scope, global, slot) - .ok_or_else(|| anyhow!("materialized Trusted Types value is missing")) -} - fn cached_object<'s>( scope: &mut v8::PinScope<'s, '_>, object: v8::Local<'s, v8::Object>, diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/trusted_types.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/trusted_types.rs index e105f705a6..75d8b7dce9 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/trusted_types.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/misc/extracted/trusted_types.rs @@ -129,24 +129,24 @@ fn trusted_type_prototype_declared_methods_preserve_descriptors() { script: internalNames(script), scriptURL: internalNames(scriptURL) }, - htmlMethods: ["toString", "valueOf"].map(name => describe(TrustedHTML.prototype, name)), - scriptMethods: ["toString", "valueOf"].map(name => describe(TrustedScript.prototype, name)), - scriptURLMethods: ["toString", "valueOf"].map(name => describe(TrustedScriptURL.prototype, name)), + htmlMethods: ["toString", "toJSON"].map(name => describe(TrustedHTML.prototype, name)), + scriptMethods: ["toString", "toJSON"].map(name => describe(TrustedScript.prototype, name)), + scriptURLMethods: ["toString", "toJSON"].map(name => describe(TrustedScriptURL.prototype, name)), values: [ String(html), - html.valueOf(), + html.toJSON(), String(script), - script.valueOf(), + script.toJSON(), String(scriptURL), - scriptURL.valueOf() + scriptURL.toJSON() ], fakeResults: [ probe(() => TrustedHTML.prototype.toString.call(fakeHTML)), - probe(() => TrustedHTML.prototype.valueOf.call(fakeHTML)), + probe(() => TrustedHTML.prototype.toJSON.call(fakeHTML)), probe(() => TrustedScript.prototype.toString.call(fakeScript)), - probe(() => TrustedScript.prototype.valueOf.call(fakeScript)), + probe(() => TrustedScript.prototype.toJSON.call(fakeScript)), probe(() => TrustedScriptURL.prototype.toString.call(fakeScriptURL)), - probe(() => TrustedScriptURL.prototype.valueOf.call(fakeScriptURL)) + probe(() => TrustedScriptURL.prototype.toJSON.call(fakeScriptURL)) ].join("|"), trustedChecks: [ trustedTypes.isHTML(fakeHTML), @@ -164,7 +164,7 @@ fn trusted_type_prototype_declared_methods_preserve_descriptors() { assert_eq!( result, - r#"{"prototypes":[true,true,true],"initialOwnSlots":{"html":[],"script":[],"scriptURL":[]},"spoofedOwnSlots":{"html":["__moliTrustedTypeKind","__moliTrustedTypeValue"],"script":["__moliTrustedTypeKind","__moliTrustedTypeValue"],"scriptURL":["__moliTrustedTypeKind","__moliTrustedTypeValue"]},"htmlMethods":["false:true:true:toString:0","false:true:true:valueOf:0"],"scriptMethods":["false:true:true:toString:0","false:true:true:valueOf:0"],"scriptURLMethods":["false:true:true:toString:0","false:true:true:valueOf:0"],"values":["ok","ok","1 + 1","1 + 1","data:text/javascript,","data:text/javascript,"],"fakeResults":"TypeError|TypeError|TypeError|TypeError|TypeError|TypeError","trustedChecks":[false,false,false,true,true,true]}"# + r#"{"prototypes":[true,true,true],"initialOwnSlots":{"html":[],"script":[],"scriptURL":[]},"spoofedOwnSlots":{"html":["__moliTrustedTypeKind","__moliTrustedTypeValue"],"script":["__moliTrustedTypeKind","__moliTrustedTypeValue"],"scriptURL":["__moliTrustedTypeKind","__moliTrustedTypeValue"]},"htmlMethods":["true:true:true:toString:0","true:true:true:toJSON:0"],"scriptMethods":["true:true:true:toString:0","true:true:true:toJSON:0"],"scriptURLMethods":["true:true:true:toString:0","true:true:true:toJSON:0"],"values":["ok","ok","1 + 1","1 + 1","data:text/javascript,","data:text/javascript,"],"fakeResults":"TypeError|TypeError|TypeError|TypeError|TypeError|TypeError","trustedChecks":[false,false,false,true,true,true]}"# ); } #[test] diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/trusted_types.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/trusted_types.rs index 5417078f26..e3ac306560 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/trusted_types.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/trusted_types.rs @@ -40,6 +40,87 @@ fn trusted_types_csp_policy_creation_matches_only_valid_name_tokens() { } } +#[test] +fn trusted_types_webidl_surface_checks_descriptors_arguments_and_brands() { + let mut vm = new_storage_test_vm("https://trusted-types-webidl.test/"); + assert_eq!( + vm.eval(include_str!( + "../../../../tests/fixtures/trusted-types-webidl.js" + )) + .expect("Trusted Types WebIDL surface should match its declarations"), + "ok" + ); +} + +#[tokio::test] +async fn trusted_types_webidl_getter_and_stringifiers_preserve_cross_realm_semantics() { + let mut vm = new_parsed_test_vm( + "https://trusted-types-webidl-realms.test/", + "", + ); + vm.eval( + r#" + const frame = document.createElement('iframe'); + frame.id = 'trusted-types-webidl-child'; + frame.srcdoc = '

child

'; + document.body.appendChild(frame); + "#, + ) + .expect("create child realm for Trusted Types getter checks"); + run_child_navigation_commit_and_host_load_for_test(&mut vm, "Trusted Types child realm").await; + assert_eq!( + vm.eval( + r#" + (() => { + const assert = (condition, message) => { + if (!condition) throw new Error(message); + }; + const child = document.getElementById('trusted-types-webidl-child').contentWindow; + const parentGetter = Object.getOwnPropertyDescriptor(window, 'trustedTypes').get; + const childGetter = Object.getOwnPropertyDescriptor(child, 'trustedTypes').get; + // Borrow the parent getter before the child's factory has been accessed. + const childFactory = parentGetter.call(child); + assert(childFactory === child.trustedTypes && + childFactory instanceof child.TrustedTypePolicyFactory && + !(childFactory instanceof TrustedTypePolicyFactory), 'factory receiver realm'); + assert(childGetter.call(window) === trustedTypes, 'borrowed child getter'); + assert(childGetter.call(null) === childFactory && + childGetter.call(undefined) === childFactory, 'nullish receiver uses getter realm'); + for (const [getter, ErrorType] of [[parentGetter, TypeError], [childGetter, child.TypeError]]) { + let error; + try { getter.call({}); } catch (caught) { error = caught; } + assert(error instanceof ErrorType, 'getter exception belongs to function realm'); + } + const policy = childFactory.createPolicy('child', { + createHTML: value => value, + createScript: value => value, + createScriptURL: value => value, + }); + const cases = [ + [TrustedHTML, child.TrustedHTML, policy.createHTML('html'), 'html', 'isHTML'], + [TrustedScript, child.TrustedScript, policy.createScript('script'), 'script', 'isScript'], + [TrustedScriptURL, child.TrustedScriptURL, policy.createScriptURL('/app.js'), '/app.js', 'isScriptURL'], + ]; + for (const [Parent, Child, value, text, predicate] of cases) { + assert(value instanceof Child && !(value instanceof Parent), 'value realm'); + assert(trustedTypes[predicate](value), 'cross-realm predicate'); + for (const method of ['toString', 'toJSON']) { + assert(Parent.prototype[method].call(value) === text, 'cross-realm stringifier'); + let error; + try { Child.prototype[method].call({}); } catch (caught) { error = caught; } + assert(error instanceof child.TypeError && !(error instanceof TypeError), + 'stringifier exception belongs to function realm'); + } + } + return 'ok'; + })() + "#, + ) + .expect("Trusted Types getter and stringifier realms should be preserved"), + "ok" + ); +} + #[test] fn trusted_type_policy_callbacks_follow_webidl_dictionary_and_callback_rules() { let mut vm = new_storage_test_vm("https://trusted-type-policy-webidl.test/"); diff --git a/moli-renderer-v8/src/script_vm/tests/lazy_window_surfaces.rs b/moli-renderer-v8/src/script_vm/tests/lazy_window_surfaces.rs index 3425b5abdd..2f7577ceb2 100644 --- a/moli-renderer-v8/src/script_vm/tests/lazy_window_surfaces.rs +++ b/moli-renderer-v8/src/script_vm/tests/lazy_window_surfaces.rs @@ -495,6 +495,18 @@ fn trusted_types_globals_share_one_lazy_native_realm_state() { let mut vm = new_storage_test_vm("https://lazy-trusted-types.test/"); assert!(!default_trusted_types_materialized(&mut vm)); + assert_eq!( + vm.eval( + "String(typeof Object.getOwnPropertyDescriptor(globalThis, 'trustedTypes').get === 'function')" + ) + .expect("reading the trustedTypes getter descriptor should not invoke it"), + "true" + ); + assert!( + !default_trusted_types_materialized(&mut vm), + "reflecting the accessor must not build Trusted Types prototypes or factory" + ); + assert_eq!( vm.eval( r#" @@ -551,6 +563,51 @@ fn trusted_types_globals_share_one_lazy_native_realm_state() { ); } +#[test] +fn trusted_types_getter_materializes_intrinsics_without_replacing_public_overrides() { + let mut vm = new_storage_test_vm("https://lazy-trusted-types-overrides.test/"); + vm.eval( + r#" + const replacement = function Replacement() {}; + globalThis.TrustedHTML = replacement; + globalThis.TrustedScript = replacement; + globalThis.TrustedScriptURL = replacement; + globalThis.TrustedTypePolicy = replacement; + globalThis.TrustedTypePolicyFactory = replacement; + "#, + ) + .expect("overriding public constructor names should not materialize Trusted Types"); + assert!(!default_trusted_types_materialized(&mut vm)); + assert_eq!( + vm.eval( + r#" + (() => { + const factory = trustedTypes; + const policy = factory.createPolicy('private-intrinsics', { + createHTML: value => value, + createScript: value => value, + createScriptURL: value => value, + }); + const values = [policy.createHTML('html'), policy.createScript('script'), + policy.createScriptURL('/app.js')]; + return String( + [TrustedHTML, TrustedScript, TrustedScriptURL, TrustedTypePolicy, + TrustedTypePolicyFactory].every(value => value === replacement) && + !(factory instanceof replacement) && !(policy instanceof replacement) && + values.every(value => !(value instanceof replacement)) && + factory.isHTML(values[0]) && factory.isScript(values[1]) && + factory.isScriptURL(values[2]) && + values.map(String).join('|') === 'html|script|/app.js' + ); + })() + "#, + ) + .expect("lazy Trusted Types materialization should use private intrinsic identities"), + "true" + ); + assert!(default_trusted_types_materialized(&mut vm)); +} + #[test] fn media_source_uses_the_shared_lazy_interface_registry() { let mut vm = new_storage_test_vm("https://lazy-media-source.test/"); diff --git a/moli-renderer-v8/src/worker/thread/tests/postmessage.rs b/moli-renderer-v8/src/worker/thread/tests/postmessage.rs index eff6c3f59f..72352f0455 100644 --- a/moli-renderer-v8/src/worker/thread/tests/postmessage.rs +++ b/moli-renderer-v8/src/worker/thread/tests/postmessage.rs @@ -1,5 +1,22 @@ use super::*; +#[tokio::test] +async fn worker_trusted_types_webidl_surface_checks_descriptors_arguments_and_brands() { + ensure_v8(); + let mut handle = spawn_worker( + format!( + "postMessage({}); close();", + include_str!("../../../../tests/fixtures/trusted-types-webidl.js") + ), + "https://trusted-types-webidl.test/worker.js".into(), + ); + let message = timeout(TIMEOUT, handle.recv()) + .await + .expect("worker Trusted Types probe should settle") + .expect("worker Trusted Types probe should return a result"); + assert_eq!(expect_post_json(message), r#""ok""#); +} + #[tokio::test] async fn worker_compression_streams_roundtrip_all_formats() { ensure_v8(); diff --git a/moli-renderer-v8/tests/fixtures/trusted-types-webidl.js b/moli-renderer-v8/tests/fixtures/trusted-types-webidl.js new file mode 100644 index 0000000000..f067dda793 --- /dev/null +++ b/moli-renderer-v8/tests/fixtures/trusted-types-webidl.js @@ -0,0 +1,108 @@ +(() => { + const assert = (condition, message) => { + if (!condition) throw new Error(message); + }; + const rejects = (callback, message) => { + let error; + try { callback(); } catch (caught) { error = caught; } + assert(error instanceof TypeError, message); + }; + const isWindow = typeof window !== "undefined"; + const owner = isWindow ? globalThis : WorkerGlobalScope.prototype; + const descriptor = Object.getOwnPropertyDescriptor(owner, "trustedTypes"); + assert(typeof descriptor?.get === "function", "trustedTypes is an accessor"); + assert(descriptor.set === undefined && !Object.hasOwn(descriptor, "value"), + "trustedTypes has no setter or data value"); + assert(descriptor.enumerable && descriptor.configurable, "trustedTypes flags"); + const getter = descriptor.get; + assert(getter.name === "get trustedTypes" && getter.length === 0, "getter metadata"); + assert(!Object.hasOwn(getter, "prototype"), "getter is not a constructor"); + rejects(() => Reflect.construct(getter, []), "new getter"); + if (!isWindow) assert(!Object.hasOwn(globalThis, "trustedTypes"), "worker placement"); + + const factory = trustedTypes; + assert(factory instanceof TrustedTypePolicyFactory, "factory interface"); + for (const receiver of [globalThis, null, undefined]) { + assert(getter.call(receiver) === factory, "getter global and nullish receivers"); + } + for (const receiver of [{}, Object.create(globalThis), new Proxy(globalThis, {}), 1]) { + rejects(() => getter.call(receiver), "getter receiver brand"); + } + assert(!Reflect.set(globalThis, "trustedTypes", {}), "readonly write fails"); + rejects(() => { "use strict"; globalThis.trustedTypes = {}; }, "strict readonly write"); + assert(trustedTypes === factory, "readonly write preserves factory"); + assert(Object.getOwnPropertyDescriptor(owner, "trustedTypes").get === getter, + "getter remains an accessor after materialization"); + assert(factory.createPolicy.length === 1, "optional policy options do not count in length"); + const policy = factory.createPolicy("webidl-surface", { + createHTML: value => value, + createScript: value => value, + createScriptURL: value => value, + }); + const entries = [ + [TrustedHTML, policy.createHTML("html"), "html", "isHTML"], + [TrustedScript, policy.createScript("1 + 1"), "1 + 1", "isScript"], + [TrustedScriptURL, policy.createScriptURL("/app.js"), "/app.js", "isScriptURL"], + ]; + for (const [Constructor, value, text, predicate] of entries) { + const proto = Constructor.prototype; + const prototypeDescriptor = Object.getOwnPropertyDescriptor(Constructor, "prototype"); + assert(!prototypeDescriptor.writable && !prototypeDescriptor.enumerable && + !prototypeDescriptor.configurable, `${Constructor.name}.prototype flags`); + const constructorDescriptor = Object.getOwnPropertyDescriptor(proto, "constructor"); + assert(constructorDescriptor.value === Constructor && constructorDescriptor.writable && + !constructorDescriptor.enumerable && constructorDescriptor.configurable, + `${Constructor.name} constructor backlink`); + rejects(() => Constructor(), `${Constructor.name} call`); + rejects(() => new Constructor(), `${Constructor.name} construction`); + const tag = Object.getOwnPropertyDescriptor(proto, Symbol.toStringTag); + assert(tag.value === Constructor.name && !tag.writable && !tag.enumerable && tag.configurable, + `${Constructor.name} tag flags`); + assert(!Object.hasOwn(proto, "valueOf") && value.valueOf() === value, + `${Constructor.name} inherits Object valueOf`); + const fake = Object.create(proto); + const revoked = Proxy.revocable(value, {}); + revoked.revoke(); + const fakeValues = [fake, Object.create(value), new Proxy(value, {}), revoked.proxy, + {}, null, undefined, text]; + for (const name of ["toString", "toJSON"]) { + const method = Object.getOwnPropertyDescriptor(proto, name); + assert(method.enumerable && method.configurable && method.writable, + `${Constructor.name}.${name} flags`); + assert(method.value.name === name && method.value.length === 0 && + !Object.hasOwn(method.value, "prototype"), `${name} metadata`); + rejects(() => Reflect.construct(method.value, []), `new ${name}`); + assert(method.value.call(value) === text, `${name} data`); + for (const other of [...fakeValues, ...entries.filter(entry => entry[0] !== Constructor) + .map(entry => entry[1])]) { + rejects(() => method.value.call(other), `${Constructor.name}.${name} receiver brand`); + } + } + rejects(() => factory[predicate](), `${predicate} missing argument`); + for (const other of fakeValues) { + assert(factory[predicate](other) === false, `${predicate} non-instance`); + } + for (const [Other, other] of entries) { + assert(factory[predicate](other) === (Other === Constructor), `${predicate} kind`); + } + const trap = new Proxy({}, { get() { throw new Error("predicate must not coerce"); } }); + assert(factory[predicate](trap) === false, `${predicate} does not coerce input`); + rejects(() => factory[predicate].call({}, value), `${predicate} factory brand`); + assert(String(value) === text && JSON.stringify(value) === JSON.stringify(text), + `${Constructor.name} string and JSON conversion`); + value.toString = () => "7"; + assert(Number(value) === 7, `${Constructor.name} default primitive conversion`); + delete value.toString; + } + + const Policy = TrustedTypePolicy; + globalThis.TrustedTypePolicy = function ReplacementPolicy() {}; + globalThis.TrustedTypePolicyFactory = function ReplacementFactory() {}; + const afterOverride = factory.createPolicy("after-public-overrides", {createHTML: value => value}); + assert(afterOverride instanceof Policy && String(afterOverride.createHTML("safe")) === "safe", + "policy creation uses intrinsic prototypes after public constructor replacement"); + + Object.defineProperty(globalThis, "trustedTypes", {value: "shadowed", configurable: true}); + assert(getter.call(globalThis) === factory, "captured getter ignores public replacement"); + return "ok"; +})()