diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /js/xpconnect/src/XPCWrappedJSClass.cpp | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'js/xpconnect/src/XPCWrappedJSClass.cpp')
-rw-r--r-- | js/xpconnect/src/XPCWrappedJSClass.cpp | 1149 |
1 files changed, 1149 insertions, 0 deletions
diff --git a/js/xpconnect/src/XPCWrappedJSClass.cpp b/js/xpconnect/src/XPCWrappedJSClass.cpp new file mode 100644 index 0000000000..072d0c06ed --- /dev/null +++ b/js/xpconnect/src/XPCWrappedJSClass.cpp @@ -0,0 +1,1149 @@ +/* -*- 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/. */ + +/* Sharable code and data for wrapper around JSObjects. */ + +#include "xpcprivate.h" +#include "js/Object.h" // JS::GetClass +#include "js/Printf.h" +#include "nsArrayEnumerator.h" +#include "nsINamed.h" +#include "nsIScriptError.h" +#include "nsWrapperCache.h" +#include "AccessCheck.h" +#include "nsJSUtils.h" +#include "nsPrintfCString.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/BindingUtils.h" +#include "mozilla/dom/DOMException.h" +#include "mozilla/dom/DOMExceptionBinding.h" +#include "mozilla/dom/MozQueryInterface.h" + +#include "jsapi.h" +#include "jsfriendapi.h" + +using namespace xpc; +using namespace JS; +using namespace mozilla; +using namespace mozilla::dom; + +bool AutoScriptEvaluate::StartEvaluating(HandleObject scope) { + MOZ_ASSERT(!mEvaluated, + "AutoScriptEvaluate::Evaluate should only be called once"); + + if (!mJSContext) { + return true; + } + + mEvaluated = true; + + mAutoRealm.emplace(mJSContext, scope); + + // Saving the exception state keeps us from interfering with another script + // that may also be running on this context. This occurred first with the + // js debugger, as described in + // http://bugzilla.mozilla.org/show_bug.cgi?id=88130 but presumably could + // show up in any situation where a script calls into a wrapped js component + // on the same context, while the context has a nonzero exception state. + mState.emplace(mJSContext); + + return true; +} + +AutoScriptEvaluate::~AutoScriptEvaluate() { + if (!mJSContext || !mEvaluated) { + return; + } + mState->restore(); +} + +// It turns out that some errors may be not worth reporting. So, this +// function is factored out to manage that. +bool xpc_IsReportableErrorCode(nsresult code) { + if (NS_SUCCEEDED(code)) { + return false; + } + + switch (code) { + // Error codes that we don't want to report as errors... + // These generally indicate bad interface design AFAIC. + case NS_ERROR_FACTORY_REGISTER_AGAIN: + case NS_BASE_STREAM_WOULD_BLOCK: + return false; + default: + return true; + } +} + +// A little stack-based RAII class to help management of the XPCJSContext +// PendingResult. +class MOZ_STACK_CLASS AutoSavePendingResult { + public: + explicit AutoSavePendingResult(XPCJSContext* xpccx) : mXPCContext(xpccx) { + // Save any existing pending result and reset to NS_OK for this invocation. + mSavedResult = xpccx->GetPendingResult(); + xpccx->SetPendingResult(NS_OK); + } + ~AutoSavePendingResult() { mXPCContext->SetPendingResult(mSavedResult); } + + private: + XPCJSContext* mXPCContext; + nsresult mSavedResult; +}; + +// static +const nsXPTInterfaceInfo* nsXPCWrappedJS::GetInterfaceInfo(REFNSIID aIID) { + const nsXPTInterfaceInfo* info = nsXPTInterfaceInfo::ByIID(aIID); + if (!info || info->IsBuiltinClass()) { + return nullptr; + } + + return info; +} + +// static +JSObject* nsXPCWrappedJS::CallQueryInterfaceOnJSObject(JSContext* cx, + JSObject* jsobjArg, + HandleObject scope, + REFNSIID aIID) { + js::AssertSameCompartment(scope, jsobjArg); + + RootedObject jsobj(cx, jsobjArg); + RootedValue arg(cx); + RootedValue retval(cx); + RootedObject retObj(cx); + RootedValue fun(cx); + + // In bug 503926, we added a security check to make sure that we don't + // invoke content QI functions. In the modern world, this is probably + // unnecessary, because invoking QI involves passing an IID object to + // content, which will fail. But we do a belt-and-suspenders check to + // make sure that content can never trigger the rat's nest of code below. + // Once we completely turn off XPConnect for the web, this can definitely + // go away. + if (!AccessCheck::isChrome(jsobj) || + !AccessCheck::isChrome(js::UncheckedUnwrap(jsobj))) { + return nullptr; + } + + // OK, it looks like we'll be calling into JS code. + AutoScriptEvaluate scriptEval(cx); + + // XXX we should install an error reporter that will send reports to + // the JS error console service. + if (!scriptEval.StartEvaluating(scope)) { + return nullptr; + } + + // check upfront for the existence of the function property + HandleId funid = + XPCJSRuntime::Get()->GetStringID(XPCJSContext::IDX_QUERY_INTERFACE); + if (!JS_GetPropertyById(cx, jsobj, funid, &fun) || fun.isPrimitive()) { + return nullptr; + } + + dom::MozQueryInterface* mozQI = nullptr; + if (NS_SUCCEEDED(UNWRAP_OBJECT(MozQueryInterface, &fun, mozQI))) { + if (mozQI->QueriesTo(aIID)) { + return jsobj.get(); + } + return nullptr; + } + + if (!xpc::ID2JSValue(cx, aIID, &arg)) { + return nullptr; + } + + // Throwing NS_NOINTERFACE is the prescribed way to fail QI from JS. It is + // not an exception that is ever worth reporting, but we don't want to eat + // all exceptions either. + + bool success = + JS_CallFunctionValue(cx, jsobj, fun, HandleValueArray(arg), &retval); + if (!success && JS_IsExceptionPending(cx)) { + RootedValue jsexception(cx, NullValue()); + + if (JS_GetPendingException(cx, &jsexception)) { + if (jsexception.isObject()) { + // XPConnect may have constructed an object to represent a + // C++ QI failure. See if that is the case. + JS::Rooted<JSObject*> exceptionObj(cx, &jsexception.toObject()); + Exception* e = nullptr; + UNWRAP_OBJECT(Exception, &exceptionObj, e); + + if (e && e->GetResult() == NS_NOINTERFACE) { + JS_ClearPendingException(cx); + } + } else if (jsexception.isNumber()) { + nsresult rv; + // JS often throws an nsresult. + if (jsexception.isDouble()) + // Visual Studio 9 doesn't allow casting directly from + // a double to an enumeration type, contrary to + // 5.2.9(10) of C++11, so add an intermediate cast. + rv = (nsresult)(uint32_t)(jsexception.toDouble()); + else + rv = (nsresult)(jsexception.toInt32()); + + if (rv == NS_NOINTERFACE) JS_ClearPendingException(cx); + } + } + } else if (!success) { + NS_WARNING("QI hook ran OOMed - this is probably a bug!"); + } + + if (success) success = JS_ValueToObject(cx, retval, &retObj); + + return success ? retObj.get() : nullptr; +} + +/***************************************************************************/ + +namespace { + +class WrappedJSNamed final : public nsINamed { + nsCString mName; + + ~WrappedJSNamed() = default; + + public: + NS_DECL_ISUPPORTS + + explicit WrappedJSNamed(const nsACString& aName) : mName(aName) {} + + NS_IMETHOD GetName(nsACString& aName) override { + aName = mName; + aName.AppendLiteral(":JS"); + return NS_OK; + } +}; + +NS_IMPL_ISUPPORTS(WrappedJSNamed, nsINamed) + +nsCString GetFunctionName(JSContext* cx, HandleObject obj) { + RootedObject inner(cx, js::UncheckedUnwrap(obj)); + JSAutoRealm ar(cx, inner); + + RootedFunction fun(cx, JS_GetObjectFunction(inner)); + if (!fun) { + // If the object isn't a function, it's likely that it has a single + // function property (for things like nsITimerCallback). In this case, + // return the name of that function property. + + Rooted<IdVector> idArray(cx, IdVector(cx)); + if (!JS_Enumerate(cx, inner, &idArray)) { + JS_ClearPendingException(cx); + return nsCString("error"); + } + + if (idArray.length() != 1) { + return nsCString("nonfunction"); + } + + RootedId id(cx, idArray[0]); + RootedValue v(cx); + if (!JS_GetPropertyById(cx, inner, id, &v)) { + JS_ClearPendingException(cx); + return nsCString("nonfunction"); + } + + if (!v.isObject()) { + return nsCString("nonfunction"); + } + + RootedObject vobj(cx, &v.toObject()); + return GetFunctionName(cx, vobj); + } + + RootedString funName(cx, JS_GetFunctionDisplayId(fun)); + RootedScript script(cx, JS_GetFunctionScript(cx, fun)); + const char* filename = script ? JS_GetScriptFilename(script) : "anonymous"; + const char* filenameSuffix = strrchr(filename, '/'); + + if (filenameSuffix) { + filenameSuffix++; + } else { + filenameSuffix = filename; + } + + nsCString displayName("anonymous"); + if (funName) { + RootedValue funNameVal(cx, StringValue(funName)); + if (!XPCConvert::JSData2Native(cx, &displayName, funNameVal, + {nsXPTType::T_UTF8STRING}, nullptr, 0, + nullptr)) { + JS_ClearPendingException(cx); + return nsCString("anonymous"); + } + } + + displayName.Append('['); + displayName.Append(filenameSuffix, strlen(filenameSuffix)); + displayName.Append(']'); + return displayName; +} + +} // anonymous namespace + +/***************************************************************************/ + +// static +nsresult nsXPCWrappedJS::DelegatedQueryInterface(REFNSIID aIID, + void** aInstancePtr) { + if (aIID.Equals(NS_GET_IID(nsIXPConnectJSObjectHolder))) { + // This needs to call NS_ADDREF directly instead of using nsCOMPtr<>, + // because the latter does a QI in an assert, and we're already in a QI, so + // it would cause infinite recursion. + NS_ADDREF(this); + *aInstancePtr = (void*)static_cast<nsIXPConnectJSObjectHolder*>(this); + return NS_OK; + } + + // Ensure that we are asking for a non-builtinclass interface, and avoid even + // setting up our AutoEntryScript if we are. Don't bother doing that check + // if our IID is nsISupports: we know that's not builtinclass, and we QI to + // it a _lot_. + if (!aIID.Equals(NS_GET_IID(nsISupports))) { + const nsXPTInterfaceInfo* info = nsXPTInterfaceInfo::ByIID(aIID); + if (!info || info->IsBuiltinClass()) { + MOZ_ASSERT(!aIID.Equals(NS_GET_IID(nsISupportsWeakReference)), + "Later code for nsISupportsWeakReference is being skipped"); + MOZ_ASSERT(!aIID.Equals(NS_GET_IID(nsISimpleEnumerator)), + "Later code for nsISimpleEnumerator is being skipped"); + MOZ_ASSERT(!aIID.Equals(NS_GET_IID(nsINamed)), + "Later code for nsINamed is being skipped"); + *aInstancePtr = nullptr; + return NS_NOINTERFACE; + } + } + + MOZ_ASSERT(!aIID.Equals(NS_GET_IID(nsWrapperCache)), + "Where did we get non-builtinclass interface info for this??"); + + // QI on an XPCWrappedJS can run script, so we need an AutoEntryScript. + // This is inherently Gecko-specific. + // We check both nativeGlobal and nativeGlobal->GetGlobalJSObject() even + // though we have derived nativeGlobal from the JS global, because we know + // there are cases where this can happen. See bug 1094953. + RootedObject obj(RootingCx(), GetJSObject()); + nsIGlobalObject* nativeGlobal = NativeGlobal(js::UncheckedUnwrap(obj)); + NS_ENSURE_TRUE(nativeGlobal, NS_ERROR_FAILURE); + NS_ENSURE_TRUE(nativeGlobal->HasJSGlobal(), NS_ERROR_FAILURE); + + AutoAllowLegacyScriptExecution exemption; + + AutoEntryScript aes(nativeGlobal, "XPCWrappedJS QueryInterface", + /* aIsMainThread = */ true); + XPCCallContext ccx(aes.cx()); + if (!ccx.IsValid()) { + *aInstancePtr = nullptr; + return NS_NOINTERFACE; + } + + // We now need to enter the realm of the actual JSObject* we are pointing at. + // But that may be a cross-compartment wrapper and therefore not have a + // well-defined realm, so enter the realm of the global that we grabbed back + // when we started pointing to our JSObject*. + RootedObject objScope(RootingCx(), GetJSObjectGlobal()); + JSAutoRealm ar(aes.cx(), objScope); + + // We support nsISupportsWeakReference iff the root wrapped JSObject + // claims to support it in its QueryInterface implementation. + if (aIID.Equals(NS_GET_IID(nsISupportsWeakReference))) { + // We only want to expose one implementation from our aggregate. + nsXPCWrappedJS* root = GetRootWrapper(); + RootedObject rootScope(ccx, root->GetJSObjectGlobal()); + + // Fail if JSObject doesn't claim support for nsISupportsWeakReference + if (!root->IsValid() || !CallQueryInterfaceOnJSObject( + ccx, root->GetJSObject(), rootScope, aIID)) { + *aInstancePtr = nullptr; + return NS_NOINTERFACE; + } + + NS_ADDREF(root); + *aInstancePtr = (void*)static_cast<nsISupportsWeakReference*>(root); + return NS_OK; + } + + // If we're asked to QI to nsISimpleEnumerator and the wrapped object does not + // have a QueryInterface method, assume it is a JS iterator, and wrap it into + // an equivalent nsISimpleEnumerator. + if (aIID.Equals(NS_GET_IID(nsISimpleEnumerator))) { + bool found; + XPCJSContext* xpccx = ccx.GetContext(); + if (JS_HasPropertyById(aes.cx(), obj, + xpccx->GetStringID(xpccx->IDX_QUERY_INTERFACE), + &found) && + !found) { + nsresult rv; + nsCOMPtr<nsIJSEnumerator> jsEnum; + if (!XPCConvert::JSObject2NativeInterface( + aes.cx(), getter_AddRefs(jsEnum), obj, + &NS_GET_IID(nsIJSEnumerator), nullptr, &rv)) { + return rv; + } + nsCOMPtr<nsISimpleEnumerator> res = new XPCWrappedJSIterator(jsEnum); + res.forget(aInstancePtr); + return NS_OK; + } + } + + // Checks for any existing wrapper explicitly constructed for this iid. + // This includes the current wrapper. This also deals with the + // nsISupports case (for which it returns mRoot). + // Also check if asking for an interface from which one of our wrappers + // inherits. + if (nsXPCWrappedJS* sibling = FindOrFindInherited(aIID)) { + NS_ADDREF(sibling); + *aInstancePtr = sibling->GetXPTCStub(); + return NS_OK; + } + + // Check if the desired interface is a function interface. If so, we don't + // want to QI, because the function almost certainly doesn't have a + // QueryInterface property, and doesn't need one. + const nsXPTInterfaceInfo* info = nsXPTInterfaceInfo::ByIID(aIID); + if (info && info->IsFunction()) { + RefPtr<nsXPCWrappedJS> wrapper; + nsresult rv = + nsXPCWrappedJS::GetNewOrUsed(ccx, obj, aIID, getter_AddRefs(wrapper)); + + // Do the same thing we do for the "check for any existing wrapper" case + // above. + if (NS_SUCCEEDED(rv) && wrapper) { + *aInstancePtr = wrapper.forget().take()->GetXPTCStub(); + } + return rv; + } + + // else we do the more expensive stuff... + + // check if the JSObject claims to implement this interface + RootedObject jsobj(ccx, + CallQueryInterfaceOnJSObject(ccx, obj, objScope, aIID)); + if (jsobj) { + // We can't use XPConvert::JSObject2NativeInterface() here + // since that can find a XPCWrappedNative directly on the + // proto chain, and we don't want that here. We need to find + // the actual JS object that claimed it supports the interface + // we're looking for or we'll potentially bypass security + // checks etc by calling directly through to a native found on + // the prototype chain. + // + // Instead, simply do the nsXPCWrappedJS part of + // XPConvert::JSObject2NativeInterface() here to make sure we + // get a new (or used) nsXPCWrappedJS. + RefPtr<nsXPCWrappedJS> wrapper; + nsresult rv = + nsXPCWrappedJS::GetNewOrUsed(ccx, jsobj, aIID, getter_AddRefs(wrapper)); + if (NS_SUCCEEDED(rv) && wrapper) { + // We need to go through the QueryInterface logic to make + // this return the right thing for the various 'special' + // interfaces; e.g. nsISimpleEnumerator. + rv = wrapper->QueryInterface(aIID, aInstancePtr); + return rv; + } + } + + // If we're asked to QI to nsINamed, we pretend that this is possible. We'll + // try to return a name that makes sense for the wrapped JS value. + if (aIID.Equals(NS_GET_IID(nsINamed))) { + nsCString name = GetFunctionName(ccx, obj); + RefPtr<WrappedJSNamed> named = new WrappedJSNamed(name); + *aInstancePtr = named.forget().take(); + return NS_OK; + } + + // else... + // no can do + *aInstancePtr = nullptr; + return NS_NOINTERFACE; +} + +// static +JSObject* nsXPCWrappedJS::GetRootJSObject(JSContext* cx, JSObject* aJSObjArg) { + RootedObject aJSObj(cx, aJSObjArg); + RootedObject global(cx, JS::CurrentGlobalOrNull(cx)); + JSObject* result = + CallQueryInterfaceOnJSObject(cx, aJSObj, global, NS_GET_IID(nsISupports)); + if (!result) { + result = aJSObj; + } + return js::UncheckedUnwrap(result); +} + +// static +bool nsXPCWrappedJS::GetArraySizeFromParam(const nsXPTMethodInfo* method, + const nsXPTType& type, + nsXPTCMiniVariant* nativeParams, + uint32_t* result) { + if (type.Tag() != nsXPTType::T_LEGACY_ARRAY && + type.Tag() != nsXPTType::T_PSTRING_SIZE_IS && + type.Tag() != nsXPTType::T_PWSTRING_SIZE_IS) { + *result = 0; + return true; + } + + uint8_t argnum = type.ArgNum(); + const nsXPTParamInfo& param = method->Param(argnum); + + // This should be enforced by the xpidl compiler, but it's not. + // See bug 695235. + if (param.Type().Tag() != nsXPTType::T_U32) { + return false; + } + + // If the length is passed indirectly (as an outparam), dereference by an + // extra level. + if (param.IsIndirect()) { + *result = *(uint32_t*)nativeParams[argnum].val.p; + } else { + *result = nativeParams[argnum].val.u32; + } + return true; +} + +// static +bool nsXPCWrappedJS::GetInterfaceTypeFromParam(const nsXPTMethodInfo* method, + const nsXPTType& type, + nsXPTCMiniVariant* nativeParams, + nsID* result) { + result->Clear(); + + const nsXPTType& inner = type.InnermostType(); + if (inner.Tag() == nsXPTType::T_INTERFACE) { + // Directly get IID from nsXPTInterfaceInfo. + if (!inner.GetInterface()) { + return false; + } + + *result = inner.GetInterface()->IID(); + } else if (inner.Tag() == nsXPTType::T_INTERFACE_IS) { + // Get IID from a passed parameter. + const nsXPTParamInfo& param = method->Param(inner.ArgNum()); + if (param.Type().Tag() != nsXPTType::T_NSID && + param.Type().Tag() != nsXPTType::T_NSIDPTR) { + return false; + } + + void* ptr = nativeParams[inner.ArgNum()].val.p; + + // If our IID is passed as a pointer outparameter, an extra level of + // dereferencing is required. + if (ptr && param.Type().Tag() == nsXPTType::T_NSIDPTR && + param.IsIndirect()) { + ptr = *(nsID**)ptr; + } + + if (!ptr) { + return false; + } + + *result = *(nsID*)ptr; + } + return true; +} + +// static +void nsXPCWrappedJS::CleanupOutparams(const nsXPTMethodInfo* info, + nsXPTCMiniVariant* nativeParams, + bool inOutOnly, uint8_t count) { + // clean up any 'out' params handed in + for (uint8_t i = 0; i < count; i++) { + const nsXPTParamInfo& param = info->GetParam(i); + if (!param.IsOut()) { + continue; + } + + MOZ_ASSERT(param.IsIndirect(), "Outparams are always indirect"); + + // Don't try to clear optional out params that are not set. + if (param.IsOptional() && !nativeParams[i].val.p) { + continue; + } + + // Call 'CleanupValue' on parameters which we know to be initialized: + // 1. Complex parameters (initialized by caller) + // 2. 'inout' parameters (initialized by caller) + // 3. 'out' parameters when 'inOutOnly' is 'false' (initialized by us) + // + // We skip non-complex 'out' parameters before the call, as they may + // contain random junk. + if (param.Type().IsComplex() || param.IsIn() || !inOutOnly) { + uint32_t arrayLen = 0; + if (!GetArraySizeFromParam(info, param.Type(), nativeParams, &arrayLen)) { + continue; + } + + xpc::CleanupValue(param.Type(), nativeParams[i].val.p, arrayLen); + } + + // Ensure our parameters are in a clean state. Complex values are always + // handled by CleanupValue, and others have a valid null representation. + if (!param.Type().IsComplex()) { + param.Type().ZeroValue(nativeParams[i].val.p); + } + } +} + +nsresult nsXPCWrappedJS::CheckForException(XPCCallContext& ccx, + AutoEntryScript& aes, + HandleObject aObj, + const char* aPropertyName, + const char* anInterfaceName, + Exception* aSyntheticException) { + JSContext* cx = ccx.GetJSContext(); + MOZ_ASSERT(cx == aes.cx()); + RefPtr<Exception> xpc_exception = aSyntheticException; + /* this one would be set by our error reporter */ + + XPCJSContext* xpccx = ccx.GetContext(); + + // Get this right away in case we do something below to cause JS code + // to run. + nsresult pending_result = xpccx->GetPendingResult(); + + RootedValue js_exception(cx); + bool is_js_exception = JS_GetPendingException(cx, &js_exception); + + /* JS might throw an exception whether the reporter was called or not */ + if (is_js_exception) { + if (!xpc_exception) { + XPCConvert::JSValToXPCException(cx, &js_exception, anInterfaceName, + aPropertyName, + getter_AddRefs(xpc_exception)); + } + + /* cleanup and set failed even if we can't build an exception */ + if (!xpc_exception) { + xpccx->SetPendingException(nullptr); // XXX necessary? + } + } + + // Clear the pending exception now, because xpc_exception might be JS- + // implemented, so invoking methods on it might re-enter JS, which we can't + // do with an exception on the stack. + aes.ClearException(); + + if (xpc_exception) { + nsresult e_result = xpc_exception->GetResult(); + // Figure out whether or not we should report this exception. + bool reportable = xpc_IsReportableErrorCode(e_result); + if (reportable) { + // Ugly special case for GetInterface. It's "special" in the + // same way as QueryInterface in that a failure is not + // exceptional and shouldn't be reported. We have to do this + // check here instead of in xpcwrappedjs (like we do for QI) to + // avoid adding extra code to all xpcwrappedjs objects. + if (e_result == NS_ERROR_NO_INTERFACE && + !strcmp(anInterfaceName, "nsIInterfaceRequestor") && + !strcmp(aPropertyName, "getInterface")) { + reportable = false; + } + + // More special case, see bug 877760. + if (e_result == NS_ERROR_XPC_JSOBJECT_HAS_NO_FUNCTION_NAMED) { + reportable = false; + } + } + + // Try to use the error reporter set on the context to handle this + // error if it came from a JS exception. + if (reportable && is_js_exception) { + // Note that we cleared the exception above, so we need to set it again, + // just so that we can tell the JS engine to pass it back to us via the + // error reporting callback. This is all very dumb. + JS_SetPendingException(cx, js_exception); + + // Enter the unwrapped object's realm. This is the realm that was used to + // enter the AutoEntryScript. + JSAutoRealm ar(cx, js::UncheckedUnwrap(aObj)); + aes.ReportException(); + reportable = false; + } + + if (reportable) { + if (nsJSUtils::DumpEnabled()) { + static const char line[] = + "************************************************************\n"; + static const char preamble[] = + "* Call to xpconnect wrapped JSObject produced this error: *\n"; + static const char cant_get_text[] = + "FAILED TO GET TEXT FROM EXCEPTION\n"; + + fputs(line, stdout); + fputs(preamble, stdout); + nsCString text; + xpc_exception->ToString(cx, text); + if (!text.IsEmpty()) { + fputs(text.get(), stdout); + fputs("\n", stdout); + } else + fputs(cant_get_text, stdout); + fputs(line, stdout); + } + + // Log the exception to the JS Console, so that users can do + // something with it. + nsCOMPtr<nsIConsoleService> consoleService( + do_GetService(XPC_CONSOLE_CONTRACTID)); + if (nullptr != consoleService) { + nsCOMPtr<nsIScriptError> scriptError = + do_QueryInterface(xpc_exception->GetData()); + + if (nullptr == scriptError) { + // No luck getting one from the exception, so + // try to cook one up. + scriptError = do_CreateInstance(XPC_SCRIPT_ERROR_CONTRACTID); + if (nullptr != scriptError) { + nsCString newMessage; + xpc_exception->ToString(cx, newMessage); + // try to get filename, lineno from the first + // stack frame location. + int32_t lineNumber = 0; + nsString sourceName; + + nsCOMPtr<nsIStackFrame> location = xpc_exception->GetLocation(); + if (location) { + // Get line number. + lineNumber = location->GetLineNumber(cx); + + // get a filename. + location->GetFilename(cx, sourceName); + } + + nsresult rv = scriptError->InitWithWindowID( + NS_ConvertUTF8toUTF16(newMessage), sourceName, u""_ns, + lineNumber, 0, 0, "XPConnect JavaScript", + nsJSUtils::GetCurrentlyRunningCodeInnerWindowID(cx)); + if (NS_FAILED(rv)) { + scriptError = nullptr; + } + + rv = scriptError->InitSourceId(location->GetSourceId(cx)); + if (NS_FAILED(rv)) { + scriptError = nullptr; + } + } + } + if (nullptr != scriptError) { + consoleService->LogMessage(scriptError); + } + } + } + // Whether or not it passes the 'reportable' test, it might + // still be an error and we have to do the right thing here... + if (NS_FAILED(e_result)) { + xpccx->SetPendingException(xpc_exception); + return e_result; + } + } else { + // see if JS code signaled failure result without throwing exception + if (NS_FAILED(pending_result)) { + return pending_result; + } + } + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +nsXPCWrappedJS::CallMethod(uint16_t methodIndex, const nsXPTMethodInfo* info, + nsXPTCMiniVariant* nativeParams) { + // Do a release-mode assert against accessing nsXPCWrappedJS off-main-thread. + MOZ_RELEASE_ASSERT(NS_IsMainThread(), + "nsXPCWrappedJS::CallMethod called off main thread"); + + if (!IsValid()) { + return NS_ERROR_UNEXPECTED; + } + + Value* sp = nullptr; + Value* argv = nullptr; + uint8_t i; + nsresult retval = NS_ERROR_FAILURE; + bool success; + bool readyToDoTheCall = false; + nsID param_iid; + bool foundDependentParam; + + // We're about to call into script via an XPCWrappedJS, so we need an + // AutoEntryScript. This is probably Gecko-specific at this point, and + // definitely will be when we turn off XPConnect for the web. + RootedObject obj(RootingCx(), GetJSObject()); + nsIGlobalObject* nativeGlobal = NativeGlobal(js::UncheckedUnwrap(obj)); + + AutoAllowLegacyScriptExecution exemption; + + AutoEntryScript aes(nativeGlobal, "XPCWrappedJS method call", + /* aIsMainThread = */ true); + XPCCallContext ccx(aes.cx()); + if (!ccx.IsValid()) { + return retval; + } + + JSContext* cx = ccx.GetJSContext(); + + if (!cx || !info->IsReflectable()) { + return NS_ERROR_FAILURE; + } + + const nsXPTInterfaceInfo* interfaceInfo = GetInfo(); + JS::RootedId id(cx); + const char* name = info->NameOrDescription(); + if (!info->GetId(cx, id.get())) { + return NS_ERROR_FAILURE; + } + + // We now need to enter the realm of the actual JSObject* we are pointing at. + // But that may be a cross-compartment wrapper and therefore not have a + // well-defined realm, so enter the realm of the global that we grabbed back + // when we started pointing to our JSObject*. + RootedObject scope(cx, GetJSObjectGlobal()); + JSAutoRealm ar(cx, scope); + + // [optional_argc] has a different calling convention, which we don't + // support for JS-implemented components. + if (info->WantsOptArgc()) { + const char* str = + "IDL methods marked with [optional_argc] may not " + "be implemented in JS"; + // Throw and warn for good measure. + JS_ReportErrorASCII(cx, "%s", str); + NS_WARNING(str); + return CheckForException(ccx, aes, obj, name, interfaceInfo->Name()); + } + + RootedValue fval(cx); + RootedObject thisObj(cx, obj); + + RootedValueVector args(cx); + AutoScriptEvaluate scriptEval(cx); + + XPCJSRuntime* xpcrt = XPCJSRuntime::Get(); + XPCJSContext* xpccx = ccx.GetContext(); + AutoSavePendingResult apr(xpccx); + + // XXX ASSUMES that retval is last arg. The xpidl compiler ensures this. + uint8_t paramCount = info->GetParamCount(); + uint8_t argc = paramCount; + if (info->HasRetval()) { + argc -= 1; + } + + if (!scriptEval.StartEvaluating(scope)) { + goto pre_call_clean_up; + } + + xpccx->SetPendingException(nullptr); + + // We use js_Invoke so that the gcthings we use as args will be rooted by + // the engine as we do conversions and prepare to do the function call. + + // setup stack + + // if this isn't a function call then we don't need to push extra stuff + if (!(info->IsSetter() || info->IsGetter())) { + // We get fval before allocating the stack to avoid gc badness that can + // happen if the GetProperty call leaves our request and the gc runs + // while the stack we allocate contains garbage. + + // If the interface is marked as a [function] then we will assume that + // our JSObject is a function and not an object with a named method. + + // In the xpidl [function] case we are making sure now that the + // JSObject is callable. If it is *not* callable then we silently + // fallback to looking up the named property... + // (because jst says he thinks this fallback is 'The Right Thing'.) + // + // In the normal (non-function) case we just lookup the property by + // name and as long as the object has such a named property we go ahead + // and try to make the call. If it turns out the named property is not + // a callable object then the JS engine will throw an error and we'll + // pass this along to the caller as an exception/result code. + + fval = ObjectValue(*obj); + if (!interfaceInfo->IsFunction() || + JS_TypeOfValue(ccx, fval) != JSTYPE_FUNCTION) { + if (!JS_GetPropertyById(cx, obj, id, &fval)) { + goto pre_call_clean_up; + } + // XXX We really want to factor out the error reporting better and + // specifically report the failure to find a function with this name. + // This is what we do below if the property is found but is not a + // function. We just need to factor better so we can get to that + // reporting path from here. + + thisObj = obj; + } + } + + if (!args.resize(argc)) { + retval = NS_ERROR_OUT_OF_MEMORY; + goto pre_call_clean_up; + } + + argv = args.begin(); + sp = argv; + + // build the args + // NB: This assignment *looks* wrong because we haven't yet called our + // function. However, we *have* already entered the compartmen that we're + // about to call, and that's the global that we want here. In other words: + // we're trusting the JS engine to come up with a good global to use for + // our object (whatever it was). + for (i = 0; i < argc; i++) { + const nsXPTParamInfo& param = info->GetParam(i); + const nsXPTType& type = param.GetType(); + uint32_t array_count; + RootedValue val(cx, NullValue()); + + // Verify that null was not passed for a non-optional 'out' param. + if (param.IsOut() && !nativeParams[i].val.p && !param.IsOptional()) { + retval = NS_ERROR_INVALID_ARG; + goto pre_call_clean_up; + } + + if (param.IsIn()) { + const void* pv; + if (param.IsIndirect()) { + pv = nativeParams[i].val.p; + } else { + pv = &nativeParams[i]; + } + + if (!GetInterfaceTypeFromParam(info, type, nativeParams, ¶m_iid) || + !GetArraySizeFromParam(info, type, nativeParams, &array_count)) + goto pre_call_clean_up; + + if (!XPCConvert::NativeData2JS(cx, &val, pv, type, ¶m_iid, + array_count, nullptr)) + goto pre_call_clean_up; + } + + if (param.IsOut()) { + // create an 'out' object + RootedObject out_obj(cx, NewOutObject(cx)); + if (!out_obj) { + retval = NS_ERROR_OUT_OF_MEMORY; + goto pre_call_clean_up; + } + + if (param.IsIn()) { + if (!JS_SetPropertyById(cx, out_obj, + xpcrt->GetStringID(XPCJSContext::IDX_VALUE), + val)) { + goto pre_call_clean_up; + } + } + *sp++ = JS::ObjectValue(*out_obj); + } else + *sp++ = val; + } + + readyToDoTheCall = true; + +pre_call_clean_up: + // clean up any 'out' params handed in + CleanupOutparams(info, nativeParams, /* inOutOnly = */ true, paramCount); + + if (!readyToDoTheCall) { + return retval; + } + + // do the deed - note exceptions + + MOZ_ASSERT(!aes.HasException()); + + RefPtr<Exception> syntheticException; + RootedValue rval(cx); + if (info->IsGetter()) { + success = JS_GetProperty(cx, obj, name, &rval); + } else if (info->IsSetter()) { + rval = *argv; + success = JS_SetProperty(cx, obj, name, rval); + } else { + if (!fval.isPrimitive()) { + success = JS_CallFunctionValue(cx, thisObj, fval, args, &rval); + } else { + // The property was not an object so can't be a function. + // Let's build and 'throw' an exception. + + static const nsresult code = NS_ERROR_XPC_JSOBJECT_HAS_NO_FUNCTION_NAMED; + static const char format[] = "%s \"%s\""; + const char* msg; + UniqueChars sz; + + if (nsXPCException::NameAndFormatForNSResult(code, nullptr, &msg) && + msg) { + sz = JS_smprintf(format, msg, name); + } + + XPCConvert::ConstructException( + code, sz.get(), interfaceInfo->Name(), name, nullptr, + getter_AddRefs(syntheticException), nullptr, nullptr); + success = false; + } + } + + if (!success) { + return CheckForException(ccx, aes, obj, name, interfaceInfo->Name(), + syntheticException); + } + + xpccx->SetPendingException(nullptr); // XXX necessary? + + // convert out args and result + // NOTE: this is the total number of native params, not just the args + // Convert independent params only. + // When we later convert the dependent params (if any) we will know that + // the params upon which they depend will have already been converted - + // regardless of ordering. + + foundDependentParam = false; + for (i = 0; i < paramCount; i++) { + const nsXPTParamInfo& param = info->GetParam(i); + MOZ_ASSERT(!param.IsShared(), "[shared] implies [noscript]!"); + if (!param.IsOut() || !nativeParams[i].val.p) { + continue; + } + + const nsXPTType& type = param.GetType(); + if (type.IsDependent()) { + foundDependentParam = true; + continue; + } + + RootedValue val(cx); + + if (¶m == info->GetRetval()) { + val = rval; + } else if (argv[i].isPrimitive()) { + break; + } else { + RootedObject obj(cx, &argv[i].toObject()); + if (!JS_GetPropertyById( + cx, obj, xpcrt->GetStringID(XPCJSContext::IDX_VALUE), &val)) { + break; + } + } + + // setup allocator and/or iid + + const nsXPTType& inner = type.InnermostType(); + if (inner.Tag() == nsXPTType::T_INTERFACE) { + if (!inner.GetInterface()) { + break; + } + param_iid = inner.GetInterface()->IID(); + } + + MOZ_ASSERT(param.IsIndirect(), "outparams are always indirect"); + if (!XPCConvert::JSData2Native(cx, nativeParams[i].val.p, val, type, + ¶m_iid, 0, nullptr)) + break; + } + + // if any params were dependent, then we must iterate again to convert them. + if (foundDependentParam && i == paramCount) { + for (i = 0; i < paramCount; i++) { + const nsXPTParamInfo& param = info->GetParam(i); + if (!param.IsOut()) { + continue; + } + + const nsXPTType& type = param.GetType(); + if (!type.IsDependent()) { + continue; + } + + RootedValue val(cx); + uint32_t array_count; + + if (¶m == info->GetRetval()) { + val = rval; + } else { + RootedObject obj(cx, &argv[i].toObject()); + if (!JS_GetPropertyById( + cx, obj, xpcrt->GetStringID(XPCJSContext::IDX_VALUE), &val)) { + break; + } + } + + // setup allocator and/or iid + + if (!GetInterfaceTypeFromParam(info, type, nativeParams, ¶m_iid) || + !GetArraySizeFromParam(info, type, nativeParams, &array_count)) + break; + + MOZ_ASSERT(param.IsIndirect(), "outparams are always indirect"); + if (!XPCConvert::JSData2Native(cx, nativeParams[i].val.p, val, type, + ¶m_iid, array_count, nullptr)) + break; + } + } + + if (i != paramCount) { + // We didn't manage all the result conversions! + // We have to cleanup any junk that *did* get converted. + CleanupOutparams(info, nativeParams, /* inOutOnly = */ false, i); + } else { + // set to whatever the JS code might have set as the result + retval = xpccx->GetPendingResult(); + } + + return retval; +} + +static const JSClass XPCOutParamClass = {"XPCOutParam", 0, JS_NULL_CLASS_OPS}; + +bool xpc::IsOutObject(JSContext* cx, JSObject* obj) { + return JS::GetClass(obj) == &XPCOutParamClass; +} + +JSObject* xpc::NewOutObject(JSContext* cx) { + return JS_NewObject(cx, &XPCOutParamClass); +} + +// static +void nsXPCWrappedJS::DebugDumpInterfaceInfo(const nsXPTInterfaceInfo* aInfo, + int16_t depth) { +#ifdef DEBUG + depth--; + XPC_LOG_ALWAYS(("nsXPTInterfaceInfo @ %p = ", aInfo)); + XPC_LOG_INDENT(); + const char* name = aInfo->Name(); + XPC_LOG_ALWAYS(("interface name is %s", name)); + char* iid = aInfo->IID().ToString(); + XPC_LOG_ALWAYS(("IID number is %s", iid ? iid : "invalid")); + if (iid) { + free(iid); + } + XPC_LOG_ALWAYS(("InterfaceInfo @ %p", aInfo)); + uint16_t methodCount = 0; + if (depth) { + XPC_LOG_INDENT(); + XPC_LOG_ALWAYS(("parent @ %p", aInfo->GetParent())); + methodCount = aInfo->MethodCount(); + XPC_LOG_ALWAYS(("MethodCount = %d", methodCount)); + XPC_LOG_ALWAYS(("ConstantCount = %d", aInfo->ConstantCount())); + XPC_LOG_OUTDENT(); + } + XPC_LOG_ALWAYS(("method count = %d", methodCount)); + if (depth && methodCount) { + depth--; + XPC_LOG_INDENT(); + for (uint16_t i = 0; i < methodCount; i++) { + XPC_LOG_ALWAYS(("Method %d is %s%s", i, + aInfo->Method(i).IsReflectable() ? "" : " NOT ", + "reflectable")); + } + XPC_LOG_OUTDENT(); + depth++; + } + XPC_LOG_OUTDENT(); +#endif +} |