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