summaryrefslogtreecommitdiffstats
path: root/js/xpconnect/src/ExportHelpers.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'js/xpconnect/src/ExportHelpers.cpp')
-rw-r--r--js/xpconnect/src/ExportHelpers.cpp491
1 files changed, 491 insertions, 0 deletions
diff --git a/js/xpconnect/src/ExportHelpers.cpp b/js/xpconnect/src/ExportHelpers.cpp
new file mode 100644
index 000000000..3dbf83e3b
--- /dev/null
+++ b/js/xpconnect/src/ExportHelpers.cpp
@@ -0,0 +1,491 @@
+/* -*- 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/FileListBinding.h"
+#include "mozilla/dom/StructuredCloneHolder.h"
+#include "nsGlobalWindow.h"
+#include "nsJSUtils.h"
+#include "nsIDOMFileList.h"
+
+using namespace mozilla;
+using namespace mozilla::dom;
+using namespace JS;
+
+namespace xpc {
+
+bool
+IsReflector(JSObject* obj)
+{
+ obj = js::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,
+};
+
+// 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)
+{
+ return IS_INSTANCE_OF(FileList, obj);
+}
+
+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();
+ }
+
+ MOZ_ASSERT_UNREACHABLE("Encountered garbage in the clone stream!");
+ return nullptr;
+ }
+
+ bool CustomWriteHandler(JSContext* aCx,
+ JSStructuredCloneWriter* aWriter,
+ JS::Handle<JSObject*> aObj)
+ {
+ {
+ JS::Rooted<JSObject*> obj(aCx, aObj);
+ Blob* blob = nullptr;
+ if (NS_SUCCEEDED(UNWRAP_OBJECT(Blob, &obj, 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) {
+ if (!mFunctions.append(aObj))
+ return false;
+ return JS_WriteUint32Pair(aWriter, SCTAG_FUNCTION, mFunctions.length() - 1);
+ } else {
+ JS_ReportErrorASCII(aCx, "Permission denied to pass a Function via structured clone");
+ return false;
+ }
+ }
+
+ JS_ReportErrorASCII(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_ReportErrorASCII(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()) {
+ RootedObject obj(cx);
+ if (!JS::Construct(cx, fval, args, &obj))
+ return false;
+ args.rval().setObject(*obj);
+ } 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 = GetJSIDByIndex(cx, XPCJSContext::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_ReportErrorASCII(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 = js::CheckedUnwrap(targetScope);
+ funObj = js::CheckedUnwrap(funObj);
+ if (!targetScope || !funObj) {
+ JS_ReportErrorASCII(cx, "Permission denied to export function into scope");
+ return false;
+ }
+
+ if (js::IsScriptedProxy(targetScope)) {
+ JS_ReportErrorASCII(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_ReportErrorASCII(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_ReportErrorASCII(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_ReportErrorASCII(cx, "Expected an object as the target scope");
+ return false;
+ }
+
+ RootedObject scope(cx, js::CheckedUnwrap(&vobj.toObject()));
+ if (!scope) {
+ JS_ReportErrorASCII(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_ReportErrorASCII(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 */