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