Fix crash when enumerating properties of global object (#36491)

These changes make our implementation of the enumeration hook for
globals [match
Gecko's](https://searchfox.org/mozilla-central/rev/1f65969e57c757146e3e548614b49d3a4168eeb8/dom/base/nsGlobalWindowInner.cpp#3297),
fixing an assertion failure that occurred in the previous
implementation.

Our enumeration hook is supposed to fill a vector with names of
properties on the global object without modifying the global in any way;
instead we were defining all of the missing webidl interfaces. We now do
much less work and crash less.

Testing: New crashtest based on manual testcase.
Fixes: #34686

---------

Signed-off-by: Josh Matthews <josh@joshmatthews.net>
This commit is contained in:
Josh Matthews 2025-04-16 23:32:53 -04:00 committed by GitHub
parent a1b9949f75
commit 30390f8c5e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 98 additions and 34 deletions

16
Cargo.lock generated
View file

@ -526,7 +526,7 @@ dependencies = [
"bitflags 2.9.0", "bitflags 2.9.0",
"cexpr", "cexpr",
"clang-sys", "clang-sys",
"itertools 0.13.0", "itertools 0.10.5",
"proc-macro2", "proc-macro2",
"quote", "quote",
"regex", "regex",
@ -1071,7 +1071,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c" checksum = "117725a109d387c937a1533ce01b450cbde6b88abceea8473c4d7a85853cda3c"
dependencies = [ dependencies = [
"lazy_static", "lazy_static",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -2026,7 +2026,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e"
dependencies = [ dependencies = [
"libc", "libc",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -2552,7 +2552,7 @@ dependencies = [
"gobject-sys", "gobject-sys",
"libc", "libc",
"system-deps", "system-deps",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -4002,7 +4002,7 @@ checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
dependencies = [ dependencies = [
"hermit-abi 0.5.0", "hermit-abi 0.5.0",
"libc", "libc",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -6168,7 +6168,7 @@ dependencies = [
"errno", "errno",
"libc", "libc",
"linux-raw-sys", "linux-raw-sys",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -7505,7 +7505,7 @@ dependencies = [
"getrandom", "getrandom",
"once_cell", "once_cell",
"rustix", "rustix",
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]
@ -8768,7 +8768,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [ dependencies = [
"windows-sys 0.59.0", "windows-sys 0.52.0",
] ]
[[package]] [[package]]

View file

@ -13,7 +13,7 @@ use js::jsapi::{
CallArgs, DOMCallbacks, HandleObject as RawHandleObject, JS_FreezeObject, JSContext, JSObject, CallArgs, DOMCallbacks, HandleObject as RawHandleObject, JS_FreezeObject, JSContext, JSObject,
}; };
use js::rust::{HandleObject, MutableHandleValue, get_object_class, is_dom_class}; use js::rust::{HandleObject, MutableHandleValue, get_object_class, is_dom_class};
use script_bindings::interfaces::DomHelpers; use script_bindings::interfaces::{DomHelpers, Interface};
use script_bindings::settings_stack::StackEntry; use script_bindings::settings_stack::StackEntry;
use crate::DomTypes; use crate::DomTypes;
@ -153,8 +153,7 @@ impl DomHelpers<crate::DomTypeHolder> for crate::DomTypeHolder {
unsafe { is_platform_object_same_origin(cx, obj) } unsafe { is_platform_object_same_origin(cx, obj) }
} }
fn interface_map() -> &'static phf::Map<&'static [u8], for<'a> fn(SafeJSContext, HandleObject)> fn interface_map() -> &'static phf::Map<&'static [u8], Interface> {
{
&InterfaceObjectMap::MAP &InterfaceObjectMap::MAP
} }

View file

@ -43,13 +43,21 @@ fn main() {
let json: Value = serde_json::from_reader(File::open(json).unwrap()).unwrap(); let json: Value = serde_json::from_reader(File::open(json).unwrap()).unwrap();
let mut map = phf_codegen::Map::new(); let mut map = phf_codegen::Map::new();
for (key, value) in json.as_object().unwrap() { for (key, value) in json.as_object().unwrap() {
map.entry(Bytes(key), value.as_str().unwrap()); let parts = value.as_array().unwrap();
map.entry(
Bytes(key),
&format!(
"Interface {{ define: {}, enabled: {} }}",
parts[0].as_str().unwrap(),
parts[1].as_str().unwrap()
),
);
} }
let phf = PathBuf::from(env::var_os("OUT_DIR").unwrap()).join("InterfaceObjectMapPhf.rs"); let phf = PathBuf::from(env::var_os("OUT_DIR").unwrap()).join("InterfaceObjectMapPhf.rs");
let mut phf = File::create(phf).unwrap(); let mut phf = File::create(phf).unwrap();
writeln!( writeln!(
&mut phf, &mut phf,
"pub(crate) static MAP: phf::Map<&'static [u8], fn(JSContext, HandleObject)> = {};", "pub(crate) static MAP: phf::Map<&'static [u8], Interface> = {};",
map.build(), map.build(),
) )
.unwrap(); .unwrap();

View file

@ -2972,7 +2972,8 @@ class CGConstructorEnabled(CGAbstractMethod):
'ConstructorEnabled', 'bool', 'ConstructorEnabled', 'bool',
[Argument("SafeJSContext", "aCx"), [Argument("SafeJSContext", "aCx"),
Argument("HandleObject", "aObj")], Argument("HandleObject", "aObj")],
templateArgs=['D: DomTypes']) templateArgs=['D: DomTypes'],
pub=True)
def definition_body(self): def definition_body(self):
conditions = [] conditions = []
@ -8531,8 +8532,7 @@ class GlobalGenRoots():
def InterfaceObjectMap(config): def InterfaceObjectMap(config):
mods = [ mods = [
"crate::dom::bindings::codegen", "crate::dom::bindings::codegen",
"crate::script_runtime::JSContext", "script_bindings::interfaces::Interface",
"js::rust::HandleObject",
] ]
imports = CGList([CGGeneric(f"use {mod};") for mod in mods], "\n") imports = CGList([CGGeneric(f"use {mod};") for mod in mods], "\n")
@ -8555,9 +8555,13 @@ class GlobalGenRoots():
for ctor in d.interface.legacyFactoryFunctions: for ctor in d.interface.legacyFactoryFunctions:
pairs.append((ctor.identifier.name, binding_mod, binding_ns)) pairs.append((ctor.identifier.name, binding_mod, binding_ns))
pairs.sort(key=operator.itemgetter(0)) pairs.sort(key=operator.itemgetter(0))
def bindingPath(pair):
return f'codegen::Bindings::{pair[1]}::{pair[2]}'
mappings = [ mappings = [
CGGeneric(f'"{pair[0]}": "codegen::Bindings::{pair[1]}' CGGeneric(f'"{pair[0]}": ["{bindingPath(pair)}::DefineDOMInterface::<crate::DomTypeHolder>", '
f'::{pair[2]}::DefineDOMInterface::<crate::DomTypeHolder>"') f'"{bindingPath(pair)}::ConstructorEnabled::<crate::DomTypeHolder>"]')
for pair in pairs for pair in pairs
] ]
return CGWrapper( return CGWrapper(

View file

@ -23,6 +23,17 @@ use crate::script_runtime::{CanGc, JSContext};
use crate::settings_stack::StackEntry; use crate::settings_stack::StackEntry;
use crate::utils::ProtoOrIfaceArray; use crate::utils::ProtoOrIfaceArray;
/// Operations that can be invoked for a WebIDL interface against
/// a global object.
///
/// <https://github.com/mozilla/gecko-dev/blob/3fd619f47/dom/bindings/WebIDLGlobalNameHash.h#L24>
pub struct Interface {
/// Define the JS object for this interface on the given global.
pub define: fn(JSContext, HandleObject),
/// Returns true if this interface's conditions are met for the given global.
pub enabled: fn(JSContext, HandleObject) -> bool,
}
/// Operations that must be invoked from the generated bindings. /// Operations that must be invoked from the generated bindings.
pub trait DomHelpers<D: DomTypes> { pub trait DomHelpers<D: DomTypes> {
fn throw_dom_exception(cx: JSContext, global: &D::GlobalScope, result: Error, can_gc: CanGc); fn throw_dom_exception(cx: JSContext, global: &D::GlobalScope, result: Error, can_gc: CanGc);
@ -42,7 +53,7 @@ pub trait DomHelpers<D: DomTypes> {
fn is_platform_object_same_origin(cx: JSContext, obj: RawHandleObject) -> bool; fn is_platform_object_same_origin(cx: JSContext, obj: RawHandleObject) -> bool;
fn interface_map() -> &'static phf::Map<&'static [u8], for<'a> fn(JSContext, HandleObject)>; fn interface_map() -> &'static phf::Map<&'static [u8], Interface>;
fn push_new_element_queue(); fn push_new_element_queue();
fn pop_current_element_queue(can_gc: CanGc); fn pop_current_element_queue(can_gc: CanGc);

View file

@ -10,19 +10,19 @@ use std::slice;
use js::conversions::ToJSValConvertible; use js::conversions::ToJSValConvertible;
use js::gc::Handle; use js::gc::Handle;
use js::glue::{ use js::glue::{
CallJitGetterOp, CallJitMethodOp, CallJitSetterOp, JS_GetReservedSlot, AppendToIdVector, CallJitGetterOp, CallJitMethodOp, CallJitSetterOp, JS_GetReservedSlot,
RUST_FUNCTION_VALUE_TO_JITINFO, RUST_FUNCTION_VALUE_TO_JITINFO,
}; };
use js::jsapi::{ use js::jsapi::{
AtomToLinearString, CallArgs, ExceptionStackBehavior, GetLinearStringCharAt, AtomToLinearString, CallArgs, ExceptionStackBehavior, GetLinearStringCharAt,
GetLinearStringLength, GetNonCCWObjectGlobal, HandleId as RawHandleId, GetLinearStringLength, GetNonCCWObjectGlobal, HandleId as RawHandleId,
HandleObject as RawHandleObject, Heap, JS_ClearPendingException, HandleObject as RawHandleObject, Heap, JS_AtomizeStringN, JS_ClearPendingException,
JS_DeprecatedStringHasLatin1Chars, JS_EnumerateStandardClasses, JS_DeprecatedStringHasLatin1Chars, JS_GetLatin1StringCharsAndLength, JS_IsExceptionPending,
JS_GetLatin1StringCharsAndLength, JS_IsExceptionPending, JS_IsGlobalObject, JS_IsGlobalObject, JS_NewEnumerateStandardClasses, JS_ResolveStandardClass, JSAtom, JSContext,
JS_ResolveStandardClass, JSAtom, JSContext, JSJitInfo, JSObject, JSTracer, JSJitInfo, JSObject, JSTracer, MutableHandleIdVector as RawMutableHandleIdVector,
MutableHandleIdVector as RawMutableHandleIdVector, MutableHandleValue as RawMutableHandleValue, MutableHandleValue as RawMutableHandleValue, ObjectOpResult, StringIsArrayIndex,
ObjectOpResult, StringIsArrayIndex,
}; };
use js::jsid::StringId;
use js::jsval::{JSVal, UndefinedValue}; use js::jsval::{JSVal, UndefinedValue};
use js::rust::wrappers::{ use js::rust::wrappers::{
CallOriginalPromiseReject, JS_DeletePropertyById, JS_ForwardGetPropertyTo, CallOriginalPromiseReject, JS_DeletePropertyById, JS_ForwardGetPropertyTo,
@ -576,18 +576,36 @@ impl AsCCharPtrPtr for [u8] {
} }
/// Enumerate lazy properties of a global object. /// Enumerate lazy properties of a global object.
/// Modeled after <https://github.com/mozilla/gecko-dev/blob/3fd619f47/dom/bindings/BindingUtils.cpp#L2814>
/// and <https://github.com/mozilla/gecko-dev/blob/3fd619f47/dom/base/nsGlobalWindowInner.cpp#3297>
pub(crate) unsafe extern "C" fn enumerate_global<D: DomTypes>( pub(crate) unsafe extern "C" fn enumerate_global<D: DomTypes>(
cx: *mut JSContext, cx: *mut JSContext,
obj: RawHandleObject, obj: RawHandleObject,
_props: RawMutableHandleIdVector, props: RawMutableHandleIdVector,
_enumerable_only: bool, enumerable_only: bool,
) -> bool { ) -> bool {
assert!(JS_IsGlobalObject(obj.get())); assert!(JS_IsGlobalObject(obj.get()));
if !JS_EnumerateStandardClasses(cx, obj) { if !JS_NewEnumerateStandardClasses(cx, obj, props, enumerable_only) {
return false; return false;
} }
for init_fun in <D as DomHelpers<D>>::interface_map().values() {
init_fun(SafeJSContext::from_ptr(cx), Handle::from_raw(obj)); if enumerable_only {
// All WebIDL interface names are defined as non-enumerable, so there's
// no point in checking them if we're only returning enumerable names.
return true;
}
let cx = SafeJSContext::from_ptr(cx);
let obj = Handle::from_raw(obj);
for (name, interface) in <D as DomHelpers<D>>::interface_map() {
if !(interface.enabled)(cx, obj) {
continue;
}
let s = JS_AtomizeStringN(*cx, name.as_c_char_ptr(), name.len());
rooted!(in(*cx) let id = StringId(s));
if s.is_null() || !AppendToIdVector(props, id.handle().into()) {
return false;
}
} }
true true
} }
@ -621,8 +639,8 @@ pub(crate) unsafe extern "C" fn resolve_global<D: DomTypes>(
assert!(!ptr.is_null()); assert!(!ptr.is_null());
let bytes = slice::from_raw_parts(ptr, length); let bytes = slice::from_raw_parts(ptr, length);
if let Some(init_fun) = <D as DomHelpers<D>>::interface_map().get(bytes) { if let Some(interface) = <D as DomHelpers<D>>::interface_map().get(bytes) {
init_fun(SafeJSContext::from_ptr(cx), Handle::from_raw(obj)); (interface.define)(SafeJSContext::from_ptr(cx), Handle::from_raw(obj));
*rval = true; *rval = true;
} else { } else {
*rval = false; *rval = false;

View file

@ -16,6 +16,13 @@
{} {}
] ]
], ],
"global-enumerate-crash.html": [
"a77e79b1465bf7555340dd5e9bf94a4c8caa85f2",
[
null,
{}
]
],
"iframe_focus-crash.html": [ "iframe_focus-crash.html": [
"f991b1a563f3cc44870640ab194708fa239ad89d", "f991b1a563f3cc44870640ab194708fa239ad89d",
[ [
@ -13517,7 +13524,7 @@
] ]
], ],
"interfaces.worker.js": [ "interfaces.worker.js": [
"8d109502622fac7266a4564de09684a3ab94118c", "8af942c0f99218fb39770cdcf299eaa230339010",
[ [
"mozilla/interfaces.worker.html", "mozilla/interfaces.worker.html",
{} {}

View file

@ -0,0 +1,15 @@
<html class=test-wait>
<body>
<iframe id="if"></iframe>
<script>
const frame = document.getElementById('if').contentWindow;
for (const i of Object.getOwnPropertyNames(frame)) {
try {
frame[i]['foo'];
} catch (e) {
}
}
document.documentElement.classList.remove("test-wait");
</script>
</body>
</html>

View file

@ -107,6 +107,7 @@ test_interfaces([
"Response", "Response",
"SecurityPolicyViolationEvent", "SecurityPolicyViolationEvent",
"ServiceWorkerContainer", "ServiceWorkerContainer",
"SVGRect",
"TextDecoder", "TextDecoder",
"TextEncoder", "TextEncoder",
"TrustedHTML", "TrustedHTML",
@ -128,6 +129,7 @@ test_interfaces([
"WebGLShaderPrecisionFormat", "WebGLShaderPrecisionFormat",
"WebGLTexture", "WebGLTexture",
"WebGLUniformLocation", "WebGLUniformLocation",
"WebKitCSSMatrix",
"WebSocket", "WebSocket",
"WeakRef", "WeakRef",
"Worker", "Worker",