diff options
Diffstat (limited to 'js/xpconnect/src/ExportHelpers.cpp')
-rw-r--r-- | js/xpconnect/src/ExportHelpers.cpp | 599 |
1 files changed, 599 insertions, 0 deletions
diff --git a/js/xpconnect/src/ExportHelpers.cpp b/js/xpconnect/src/ExportHelpers.cpp new file mode 100644 index 0000000000..a59e9087ec --- /dev/null +++ b/js/xpconnect/src/ExportHelpers.cpp @@ -0,0 +1,599 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "js/CallAndConstruct.h" // JS::Call, JS::Construct, JS::IsCallable +#include "js/Exception.h" +#include "js/PropertyAndElement.h" // JS_DefineProperty, JS_DefinePropertyById +#include "js/Proxy.h" +#include "js/Wrapper.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/BlobBinding.h" +#include "mozilla/dom/BlobImpl.h" +#include "mozilla/dom/File.h" +#include "mozilla/dom/StructuredCloneHolder.h" +#include "nsContentUtils.h" +#include "nsJSUtils.h" +#include "js/Object.h" // JS::GetCompartment + +using namespace mozilla; +using namespace mozilla::dom; +using namespace JS; + +namespace xpc { + +bool IsReflector(JSObject* obj, JSContext* cx) { + obj = js::CheckedUnwrapDynamic(obj, cx, /* stopAtWindowProxy = */ false); + if (!obj) { + return false; + } + return IsWrappedNativeReflector(obj) || dom::IsDOMObject(obj); +} + +enum StackScopedCloneTags : uint32_t { + SCTAG_BASE = JS_SCTAG_USER_MIN, + SCTAG_REFLECTOR, + SCTAG_BLOB, + SCTAG_FUNCTION, +}; + +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, + const JS::CloneDataPolicy& aCloneDataPolicy, + uint32_t aTag, uint32_t aData) override { + 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, aCx), + "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, JS::VoidHandlePropertyKey, 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::CurrentNativeGlobal(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 (NS_WARN_IF(!blob)) { + return nullptr; + } + + 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, + bool* aSameProcessScopeRequired) override { + { + JS::Rooted<JSObject*> obj(aCx, aObj); + Blob* blob = nullptr; + if (NS_SUCCEEDED(UNWRAP_OBJECT(Blob, &obj, blob))) { + BlobImpl* blobImpl = blob->Impl(); + MOZ_ASSERT(blobImpl); + + // XXX(Bug 1631371) Check if this should use a fallible operation as it + // pretended earlier. + mBlobImpls.AppendElement(blobImpl); + + 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, aCx)) { + 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; + RootedObjectVector mReflectors; + RootedObjectVector 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, + HandleObject sourceScope, MutableHandleValue val) { + StackScopedCloneData data(cx, &options); + { + // For parsing val we have to enter (a realm in) its compartment. + JSAutoRealm ar(cx, sourceScope); + if (!data.Write(cx, val)) { + return false; + } + } + + // Now recreate the clones in the target realm. + 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::GetCompartment(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::GetCompartment(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; +} + +// Sanitize the exception on cx (which comes from calling unwrappedFun), if the +// current Realm of cx shouldn't have access to it. unwrappedFun is generally +// _not_ in the current Realm of cx here. +static void MaybeSanitizeException(JSContext* cx, + JS::Handle<JSObject*> unwrappedFun) { + // Ensure that we are not propagating more-privileged exceptions + // to less-privileged code. + nsIPrincipal* callerPrincipal = nsContentUtils::SubjectPrincipal(cx); + + // No need to sanitize uncatchable exceptions, just return. + if (!JS_IsExceptionPending(cx)) { + return; + } + + // Re-enter the unwrappedFun Realm to do get the current exception, so we + // don't end up unnecessarily wrapping exceptions. + { // Scope for JSAutoRealm + JSAutoRealm ar(cx, unwrappedFun); + + JS::ExceptionStack exnStack(cx); + + // If JS::GetPendingExceptionStack returns false, we somehow failed to wrap + // the exception into our compartment. It seems fine to treat this as an + // uncatchable exception by returning without setting any exception on the + // JS context. + if (!JS::GetPendingExceptionStack(cx, &exnStack)) { + JS_ClearPendingException(cx); + return; + } + + // Let through non-objects as-is, because some APIs rely on + // that and accidental exceptions are never non-objects. + if (!exnStack.exception().isObject() || + callerPrincipal->Subsumes(nsContentUtils::ObjectPrincipal( + js::UncheckedUnwrap(&exnStack.exception().toObject())))) { + // Just leave exn as-is. + return; + } + + // Whoever we are throwing the exception to should not have access to + // the exception. Sanitize it. First clear the existing exception. + JS_ClearPendingException(cx); + { // Scope for AutoJSAPI + AutoJSAPI jsapi; + if (jsapi.Init(unwrappedFun)) { + JS::SetPendingExceptionStack(cx, exnStack); + } + // If Init() fails, we can't report the exception, but oh, well. + + // Now just let the AutoJSAPI go out of scope and it will report the + // exception in its destructor. + } + } + + // Now back in our original Realm again, throw a sanitized exception. + ErrorResult rv; + rv.ThrowInvalidStateError("An exception was thrown"); + // Can we provide a better context here? + Unused << rv.MaybeSetPendingException(cx); +} + +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())); + + RootedValue thisVal(cx, NullValue()); + if (!args.isConstructing()) { + RootedObject thisObject(cx); + if (!args.computeThis(cx, &thisObject)) { + return false; + } + thisVal.setObject(*thisObject); + } + + bool ok = true; + { + // We manually implement the contents of CrossCompartmentWrapper::call + // here, because certain function wrappers (notably content->nsEP) are + // not callable. + JSAutoRealm ar(cx, unwrappedFun); + bool crossCompartment = + JS::GetCompartment(unwrappedFun) != JS::GetCompartment(&args.callee()); + if (crossCompartment) { + if (!CheckSameOriginArg(cx, options, thisVal) || + !JS_WrapValue(cx, &thisVal)) { + 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); + ok = JS::Construct(cx, fval, args, &obj); + if (ok) { + args.rval().setObject(*obj); + } + } else { + ok = JS::Call(cx, thisVal, fval, args, args.rval()); + } + } + + // Now that we are back in our original Realm, we can check whether to + // sanitize the exception. + if (!ok) { + MaybeSanitizeException(cx, unwrappedFun); + 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.isVoid()) { + id = GetJSIDByIndex(cx, XPCJSContext::IDX_EMPTYSTRING); + } + + // If our callable is a (possibly wrapped) function, we can give + // the exported thing the right number of args. + unsigned nargs = 0; + RootedObject unwrapped(cx, js::UncheckedUnwrap(callable)); + if (unwrapped) { + if (JSFunction* fun = JS_GetObjectFunction(unwrapped)) { + nargs = JS_GetFunctionArity(fun); + } + } + + // 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, nargs, 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::CheckedUnwrapDynamic(targetScope, cx); + // For the function we can just CheckedUnwrapStatic, because if it's + // not callable we're going to fail out anyway. + funObj = js::CheckedUnwrapStatic(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 realm. + JSAutoRealm ar(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 (id.isVoid()) { + // If there wasn't any function name specified, copy the name from the + // function being imported. But be careful in case the callable we have + // is not actually a JSFunction. + RootedString funName(cx); + JS::Rooted<JSFunction*> fun(cx, JS_GetObjectFunction(funObj)); + if (fun) { + if (!JS_GetFunctionId(cx, fun, &funName)) { + return false; + } + } + 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(id.isString()); + + // 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 (!options.defineAs.isVoid()) { + if (!JS_DefinePropertyById(cx, targetScope, id, rval, JSPROP_ENUMERATE)) { + 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; + } + + // cx represents the caller Realm. + RootedObject scope(cx, js::CheckedUnwrapDynamic(&vobj.toObject(), cx)); + if (!scope) { + JS_ReportErrorASCII( + cx, "Permission denied to create object in the target scope"); + return false; + } + + bool define = !options.defineAs.isVoid(); + + if (define && js::IsScriptedProxy(scope)) { + JS_ReportErrorASCII(cx, "Defining property on proxy object is not allowed"); + return false; + } + + RootedObject obj(cx); + { + JSAutoRealm ar(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)) + return false; + } + } + + rval.setObject(*obj); + if (!WrapperFactory::WaiveXrayAndWrap(cx, rval)) { + return false; + } + + return true; +} + +} /* namespace xpc */ |