js/xpconnect/src/ExportHelpers.cpp
author Boris Zbarsky <bzbarsky@mit.edu>
Mon, 10 Jul 2017 18:06:18 -0400
changeset 411894 e3d205dc31f33fd2897fd68ffb9ff15e9e3bc079
parent 378343 7311c06a7271a8f33f4f73aa0c88e8d4806e488b
permissions -rw-r--r--
Bug 1371259. Rejigger DOM object unwrapping to take mutable handles to the JS value/object in a bunch of cases. r=peterv,mccr8 a=jcristau

/* -*- 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, "");
            JS_MarkCrossZoneIdValue(cx, StringValue(funName));

            if (!JS_StringToId(cx, funName, &id))
                return false;
        } else {
            JS_MarkCrossZoneId(cx, id);
        }
        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);
        JS_MarkCrossZoneId(cx, options.defineAs);

        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 */