From 0d45137191d97780ca45384e6f06ec0ca7387c97 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Tue, 8 Sep 2026 05:42:15 +0800 Subject: [PATCH 1/3] fix(webidl): reject invalid native attribute receivers --- .../css_fontface_runtime/font_face.rs | 32 ++++- .../src/native_bridge/document.rs | 2 +- moli-renderer-v8/src/native_bridge/element.rs | 22 ++-- .../src/native_bridge/element/canvas.rs | 25 +++- .../native_bridge/element/geometry/metrics.rs | 25 ++++ moli-renderer-v8/src/observer_runtime/mod.rs | 77 +++++------ .../src/script_vm/tests/browser_api/misc.rs | 12 +- moli-renderer-v8/src/script_vm/tests/mod.rs | 1 + .../src/script_vm/tests/webidl_receivers.rs | 121 ++++++++++++++++++ 9 files changed, 251 insertions(+), 66 deletions(-) create mode 100644 moli-renderer-v8/src/script_vm/tests/webidl_receivers.rs diff --git a/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/font_face.rs b/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/font_face.rs index 8b62a322e..26615b8a9 100644 --- a/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/font_face.rs +++ b/moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/font_face.rs @@ -1,6 +1,8 @@ use super::*; use crate::{ - util::{callback_data_index_value, callback_data_item, get_private_value, set_private_value}, + util::{ + callback_data_index_value, callback_data_item, get_private_value, set_private_value, v8str, + }, webidl, }; use moli_webapi_declare::{WebApiFunctionTemplate, WebApiObject}; @@ -153,6 +155,9 @@ fn font_face_writable_attribute_getter_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'s, v8::Value>, ) { + if !require_font_face_receiver(scope, args.this()) { + return; + } let Some(slot) = callback_data_item( scope, &args, @@ -172,6 +177,9 @@ fn font_face_readonly_attribute_getter_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'s, v8::Value>, ) { + if !require_font_face_receiver(scope, args.this()) { + return; + } let Some(slot) = callback_data_item( scope, &args, @@ -191,6 +199,9 @@ fn font_face_attribute_setter_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'s, v8::Value>, ) { + if !require_font_face_receiver(scope, args.this()) { + return; + } let Some(slot) = callback_data_item( scope, &args, @@ -332,6 +343,14 @@ pub(in crate::context_bootstrap) fn font_face_load_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'s, v8::Value>, ) { + if get_private_value(scope, args.this(), FONT_FACE_STATUS_SLOT).is_none() { + let resolver = v8::PromiseResolver::new(scope).expect("FontFace.load resolver"); + let message = v8str(scope, "Illegal invocation"); + let error = v8::Exception::type_error(scope, message); + let _ = resolver.reject(scope, error); + rv.set(resolver.get_promise(scope).into()); + return; + } let this = args.this(); super::events::notify_font_face_set_owners_of_load(scope, this); if let Some(loaded) = font_face_slot_value(scope, this, FONT_FACE_LOADED_SLOT) { @@ -344,6 +363,17 @@ pub(in crate::context_bootstrap) fn font_face_load_callback<'s>( } } +fn require_font_face_receiver<'s>( + scope: &mut v8::PinScope<'s, '_>, + receiver: v8::Local<'s, v8::Object>, +) -> bool { + if get_private_value(scope, receiver, FONT_FACE_STATUS_SLOT).is_some() { + return true; + } + throw_type_error(scope, "Illegal invocation"); + false +} + fn descriptor_string_property( scope: &mut v8::PinScope<'_, '_>, object: Option>, diff --git a/moli-renderer-v8/src/native_bridge/document.rs b/moli-renderer-v8/src/native_bridge/document.rs index 946c9e932..ed625658f 100644 --- a/moli-renderer-v8/src/native_bridge/document.rs +++ b/moli-renderer-v8/src/native_bridge/document.rs @@ -984,7 +984,7 @@ fn document_referrer_getter_function<'s>( ) { let Some((runtime_ptr, handle)) = document_receiver_runtime_and_handle(scope, args.this()) else { - rv.set_undefined(); + crate::util::throw_type_error(scope, "Illegal invocation"); return; }; let runtime = unsafe { &*runtime_ptr }; diff --git a/moli-renderer-v8/src/native_bridge/element.rs b/moli-renderer-v8/src/native_bridge/element.rs index 3d4488073..a45ee1b17 100644 --- a/moli-renderer-v8/src/native_bridge/element.rs +++ b/moli-renderer-v8/src/native_bridge/element.rs @@ -3244,10 +3244,13 @@ fn iframe_content_document_getter_function<'s>( mut rv: v8::ReturnValue<'_, v8::Value>, ) { let receiver = args.this(); - let Ok((runtime_ptr, handle)) = - node_runtime_and_handle_from_object_or_detached(scope, receiver) - else { - rv.set_null(); + let Some((runtime_ptr, handle)) = html_element_getter_receiver( + scope, + receiver, + "HTMLIFrameElement", + "contentDocument", + "iframe", + ) else { return; }; if iframe_is_inside_its_own_child_context_document(scope, runtime_ptr, handle) { @@ -3311,10 +3314,13 @@ fn iframe_content_window_getter_function<'s>( mut rv: v8::ReturnValue<'_, v8::Value>, ) { let receiver = args.this(); - let Ok((runtime_ptr, handle)) = - node_runtime_and_handle_from_object_or_detached(scope, receiver) - else { - rv.set_null(); + let Some((runtime_ptr, handle)) = html_element_getter_receiver( + scope, + receiver, + "HTMLIFrameElement", + "contentWindow", + "iframe", + ) else { return; }; if iframe_is_inside_its_own_child_context_document(scope, runtime_ptr, handle) { diff --git a/moli-renderer-v8/src/native_bridge/element/canvas.rs b/moli-renderer-v8/src/native_bridge/element/canvas.rs index 2a9d75408..e7d35159f 100644 --- a/moli-renderer-v8/src/native_bridge/element/canvas.rs +++ b/moli-renderer-v8/src/native_bridge/element/canvas.rs @@ -9,7 +9,10 @@ use crate::{ }; use super::super::node::node_runtime_and_handle_from_object_or_detached; -use super::{element_attribute, set_reflected_attribute}; +use super::{ + element_attribute, html_element_getter_receiver, html_element_setter_receiver, + set_reflected_attribute, +}; const CANVAS_CONTEXT_KIND_SLOT: &str = "__moliCanvasContextKind"; const CANVAS_CONTEXT_2D_SLOT: &str = "__moliCanvasContext2D"; @@ -36,6 +39,11 @@ pub(crate) fn html_canvas_width_getter_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { + if html_element_getter_receiver(scope, args.this(), "HTMLCanvasElement", "width", "canvas") + .is_none() + { + return; + } rv.set_uint32(canvas_dimension_value(scope, args.this(), "width", 300)); } @@ -44,6 +52,11 @@ pub(crate) fn html_canvas_width_setter_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'s, v8::Value>, ) { + if html_element_setter_receiver(scope, args.this(), "HTMLCanvasElement", "width", "canvas") + .is_none() + { + return; + } let canvas = args.this(); let _ = set_canvas_dimension_attribute( scope, @@ -62,6 +75,11 @@ pub(crate) fn html_canvas_height_getter_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'_, v8::Value>, ) { + if html_element_getter_receiver(scope, args.this(), "HTMLCanvasElement", "height", "canvas") + .is_none() + { + return; + } rv.set_uint32(canvas_dimension_value(scope, args.this(), "height", 150)); } @@ -70,6 +88,11 @@ pub(crate) fn html_canvas_height_setter_callback<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'s, v8::Value>, ) { + if html_element_setter_receiver(scope, args.this(), "HTMLCanvasElement", "height", "canvas") + .is_none() + { + return; + } let canvas = args.this(); let _ = set_canvas_dimension_attribute( scope, diff --git a/moli-renderer-v8/src/native_bridge/element/geometry/metrics.rs b/moli-renderer-v8/src/native_bridge/element/geometry/metrics.rs index b72e1545c..3db40d426 100644 --- a/moli-renderer-v8/src/native_bridge/element/geometry/metrics.rs +++ b/moli-renderer-v8/src/native_bridge/element/geometry/metrics.rs @@ -540,6 +540,9 @@ pub(in crate::native_bridge) fn node_offset_width_getter_function<'s>( args: v8::FunctionCallbackArguments<'s>, rv: v8::ReturnValue<'_, v8::Value>, ) { + if !require_html_geometry_receiver(scope, args.this()) { + return; + } set_box_metric_return_value(scope, args.this(), "offsetWidth", rv); } @@ -548,9 +551,31 @@ pub(in crate::native_bridge) fn node_offset_height_getter_function<'s>( args: v8::FunctionCallbackArguments<'s>, rv: v8::ReturnValue<'_, v8::Value>, ) { + if !require_html_geometry_receiver(scope, args.this()) { + return; + } set_box_metric_return_value(scope, args.this(), "offsetHeight", rv); } +fn require_html_geometry_receiver<'s>( + scope: &mut v8::PinScope<'s, '_>, + receiver: v8::Local<'s, v8::Object>, +) -> bool { + let valid = node_runtime_and_handle_from_object_or_detached(scope, receiver) + .ok() + .is_some_and(|(runtime, handle)| { + unsafe { &*runtime } + .dom_host() + .node(handle) + .and_then(|node| node.as_element()) + .is_some_and(|element| element.namespace() == "http://www.w3.org/1999/xhtml") + }); + if !valid { + crate::util::throw_type_error(scope, "Illegal invocation"); + } + valid +} + pub(in crate::native_bridge) fn node_offset_parent_getter_function<'s>( scope: &mut v8::PinScope<'s, '_>, args: v8::FunctionCallbackArguments<'s>, diff --git a/moli-renderer-v8/src/observer_runtime/mod.rs b/moli-renderer-v8/src/observer_runtime/mod.rs index d4300c632..09ebbf815 100644 --- a/moli-renderer-v8/src/observer_runtime/mod.rs +++ b/moli-renderer-v8/src/observer_runtime/mod.rs @@ -43,7 +43,7 @@ use super::{ }, util::{ callback_data_index_value, callback_data_item, context_host_ptr_from_global_bridge, - get_private_object, global_constructor_prototype, serialize_v8_array, + get_private_object, get_private_value, global_constructor_prototype, serialize_v8_array, serialize_v8_iter_array, throw_range_error, throw_type_error, v8_string, v8str, }, window_webidl_callback::WindowWebIdlCallbackFunctionOutcome, @@ -97,31 +97,26 @@ struct MutationRecordDeclaration<'scope> { } #[derive(WebApiObject)] -#[webapi(interface = "IntersectionObserverEntry", data_properties, enumerable)] +#[webapi(interface = "IntersectionObserverEntry")] struct IntersectionObserverEntryDeclaration<'scope> { + #[webapi(slot = "__moliIntersectionEntryTarget")] target: v8::Local<'scope, v8::Value>, + #[webapi(slot = "__moliIntersectionEntryIntersecting")] is_intersecting: bool, + #[webapi(slot = "__moliIntersectionEntryVisible")] is_visible: bool, + #[webapi(slot = "__moliIntersectionEntryRatio")] intersection_ratio: f64, + #[webapi(slot = "__moliIntersectionEntryBoundingRect")] bounding_client_rect: v8::Local<'scope, v8::Value>, + #[webapi(slot = "__moliIntersectionEntryIntersectionRect")] intersection_rect: v8::Local<'scope, v8::Value>, + #[webapi(slot = "__moliIntersectionEntryRootBounds")] root_bounds: v8::Local<'scope, v8::Value>, + #[webapi(slot = "__moliIntersectionEntryTime")] time: f64, } -#[derive(WebApiObject)] -#[webapi(interface = "IntersectionObserverEntry", data_properties, enumerable)] -struct IntersectionObserverEntryInitDeclaration<'scope> { - time: f64, - root_bounds: v8::Local<'scope, v8::Value>, - bounding_client_rect: v8::Local<'scope, v8::Value>, - intersection_rect: v8::Local<'scope, v8::Value>, - target: v8::Local<'scope, v8::Value>, - is_intersecting: bool, - is_visible: bool, - intersection_ratio: f64, -} - #[derive(WebApiFunctionTemplate)] #[webapi(name = "IntersectionObserver", enumerable)] struct IntersectionObserverPrototypeAccessorsDeclaration { @@ -1483,7 +1478,7 @@ fn initialize_intersection_observer_entry_from_init<'s>( .number_value(scope) .unwrap_or(0.0); - let _ = IntersectionObserverEntryInitDeclaration { + let _ = IntersectionObserverEntryDeclaration { time, root_bounds, bounding_client_rect, @@ -2518,37 +2513,25 @@ fn intersection_observer_attribute_getter_callback( ); } -fn intersection_observer_entry_attribute_getter_callback( - scope: &mut v8::PinScope<'_, '_>, - args: v8::FunctionCallbackArguments<'_>, - mut rv: v8::ReturnValue<'_, v8::Value>, +fn intersection_observer_entry_attribute_getter_callback<'s>( + scope: &mut v8::PinScope<'s, '_>, + args: v8::FunctionCallbackArguments<'s>, + mut rv: v8::ReturnValue<'s, v8::Value>, ) { - let Some(name) = callback_data_item( + let Some(slot) = callback_data_item( scope, &args, - INTERSECTION_OBSERVER_ENTRY_ATTRIBUTE_NAMES, - "IntersectionObserverEntry attribute names", + INTERSECTION_OBSERVER_ENTRY_ATTRIBUTE_SLOTS, + "IntersectionObserverEntry attribute slots", ) else { rv.set_undefined(); return; }; - // Runtime-created entries define their values as own data properties. The - // prototype getter exists for WebIDL shape and should read that own value - // without re-entering the same accessor through normal property lookup. - let key = v8str(scope, name); - let Some(descriptor) = args.this().get_own_property_descriptor(scope, key.into()) else { - rv.set_undefined(); - return; - }; - let Ok(descriptor) = v8::Local::::try_from(descriptor) else { - rv.set_undefined(); + let Some(value) = get_private_value(scope, args.this(), slot) else { + throw_type_error(scope, "Illegal invocation"); return; }; - rv.set( - descriptor - .get(scope, v8str(scope, "value").into()) - .unwrap_or_else(|| v8::undefined(scope).into()), - ); + rv.set(value); } const INTERSECTION_OBSERVER_ATTRIBUTE_NAMES: &[&str] = &[ @@ -2560,15 +2543,15 @@ const INTERSECTION_OBSERVER_ATTRIBUTE_NAMES: &[&str] = &[ "trackVisibility", ]; -const INTERSECTION_OBSERVER_ENTRY_ATTRIBUTE_NAMES: &[&str] = &[ - "time", - "rootBounds", - "boundingClientRect", - "intersectionRect", - "isIntersecting", - "isVisible", - "intersectionRatio", - "target", +const INTERSECTION_OBSERVER_ENTRY_ATTRIBUTE_SLOTS: &[&str] = &[ + "__moliIntersectionEntryTime", + "__moliIntersectionEntryRootBounds", + "__moliIntersectionEntryBoundingRect", + "__moliIntersectionEntryIntersectionRect", + "__moliIntersectionEntryIntersecting", + "__moliIntersectionEntryVisible", + "__moliIntersectionEntryRatio", + "__moliIntersectionEntryTarget", ]; fn timestamp_millis() -> f64 { diff --git a/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs b/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs index 2b3c756f1..9e333b03d 100644 --- a/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs +++ b/moli-renderer-v8/src/script_vm/tests/browser_api/misc.rs @@ -1410,13 +1410,9 @@ fn font_face_declared_slots_ignore_prototype_spoofing() { face.status, typeof face.loaded.then ].join('|'), - fake: [ - fake.family, - fake.source, - fake.style, - fake.status, - fake.loaded - ].map(value => value === undefined ? 'undefined' : String(value)).join('|'), + fake: ['family', 'source', 'style', 'status', 'loaded'].map(name => { + try { return String(fake[name]); } catch (error) { return error.name; } + }).join('|'), descriptors: [ 'family', 'style', @@ -1438,7 +1434,7 @@ fn font_face_declared_slots_ignore_prototype_spoofing() { assert_eq!( result, - r#"{"values":"Changed|url(demo.woff)|italic|700|condensed|small-caps|\"kern\"|swap|loaded|function","fake":"undefined|undefined|undefined|undefined|undefined","descriptors":["family:function:get family:0:function:set family:1:true:true:false","style:function:get style:0:function:set style:1:true:true:false","weight:function:get weight:0:function:set weight:1:true:true:false","stretch:function:get stretch:0:function:set stretch:1:true:true:false","variant:function:get variant:0:function:set variant:1:true:true:false","featureSettings:function:get featureSettings:0:function:set featureSettings:1:true:true:false","display:function:get display:0:function:set display:1:true:true:false","source:function:get source:0:undefined:undefined:undefined:true:true:false","status:function:get status:0:undefined:undefined:undefined:true:true:false","loaded:function:get loaded:0:undefined:undefined:undefined:true:true:false"],"ownSlots":[]}"# + r#"{"values":"Changed|url(demo.woff)|italic|700|condensed|small-caps|\"kern\"|swap|loaded|function","fake":"TypeError|TypeError|TypeError|TypeError|TypeError","descriptors":["family:function:get family:0:function:set family:1:true:true:false","style:function:get style:0:function:set style:1:true:true:false","weight:function:get weight:0:function:set weight:1:true:true:false","stretch:function:get stretch:0:function:set stretch:1:true:true:false","variant:function:get variant:0:function:set variant:1:true:true:false","featureSettings:function:get featureSettings:0:function:set featureSettings:1:true:true:false","display:function:get display:0:function:set display:1:true:true:false","source:function:get source:0:undefined:undefined:undefined:true:true:false","status:function:get status:0:undefined:undefined:undefined:true:true:false","loaded:function:get loaded:0:undefined:undefined:undefined:true:true:false"],"ownSlots":[]}"# ); } diff --git a/moli-renderer-v8/src/script_vm/tests/mod.rs b/moli-renderer-v8/src/script_vm/tests/mod.rs index 9b8f68082..36e26bb2f 100644 --- a/moli-renderer-v8/src/script_vm/tests/mod.rs +++ b/moli-renderer-v8/src/script_vm/tests/mod.rs @@ -15163,6 +15163,7 @@ mod script_terminal_completion; mod streams; mod webidl_collections; mod webidl_fetch; +mod webidl_receivers; mod webidl_trusted_types; mod websocket; mod window_execution_context; diff --git a/moli-renderer-v8/src/script_vm/tests/webidl_receivers.rs b/moli-renderer-v8/src/script_vm/tests/webidl_receivers.rs new file mode 100644 index 000000000..6e1c878ae --- /dev/null +++ b/moli-renderer-v8/src/script_vm/tests/webidl_receivers.rs @@ -0,0 +1,121 @@ +use super::*; + +#[test] +fn webidl_receiver_checks_reject_prototypes_plain_objects_and_forged_instances() { + let mut vm = new_storage_test_vm("https://receiver-check.test/"); + let result = vm.eval(r#" +JSON.stringify((() => { + const failures = []; + const html = document.appendChild(document.createElement('html')); + html.appendChild(document.createElement('body')); + const groups = [ + [Document, ['referrer']], + [FontFace, ['family','status']], + [HTMLCanvasElement, ['width','height']], + [HTMLElement, ['offsetWidth','offsetHeight']], + [HTMLIFrameElement, ['contentDocument','contentWindow']], + [IntersectionObserverEntry, ['boundingClientRect','intersectionRect','rootBounds']], + ]; + for (const [C, keys] of groups) { + for (const key of keys) { + const get = Object.getOwnPropertyDescriptor(C.prototype, key).get; + for (const receiver of [C.prototype, {}, Object.create(C.prototype), new Proxy({}, {}), null]) { + try { get.call(receiver); failures.push(`${C.name}.${key}:accepted`); } + catch (e) { if (!(e instanceof TypeError)) failures.push(`${C.name}.${key}:${e.name}`); } + } + try { C.prototype[key]; failures.push(`${C.name}.${key}:prototype`); } + catch (e) { if (!(e instanceof TypeError)) failures.push(e.name); } + } + } + // A genuine DOM reflector of the wrong interface is also not a receiver. + for (const [C, key, receiver] of [ + [Document, 'referrer', document.body], + [HTMLCanvasElement, 'width', document.createElement('div')], + [HTMLIFrameElement, 'contentWindow', document.createElement('div')], + [HTMLElement, 'offsetHeight', document.createElementNS('http://www.w3.org/2000/svg', 'svg')], + ]) { + try { Object.getOwnPropertyDescriptor(C.prototype, key).get.call(receiver); failures.push(`${C.name}:wrong interface`); } + catch (e) { if (!(e instanceof TypeError)) failures.push(e.name); } + } + return failures; +})()) +"#).unwrap(); + assert_eq!(result, "[]"); +} + +#[test] +fn webidl_receiver_checks_preserve_native_values_and_cross_realm_receivers() { + let mut vm = new_storage_test_vm("https://receiver-check.test/"); + let result = vm.eval(r#" +JSON.stringify((() => { + const html = document.appendChild(document.createElement('html')); + html.appendChild(document.createElement('body')); + const frame = document.createElement('iframe'); + document.body.appendChild(frame); + const child = frame.contentWindow; + const canvas = child.document.createElement('canvas'); + canvas.width = 123; + const face = new child.FontFace('ReceiverTest', 'local("sans-serif")'); + const rect = new DOMRect(1,2,3,4); + const entry = new IntersectionObserverEntry({ + time: 17, rootBounds: rect, boundingClientRect: rect, intersectionRect: rect, + target: document.body, isIntersecting: true, intersectionRatio: 1, + }); + const get = (C, key, receiver) => Object.getOwnPropertyDescriptor(C.prototype, key).get.call(receiver); + const before = get(IntersectionObserverEntry, 'boundingClientRect', entry); + Object.defineProperty(entry, 'boundingClientRect', {value: 'shadow'}); + return [ + get(HTMLCanvasElement, 'width', canvas) === 123, + get(HTMLCanvasElement, 'height', canvas) === 150, + get(FontFace, 'family', face) === 'ReceiverTest', + typeof get(FontFace, 'status', face) === 'string', + typeof get(Document, 'referrer', child.document) === 'string', + typeof get(HTMLElement, 'offsetWidth', child.document.body) === 'number', + typeof get(HTMLElement, 'offsetHeight', child.document.body) === 'number', + get(HTMLIFrameElement, 'contentWindow', frame) === child, + get(HTMLIFrameElement, 'contentDocument', frame) === child.document, + before.width === 3 && get(IntersectionObserverEntry, 'boundingClientRect', entry) === before, + !Object.hasOwn(entry, 'time') && get(IntersectionObserverEntry, 'time', entry) === 17, + get(IntersectionObserverEntry, 'rootBounds', entry) === rect, + ]; +})()) +"#).unwrap(); + assert_eq!( + result, + "[true,true,true,true,true,true,true,true,true,true,true,true]" + ); +} + +#[test] +fn webidl_receiver_checks_precede_canvas_and_fontface_setter_conversion() { + let mut vm = new_storage_test_vm("https://receiver-check.test/"); + let result = vm.eval(r#" +JSON.stringify((() => { + let conversions = 0; + const value = {valueOf() { conversions++; return 1; }, toString() { conversions++; return 'x'; }}; + const errors = []; + for (const [C, key] of [[HTMLCanvasElement,'width'],[HTMLCanvasElement,'height'],[FontFace,'family']]) { + try { Object.getOwnPropertyDescriptor(C.prototype,key).set.call({},value); errors.push('accepted'); } + catch (e) { errors.push(e.name); } + } + return [conversions,...errors]; +})()) +"#).unwrap(); + assert_eq!(result, r#"[0,"TypeError","TypeError","TypeError"]"#); +} + +#[test] +fn webidl_receiver_fontface_load_rejects_its_promise_for_an_invalid_receiver() { + let mut vm = new_storage_test_vm("https://receiver-check.test/"); + vm.eval( + r#" +globalThis.loadResult = 'pending'; +FontFace.prototype.load.call({}).then( + () => loadResult = 'resolved', + error => loadResult = error instanceof TypeError ? 'rejected:TypeError' : error.name +); +"#, + ) + .unwrap(); + assert_eq!(vm.eval("loadResult").unwrap(), "rejected:TypeError"); +} From 67882b7f50236d3b1ab033d738e7b2ade8e0dcd0 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Tue, 8 Sep 2026 06:44:22 +0800 Subject: [PATCH 2/3] fix(fonts): load and register real FontFace sources --- Cargo.lock | 1 + .../chrome_fontfaceset_events_subset.html | 5 +- .../runtime/document_fonts_events.html | 13 +- ...local_event_target_callback_exception.html | 3 + .../runtime/servo_fontfaceset_load_ready.html | 3 + moli-css-parse/src/font_face.rs | 149 ++++++++ moli-css-parse/src/lib.rs | 5 +- moli-css-parse/src/stylo_stylesheet.rs | 2 +- moli-layout/Cargo.toml | 1 + moli-layout/src/lib.rs | 2 +- moli-layout/src/text.rs | 18 + moli-layout/src/text/font_source.rs | 250 +++++++++++++ .../src/content_security_policy.rs | 3 + moli-renderer-v8/src/context_bootstrap.rs | 1 + .../context_bootstrap/css_fontface_runtime.rs | 2 + .../css_fontface_runtime/events.rs | 91 ++++- .../css_fontface_runtime/font_face.rs | 74 ++-- .../css_fontface_runtime/font_face_set.rs | 8 +- .../font_face_set/loading.rs | 37 +- .../css_fontface_runtime/loading.rs | 332 ++++++++++++++++++ .../css_fontface_runtime/query.rs | 24 +- .../css_fontface_runtime/storage.rs | 41 +-- .../src/context_bootstrap/shared.rs | 2 + .../src/context_bootstrap/shared/fonts.rs | 32 ++ .../src/document_runtime/security_policy.rs | 22 ++ .../src/native_bridge/context_host/layout.rs | 14 + .../context_host/resource_loading.rs | 42 +++ .../context_host/security_policy.rs | 36 +- moli-renderer-v8/src/network_host.rs | 2 + .../src/network_host/font_face.rs | 188 ++++++++++ .../src/script_vm/subresource_fetch.rs | 24 ++ .../src/script_vm/tests/browser_api/fonts.rs | 183 ++++++++++ .../src/script_vm/tests/browser_api/misc.rs | 31 +- .../src/script_vm/tests/browser_api/mod.rs | 1 + moli-renderer-v8/src/types.rs | 1 + .../css-font-loading/fontface-basic.html | 13 +- .../wpt/ported/idl/fontface-idlharness.html | 11 +- 37 files changed, 1532 insertions(+), 135 deletions(-) create mode 100644 moli-layout/src/text/font_source.rs create mode 100644 moli-renderer-v8/src/context_bootstrap/css_fontface_runtime/loading.rs create mode 100644 moli-renderer-v8/src/context_bootstrap/shared/fonts.rs create mode 100644 moli-renderer-v8/src/network_host/font_face.rs create mode 100644 moli-renderer-v8/src/script_vm/tests/browser_api/fonts.rs diff --git a/Cargo.lock b/Cargo.lock index 1e383d2c7..9c2092dd6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2621,6 +2621,7 @@ dependencies = [ "moli-image", "parking_lot", "parley", + "read-fonts", "stylo", "stylo_taffy", "taffy", diff --git a/moli-core/tests/fixtures/runtime/chrome_fontfaceset_events_subset.html b/moli-core/tests/fixtures/runtime/chrome_fontfaceset_events_subset.html index 4350f7687..05cc7dab8 100644 --- a/moli-core/tests/fixtures/runtime/chrome_fontfaceset_events_subset.html +++ b/moli-core/tests/fixtures/runtime/chrome_fontfaceset_events_subset.html @@ -2,6 +2,9 @@ diff --git a/moli-core/tests/fixtures/runtime/local_event_target_callback_exception.html b/moli-core/tests/fixtures/runtime/local_event_target_callback_exception.html index 91ba69495..b365c92d6 100644 --- a/moli-core/tests/fixtures/runtime/local_event_target_callback_exception.html +++ b/moli-core/tests/fixtures/runtime/local_event_target_callback_exception.html @@ -12,6 +12,9 @@ data-simple-error="" >