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