From cb27aad5b90914cba4dfd072be2984f433e35762 Mon Sep 17 00:00:00 2001 From: ldm0 Date: Sun, 4 Oct 2026 03:02:26 +0800 Subject: [PATCH] fix(renderer): use receiver host for NamedNodeMap wrappers --- .../attributes/named_node_map/cache.rs | 6 +- moli-renderer-v8/src/native_bridge/element.rs | 4 +- .../dom_interfaces_and_collections.rs | 80 +++++++++++++++++++ 3 files changed, 85 insertions(+), 5 deletions(-) diff --git a/moli-renderer-v8/src/native_bridge/document/attributes/named_node_map/cache.rs b/moli-renderer-v8/src/native_bridge/document/attributes/named_node_map/cache.rs index 51152bec16..5d57fae7b4 100644 --- a/moli-renderer-v8/src/native_bridge/document/attributes/named_node_map/cache.rs +++ b/moli-renderer-v8/src/native_bridge/document/attributes/named_node_map/cache.rs @@ -23,15 +23,15 @@ fn set_live_named_node_map_cache<'s>( pub(crate) fn live_named_node_map_wrapper<'s>( scope: &mut v8::PinScope<'s, '_>, + runtime_ptr: *mut JsContextHost, element: v8::Local<'s, v8::Object>, ) -> v8::Local<'s, v8::Object> { if let Some(wrapper) = live_named_node_map_cache(scope, element) { refresh_named_node_map_wrapper(scope, wrapper, element); return wrapper; } - let bridge = global_bridge_object(scope).expect("NamedNodeMap requires the native bridge"); - let runtime_ptr = runtime_ptr_from_object(scope, bridge) - .expect("NamedNodeMap bridge must expose its runtime pointer"); + // The attributes getter already resolved this host from the receiver. + // The public bridge can be absent while that host is still installed. let template = unsafe { &mut *runtime_ptr } .native_bridge_mut() .named_node_map_wrapper_template(); diff --git a/moli-renderer-v8/src/native_bridge/element.rs b/moli-renderer-v8/src/native_bridge/element.rs index e3cee4d6dc..29df6ca2f4 100644 --- a/moli-renderer-v8/src/native_bridge/element.rs +++ b/moli-renderer-v8/src/native_bridge/element.rs @@ -1326,12 +1326,12 @@ fn element_attributes_getter_function<'s>( args: v8::FunctionCallbackArguments<'s>, mut rv: v8::ReturnValue<'s, v8::Value>, ) { - let Some((_runtime_ptr, _handle)) = element_getter_receiver(scope, args.this(), "attributes") + let Some((runtime_ptr, _handle)) = element_getter_receiver(scope, args.this(), "attributes") else { rv.set_undefined(); return; }; - let wrapper = super::document::live_named_node_map_wrapper(scope, args.this()); + let wrapper = super::document::live_named_node_map_wrapper(scope, runtime_ptr, args.this()); rv.set(wrapper.into()); } diff --git a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/dom_interfaces_and_collections.rs b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/dom_interfaces_and_collections.rs index 4e58dccfb5..8f8c98dce2 100644 --- a/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/dom_interfaces_and_collections.rs +++ b/moli-renderer-v8/src/script_vm/tests/dom_elements/dom_surface/extracted/dom_interfaces_and_collections.rs @@ -995,6 +995,86 @@ fn access_key_label_matches_chromium_single_key_surface() { r#"{"descriptor":[null,true,true],"valid":"Alt+b","missing":"","empty":"","multiple":"","nonBmp":"","incompatible":"TypeError"}"# ); } +#[test] +fn named_node_map_uses_receiver_host_when_global_bridge_is_missing() { + assert_named_node_map_uses_receiver_host("delete globalThis.__moliNativeBridge"); +} + +#[test] +fn named_node_map_uses_receiver_host_when_global_bridge_is_replaced() { + assert_named_node_map_uses_receiver_host("globalThis.__moliNativeBridge = {}; true"); +} + +#[test] +fn named_node_map_uses_receiver_host_without_reading_global_bridge_accessor() { + assert_named_node_map_uses_receiver_host( + r#"Object.defineProperty(globalThis, '__moliNativeBridge', { + configurable: true, + get() { throw new Error('the public bridge accessor must not run'); } + }); true"#, + ); +} + +fn assert_named_node_map_uses_receiver_host(bridge_change: &str) { + let mut vm = new_storage_test_vm("https://named-node-map-receiver-host.test/"); + vm.eval( + r#" +globalThis.liveElement = document.createElement('div'); +liveElement.setAttribute('data-real', 'one'); +globalThis.detachedElement = new DOMParser() + .parseFromString('
', 'text/html') + .querySelector('div'); +"#, + ) + .expect("live and detached elements should be created before the bridge changes"); + assert_eq!( + vm.eval(bridge_change) + .expect("the public bridge should be removable or replaceable"), + "true" + ); + + let result = vm + .eval( + r#" +(() => { + const probe = element => { + // The first access must construct a wrapper without the public bridge. + const attributes = element.attributes; + const attribute = attributes.item(0); + // Mutate through Attr so detached Element's JS bridge shim is not involved. + attribute.value = 'two'; + return { + tag: Object.prototype.toString.call(attributes), + prototype: Object.getPrototypeOf(attributes) === NamedNodeMap.prototype, + sameWrapper: attributes === element.attributes, + sameAttr: attribute === attributes.item(0), + length: attributes.length, + indexedValue: attributes[0].value, + namedValue: attributes.getNamedItem('data-real').value, + ownerElement: attribute.ownerElement === element + }; + }; + return JSON.stringify([probe(liveElement), probe(detachedElement)]); +})() +"#, + ) + .expect("NamedNodeMap should use the validated receiver host without the public bridge"); + let expected = serde_json::json!({ + "tag": "[object NamedNodeMap]", + "prototype": true, + "sameWrapper": true, + "sameAttr": true, + "length": 1, + "indexedValue": "two", + "namedValue": "two", + "ownerElement": true + }); + assert_eq!( + serde_json::from_str::(&result).expect("probe result should be JSON"), + serde_json::json!([expected.clone(), expected]) + ); +} + #[test] fn named_node_map_accessor_expandos_remain_writable_through_setters() { let mut vm = new_storage_test_vm("https://named-node-map-accessor-expando.test/");