Skip to main content

script_bindings/
proxyhandler.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5//! Utilities for the implementation of JSAPI proxy handlers.
6
7use std::ffi::CString;
8use std::ops::{Deref, DerefMut};
9use std::os::raw::c_char;
10use std::ptr;
11use std::ptr::NonNull;
12
13use js::context::{JSContext, RawJSContext};
14use js::conversions::{ToJSValConvertible, jsstr_to_string};
15use js::glue::{
16    GetProxyHandlerFamily, GetProxyPrivate, GetProxyReservedSlot, SetProxyPrivate,
17    SetProxyReservedSlot, UncheckedUnwrapObject,
18};
19use js::jsapi::{
20    DOMProxyShadowsResult, GetObjectRealmOrNull, GetRealmPrincipals, GetStaticPrototype,
21    Handle as RawHandle, HandleId as RawHandleId, HandleObject as RawHandleObject,
22    HandleValue as RawHandleValue, HandleValueArray, IsWeakMapObject, IsWindowProxy,
23    JS_IsDeadWrapper, JSErrNum, JSFunctionSpec, JSITER_HIDDEN, JSITER_OWNONLY, JSITER_SYMBOLS,
24    JSObject, JSPROP_READONLY, JSPropertySpec, JSString,
25    MutableHandleIdVector as RawMutableHandleIdVector,
26    MutableHandleObject as RawMutableHandleObject, ObjectOpResult, PropertyDescriptor,
27    SetDOMProxyInformation, SymbolCode, jsid,
28};
29use js::jsid::SymbolId;
30use js::jsval::{ObjectValue, UndefinedValue};
31use js::realm::{AutoRealm, CurrentRealm};
32use js::rust::wrappers2::{
33    AppendToIdVector, Call, GetObjectProto, GetPropertyKeys, GetRealmKeyObject, GetWeakMapEntry,
34    GetWellKnownSymbol, JS_AlreadyHasOwnPropertyById, JS_AtomizeAndPinString, JS_DefineFunctions,
35    JS_DefineProperties, JS_DefinePropertyById, JS_DeletePropertyById,
36    JS_GetOwnPropertyDescriptorById, JS_IdToValue, JS_IsExceptionPending,
37    JS_NewObjectWithGivenProto, JS_ValueToSource, JS_WrapObject, JS_WrapValue, NewWeakMapObject,
38    RUST_INTERNED_STRING_TO_JSID, RUST_JSID_IS_VOID, SetDataPropertyDescriptor,
39    SetPropertyIgnoringNamedGetter, SetWeakMapEntry, int_to_jsid,
40};
41use js::rust::{
42    Handle, HandleId, HandleObject, HandleValue, IntoHandle, MutableHandle, MutableHandleObject,
43    MutableHandleValue,
44};
45
46use crate::DomTypes;
47use crate::conversions::{is_dom_proxy, jsid_to_string, native_from_object};
48use crate::error::Error;
49use crate::interfaces::{DomHelpers, GlobalScopeHelpers};
50use crate::principals::ServoJSPrincipalsRef;
51use crate::reflector::DomObject;
52use crate::str::DOMString;
53
54/// In WindowProxy and ProxyObject, the second slot is reserved for the
55/// cross-origin property holder weak map. This is a weak map between the realm
56/// and a property holder which stores objects that are used in cross origin
57/// realms.
58pub const CROSS_ORIGIN_PROPERTY_HOLDER_WEAK_MAP_SLOT: u32 = 1;
59
60/// Determine if this id shadows any existing properties for this proxy.
61///
62/// # Safety
63/// `cx` must point to a valid, non-null JSContext.
64pub(crate) unsafe extern "C" fn shadow_check_callback(
65    cx: *mut RawJSContext,
66    object: RawHandleObject,
67    id: RawHandleId,
68) -> DOMProxyShadowsResult {
69    // TODO: support OverrideBuiltins when #12978 is fixed.
70
71    // SAFETY: it is safe to construct a JSContext from engine hook.
72    let mut cx = unsafe { JSContext::from_ptr(NonNull::new(cx).unwrap()) };
73    let cx = &mut cx;
74
75    let object = unsafe { HandleObject::from_raw(object) };
76    rooted!(&in(cx) let mut expando = ptr::null_mut::<JSObject>());
77    get_expando_object(object, expando.handle_mut());
78    if !expando.get().is_null() {
79        let mut has_own = false;
80        let raw_id = unsafe { Handle::from_raw(id) };
81
82        if !unsafe { JS_AlreadyHasOwnPropertyById(cx, expando.handle(), raw_id, &mut has_own) } {
83            return DOMProxyShadowsResult::ShadowCheckFailed;
84        }
85
86        if has_own {
87            return DOMProxyShadowsResult::ShadowsViaDirectExpando;
88        }
89    }
90
91    // Our expando, if any, didn't shadow, so we're not shadowing at all.
92    DOMProxyShadowsResult::DoesntShadow
93}
94
95/// Initialize the infrastructure for DOM proxy objects.
96pub fn init() {
97    unsafe {
98        SetDOMProxyInformation(
99            GetProxyHandlerFamily(),
100            Some(shadow_check_callback),
101            ptr::null(),
102        );
103    }
104}
105
106/// Defines an expando on the given `proxy`.
107///
108/// # Safety
109/// `cx` must point to a valid, non-null JSContext.
110/// `result` must point to a valid, non-null ObjectOpResult.
111pub(crate) unsafe extern "C" fn define_property(
112    cx: *mut RawJSContext,
113    proxy: RawHandleObject,
114    id: RawHandleId,
115    desc: RawHandle<PropertyDescriptor>,
116    result: *mut ObjectOpResult,
117) -> bool {
118    // SAFETY: it is safe to construct a JSContext from engine hook.
119    let mut cx = unsafe { JSContext::from_ptr(NonNull::new(cx).unwrap()) };
120    let cx = &mut cx;
121
122    let proxy = unsafe { Handle::from_raw(proxy) };
123    let id = unsafe { Handle::from_raw(id) };
124    let desc = unsafe { Handle::from_raw(desc) };
125
126    rooted!(&in(cx) let mut expando = ptr::null_mut::<JSObject>());
127    ensure_expando_object(cx, proxy, expando.handle_mut());
128
129    unsafe { JS_DefinePropertyById(cx, expando.handle(), id, desc, result) }
130}
131
132/// Deletes an expando off the given `proxy`.
133///
134/// # Safety
135/// `cx` must point to a valid, non-null JSContext.
136/// `bp` must point to a valid, non-null ObjectOpResult.
137pub(crate) unsafe extern "C" fn delete(
138    cx: *mut RawJSContext,
139    proxy: RawHandleObject,
140    id: RawHandleId,
141    bp: *mut ObjectOpResult,
142) -> bool {
143    // SAFETY: it is safe to construct a JSContext from engine hook.
144    let mut cx = unsafe { JSContext::from_ptr(NonNull::new(cx).unwrap()) };
145    let cx = &mut cx;
146
147    let proxy = unsafe { Handle::from_raw(proxy) };
148    let id = unsafe { Handle::from_raw(id) };
149
150    rooted!(&in(cx) let mut expando = ptr::null_mut::<JSObject>());
151    get_expando_object(proxy, expando.handle_mut());
152
153    if expando.is_null() {
154        unsafe {
155            (*bp).code_ = 0 /* OkCode */
156        };
157        return true;
158    }
159
160    unsafe { JS_DeletePropertyById(cx, expando.handle(), id, bp) }
161}
162
163/// Controls whether the Extensible bit can be changed
164///
165/// [`Location`]: https://html.spec.whatwg.org/multipage/#location-preventextensions
166/// [`WindowProxy`]: <https://html.spec.whatwg.org/multipage/#windowproxy-preventextensions>
167///
168/// # Safety
169/// `result` must point to a valid, non-null ObjectOpResult.
170pub unsafe extern "C" fn prevent_extensions(
171    _cx: *mut RawJSContext,
172    _proxy: RawHandleObject,
173    result: *mut ObjectOpResult,
174) -> bool {
175    unsafe { (*result).code_ = JSErrNum::JSMSG_CANT_PREVENT_EXTENSIONS as ::libc::uintptr_t };
176    true
177}
178
179/// Reports whether the object is Extensible
180///
181/// [`Location`]: https://html.spec.whatwg.org/multipage/#location-isextensible
182/// [`WindowProxy`]: <https://html.spec.whatwg.org/multipage/#windowproxy-isextensible>
183///
184/// # Safety
185/// `succeeded` must point to a valid, non-null bool.
186pub unsafe extern "C" fn is_extensible(
187    _cx: *mut RawJSContext,
188    _proxy: RawHandleObject,
189    succeeded: *mut bool,
190) -> bool {
191    unsafe { *succeeded = true };
192    true
193}
194
195/// If `proxy` (underneath any functionally-transparent wrapper proxies) has as
196/// its `[[GetPrototypeOf]]` trap the ordinary `[[GetPrototypeOf]]` behavior
197/// defined for ordinary objects, set `*is_ordinary` to true and store `obj`'s
198/// prototype in `proto`.  Otherwise set `*isOrdinary` to false. In case of
199/// error, both outparams have unspecified value.
200///
201/// This implementation always handles the case of the ordinary
202/// `[[GetPrototypeOf]]` behavior. An alternative implementation will be
203/// necessary for maybe-cross-origin objects.
204///
205/// # Safety
206/// `is_ordinary` must point to a valid, non-null bool.
207pub(crate) unsafe extern "C" fn get_prototype_if_ordinary(
208    _: *mut RawJSContext,
209    proxy: RawHandleObject,
210    is_ordinary: *mut bool,
211    proto: RawMutableHandleObject,
212) -> bool {
213    unsafe { *is_ordinary = true };
214    proto.set(unsafe { GetStaticPrototype(proxy.get()) });
215    true
216}
217
218/// Get the expando object, or null if there is none.
219pub(crate) fn get_expando_object(obj: HandleObject, mut expando: MutableHandleObject) {
220    unsafe {
221        assert!(is_dom_proxy(obj.get()));
222        let val = &mut UndefinedValue();
223        GetProxyPrivate(obj.get(), val);
224        expando.set(if val.is_undefined() {
225            ptr::null_mut()
226        } else {
227            val.to_object()
228        });
229    }
230}
231
232/// Get the expando object, or create it if it doesn't exist yet.
233/// Fails on JSAPI failure.
234pub(crate) fn ensure_expando_object(
235    cx: &mut JSContext,
236    obj: HandleObject,
237    mut expando: MutableHandleObject,
238) {
239    unsafe {
240        assert!(is_dom_proxy(obj.get()));
241        get_expando_object(obj, expando.reborrow());
242        if expando.is_null() {
243            expando.set(JS_NewObjectWithGivenProto(
244                cx,
245                ptr::null_mut(),
246                HandleObject::null(),
247            ));
248            assert!(!expando.is_null());
249
250            SetProxyPrivate(obj.get(), &ObjectValue(expando.get()));
251        }
252    }
253}
254
255/// Set the property descriptor's object to `obj` and set it to enumerable,
256/// and writable if `readonly` is true.
257pub fn set_property_descriptor(
258    desc: MutableHandle<PropertyDescriptor>,
259    value: HandleValue,
260    attrs: u32,
261    is_none: &mut bool,
262) {
263    unsafe { SetDataPropertyDescriptor(desc, value, attrs) };
264    *is_none = false;
265}
266
267fn id_to_source(cx: &mut JSContext, id: HandleId) -> Option<DOMString> {
268    unsafe {
269        if RUST_JSID_IS_VOID(id) {
270            return None;
271        }
272        rooted!(&in(cx) let mut value = UndefinedValue());
273        rooted!(&in(cx) let mut jsstr = ptr::null_mut::<JSString>());
274        JS_IdToValue(cx, id.get(), value.handle_mut())
275            .then(|| {
276                jsstr.set(JS_ValueToSource(cx, value.handle()));
277                jsstr.get()
278            })
279            .and_then(NonNull::new)
280            .map(|jsstr| jsstr_to_string(cx, jsstr).into())
281    }
282}
283
284/// Property and method specs that correspond to the elements of
285/// [`CrossOriginProperties(O)`].
286///
287/// [`CrossOriginProperties(O)`]: https://html.spec.whatwg.org/multipage/#crossoriginproperties-(-o-)
288pub struct CrossOriginProperties {
289    pub(crate) attributes: &'static [JSPropertySpec],
290    pub(crate) methods: &'static [JSFunctionSpec],
291}
292
293impl CrossOriginProperties {
294    /// Enumerate the property keys defined by `self`.
295    fn keys(&self) -> impl Iterator<Item = *const c_char> + '_ {
296        // Safety: All cross-origin property keys are strings, not symbols
297        self.attributes
298            .iter()
299            .map(|spec| unsafe { spec.name.string_ })
300            .chain(self.methods.iter().map(|spec| unsafe { spec.name.string_ }))
301            .filter(|ptr| !ptr.is_null())
302    }
303}
304
305/// Implementation of [`CrossOriginOwnPropertyKeys`].
306///
307/// [`CrossOriginOwnPropertyKeys`]: https://html.spec.whatwg.org/multipage/#crossoriginownpropertykeys-(-o-)
308pub fn cross_origin_own_property_keys(
309    cx: &mut JSContext,
310    _proxy: HandleObject,
311    cross_origin_properties: &'static CrossOriginProperties,
312    props: RawMutableHandleIdVector,
313) -> bool {
314    // > 2. For each `e` of `! CrossOriginProperties(O)`, append
315    // >    `e.[[Property]]` to `keys`.
316    for key in cross_origin_properties.keys() {
317        unsafe {
318            rooted!(&in(cx) let rooted = JS_AtomizeAndPinString(cx, key));
319            rooted!(&in(cx) let mut rooted_jsid: jsid);
320            RUST_INTERNED_STRING_TO_JSID(cx, rooted.handle().get(), rooted_jsid.handle_mut());
321            AppendToIdVector(props, rooted_jsid.handle());
322        }
323    }
324
325    // > 3. Return the concatenation of `keys` and `« "then", @@toStringTag,
326    // > @@hasInstance, @@isConcatSpreadable »`.
327    append_cross_origin_allowlisted_prop_keys(cx, props);
328
329    true
330}
331
332/// # Safety
333/// `is_ordinary` must point to a valid, non-null bool.
334pub unsafe extern "C" fn maybe_cross_origin_get_prototype_if_ordinary_rawcx(
335    _: *mut RawJSContext,
336    _proxy: RawHandleObject,
337    is_ordinary: *mut bool,
338    _proto: RawMutableHandleObject,
339) -> bool {
340    // We have a custom `[[GetPrototypeOf]]`, so return `false`
341    unsafe { *is_ordinary = false };
342    true
343}
344
345/// Implementation of `[[SetPrototypeOf]]` for [`Location`] and [`WindowProxy`].
346///
347/// [`Location`]: https://html.spec.whatwg.org/multipage/#location-setprototypeof
348/// [`WindowProxy`]: https://html.spec.whatwg.org/multipage/#windowproxy-setprototypeof
349///
350/// # Safety
351/// `result` must point to a valid, non-null ObjectOpResult.
352pub unsafe extern "C" fn maybe_cross_origin_set_prototype_rawcx(
353    cx: *mut RawJSContext,
354    proxy: RawHandleObject,
355    proto: RawHandleObject,
356    result: *mut ObjectOpResult,
357) -> bool {
358    // SAFETY: it is safe to construct a JSContext from engine hook.
359    let mut cx = unsafe { JSContext::from_ptr(NonNull::new(cx).unwrap()) };
360    let cx = &mut cx;
361    // > 1. Return `! SetImmutablePrototype(this, V)`.
362    //
363    // <https://tc39.es/ecma262/#sec-set-immutable-prototype>:
364    //
365    // > 1. Assert: Either `Type(V)` is Object or `Type(V)` is Null.
366    //
367    // > 2. Let current be `? O.[[GetPrototypeOf]]()`.
368    rooted!(&in(cx) let mut current = ptr::null_mut::<JSObject>());
369    if !unsafe { GetObjectProto(cx, Handle::from_raw(proxy), current.handle_mut()) } {
370        return false;
371    }
372
373    // > 3. If `SameValue(V, current)` is true, return true.
374    if proto.get() == current.get() {
375        unsafe {
376            (*result).code_ = 0 /* OkCode */
377        };
378        return true;
379    }
380
381    // > 4. Return false.
382    unsafe { (*result).code_ = JSErrNum::JSMSG_CANT_SET_PROTO as usize };
383    true
384}
385
386fn get_getter_object(d: &PropertyDescriptor, out: RawMutableHandleObject) {
387    if d.hasGetter_() {
388        out.set(d.getter_);
389    }
390}
391
392fn get_setter_object(d: &PropertyDescriptor, out: RawMutableHandleObject) {
393    if d.hasSetter_() {
394        out.set(d.setter_);
395    }
396}
397
398/// <https://tc39.es/ecma262/#sec-isaccessordescriptor>
399fn is_accessor_descriptor(d: &PropertyDescriptor) -> bool {
400    d.hasSetter_() || d.hasGetter_()
401}
402
403/// <https://tc39.es/ecma262/#sec-isdatadescriptor>
404fn is_data_descriptor(d: &PropertyDescriptor) -> bool {
405    d.hasWritable_() || d.hasValue_()
406}
407
408/// Evaluate whether proxy has the own property 'id' in the cross-origin case.
409///
410/// `cx` and `proxy` are expected to be different-Realm here. `proxy` is a proxy
411/// for a maybe-cross-origin object.
412pub(crate) fn cross_origin_has_own<D: DomTypes>(
413    cx: &mut CurrentRealm,
414    proxy: HandleObject,
415    cross_origin_properties: &'static CrossOriginProperties,
416    id: HandleId,
417    exists: &mut bool,
418) -> bool {
419    rooted!(&in(cx) let mut property_descriptor = PropertyDescriptor::default());
420    let mut is_none = false;
421    if !cross_origin_get_own_property_helper(
422        cx,
423        proxy,
424        cross_origin_properties,
425        id,
426        property_descriptor.handle_mut(),
427        &mut is_none,
428    ) {
429        return false;
430    }
431
432    if is_none &&
433        !cross_origin_property_fallback::<D>(
434            cx,
435            proxy,
436            id,
437            property_descriptor.handle_mut(),
438            &mut is_none,
439        )
440    {
441        return false;
442    }
443
444    *exists = !is_none;
445    true
446}
447
448/// Implementation of [`CrossOriginGetOwnPropertyHelper`].
449///
450/// `cx` and `proxy` are expected to be different-Realm here. `proxy` is a proxy
451/// for a maybe-cross-origin object.
452///
453/// [`CrossOriginGetOwnPropertyHelper`]: https://html.spec.whatwg.org/multipage/#crossorigingetownpropertyhelper-(-o,-p-)
454pub fn cross_origin_get_own_property_helper(
455    cx: &mut CurrentRealm,
456    proxy: HandleObject,
457    cross_origin_properties: &'static CrossOriginProperties,
458    id: HandleId,
459    desc: MutableHandle<PropertyDescriptor>,
460    is_none: &mut bool,
461) -> bool {
462    rooted!(&in(cx) let mut holder = ptr::null_mut::<JSObject>());
463    if !ensure_cross_origin_property_holder(cx, proxy, cross_origin_properties, holder.handle_mut())
464    {
465        return false;
466    }
467    unsafe { JS_GetOwnPropertyDescriptorById(cx, holder.handle(), id, desc, is_none) }
468}
469
470const ALLOWLISTED_SYMBOL_CODES: &[SymbolCode] = &[
471    SymbolCode::toStringTag,
472    SymbolCode::hasInstance,
473    SymbolCode::isConcatSpreadable,
474];
475
476fn is_cross_origin_allowlisted_prop(cx: &mut JSContext, id: HandleId) -> bool {
477    unsafe {
478        if jsid_to_string(cx, id).is_some_and(|st| st == "then") {
479            return true;
480        }
481
482        rooted!(&in(cx) let mut allowed_id: jsid);
483        ALLOWLISTED_SYMBOL_CODES.iter().any(|&allowed_code| {
484            allowed_id.set(SymbolId(GetWellKnownSymbol(cx, allowed_code)));
485            // `jsid`s containing `JS::Symbol *` can be compared by
486            // referential equality
487            allowed_id.get().asBits_ == id.asBits_
488        })
489    }
490}
491
492/// Append `« "then", @@toStringTag, @@hasInstance, @@isConcatSpreadable »` to
493/// `props`. This is used to implement [`CrossOriginOwnPropertyKeys`].
494///
495/// [`CrossOriginOwnPropertyKeys`]: https://html.spec.whatwg.org/multipage/#crossoriginownpropertykeys-(-o-)
496fn append_cross_origin_allowlisted_prop_keys(cx: &mut JSContext, props: RawMutableHandleIdVector) {
497    unsafe {
498        rooted!(&in(cx) let mut id: jsid);
499
500        let jsstring = JS_AtomizeAndPinString(cx, c"then".as_ptr());
501        rooted!(&in(cx) let rooted = jsstring);
502        RUST_INTERNED_STRING_TO_JSID(cx, rooted.handle().get(), id.handle_mut());
503        AppendToIdVector(props, id.handle());
504
505        for &allowed_code in ALLOWLISTED_SYMBOL_CODES.iter() {
506            id.set(SymbolId(GetWellKnownSymbol(cx, allowed_code)));
507            AppendToIdVector(props, id.handle());
508        }
509    }
510}
511
512/// Get the holder for cross-origin properties for the current global of the
513/// `JSContext`, creating one and storing it in a slot of the proxy object if it
514/// doesn't exist yet.
515///
516/// This essentially creates a cache of [`CrossOriginGetOwnPropertyHelper`]'s
517/// results for all property keys.
518///
519/// `cx` and `proxy` are expected to be different-Realm here. `proxy` is a proxy
520/// for a maybe-cross-origin object. The `out_holder` return value will always
521/// be in the Realm of `cx`.
522///
523/// [`CrossOriginGetOwnPropertyHelper`]: https://html.spec.whatwg.org/multipage/#crossorigingetownpropertyhelper-(-o,-p-)
524fn ensure_cross_origin_property_holder(
525    cx: &mut CurrentRealm,
526    proxy: HandleObject,
527    cross_origin_properties: &'static CrossOriginProperties,
528    mut out_holder: MutableHandleObject,
529) -> bool {
530    rooted!(&in(cx) let mut weak_value_map = UndefinedValue());
531    unsafe {
532        GetProxyReservedSlot(
533            proxy.get(),
534            CROSS_ORIGIN_PROPERTY_HOLDER_WEAK_MAP_SLOT,
535            weak_value_map.as_ptr(),
536        )
537    };
538    if weak_value_map.is_undefined() {
539        let mut realm = AutoRealm::new_from_handle(cx, proxy);
540        let new_map = unsafe { NewWeakMapObject(&mut realm) };
541        if new_map.is_null() {
542            return false;
543        }
544        weak_value_map.set(ObjectValue(new_map));
545        unsafe {
546            SetProxyReservedSlot(
547                proxy.get(),
548                CROSS_ORIGIN_PROPERTY_HOLDER_WEAK_MAP_SLOT,
549                weak_value_map.as_ptr(),
550            )
551        };
552    }
553
554    rooted!(&in(cx) let map = weak_value_map.to_object());
555    debug_assert!(unsafe { IsWeakMapObject(map.get()) });
556
557    // We need to be in "map"'s compartment to work with it.  Per spec, the key
558    // for this map is supposed to be the pair (current settings, relevant
559    // settings).  The current settings corresponds to the current Realm of cx.
560    // The relevant settings corresponds to the Realm of "obj", but since all of
561    // our objects are per-Realm singletons, we are basically using "obj" itself
562    // as part of the key.
563    //
564    // To represent the current settings, we use a dedicated key object of the
565    // current-Realm.
566    //
567    // We can't use the current global, because we can't get a useful
568    // cross-compartment wrapper for it; such wrappers would always go
569    // through a WindowProxy and would not be guarantee to keep pointing to a
570    // single Realm when unwrapped.  We want to grab this key before we start
571    // changing Realms.
572    //
573    // Also we can't use arbitrary object (e.g.: Object.prototype), because at
574    // this point those compartments are not same-origin, and don't have access to
575    // each other, and the object retrieved here will be wrapped by a security
576    // wrapper below, and the wrapper will be stored into the cache
577    // (see Compartment::wrap).  Those compartments can get access later by
578    // modifying `document.domain`, and wrapping objects after that point
579    // shouldn't result in a security wrapper.  Wrap operation looks up the
580    // existing wrapper in the cache, that contains the security wrapper created
581    // here.  We should use unique/private object here, so that this doesn't
582    // affect later wrap operation.
583    rooted!(&in(cx) let mut key = unsafe { GetRealmKeyObject(cx) });
584    if key.is_null() {
585        return false;
586    }
587
588    rooted!(&in(cx) let mut holder_value = UndefinedValue());
589    {
590        let mut realm = AutoRealm::new_from_handle(cx, map.handle());
591        let cx = &mut realm;
592
593        if !unsafe { JS_WrapObject(cx, key.handle_mut()) } {
594            return false;
595        }
596
597        rooted!(&in(cx) let key_value = ObjectValue(key.get()));
598        if !unsafe {
599            GetWeakMapEntry(
600                cx,
601                map.handle(),
602                key_value.handle(),
603                holder_value.handle_mut(),
604            )
605        } {
606            return false;
607        }
608    }
609
610    if holder_value.is_object() {
611        // We want to do an unchecked unwrap, because the holder (and the current
612        // caller) may actually be more privileged than our map.
613        out_holder.set(unsafe { UncheckedUnwrapObject(holder_value.to_object(), true) });
614
615        // holder might be a dead object proxy if things got nuked.
616        if !unsafe { JS_IsDeadWrapper(out_holder.get()) } {
617            return true;
618        }
619    }
620
621    // We didn't find a usable holder. Go ahead and allocate one. At this point
622    // we have two options: we could allocate the holder in the current Realm
623    // and store a cross-compartment wrapper for it in the map as needed, or we
624    // could allocate the holder in the Realm of the map and have it hold
625    // cross-compartment references to all the methods it holds, since those
626    // methods need to be in our current Realm. It seems better to allocate the
627    // holder in our current Realm.
628    out_holder
629        .set(unsafe { JS_NewObjectWithGivenProto(cx, ptr::null_mut(), HandleObject::null()) });
630
631    if out_holder.get().is_null() ||
632        !unsafe {
633            JS_DefineProperties(
634                cx,
635                out_holder.handle(),
636                cross_origin_properties.attributes.as_ptr(),
637            )
638        } ||
639        !unsafe {
640            JS_DefineFunctions(
641                cx,
642                out_holder.handle(),
643                cross_origin_properties.methods.as_ptr(),
644            )
645        }
646    {
647        return false;
648    }
649
650    holder_value.set(ObjectValue(out_holder.get()));
651
652    {
653        let mut realm = AutoRealm::new_from_handle(cx, map.handle());
654        let cx = &mut realm;
655
656        // Key is already in the right Realm, but we need to wrap the value.
657        if !unsafe { JS_WrapValue(cx, holder_value.handle_mut()) } {
658            return false;
659        }
660
661        rooted!(&in(cx) let key_value = ObjectValue(key.get()));
662        if !unsafe { SetWeakMapEntry(cx, map.handle(), key_value.handle(), holder_value.handle()) }
663        {
664            return false;
665        }
666    }
667
668    true
669}
670
671/// Check if `obj` is a `Location` or `Window` object.
672///
673/// IDL operations on a cross-origin object involve [a security check][1].
674///
675/// [1]: https://html.spec.whatwg.org/multipage/#integration-with-idl
676pub(crate) fn is_cross_origin_object<D: DomTypes>(cx: &mut JSContext, obj: HandleObject) -> bool {
677    unsafe { IsWindowProxy(*obj) || native_from_object::<D::Location>(cx, *obj).is_ok() }
678}
679
680/// Report a cross-origin denial for a property, Always returns `false`, so it
681/// can be used as `return report_cross_origin_denial(...);`.
682///
683/// What this function does corresponds to the operations in
684/// <https://html.spec.whatwg.org/multipage/#the-location-interface> denoted as
685/// "Throw a `SecurityError` DOMException".
686pub fn report_cross_origin_denial<D: DomTypes>(
687    cx: &mut CurrentRealm,
688    id: HandleId,
689    access: &str,
690) -> bool {
691    if let Some(id) = id_to_source(cx, id) {
692        debug!(
693            "permission denied to {} property {} on cross-origin object",
694            access,
695            &*id.str(),
696        );
697    } else {
698        debug!("permission denied to {} on cross-origin object", access);
699    }
700    unsafe {
701        if !JS_IsExceptionPending(cx) {
702            let global = D::GlobalScope::from_current_realm(cx);
703            // TODO: include `id` and `access` in the exception message
704            <D as DomHelpers<D>>::throw_dom_exception(cx, &global, Error::Security(None));
705        }
706    }
707    false
708}
709
710/// Implementation of `[[Set]]` for [`Location`].
711///
712/// [`Location`]: https://html.spec.whatwg.org/multipage/#location-set
713pub(crate) unsafe extern "C" fn maybe_cross_origin_set_rawcx<D: DomTypes>(
714    cx: *mut RawJSContext,
715    proxy: RawHandleObject,
716    id: RawHandleId,
717    v: RawHandleValue,
718    receiver: RawHandleValue,
719    result: *mut ObjectOpResult,
720) -> bool {
721    unsafe {
722        // SAFETY: it is safe to construct a JSContext from engine hook.
723        let mut cx = JSContext::from_ptr(NonNull::new(cx).unwrap());
724        let mut realm = CurrentRealm::assert(&mut cx);
725        let proxy = HandleObject::from_raw(proxy);
726        let id = Handle::from_raw(id);
727        let v = Handle::from_raw(v);
728        let receiver = Handle::from_raw(receiver);
729
730        if !is_platform_object_same_origin(&realm, proxy) {
731            return cross_origin_set::<D>(&mut realm, proxy, id, v.into_handle(), receiver, result);
732        }
733
734        // Safe to enter the Realm of proxy now.
735        let mut realm = AutoRealm::new_from_handle(&mut realm, proxy);
736
737        // OrdinarySet
738        // <https://tc39.es/ecma262/#sec-ordinaryset>
739        rooted!(&in(&mut realm) let mut own_desc = PropertyDescriptor::default());
740        let mut is_none = false;
741        if !JS_GetOwnPropertyDescriptorById(
742            &mut realm,
743            proxy,
744            id,
745            own_desc.handle_mut(),
746            &mut is_none,
747        ) {
748            return false;
749        }
750
751        SetPropertyIgnoringNamedGetter(
752            &mut realm,
753            proxy,
754            id,
755            v,
756            receiver,
757            if is_none {
758                None
759            } else {
760                Some(own_desc.handle())
761            },
762            result,
763        )
764    }
765}
766
767/// Implementation of `[[GetPrototypeOf]]` for [`Location`].
768///
769/// [`Location`]: https://html.spec.whatwg.org/multipage/#location-getprototypeof
770/// [`WindowProxy`]: https://html.spec.whatwg.org/multipage/#windowproxy-getprototypeof
771pub fn maybe_cross_origin_get_prototype<D: DomTypes>(
772    cx: &mut CurrentRealm,
773    proxy: HandleObject,
774    get_proto_object: fn(cx: &mut JSContext, global: HandleObject, rval: MutableHandleObject),
775    mut proto: MutableHandleObject,
776) -> bool {
777    // > 1. If ! IsPlatformObjectSameOrigin(this) is true, then return ! OrdinaryGetPrototypeOf(this).
778    if is_platform_object_same_origin(cx, proxy) {
779        let mut realm = AutoRealm::new_from_handle(cx, proxy);
780        let mut realm = realm.current_realm();
781        let global = D::GlobalScope::from_current_realm(&mut realm);
782        get_proto_object(
783            &mut realm,
784            global.reflector().get_jsobject(),
785            proto.reborrow(),
786        );
787        return !proto.is_null();
788    }
789
790    // > 2. Return null.
791    proto.set(ptr::null_mut());
792    true
793}
794
795/// Implementation of [`CrossOriginGet`].
796///
797/// `cx` and `proxy` are expected to be different-Realm here. `proxy` is a proxy
798/// for a maybe-cross-origin object.
799///
800/// [`CrossOriginGet`]: https://html.spec.whatwg.org/multipage/#crossoriginget-(-o,-p,-receiver-)
801pub fn cross_origin_get<D: DomTypes>(
802    cx: &mut CurrentRealm,
803    proxy: HandleObject,
804    receiver: HandleValue,
805    id: HandleId,
806    mut vp: MutableHandleValue,
807) -> bool {
808    // > 1. Let `desc` be `? O.[[GetOwnProperty]](P)`.
809    rooted!(&in(cx) let mut descriptor = PropertyDescriptor::default());
810    let mut is_none = false;
811    if !unsafe {
812        JS_GetOwnPropertyDescriptorById(cx, proxy, id, descriptor.handle_mut(), &mut is_none)
813    } {
814        return false;
815    }
816
817    // > 2. Assert: `desc` is not undefined.
818    assert!(
819        !is_none,
820        "Callees should throw in all cases when they are not finding \
821        a property decriptor"
822    );
823
824    // > 3. If `! IsDataDescriptor(desc)` is true, then return `desc.[[Value]]`.
825    if is_data_descriptor(&descriptor) {
826        vp.set(descriptor.value_);
827        return true;
828    }
829
830    // > 4. Assert: `IsAccessorDescriptor(desc)` is `true`.
831    assert!(is_accessor_descriptor(&descriptor));
832
833    // > 5. Let `getter` be `desc.[[Get]]`.
834    // >
835    // > 6. If `getter` is `undefined`, then throw a `SecurityError`
836    // >    `DOMException`.
837    rooted!(&in(cx) let mut getter = ptr::null_mut::<JSObject>());
838    get_getter_object(&descriptor, getter.handle_mut().into());
839    if getter.get().is_null() {
840        return report_cross_origin_denial::<D>(cx, id, "get");
841    }
842
843    rooted!(&in(cx) let mut getter_jsval = UndefinedValue());
844    getter.get().to_jsval(cx, getter_jsval.handle_mut());
845
846    // > 7. Return `? Call(getter, Receiver)`.
847    unsafe {
848        Call(
849            cx,
850            receiver,
851            getter_jsval.handle(),
852            &HandleValueArray::empty(),
853            vp,
854        )
855    }
856}
857
858/// Implementation of [`CrossOriginSet`].
859///
860/// `cx` and `proxy` are expected to be different-Realm here. `proxy` is a proxy
861/// for a maybe-cross-origin object.
862///
863/// [`CrossOriginSet`]: https://html.spec.whatwg.org/multipage/#crossoriginset-(-o,-p,-v,-receiver-)
864///
865/// # Safety
866/// `result` must point to a valid, non-null `ObjectObResult`.
867pub unsafe fn cross_origin_set<D: DomTypes>(
868    cx: &mut CurrentRealm,
869    proxy: HandleObject,
870    id: HandleId,
871    v: RawHandleValue,
872    receiver: HandleValue,
873    result: *mut ObjectOpResult,
874) -> bool {
875    // > 1. Let desc be ? O.[[GetOwnProperty]](P).
876    rooted!(&in(cx) let mut descriptor = PropertyDescriptor::default());
877    let mut is_none = false;
878    if !unsafe {
879        JS_GetOwnPropertyDescriptorById(cx, proxy, id, descriptor.handle_mut(), &mut is_none)
880    } {
881        return false;
882    }
883
884    // > 2. Assert: desc is not undefined.
885    assert!(
886        !is_none,
887        "Callees should throw in all cases when they are not finding \
888        a property decriptor"
889    );
890
891    // > 3. If desc.[[Set]] is present and its value is not undefined,
892    // >    then: [...]
893    rooted!(&in(cx) let mut setter = ptr::null_mut::<JSObject>());
894    get_setter_object(&descriptor, setter.handle_mut().into());
895    if setter.get().is_null() {
896        // > 4. Throw a "SecurityError" DOMException.
897        return report_cross_origin_denial::<D>(cx, id, "set");
898    }
899
900    rooted!(&in(cx) let mut setter_jsval = UndefinedValue());
901    setter.get().to_jsval(cx, setter_jsval.handle_mut());
902
903    // > 3.1. Perform ? Call(setter, Receiver, «V»).
904    // >
905    // > 3.2. Return true.
906    rooted!(&in(cx) let mut ignored = UndefinedValue());
907    if !unsafe {
908        Call(
909            cx,
910            receiver,
911            setter_jsval.handle(),
912            // FIXME: Our binding lacks `HandleValueArray(Handle<Value>)`
913            // <https://searchfox.org/mozilla-central/rev/072710086ddfe25aa2962c8399fefb2304e8193b/js/public/ValueArray.h#54-55>
914            &HandleValueArray {
915                length_: 1,
916                elements_: v.ptr,
917            },
918            ignored.handle_mut(),
919        )
920    } {
921        return false;
922    }
923
924    unsafe {
925        (*result).code_ = 0 /* OkCode */
926    };
927    true
928}
929
930/// Implementation of [`CrossOriginPropertyFallback`].
931///
932/// `cx` and `proxy` are expected to be different-Realm here. `proxy` is a proxy
933/// for a maybe-cross-origin object.
934///
935/// [`CrossOriginPropertyFallback`]: https://html.spec.whatwg.org/multipage/#crossoriginpropertyfallback-(-p-)
936pub fn cross_origin_property_fallback<D: DomTypes>(
937    cx: &mut CurrentRealm,
938    _proxy: HandleObject,
939    id: HandleId,
940    desc: MutableHandle<PropertyDescriptor>,
941    is_none: &mut bool,
942) -> bool {
943    assert!(*is_none, "why are we being called?");
944
945    // > 1. If P is `then`, `@@toStringTag`, `@@hasInstance`, or
946    // >    `@@isConcatSpreadable`, then return `PropertyDescriptor{ [[Value]]:
947    // >    undefined, [[Writable]]: false, [[Enumerable]]: false,
948    // >    [[Configurable]]: true }`.
949    if is_cross_origin_allowlisted_prop(cx, id) {
950        set_property_descriptor(
951            desc,
952            HandleValue::undefined(),
953            JSPROP_READONLY as u32,
954            is_none,
955        );
956        return true;
957    }
958
959    // > 2. Throw a `SecurityError` `DOMException`.
960    report_cross_origin_denial::<D>(cx, id, "access")
961}
962
963// The types will be rooted in the function using them
964#[cfg_attr(crown, crown::unrooted_must_root_lint::allow_unrooted_interior)]
965pub(crate) struct JSProxyHandlerOwnPropertyKeysConfig<T: DomObject> {
966    pub(crate) indexed_getter_and_length: Option<fn(&T, &mut JSContext) -> u32>,
967    pub(crate) cross_origin: Option<&'static CrossOriginProperties>,
968    pub(crate) unwrapped_proxy: unsafe fn(RawHandleObject) -> *const T,
969    pub(crate) supported_named_properties: Option<fn(*const T, &mut JSContext) -> Vec<DOMString>>,
970}
971
972/// Helper type to keep AutoRealm and &mut CurrentRealm alive with Deref to JSContext
973enum Realm<'a> {
974    AutoRealm(AutoRealm<'a>),
975    CurrentRealm(&'a mut CurrentRealm<'a>),
976}
977
978impl<'cx> Deref for Realm<'cx> {
979    type Target = JSContext;
980
981    fn deref(&'_ self) -> &'_ Self::Target {
982        match self {
983            Realm::AutoRealm(auto_realm) => auto_realm,
984            Realm::CurrentRealm(current_realm) => current_realm,
985        }
986    }
987}
988
989impl<'cx> DerefMut for Realm<'cx> {
990    fn deref_mut(&'_ mut self) -> &'_ mut Self::Target {
991        match self {
992            Realm::AutoRealm(auto_realm) => auto_realm,
993            Realm::CurrentRealm(current_realm) => current_realm,
994        }
995    }
996}
997
998#[expect(non_snake_case)]
999/// SAFETY: cx must point to a valid, non-null JS context.
1000pub(crate) unsafe fn JSProxyHandlerOwnPropertyKeys<T>(
1001    config: JSProxyHandlerOwnPropertyKeysConfig<T>,
1002    cx: *mut RawJSContext,
1003    proxy: RawHandleObject,
1004    props: RawMutableHandleIdVector,
1005) -> bool
1006where
1007    T: DomObject,
1008{
1009    unsafe {
1010        // SAFETY: it is safe to construct a JSContext from engine hook.
1011        let mut cx = JSContext::from_ptr(ptr::NonNull::new(cx).unwrap());
1012        let mut cx = CurrentRealm::assert(&mut cx);
1013        let current_realm = &mut cx;
1014        let unwrapped_proxy = (config.unwrapped_proxy)(proxy);
1015
1016        let proxy = Handle::from_raw(proxy);
1017
1018        let mut cx = if let Some(cross_origin_properties) = config.cross_origin {
1019            if !is_platform_object_same_origin(current_realm, proxy) {
1020                return cross_origin_own_property_keys(
1021                    current_realm,
1022                    proxy,
1023                    cross_origin_properties,
1024                    props,
1025                );
1026            }
1027
1028            // Safe to enter the Realm of proxy now.
1029            let cx = AutoRealm::new_from_handle(current_realm, proxy);
1030            Realm::AutoRealm(cx)
1031        } else {
1032            Realm::CurrentRealm(current_realm)
1033        };
1034
1035        if let Some(length_fn) = config.indexed_getter_and_length {
1036            let length = (length_fn)(&*unwrapped_proxy, &mut cx);
1037            rooted!(&in(cx) let mut rooted_jsid: jsid);
1038            for i in 0..length {
1039                int_to_jsid(i as i32, rooted_jsid.handle_mut());
1040                AppendToIdVector(props, rooted_jsid.handle());
1041            }
1042        }
1043
1044        if let Some(properties) = config.supported_named_properties {
1045            for name in properties(unwrapped_proxy, &mut cx) {
1046                let cstring = CString::new(name).unwrap();
1047                let jsstring = JS_AtomizeAndPinString(&cx, cstring.as_ptr());
1048                rooted!(&in(cx) let rooted = jsstring);
1049                rooted!(&in(cx) let mut rooted_jsid: jsid);
1050                RUST_INTERNED_STRING_TO_JSID(
1051                    &mut cx,
1052                    rooted.handle().get(),
1053                    rooted_jsid.handle_mut(),
1054                );
1055                AppendToIdVector(props, rooted_jsid.handle());
1056            }
1057        }
1058
1059        rooted!(&in(cx) let mut expando = ptr::null_mut::<JSObject>());
1060        get_expando_object(proxy, expando.handle_mut());
1061
1062        if !expando.is_null() &&
1063            !GetPropertyKeys(
1064                &mut cx,
1065                expando.handle(),
1066                JSITER_OWNONLY | JSITER_HIDDEN | JSITER_SYMBOLS,
1067                props,
1068            )
1069        {
1070            return false;
1071        }
1072    }
1073    true
1074}
1075
1076// The types will be rooted in the function using them
1077#[cfg_attr(crown, crown::unrooted_must_root_lint::allow_unrooted_interior)]
1078pub(crate) struct JSProxyHandlerOwnEnumerablePropertyKeysConfig<T: DomObject> {
1079    pub(crate) unwrapped_proxy: unsafe fn(RawHandleObject) -> *const T,
1080    #[expect(clippy::type_complexity)]
1081    pub(crate) indexed_getter_and_length: Option<Box<dyn Fn(&T, &mut JSContext) -> u32>>,
1082    pub(crate) cross_origin: bool,
1083}
1084
1085#[expect(non_snake_case)]
1086pub(crate) fn JSProxyHandlerGetOwnEnumerablePropertyKeys<T>(
1087    config: JSProxyHandlerOwnEnumerablePropertyKeysConfig<T>,
1088    cx: *mut RawJSContext,
1089    proxy: RawHandleObject,
1090    props: RawMutableHandleIdVector,
1091) -> bool
1092where
1093    T: DomObject,
1094{
1095    unsafe {
1096        // SAFETY: it is safe to construct a JSContext from engine hook.
1097        let mut cx = JSContext::from_ptr(ptr::NonNull::new(cx).unwrap());
1098        let unwrapped_proxy = (config.unwrapped_proxy)(proxy);
1099        let mut cx = CurrentRealm::assert(&mut cx);
1100        let current_realm = &mut cx;
1101
1102        let proxy = Handle::from_raw(proxy);
1103
1104        let mut cx = if config.cross_origin {
1105            if !is_platform_object_same_origin(current_realm, proxy) {
1106                // There are no enumerable cross-origin props, so we're done.
1107                return true;
1108            }
1109
1110            // Safe to enter the Realm of proxy now.
1111            let cx = AutoRealm::new_from_handle(current_realm, proxy);
1112            Realm::AutoRealm(cx)
1113        } else {
1114            Realm::CurrentRealm(current_realm)
1115        };
1116        if let Some(length_fn) = config.indexed_getter_and_length {
1117            let length = (length_fn)(&*unwrapped_proxy, &mut cx);
1118            rooted!(&in(cx) let mut rooted_jsid: jsid);
1119            for i in 0..length {
1120                int_to_jsid(i as i32, rooted_jsid.handle_mut());
1121                AppendToIdVector(props, rooted_jsid.handle());
1122            }
1123        }
1124
1125        rooted!(&in(cx) let mut expando = ptr::null_mut::<JSObject>());
1126        get_expando_object(proxy, expando.handle_mut());
1127        if !expando.is_null() &&
1128            !GetPropertyKeys(
1129                &mut cx,
1130                expando.handle(),
1131                JSITER_OWNONLY | JSITER_HIDDEN | JSITER_SYMBOLS,
1132                props,
1133            )
1134        {
1135            return false;
1136        }
1137    }
1138
1139    true
1140}
1141
1142/// <https://html.spec.whatwg.org/multipage/#isplatformobjectsameorigin-(-o-)>
1143pub fn is_platform_object_same_origin(realm: &CurrentRealm, obj: HandleObject) -> bool {
1144    let subject_realm = realm.realm().as_ptr();
1145    let object_realm = unsafe { GetObjectRealmOrNull(*obj) };
1146    assert!(!object_realm.is_null());
1147
1148    if subject_realm == object_realm {
1149        return true;
1150    }
1151
1152    let subject_principals =
1153        unsafe { ServoJSPrincipalsRef::from_raw_unchecked(GetRealmPrincipals(subject_realm)) };
1154    let object_principals =
1155        unsafe { ServoJSPrincipalsRef::from_raw_unchecked(GetRealmPrincipals(object_realm)) };
1156
1157    let subject_origin = subject_principals.origin();
1158    let object_origin = object_principals.origin();
1159
1160    let result = subject_origin.same_origin_domain(&object_origin);
1161    log::trace!(
1162        "object {:p} (realm = {:p}, principalls = {:p}, origin = {:?}) is {} \
1163        with reference to the current Realm (realm = {:p}, principals = {:p}, \
1164        origin = {:?})",
1165        obj.get(),
1166        object_realm,
1167        object_principals.as_raw(),
1168        object_origin.immutable(),
1169        ["NOT same domain-origin", "same domain-origin"][result as usize],
1170        subject_realm,
1171        subject_principals.as_raw(),
1172        subject_origin.immutable()
1173    );
1174
1175    result
1176}