#![deny(missing_docs)]
use std::ffi::CStr;
use std::os::raw::c_char;
use std::ptr;
use js::conversions::ToJSValConvertible;
use js::glue::{
GetProxyHandler, GetProxyHandlerFamily, GetProxyPrivate, InvokeGetOwnPropertyDescriptor,
SetProxyPrivate,
};
use js::jsapi;
use js::jsapi::{
jsid, DOMProxyShadowsResult, GetObjectRealmOrNull, GetRealmPrincipals, GetStaticPrototype,
GetWellKnownSymbol, Handle as RawHandle, HandleId as RawHandleId,
HandleObject as RawHandleObject, HandleValue as RawHandleValue, JSAutoRealm, JSContext,
JSErrNum, JSFunctionSpec, JSObject, JSPropertySpec, JS_AtomizeAndPinString,
JS_DefinePropertyById, JS_GetOwnPropertyDescriptorById, JS_IsExceptionPending,
MutableHandle as RawMutableHandle, MutableHandleIdVector as RawMutableHandleIdVector,
MutableHandleObject as RawMutableHandleObject, MutableHandleValue as RawMutableHandleValue,
ObjectOpResult, PropertyDescriptor, SetDOMProxyInformation, SymbolCode,
};
use js::jsid::SymbolId;
use js::jsval::{ObjectValue, UndefinedValue};
use js::rust::wrappers::{
AppendToIdVector, JS_AlreadyHasOwnPropertyById, JS_NewObjectWithGivenProto,
SetDataPropertyDescriptor, RUST_INTERNED_STRING_TO_JSID,
};
use js::rust::{
get_context_realm, Handle, HandleObject, HandleValue, MutableHandle, MutableHandleObject,
};
use crate::dom::bindings::conversions::{is_dom_proxy, jsid_to_string, jsstring_to_str};
use crate::dom::bindings::error::{throw_dom_exception, Error};
use crate::dom::bindings::principals::ServoJSPrincipalsRef;
use crate::dom::bindings::reflector::DomObject;
use crate::dom::bindings::str::DOMString;
use crate::dom::bindings::utils::delete_property_by_id;
use crate::dom::globalscope::GlobalScope;
use crate::realms::{AlreadyInRealm, InRealm};
use crate::script_runtime::JSContext as SafeJSContext;
pub unsafe extern "C" fn shadow_check_callback(
cx: *mut JSContext,
object: RawHandleObject,
id: RawHandleId,
) -> DOMProxyShadowsResult {
rooted!(in(cx) let mut expando = ptr::null_mut::<JSObject>());
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;
}
}
DOMProxyShadowsResult::DoesntShadow
}
pub unsafe fn init() {
SetDOMProxyInformation(
GetProxyHandlerFamily(),
Some(shadow_check_callback),
ptr::null(),
);
}
pub unsafe extern "C" fn define_property(
cx: *mut JSContext,
proxy: RawHandleObject,
id: RawHandleId,
desc: RawHandle<PropertyDescriptor>,
result: *mut ObjectOpResult,
) -> bool {
rooted!(in(cx) let mut expando = ptr::null_mut::<JSObject>());
ensure_expando_object(cx, proxy, expando.handle_mut());
JS_DefinePropertyById(cx, expando.handle().into(), id, desc, result)
}
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::<JSObject>());
get_expando_object(proxy, expando.handle_mut());
if expando.is_null() {
(*bp).code_ = 0 ;
return true;
}
delete_property_by_id(cx, expando.handle(), Handle::from_raw(id), bp)
}
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
}
pub unsafe extern "C" fn is_extensible(
_cx: *mut JSContext,
_proxy: RawHandleObject,
succeeded: *mut bool,
) -> bool {
*succeeded = true;
true
}
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
}
pub unsafe fn get_expando_object(obj: RawHandleObject, mut expando: MutableHandleObject) {
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()
});
}
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);
if expando.is_null() {
expando.set(JS_NewObjectWithGivenProto(
cx,
ptr::null_mut(),
HandleObject::null(),
));
assert!(!expando.is_null());
SetProxyPrivate(obj.get(), &ObjectValue(expando.get()));
}
}
pub fn set_property_descriptor(
desc: MutableHandle<PropertyDescriptor>,
value: HandleValue,
attrs: u32,
is_none: &mut bool,
) {
unsafe {
SetDataPropertyDescriptor(desc, value, attrs);
}
*is_none = false;
}
pub unsafe fn is_platform_object_same_origin(cx: SafeJSContext, obj: RawHandleObject) -> bool {
let subject_realm = get_context_realm(*cx);
let obj_realm = GetObjectRealmOrNull(*obj);
assert!(!obj_realm.is_null());
let subject_principals =
ServoJSPrincipalsRef::from_raw_unchecked(GetRealmPrincipals(subject_realm));
let obj_principals = ServoJSPrincipalsRef::from_raw_unchecked(GetRealmPrincipals(obj_realm));
let subject_origin = subject_principals.origin();
let obj_origin = obj_principals.origin();
let result = subject_origin.same_origin_domain(&obj_origin);
log::trace!(
"object {:p} (realm = {:p}, principalls = {:p}, origin = {:?}) is {} \
with reference to the current Realm (realm = {:p}, principals = {:p}, \
origin = {:?})",
obj.get(),
obj_realm,
obj_principals.as_raw(),
obj_origin.immutable(),
["NOT same domain-origin", "same domain-origin"][result as usize],
subject_realm,
subject_principals.as_raw(),
subject_origin.immutable()
);
result
}
pub unsafe fn report_cross_origin_denial(cx: SafeJSContext, id: RawHandleId, access: &str) -> bool {
debug!(
"permission denied to {} property {} on cross-origin object",
access,
id_to_source(cx, id).as_deref().unwrap_or("< error >"),
);
let in_realm_proof = AlreadyInRealm::assert_for_cx(cx);
if !JS_IsExceptionPending(*cx) {
let global = GlobalScope::from_context(*cx, InRealm::Already(&in_realm_proof));
throw_dom_exception(cx, &global, Error::Security);
}
false
}
unsafe fn id_to_source(cx: SafeJSContext, id: RawHandleId) -> Option<DOMString> {
rooted!(in(*cx) let mut value = UndefinedValue());
rooted!(in(*cx) let mut jsstr = ptr::null_mut::<jsapi::JSString>());
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))
}
pub struct CrossOriginProperties {
pub attributes: &'static [JSPropertySpec],
pub methods: &'static [JSFunctionSpec],
}
impl CrossOriginProperties {
fn keys(&self) -> impl Iterator<Item = *const c_char> + '_ {
self.attributes
.iter()
.map(|spec| unsafe { spec.name.string_ })
.chain(self.methods.iter().map(|spec| unsafe { spec.name.string_ }))
.filter(|ptr| !ptr.is_null())
}
}
pub unsafe fn cross_origin_own_property_keys(
cx: SafeJSContext,
_proxy: RawHandleObject,
cross_origin_properties: &'static CrossOriginProperties,
props: RawMutableHandleIdVector,
) -> bool {
for key in cross_origin_properties.keys() {
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());
}
append_cross_origin_allowlisted_prop_keys(cx, props);
true
}
pub unsafe extern "C" fn maybe_cross_origin_set_rawcx(
cx: *mut JSContext,
proxy: RawHandleObject,
id: RawHandleId,
v: RawHandleValue,
receiver: RawHandleValue,
result: *mut ObjectOpResult,
) -> bool {
let cx = SafeJSContext::from_ptr(cx);
if !is_platform_object_same_origin(cx, proxy) {
return cross_origin_set(cx, proxy, id, v, receiver, result);
}
let _ac = JSAutoRealm::new(*cx, proxy.get());
rooted!(in(*cx) let mut own_desc = PropertyDescriptor::default());
let mut is_none = false;
if !InvokeGetOwnPropertyDescriptor(
GetProxyHandler(*proxy),
*cx,
proxy,
id,
own_desc.handle_mut().into(),
&mut is_none,
) {
return false;
}
js::jsapi::SetPropertyIgnoringNamedGetter(
*cx,
proxy,
id,
v,
receiver,
own_desc.handle().into(),
result,
)
}
pub unsafe extern "C" fn maybe_cross_origin_get_prototype_if_ordinary_rawcx(
_: *mut JSContext,
_proxy: RawHandleObject,
is_ordinary: *mut bool,
_proto: RawMutableHandleObject,
) -> bool {
*is_ordinary = false;
true
}
pub unsafe fn maybe_cross_origin_get_prototype(
cx: SafeJSContext,
proxy: RawHandleObject,
get_proto_object: unsafe fn(cx: SafeJSContext, global: HandleObject, rval: MutableHandleObject),
proto: RawMutableHandleObject,
) -> bool {
if is_platform_object_same_origin(cx, proxy) {
let ac = JSAutoRealm::new(*cx, proxy.get());
let global = GlobalScope::from_context(*cx, InRealm::Entered(&ac));
get_proto_object(
cx,
global.reflector().get_jsobject(),
MutableHandleObject::from_raw(proto),
);
return !proto.is_null();
}
proto.set(ptr::null_mut());
true
}
pub unsafe extern "C" fn maybe_cross_origin_set_prototype_rawcx(
cx: *mut JSContext,
proxy: RawHandleObject,
proto: RawHandleObject,
result: *mut ObjectOpResult,
) -> bool {
rooted!(in(cx) let mut current = ptr::null_mut::<JSObject>());
if !jsapi::GetObjectProto(cx, proxy, current.handle_mut().into()) {
return false;
}
if proto.get() == current.get() {
(*result).code_ = 0 ;
return true;
}
(*result).code_ = JSErrNum::JSMSG_CANT_SET_PROTO as usize;
true
}
pub unsafe fn cross_origin_get(
cx: SafeJSContext,
proxy: RawHandleObject,
receiver: RawHandleValue,
id: RawHandleId,
vp: RawMutableHandleValue,
) -> bool {
rooted!(in(*cx) let mut descriptor = PropertyDescriptor::default());
let mut is_none = false;
if !InvokeGetOwnPropertyDescriptor(
GetProxyHandler(*proxy),
*cx,
proxy,
id,
descriptor.handle_mut().into(),
&mut is_none,
) {
return false;
}
assert!(
!is_none,
"Callees should throw in all cases when they are not finding \
a property decriptor"
);
if is_data_descriptor(&descriptor) {
vp.set(descriptor.value_);
return true;
}
assert!(is_accessor_descriptor(&descriptor));
rooted!(in(*cx) let mut getter = ptr::null_mut::<JSObject>());
get_getter_object(&descriptor, getter.handle_mut().into());
if getter.get().is_null() {
return report_cross_origin_denial(cx, id, "get");
}
rooted!(in(*cx) let mut getter_jsval = UndefinedValue());
getter.get().to_jsval(*cx, getter_jsval.handle_mut());
jsapi::Call(
*cx,
receiver,
getter_jsval.handle().into(),
&jsapi::HandleValueArray::new(),
vp,
)
}
pub unsafe fn cross_origin_set(
cx: SafeJSContext,
proxy: RawHandleObject,
id: RawHandleId,
v: RawHandleValue,
receiver: RawHandleValue,
result: *mut ObjectOpResult,
) -> bool {
rooted!(in(*cx) let mut descriptor = PropertyDescriptor::default());
let mut is_none = false;
if !InvokeGetOwnPropertyDescriptor(
GetProxyHandler(*proxy),
*cx,
proxy,
id,
descriptor.handle_mut().into(),
&mut is_none,
) {
return false;
}
assert!(
!is_none,
"Callees should throw in all cases when they are not finding \
a property decriptor"
);
rooted!(in(*cx) let mut setter = ptr::null_mut::<JSObject>());
get_setter_object(&descriptor, setter.handle_mut().into());
if setter.get().is_null() {
return report_cross_origin_denial(cx, id, "set");
}
rooted!(in(*cx) let mut setter_jsval = UndefinedValue());
setter.get().to_jsval(*cx, setter_jsval.handle_mut());
rooted!(in(*cx) let mut ignored = UndefinedValue());
if !jsapi::Call(
*cx,
receiver,
setter_jsval.handle().into(),
&jsapi::HandleValueArray {
length_: 1,
elements_: v.ptr,
},
ignored.handle_mut().into(),
) {
return false;
}
(*result).code_ = 0 ;
true
}
unsafe fn get_getter_object(d: &PropertyDescriptor, out: RawMutableHandleObject) {
if d.hasGetter_() {
out.set(d.getter_);
}
}
unsafe fn get_setter_object(d: &PropertyDescriptor, out: RawMutableHandleObject) {
if d.hasSetter_() {
out.set(d.setter_);
}
}
fn is_accessor_descriptor(d: &PropertyDescriptor) -> bool {
d.hasSetter_() || d.hasGetter_()
}
fn is_data_descriptor(d: &PropertyDescriptor) -> bool {
d.hasWritable_() || d.hasValue_()
}
pub unsafe fn cross_origin_has_own(
cx: SafeJSContext,
_proxy: RawHandleObject,
cross_origin_properties: &'static CrossOriginProperties,
id: RawHandleId,
bp: *mut bool,
) -> bool {
*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
}
pub unsafe fn cross_origin_get_own_property_helper(
cx: SafeJSContext,
proxy: RawHandleObject,
cross_origin_properties: &'static CrossOriginProperties,
id: RawHandleId,
desc: RawMutableHandle<PropertyDescriptor>,
is_none: &mut bool,
) -> bool {
rooted!(in(*cx) let mut holder = ptr::null_mut::<JSObject>());
ensure_cross_origin_property_holder(
cx,
proxy,
cross_origin_properties,
holder.handle_mut().into(),
);
return JS_GetOwnPropertyDescriptorById(*cx, holder.handle().into(), id, desc, is_none);
}
pub unsafe fn cross_origin_property_fallback(
cx: SafeJSContext,
_proxy: RawHandleObject,
id: RawHandleId,
desc: RawMutableHandle<PropertyDescriptor>,
is_none: &mut bool,
) -> bool {
assert!(*is_none, "why are we being called?");
if is_cross_origin_allowlisted_prop(cx, id) {
set_property_descriptor(
MutableHandle::from_raw(desc),
HandleValue::undefined(),
jsapi::JSPROP_READONLY as u32,
is_none,
);
return true;
}
report_cross_origin_denial(cx, id, "access")
}
const ALLOWLISTED_SYMBOL_CODES: &[SymbolCode] = &[
SymbolCode::toStringTag,
SymbolCode::hasInstance,
SymbolCode::isConcatSpreadable,
];
unsafe fn is_cross_origin_allowlisted_prop(cx: SafeJSContext, id: RawHandleId) -> bool {
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.handle_mut() = SymbolId(GetWellKnownSymbol(*cx, allowed_code));
allowed_id.get().asBits_ == id.asBits_
})
}
unsafe fn append_cross_origin_allowlisted_prop_keys(
cx: SafeJSContext,
props: RawMutableHandleIdVector,
) {
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.handle_mut() = SymbolId(GetWellKnownSymbol(*cx, allowed_code));
AppendToIdVector(props, id.handle());
}
}
unsafe fn ensure_cross_origin_property_holder(
cx: SafeJSContext,
_proxy: RawHandleObject,
cross_origin_properties: &'static CrossOriginProperties,
out_holder: RawMutableHandleObject,
) -> bool {
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;
}
true
}