528 lines
17 KiB
C++
528 lines
17 KiB
C++
/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
|
|
/* vim: set ts=8 sts=4 et sw=4 tw=99: */
|
|
/* 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 http://mozilla.org/MPL/2.0/. */
|
|
|
|
#include "xpcprivate.h"
|
|
#include "WrapperFactory.h"
|
|
#include "AccessCheck.h"
|
|
#include "jsfriendapi.h"
|
|
#include "jswrapper.h"
|
|
#include "js/Proxy.h"
|
|
#include "mozilla/dom/BindingUtils.h"
|
|
#include "mozilla/dom/BlobBinding.h"
|
|
#include "mozilla/dom/File.h"
|
|
#include "mozilla/dom/StructuredCloneHolder.h"
|
|
#ifdef MOZ_NFC
|
|
#include "mozilla/dom/MozNDEFRecord.h"
|
|
#endif
|
|
#include "nsGlobalWindow.h"
|
|
#include "nsJSUtils.h"
|
|
#include "nsIDOMFileList.h"
|
|
|
|
using namespace mozilla;
|
|
using namespace mozilla::dom;
|
|
using namespace JS;
|
|
using namespace js;
|
|
|
|
namespace xpc {
|
|
|
|
bool
|
|
IsReflector(JSObject* obj)
|
|
{
|
|
obj = CheckedUnwrap(obj, /* stopAtWindowProxy = */ false);
|
|
if (!obj)
|
|
return false;
|
|
return IS_WN_REFLECTOR(obj) || dom::IsDOMObject(obj);
|
|
}
|
|
|
|
enum StackScopedCloneTags {
|
|
SCTAG_BASE = JS_SCTAG_USER_MIN,
|
|
SCTAG_REFLECTOR,
|
|
SCTAG_BLOB,
|
|
SCTAG_FUNCTION,
|
|
SCTAG_DOM_NFC_NDEF
|
|
};
|
|
|
|
// The HTML5 structured cloning algorithm includes a few DOM objects, notably
|
|
// FileList. That wouldn't in itself be a reason to support them here,
|
|
// but we've historically supported them for Cu.cloneInto (where we didn't support
|
|
// other reflectors), so we need to continue to do so in the wrapReflectors == false
|
|
// case to maintain compatibility.
|
|
//
|
|
// FileList clones are supposed to give brand new objects, rather than
|
|
// cross-compartment wrappers. For this, our current implementation relies on the
|
|
// fact that these objects are implemented with XPConnect and have one reflector
|
|
// per scope.
|
|
bool IsFileList(JSObject* obj)
|
|
{
|
|
nsISupports* supports = UnwrapReflectorToISupports(obj);
|
|
if (!supports)
|
|
return false;
|
|
nsCOMPtr<nsIDOMFileList> fileList = do_QueryInterface(supports);
|
|
if (fileList)
|
|
return true;
|
|
return false;
|
|
}
|
|
|
|
class MOZ_STACK_CLASS StackScopedCloneData
|
|
: public StructuredCloneHolderBase
|
|
{
|
|
public:
|
|
StackScopedCloneData(JSContext* aCx, StackScopedCloneOptions* aOptions)
|
|
: mOptions(aOptions)
|
|
, mReflectors(aCx)
|
|
, mFunctions(aCx)
|
|
{}
|
|
|
|
~StackScopedCloneData()
|
|
{
|
|
Clear();
|
|
}
|
|
|
|
JSObject* CustomReadHandler(JSContext* aCx,
|
|
JSStructuredCloneReader* aReader,
|
|
uint32_t aTag,
|
|
uint32_t aData)
|
|
{
|
|
if (aTag == SCTAG_REFLECTOR) {
|
|
MOZ_ASSERT(!aData);
|
|
|
|
size_t idx;
|
|
if (!JS_ReadBytes(aReader, &idx, sizeof(size_t)))
|
|
return nullptr;
|
|
|
|
RootedObject reflector(aCx, mReflectors[idx]);
|
|
MOZ_ASSERT(reflector, "No object pointer?");
|
|
MOZ_ASSERT(IsReflector(reflector), "Object pointer must be a reflector!");
|
|
|
|
if (!JS_WrapObject(aCx, &reflector))
|
|
return nullptr;
|
|
|
|
return reflector;
|
|
}
|
|
|
|
if (aTag == SCTAG_FUNCTION) {
|
|
MOZ_ASSERT(aData < mFunctions.length());
|
|
|
|
RootedValue functionValue(aCx);
|
|
RootedObject obj(aCx, mFunctions[aData]);
|
|
|
|
if (!JS_WrapObject(aCx, &obj))
|
|
return nullptr;
|
|
|
|
FunctionForwarderOptions forwarderOptions;
|
|
if (!xpc::NewFunctionForwarder(aCx, JSID_VOIDHANDLE, obj, forwarderOptions,
|
|
&functionValue))
|
|
{
|
|
return nullptr;
|
|
}
|
|
|
|
return &functionValue.toObject();
|
|
}
|
|
|
|
if (aTag == SCTAG_BLOB) {
|
|
MOZ_ASSERT(!aData);
|
|
|
|
size_t idx;
|
|
if (!JS_ReadBytes(aReader, &idx, sizeof(size_t))) {
|
|
return nullptr;
|
|
}
|
|
|
|
nsIGlobalObject* global = xpc::NativeGlobal(JS::CurrentGlobalOrNull(aCx));
|
|
MOZ_ASSERT(global);
|
|
|
|
// RefPtr<File> needs to go out of scope before toObjectOrNull() is called because
|
|
// otherwise the static analysis thinks it can gc the JSObject via the stack.
|
|
JS::Rooted<JS::Value> val(aCx);
|
|
{
|
|
RefPtr<Blob> blob = Blob::Create(global, mBlobImpls[idx]);
|
|
if (!ToJSValue(aCx, blob, &val)) {
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
return val.toObjectOrNull();
|
|
}
|
|
|
|
if (aTag == SCTAG_DOM_NFC_NDEF) {
|
|
#ifdef MOZ_NFC
|
|
nsIGlobalObject* global = xpc::NativeGlobal(JS::CurrentGlobalOrNull(aCx));
|
|
if (!global) {
|
|
return nullptr;
|
|
}
|
|
|
|
// Prevent the return value from being trashed by a GC during ~nsRefPtr.
|
|
JS::Rooted<JSObject*> result(aCx);
|
|
{
|
|
RefPtr<MozNDEFRecord> ndefRecord = new MozNDEFRecord(global);
|
|
result = ndefRecord->ReadStructuredClone(aCx, aReader) ?
|
|
ndefRecord->WrapObject(aCx, nullptr) : nullptr;
|
|
}
|
|
return result;
|
|
#else
|
|
return nullptr;
|
|
#endif
|
|
}
|
|
|
|
MOZ_ASSERT_UNREACHABLE("Encountered garbage in the clone stream!");
|
|
return nullptr;
|
|
}
|
|
|
|
bool CustomWriteHandler(JSContext* aCx,
|
|
JSStructuredCloneWriter* aWriter,
|
|
JS::Handle<JSObject*> aObj)
|
|
{
|
|
{
|
|
Blob* blob = nullptr;
|
|
if (NS_SUCCEEDED(UNWRAP_OBJECT(Blob, aObj, blob))) {
|
|
BlobImpl* blobImpl = blob->Impl();
|
|
MOZ_ASSERT(blobImpl);
|
|
|
|
if (!mBlobImpls.AppendElement(blobImpl))
|
|
return false;
|
|
|
|
size_t idx = mBlobImpls.Length() - 1;
|
|
return JS_WriteUint32Pair(aWriter, SCTAG_BLOB, 0) &&
|
|
JS_WriteBytes(aWriter, &idx, sizeof(size_t));
|
|
}
|
|
}
|
|
|
|
if ((mOptions->wrapReflectors && IsReflector(aObj)) ||
|
|
IsFileList(aObj))
|
|
{
|
|
if (!mReflectors.append(aObj))
|
|
return false;
|
|
|
|
size_t idx = mReflectors.length() - 1;
|
|
if (!JS_WriteUint32Pair(aWriter, SCTAG_REFLECTOR, 0))
|
|
return false;
|
|
if (!JS_WriteBytes(aWriter, &idx, sizeof(size_t)))
|
|
return false;
|
|
return true;
|
|
}
|
|
|
|
if (JS::IsCallable(aObj)) {
|
|
if (mOptions->cloneFunctions) {
|
|
mFunctions.append(aObj);
|
|
return JS_WriteUint32Pair(aWriter, SCTAG_FUNCTION, mFunctions.length() - 1);
|
|
} else {
|
|
JS_ReportError(aCx, "Permission denied to pass a Function via structured clone");
|
|
return false;
|
|
}
|
|
}
|
|
|
|
#ifdef MOZ_NFC
|
|
{
|
|
MozNDEFRecord* ndefRecord;
|
|
if (NS_SUCCEEDED(UNWRAP_OBJECT(MozNDEFRecord, aObj, ndefRecord))) {
|
|
return JS_WriteUint32Pair(aWriter, SCTAG_DOM_NFC_NDEF, 0) &&
|
|
ndefRecord->WriteStructuredClone(aCx, aWriter);
|
|
}
|
|
}
|
|
#endif
|
|
|
|
JS_ReportError(aCx, "Encountered unsupported value type writing stack-scoped structured clone");
|
|
return false;
|
|
}
|
|
|
|
StackScopedCloneOptions* mOptions;
|
|
AutoObjectVector mReflectors;
|
|
AutoObjectVector mFunctions;
|
|
nsTArray<RefPtr<BlobImpl>> mBlobImpls;
|
|
};
|
|
|
|
/*
|
|
* General-purpose structured-cloning utility for cases where the structured
|
|
* clone buffer is only used in stack-scope (that is to say, the buffer does
|
|
* not escape from this function). The stack-scoping allows us to pass
|
|
* references to various JSObjects directly in certain situations without
|
|
* worrying about lifetime issues.
|
|
*
|
|
* This function assumes that |cx| is already entered the compartment we want
|
|
* to clone to, and that |val| may not be same-compartment with cx. When the
|
|
* function returns, |val| is set to the result of the clone.
|
|
*/
|
|
bool
|
|
StackScopedClone(JSContext* cx, StackScopedCloneOptions& options,
|
|
MutableHandleValue val)
|
|
{
|
|
StackScopedCloneData data(cx, &options);
|
|
{
|
|
// For parsing val we have to enter its compartment.
|
|
// (unless it's a primitive)
|
|
Maybe<JSAutoCompartment> ac;
|
|
if (val.isObject()) {
|
|
ac.emplace(cx, &val.toObject());
|
|
} else if (val.isString() && !JS_WrapValue(cx, val)) {
|
|
return false;
|
|
}
|
|
|
|
if (!data.Write(cx, val))
|
|
return false;
|
|
}
|
|
|
|
// Now recreate the clones in the target compartment.
|
|
if (!data.Read(cx, val))
|
|
return false;
|
|
|
|
// Deep-freeze if requested.
|
|
if (options.deepFreeze && val.isObject()) {
|
|
RootedObject obj(cx, &val.toObject());
|
|
if (!JS_DeepFreezeObject(cx, obj))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// Note - This function mirrors the logic of CheckPassToChrome in
|
|
// ChromeObjectWrapper.cpp.
|
|
static bool
|
|
CheckSameOriginArg(JSContext* cx, FunctionForwarderOptions& options, HandleValue v)
|
|
{
|
|
// Consumers can explicitly opt out of this security check. This is used in
|
|
// the web console to allow the utility functions to accept cross-origin Windows.
|
|
if (options.allowCrossOriginArguments)
|
|
return true;
|
|
|
|
// Primitives are fine.
|
|
if (!v.isObject())
|
|
return true;
|
|
RootedObject obj(cx, &v.toObject());
|
|
MOZ_ASSERT(js::GetObjectCompartment(obj) != js::GetContextCompartment(cx),
|
|
"This should be invoked after entering the compartment but before "
|
|
"wrapping the values");
|
|
|
|
// Non-wrappers are fine.
|
|
if (!js::IsWrapper(obj))
|
|
return true;
|
|
|
|
// Wrappers leading back to the scope of the exported function are fine.
|
|
if (js::GetObjectCompartment(js::UncheckedUnwrap(obj)) == js::GetContextCompartment(cx))
|
|
return true;
|
|
|
|
// Same-origin wrappers are fine.
|
|
if (AccessCheck::wrapperSubsumes(obj))
|
|
return true;
|
|
|
|
// Badness.
|
|
JS_ReportError(cx, "Permission denied to pass object to exported function");
|
|
return false;
|
|
}
|
|
|
|
static bool
|
|
FunctionForwarder(JSContext* cx, unsigned argc, Value* vp)
|
|
{
|
|
CallArgs args = CallArgsFromVp(argc, vp);
|
|
|
|
// Grab the options from the reserved slot.
|
|
RootedObject optionsObj(cx, &js::GetFunctionNativeReserved(&args.callee(), 1).toObject());
|
|
FunctionForwarderOptions options(cx, optionsObj);
|
|
if (!options.Parse())
|
|
return false;
|
|
|
|
// Grab and unwrap the underlying callable.
|
|
RootedValue v(cx, js::GetFunctionNativeReserved(&args.callee(), 0));
|
|
RootedObject unwrappedFun(cx, js::UncheckedUnwrap(&v.toObject()));
|
|
|
|
RootedObject thisObj(cx, args.isConstructing() ? nullptr : JS_THIS_OBJECT(cx, vp));
|
|
{
|
|
// We manually implement the contents of CrossCompartmentWrapper::call
|
|
// here, because certain function wrappers (notably content->nsEP) are
|
|
// not callable.
|
|
JSAutoCompartment ac(cx, unwrappedFun);
|
|
|
|
RootedValue thisVal(cx, ObjectOrNullValue(thisObj));
|
|
if (!CheckSameOriginArg(cx, options, thisVal) || !JS_WrapObject(cx, &thisObj))
|
|
return false;
|
|
|
|
for (size_t n = 0; n < args.length(); ++n) {
|
|
if (!CheckSameOriginArg(cx, options, args[n]) || !JS_WrapValue(cx, args[n]))
|
|
return false;
|
|
}
|
|
|
|
RootedValue fval(cx, ObjectValue(*unwrappedFun));
|
|
if (args.isConstructing()) {
|
|
if (!JS::Construct(cx, fval, args, args.rval()))
|
|
return false;
|
|
} else {
|
|
if (!JS_CallFunctionValue(cx, thisObj, fval, args, args.rval()))
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Rewrap the return value into our compartment.
|
|
return JS_WrapValue(cx, args.rval());
|
|
}
|
|
|
|
bool
|
|
NewFunctionForwarder(JSContext* cx, HandleId idArg, HandleObject callable,
|
|
FunctionForwarderOptions& options, MutableHandleValue vp)
|
|
{
|
|
RootedId id(cx, idArg);
|
|
if (id == JSID_VOIDHANDLE)
|
|
id = GetRTIdByIndex(cx, XPCJSRuntime::IDX_EMPTYSTRING);
|
|
|
|
// We have no way of knowing whether the underlying function wants to be a
|
|
// constructor or not, so we just mark all forwarders as constructors, and
|
|
// let the underlying function throw for construct calls if it wants.
|
|
JSFunction* fun = js::NewFunctionByIdWithReserved(cx, FunctionForwarder,
|
|
0, JSFUN_CONSTRUCTOR, id);
|
|
if (!fun)
|
|
return false;
|
|
|
|
// Stash the callable in slot 0.
|
|
AssertSameCompartment(cx, callable);
|
|
RootedObject funobj(cx, JS_GetFunctionObject(fun));
|
|
js::SetFunctionNativeReserved(funobj, 0, ObjectValue(*callable));
|
|
|
|
// Stash the options in slot 1.
|
|
RootedObject optionsObj(cx, options.ToJSObject(cx));
|
|
if (!optionsObj)
|
|
return false;
|
|
js::SetFunctionNativeReserved(funobj, 1, ObjectValue(*optionsObj));
|
|
|
|
vp.setObject(*funobj);
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
ExportFunction(JSContext* cx, HandleValue vfunction, HandleValue vscope, HandleValue voptions,
|
|
MutableHandleValue rval)
|
|
{
|
|
bool hasOptions = !voptions.isUndefined();
|
|
if (!vscope.isObject() || !vfunction.isObject() || (hasOptions && !voptions.isObject())) {
|
|
JS_ReportError(cx, "Invalid argument");
|
|
return false;
|
|
}
|
|
|
|
RootedObject funObj(cx, &vfunction.toObject());
|
|
RootedObject targetScope(cx, &vscope.toObject());
|
|
ExportFunctionOptions options(cx, hasOptions ? &voptions.toObject() : nullptr);
|
|
if (hasOptions && !options.Parse())
|
|
return false;
|
|
|
|
// Restrictions:
|
|
// * We must subsume the scope we are exporting to.
|
|
// * We must subsume the function being exported, because the function
|
|
// forwarder manually circumvents security wrapper CALL restrictions.
|
|
targetScope = CheckedUnwrap(targetScope);
|
|
funObj = CheckedUnwrap(funObj);
|
|
if (!targetScope || !funObj) {
|
|
JS_ReportError(cx, "Permission denied to export function into scope");
|
|
return false;
|
|
}
|
|
|
|
if (js::IsScriptedProxy(targetScope)) {
|
|
JS_ReportError(cx, "Defining property on proxy object is not allowed");
|
|
return false;
|
|
}
|
|
|
|
{
|
|
// We need to operate in the target scope from here on, let's enter
|
|
// its compartment.
|
|
JSAutoCompartment ac(cx, targetScope);
|
|
|
|
// Unwrapping to see if we have a callable.
|
|
funObj = UncheckedUnwrap(funObj);
|
|
if (!JS::IsCallable(funObj)) {
|
|
JS_ReportError(cx, "First argument must be a function");
|
|
return false;
|
|
}
|
|
|
|
RootedId id(cx, options.defineAs);
|
|
if (JSID_IS_VOID(id)) {
|
|
// If there wasn't any function name specified,
|
|
// copy the name from the function being imported.
|
|
JSFunction* fun = JS_GetObjectFunction(funObj);
|
|
RootedString funName(cx, JS_GetFunctionId(fun));
|
|
if (!funName)
|
|
funName = JS_AtomizeAndPinString(cx, "");
|
|
|
|
if (!JS_StringToId(cx, funName, &id))
|
|
return false;
|
|
}
|
|
MOZ_ASSERT(JSID_IS_STRING(id));
|
|
|
|
// The function forwarder will live in the target compartment. Since
|
|
// this function will be referenced from its private slot, to avoid a
|
|
// GC hazard, we must wrap it to the same compartment.
|
|
if (!JS_WrapObject(cx, &funObj))
|
|
return false;
|
|
|
|
// And now, let's create the forwarder function in the target compartment
|
|
// for the function the be exported.
|
|
FunctionForwarderOptions forwarderOptions;
|
|
forwarderOptions.allowCrossOriginArguments = options.allowCrossOriginArguments;
|
|
if (!NewFunctionForwarder(cx, id, funObj, forwarderOptions, rval)) {
|
|
JS_ReportError(cx, "Exporting function failed");
|
|
return false;
|
|
}
|
|
|
|
// We have the forwarder function in the target compartment. If
|
|
// defineAs was set, we also need to define it as a property on
|
|
// the target.
|
|
if (!JSID_IS_VOID(options.defineAs)) {
|
|
if (!JS_DefinePropertyById(cx, targetScope, id, rval,
|
|
JSPROP_ENUMERATE,
|
|
JS_STUBGETTER, JS_STUBSETTER)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Finally we have to re-wrap the exported function back to the caller compartment.
|
|
if (!JS_WrapValue(cx, rval))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
CreateObjectIn(JSContext* cx, HandleValue vobj, CreateObjectInOptions& options,
|
|
MutableHandleValue rval)
|
|
{
|
|
if (!vobj.isObject()) {
|
|
JS_ReportError(cx, "Expected an object as the target scope");
|
|
return false;
|
|
}
|
|
|
|
RootedObject scope(cx, js::CheckedUnwrap(&vobj.toObject()));
|
|
if (!scope) {
|
|
JS_ReportError(cx, "Permission denied to create object in the target scope");
|
|
return false;
|
|
}
|
|
|
|
bool define = !JSID_IS_VOID(options.defineAs);
|
|
|
|
if (define && js::IsScriptedProxy(scope)) {
|
|
JS_ReportError(cx, "Defining property on proxy object is not allowed");
|
|
return false;
|
|
}
|
|
|
|
RootedObject obj(cx);
|
|
{
|
|
JSAutoCompartment ac(cx, scope);
|
|
obj = JS_NewPlainObject(cx);
|
|
if (!obj)
|
|
return false;
|
|
|
|
if (define) {
|
|
if (!JS_DefinePropertyById(cx, scope, options.defineAs, obj,
|
|
JSPROP_ENUMERATE,
|
|
JS_STUBGETTER, JS_STUBSETTER))
|
|
return false;
|
|
}
|
|
}
|
|
|
|
rval.setObject(*obj);
|
|
if (!WrapperFactory::WaiveXrayAndWrap(cx, rval))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
} /* namespace xpc */
|