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