/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ //! Utilities for the implementation of JSAPI proxy handlers. use std::ffi::CStr; use std::os::raw::c_char; use std::ptr; use js::glue::{GetProxyHandlerFamily, GetProxyPrivate, SetProxyPrivate}; use js::jsapi::{ DOMProxyShadowsResult, GetStaticPrototype, GetWellKnownSymbol, Handle as RawHandle, HandleId as RawHandleId, HandleObject as RawHandleObject, JS_AtomizeAndPinString, JS_DefinePropertyById, JS_GetOwnPropertyDescriptorById, JSContext, JSErrNum, JSFunctionSpec, JSObject, JSPropertySpec, MutableHandle as RawMutableHandle, MutableHandleIdVector as RawMutableHandleIdVector, MutableHandleObject as RawMutableHandleObject, ObjectOpResult, PropertyDescriptor, SetDOMProxyInformation, SymbolCode, jsid, }; use js::jsid::SymbolId; use js::jsval::{ObjectValue, UndefinedValue}; use js::rust::wrappers::{ AppendToIdVector, JS_AlreadyHasOwnPropertyById, JS_NewObjectWithGivenProto, RUST_INTERNED_STRING_TO_JSID, SetDataPropertyDescriptor, }; use js::rust::{Handle, HandleObject, HandleValue, MutableHandle, MutableHandleObject}; use js::{jsapi, rooted}; use crate::conversions::{is_dom_proxy, jsid_to_string, jsstring_to_str}; use crate::script_runtime::JSContext as SafeJSContext; use crate::str::DOMString; use crate::utils::delete_property_by_id; /// Determine if this id shadows any existing properties for this proxy. /// /// # Safety /// `cx` must point to a valid, non-null JSContext. pub unsafe extern "C" fn shadow_check_callback( cx: *mut JSContext, object: RawHandleObject, id: RawHandleId, ) -> DOMProxyShadowsResult { // TODO: support OverrideBuiltins when #12978 is fixed. rooted!(in(cx) let mut expando = ptr::null_mut::()); get_expando_object(object, expando.handle_mut()); if !expando.get().is_null() { let mut has_own = false; let raw_id = Handle::from_raw(id); if !JS_AlreadyHasOwnPropertyById(cx, expando.handle(), raw_id, &mut has_own) { return DOMProxyShadowsResult::ShadowCheckFailed; } if has_own { return DOMProxyShadowsResult::ShadowsViaDirectExpando; } } // Our expando, if any, didn't shadow, so we're not shadowing at all. DOMProxyShadowsResult::DoesntShadow } /// Initialize the infrastructure for DOM proxy objects. pub fn init() { unsafe { SetDOMProxyInformation( GetProxyHandlerFamily(), Some(shadow_check_callback), ptr::null(), ); } } /// Defines an expando on the given `proxy`. /// /// # Safety /// `cx` must point to a valid, non-null JSContext. /// `result` must point to a valid, non-null ObjectOpResult. pub unsafe extern "C" fn define_property( cx: *mut JSContext, proxy: RawHandleObject, id: RawHandleId, desc: RawHandle, result: *mut ObjectOpResult, ) -> bool { rooted!(in(cx) let mut expando = ptr::null_mut::()); ensure_expando_object(cx, proxy, expando.handle_mut()); JS_DefinePropertyById(cx, expando.handle().into(), id, desc, result) } /// Deletes an expando off the given `proxy`. /// /// # Safety /// `cx` must point to a valid, non-null JSContext. /// `bp` must point to a valid, non-null ObjectOpResult. pub unsafe extern "C" fn delete( cx: *mut JSContext, proxy: RawHandleObject, id: RawHandleId, bp: *mut ObjectOpResult, ) -> bool { rooted!(in(cx) let mut expando = ptr::null_mut::()); get_expando_object(proxy, expando.handle_mut()); if expando.is_null() { (*bp).code_ = 0 /* OkCode */; return true; } delete_property_by_id(cx, expando.handle(), Handle::from_raw(id), bp) } /// Controls whether the Extensible bit can be changed /// /// # Safety /// `result` must point to a valid, non-null ObjectOpResult. pub unsafe extern "C" fn prevent_extensions( _cx: *mut JSContext, _proxy: RawHandleObject, result: *mut ObjectOpResult, ) -> bool { (*result).code_ = JSErrNum::JSMSG_CANT_PREVENT_EXTENSIONS as ::libc::uintptr_t; true } /// Reports whether the object is Extensible /// /// # Safety /// `succeeded` must point to a valid, non-null bool. pub unsafe extern "C" fn is_extensible( _cx: *mut JSContext, _proxy: RawHandleObject, succeeded: *mut bool, ) -> bool { *succeeded = true; true } /// If `proxy` (underneath any functionally-transparent wrapper proxies) has as /// its `[[GetPrototypeOf]]` trap the ordinary `[[GetPrototypeOf]]` behavior /// defined for ordinary objects, set `*is_ordinary` to true and store `obj`'s /// prototype in `proto`. Otherwise set `*isOrdinary` to false. In case of /// error, both outparams have unspecified value. /// /// This implementation always handles the case of the ordinary /// `[[GetPrototypeOf]]` behavior. An alternative implementation will be /// necessary for maybe-cross-origin objects. /// /// # Safety /// `is_ordinary` must point to a valid, non-null bool. pub unsafe extern "C" fn get_prototype_if_ordinary( _: *mut JSContext, proxy: RawHandleObject, is_ordinary: *mut bool, proto: RawMutableHandleObject, ) -> bool { *is_ordinary = true; proto.set(GetStaticPrototype(proxy.get())); true } /// Get the expando object, or null if there is none. pub fn get_expando_object(obj: RawHandleObject, mut expando: MutableHandleObject) { unsafe { assert!(is_dom_proxy(obj.get())); let val = &mut UndefinedValue(); GetProxyPrivate(obj.get(), val); expando.set(if val.is_undefined() { ptr::null_mut() } else { val.to_object() }); } } /// Get the expando object, or create it if it doesn't exist yet. /// Fails on JSAPI failure. /// /// # Safety /// `cx` must point to a valid, non-null JSContext. pub unsafe fn ensure_expando_object( cx: *mut JSContext, obj: RawHandleObject, mut expando: MutableHandleObject, ) { assert!(is_dom_proxy(obj.get())); get_expando_object(obj, expando.reborrow()); if expando.is_null() { expando.set(JS_NewObjectWithGivenProto( cx, ptr::null_mut(), HandleObject::null(), )); assert!(!expando.is_null()); SetProxyPrivate(obj.get(), &ObjectValue(expando.get())); } } /// Set the property descriptor's object to `obj` and set it to enumerable, /// and writable if `readonly` is true. pub fn set_property_descriptor( desc: MutableHandle, value: HandleValue, attrs: u32, is_none: &mut bool, ) { unsafe { SetDataPropertyDescriptor(desc, value, attrs); } *is_none = false; } pub fn id_to_source(cx: SafeJSContext, id: RawHandleId) -> Option { unsafe { rooted!(in(*cx) let mut value = UndefinedValue()); rooted!(in(*cx) let mut jsstr = ptr::null_mut::()); jsapi::JS_IdToValue(*cx, id.get(), value.handle_mut().into()) .then(|| { jsstr.set(jsapi::JS_ValueToSource(*cx, value.handle().into())); jsstr.get() }) .and_then(ptr::NonNull::new) .map(|jsstr| jsstring_to_str(*cx, jsstr)) } } /// Property and method specs that correspond to the elements of /// [`CrossOriginProperties(O)`]. /// /// [`CrossOriginProperties(O)`]: https://html.spec.whatwg.org/multipage/#crossoriginproperties-(-o-) pub struct CrossOriginProperties { pub attributes: &'static [JSPropertySpec], pub methods: &'static [JSFunctionSpec], } impl CrossOriginProperties { /// Enumerate the property keys defined by `self`. fn keys(&self) -> impl Iterator + '_ { // Safety: All cross-origin property keys are strings, not symbols self.attributes .iter() .map(|spec| unsafe { spec.name.string_ }) .chain(self.methods.iter().map(|spec| unsafe { spec.name.string_ })) .filter(|ptr| !ptr.is_null()) } } /// Implementation of [`CrossOriginOwnPropertyKeys`]. /// /// [`CrossOriginOwnPropertyKeys`]: https://html.spec.whatwg.org/multipage/#crossoriginownpropertykeys-(-o-) pub fn cross_origin_own_property_keys( cx: SafeJSContext, _proxy: RawHandleObject, cross_origin_properties: &'static CrossOriginProperties, props: RawMutableHandleIdVector, ) -> bool { // > 2. For each `e` of `! CrossOriginProperties(O)`, append // > `e.[[Property]]` to `keys`. for key in cross_origin_properties.keys() { unsafe { rooted!(in(*cx) let rooted = JS_AtomizeAndPinString(*cx, key)); rooted!(in(*cx) let mut rooted_jsid: jsid); RUST_INTERNED_STRING_TO_JSID(*cx, rooted.handle().get(), rooted_jsid.handle_mut()); AppendToIdVector(props, rooted_jsid.handle()); } } // > 3. Return the concatenation of `keys` and `« "then", @@toStringTag, // > @@hasInstance, @@isConcatSpreadable »`. append_cross_origin_allowlisted_prop_keys(cx, props); true } /// # Safety /// `is_ordinary` must point to a valid, non-null bool. pub unsafe extern "C" fn maybe_cross_origin_get_prototype_if_ordinary_rawcx( _: *mut JSContext, _proxy: RawHandleObject, is_ordinary: *mut bool, _proto: RawMutableHandleObject, ) -> bool { // We have a custom `[[GetPrototypeOf]]`, so return `false` *is_ordinary = false; true } /// Implementation of `[[SetPrototypeOf]]` for [`Location`] and [`WindowProxy`]. /// /// [`Location`]: https://html.spec.whatwg.org/multipage/#location-setprototypeof /// [`WindowProxy`]: https://html.spec.whatwg.org/multipage/#windowproxy-setprototypeof /// /// # Safety /// `result` must point to a valid, non-null ObjectOpResult. pub unsafe extern "C" fn maybe_cross_origin_set_prototype_rawcx( cx: *mut JSContext, proxy: RawHandleObject, proto: RawHandleObject, result: *mut ObjectOpResult, ) -> bool { // > 1. Return `! SetImmutablePrototype(this, V)`. // // : // // > 1. Assert: Either `Type(V)` is Object or `Type(V)` is Null. // // > 2. Let current be `? O.[[GetPrototypeOf]]()`. rooted!(in(cx) let mut current = ptr::null_mut::()); if !jsapi::GetObjectProto(cx, proxy, current.handle_mut().into()) { return false; } // > 3. If `SameValue(V, current)` is true, return true. if proto.get() == current.get() { (*result).code_ = 0 /* OkCode */; return true; } // > 4. Return false. (*result).code_ = JSErrNum::JSMSG_CANT_SET_PROTO as usize; true } pub fn get_getter_object(d: &PropertyDescriptor, out: RawMutableHandleObject) { if d.hasGetter_() { out.set(d.getter_); } } pub fn get_setter_object(d: &PropertyDescriptor, out: RawMutableHandleObject) { if d.hasSetter_() { out.set(d.setter_); } } /// pub fn is_accessor_descriptor(d: &PropertyDescriptor) -> bool { d.hasSetter_() || d.hasGetter_() } /// pub fn is_data_descriptor(d: &PropertyDescriptor) -> bool { d.hasWritable_() || d.hasValue_() } /// Evaluate `CrossOriginGetOwnPropertyHelper(proxy, id) != null`. /// SpiderMonkey-specific. /// /// `cx` and `proxy` are expected to be different-Realm here. `proxy` is a proxy /// for a maybe-cross-origin object. /// /// # Safety /// `bp` must point to a valid, non-null bool. pub unsafe fn cross_origin_has_own( cx: SafeJSContext, _proxy: RawHandleObject, cross_origin_properties: &'static CrossOriginProperties, id: RawHandleId, bp: *mut bool, ) -> bool { // TODO: Once we have the slot for the holder, it'd be more efficient to // use `ensure_cross_origin_property_holder`. We'll need `_proxy` to // do that. *bp = jsid_to_string(*cx, Handle::from_raw(id)).is_some_and(|key| { cross_origin_properties.keys().any(|defined_key| { let defined_key = CStr::from_ptr(defined_key); defined_key.to_bytes() == key.as_bytes() }) }); true } /// Implementation of [`CrossOriginGetOwnPropertyHelper`]. /// /// `cx` and `proxy` are expected to be different-Realm here. `proxy` is a proxy /// for a maybe-cross-origin object. /// /// [`CrossOriginGetOwnPropertyHelper`]: https://html.spec.whatwg.org/multipage/#crossorigingetownpropertyhelper-(-o,-p-) pub fn cross_origin_get_own_property_helper( cx: SafeJSContext, proxy: RawHandleObject, cross_origin_properties: &'static CrossOriginProperties, id: RawHandleId, desc: RawMutableHandle, is_none: &mut bool, ) -> bool { rooted!(in(*cx) let mut holder = ptr::null_mut::()); ensure_cross_origin_property_holder( cx, proxy, cross_origin_properties, holder.handle_mut().into(), ); unsafe { JS_GetOwnPropertyDescriptorById(*cx, holder.handle().into(), id, desc, is_none) } } const ALLOWLISTED_SYMBOL_CODES: &[SymbolCode] = &[ SymbolCode::toStringTag, SymbolCode::hasInstance, SymbolCode::isConcatSpreadable, ]; pub fn is_cross_origin_allowlisted_prop(cx: SafeJSContext, id: RawHandleId) -> bool { unsafe { if jsid_to_string(*cx, Handle::from_raw(id)).is_some_and(|st| st == "then") { return true; } rooted!(in(*cx) let mut allowed_id: jsid); ALLOWLISTED_SYMBOL_CODES.iter().any(|&allowed_code| { allowed_id.set(SymbolId(GetWellKnownSymbol(*cx, allowed_code))); // `jsid`s containing `JS::Symbol *` can be compared by // referential equality allowed_id.get().asBits_ == id.asBits_ }) } } /// Append `« "then", @@toStringTag, @@hasInstance, @@isConcatSpreadable »` to /// `props`. This is used to implement [`CrossOriginOwnPropertyKeys`]. /// /// [`CrossOriginOwnPropertyKeys`]: https://html.spec.whatwg.org/multipage/#crossoriginownpropertykeys-(-o-) fn append_cross_origin_allowlisted_prop_keys(cx: SafeJSContext, props: RawMutableHandleIdVector) { unsafe { rooted!(in(*cx) let mut id: jsid); let jsstring = JS_AtomizeAndPinString(*cx, c"then".as_ptr()); rooted!(in(*cx) let rooted = jsstring); RUST_INTERNED_STRING_TO_JSID(*cx, rooted.handle().get(), id.handle_mut()); AppendToIdVector(props, id.handle()); for &allowed_code in ALLOWLISTED_SYMBOL_CODES.iter() { id.set(SymbolId(GetWellKnownSymbol(*cx, allowed_code))); AppendToIdVector(props, id.handle()); } } } /// Get the holder for cross-origin properties for the current global of the /// `JSContext`, creating one and storing it in a slot of the proxy object if it /// doesn't exist yet. /// /// This essentially creates a cache of [`CrossOriginGetOwnPropertyHelper`]'s /// results for all property keys. /// /// `cx` and `proxy` are expected to be different-Realm here. `proxy` is a proxy /// for a maybe-cross-origin object. The `out_holder` return value will always /// be in the Realm of `cx`. /// /// [`CrossOriginGetOwnPropertyHelper`]: https://html.spec.whatwg.org/multipage/#crossorigingetownpropertyhelper-(-o,-p-) fn ensure_cross_origin_property_holder( cx: SafeJSContext, _proxy: RawHandleObject, cross_origin_properties: &'static CrossOriginProperties, out_holder: RawMutableHandleObject, ) -> bool { // TODO: We don't have the slot to store the holder yet. For now, // the holder is constructed every time this function is called, // which is not only inefficient but also deviates from the // specification in a subtle yet observable way. // Create a holder for the current Realm unsafe { out_holder.set(jsapi::JS_NewObjectWithGivenProto( *cx, ptr::null_mut(), RawHandleObject::null(), )); if out_holder.get().is_null() || !jsapi::JS_DefineProperties( *cx, out_holder.handle(), cross_origin_properties.attributes.as_ptr(), ) || !jsapi::JS_DefineFunctions( *cx, out_holder.handle(), cross_origin_properties.methods.as_ptr(), ) { return false; } } // TODO: Store the holder in the slot that we don't have yet. true }