/* -*- 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/. */ /* Class that wraps JS objects to appear as XPCOM objects. */ #include "xpcprivate.h" #include "XPCMaps.h" #include "mozilla/DeferredFinalize.h" #include "mozilla/HoldDropJSObjects.h" #include "mozilla/Sprintf.h" #include "js/Object.h" // JS::GetCompartment #include "js/RealmIterators.h" #include "nsCCUncollectableMarker.h" #include "nsContentUtils.h" #include "nsThreadUtils.h" using namespace mozilla; // NOTE: much of the fancy footwork is done in xpcstubs.cpp // nsXPCWrappedJS lifetime. // // An nsXPCWrappedJS is either rooting its JS object or is subject to // finalization. The subject-to-finalization state lets wrappers support // nsSupportsWeakReference in the case where the underlying JS object // is strongly owned, but the wrapper itself is only weakly owned. // // A wrapper is rooting its JS object whenever its refcount is greater than 1. // In this state, root wrappers are always added to the cycle collector graph. // The wrapper keeps around an extra refcount, added in the constructor, to // support the possibility of an eventual transition to the // subject-to-finalization state. This extra refcount is ignored by the cycle // collector, which traverses the "self" edge for this refcount. // // When the refcount of a rooting wrapper drops to 1, if there is no weak // reference to the wrapper (which can only happen for the root wrapper), it is // immediately Destroy()'d. Otherwise, it becomes subject to finalization. // // When a wrapper is subject to finalization, the wrapper has a refcount of 1. // It is now owned exclusively by its JS object. Either a weak reference will be // turned into a strong ref which will bring its refcount up to 2 and change the // wrapper back to the rooting state, or it will stay alive until the JS object // dies. If the JS object dies, then when // JSObject2WrappedJSMap::UpdateWeakPointersAfterGC is called (via the JS // engine's weak pointer zone or compartment callbacks) it will find the wrapper // and call Release() on it, destroying the wrapper. Otherwise, the wrapper will // stay alive, even if it no longer has a weak reference to it. // // When the wrapper is subject to finalization, it is kept alive by an implicit // reference from the JS object which is invisible to the cycle collector, so // the cycle collector does not traverse any children of wrappers that are // subject to finalization. This will result in a leak if a wrapper in the // non-rooting state has an aggregated native that keeps alive the wrapper's JS // object. See bug 947049. // If traversing wrappedJS wouldn't release it, nor cause any other objects to // be added to the graph, there is no need to add it to the graph at all. bool nsXPCWrappedJS::CanSkip() { if (!nsCCUncollectableMarker::sGeneration) { return false; } // If this wrapper holds a gray object, need to trace it. // We can't skip it even if it is subject to finalization, because we want to // be able to collect it if the JS object is gray. JSObject* obj = GetJSObjectPreserveColor(); if (obj && JS::ObjectIsMarkedGray(obj)) { return false; } // For non-root wrappers, check if the root wrapper will be // added to the CC graph. if (!IsRootWrapper()) { // mRoot points to null after unlinking. NS_ENSURE_TRUE(mRoot, false); return mRoot->CanSkip(); } // At this point, the WJS must be a root wrapper with a black JS object, so // if it is subject to finalization, the JS object will be holding it alive // so it will be okay to skip it. // For the root wrapper, check if there is an aggregated // native object that will be added to the CC graph. if (!IsAggregatedToNative()) { return true; } nsISupports* agg = GetAggregatedNativeObject(); nsXPCOMCycleCollectionParticipant* cp = nullptr; CallQueryInterface(agg, &cp); nsISupports* canonical = nullptr; agg->QueryInterface(NS_GET_IID(nsCycleCollectionISupports), reinterpret_cast(&canonical)); return cp && canonical && cp->CanSkipThis(canonical); } NS_IMETHODIMP NS_CYCLE_COLLECTION_CLASSNAME(nsXPCWrappedJS)::TraverseNative( void* p, nsCycleCollectionTraversalCallback& cb) { nsISupports* s = static_cast(p); MOZ_ASSERT(CheckForRightISupports(s), "not the nsISupports pointer we expect"); nsXPCWrappedJS* tmp = Downcast(s); nsrefcnt refcnt = tmp->mRefCnt.get(); if (cb.WantDebugInfo()) { char name[72]; SprintfLiteral(name, "nsXPCWrappedJS (%s)", tmp->mInfo->Name()); cb.DescribeRefCountedNode(refcnt, name); } else { NS_IMPL_CYCLE_COLLECTION_DESCRIBE(nsXPCWrappedJS, refcnt) } if (tmp->IsSubjectToFinalization()) { // If the WJS is subject to finalization, then it can be held alive by its // JS object. We represent this edge by using NoteWeakMapping. The linked // list of subject-to-finalization WJS acts like a known-black weak map. cb.NoteWeakMapping(tmp->GetJSObjectPreserveColor(), s, NS_CYCLE_COLLECTION_PARTICIPANT(nsXPCWrappedJS)); } // Don't let the extra reference for nsSupportsWeakReference keep a WJS alive. NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "self"); cb.NoteXPCOMChild(s); if (tmp->IsRootWrapper()) { NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "aggregated native"); cb.NoteXPCOMChild(tmp->GetAggregatedNativeObject()); } else { NS_CYCLE_COLLECTION_NOTE_EDGE_NAME(cb, "root"); cb.NoteXPCOMChild(ToSupports(tmp->GetRootWrapper())); } return NS_OK; } NS_IMPL_CYCLE_COLLECTION_SINGLE_ZONE_SCRIPT_HOLDER_CLASS(nsXPCWrappedJS) NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsXPCWrappedJS) tmp->Unlink(); // Note: Unlink already calls ClearWeakReferences, so no need for // NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE here. NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN(nsXPCWrappedJS) // See the comment at the top of this file for the explanation of // the weird tracing condition. if (!tmp->IsSubjectToFinalization()) { NS_IMPL_CYCLE_COLLECTION_TRACE_JS_MEMBER_CALLBACK(mJSObj) } NS_IMPL_CYCLE_COLLECTION_TRACE_END // WJS are JS holders, so we'll always add them as roots in CCs and we can // remove them from the purple buffer in between CCs. NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_BEGIN(nsXPCWrappedJS) return true; NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_END NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_BEGIN(nsXPCWrappedJS) return tmp->CanSkip(); NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_IN_CC_END NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_BEGIN(nsXPCWrappedJS) return tmp->CanSkip(); NS_IMPL_CYCLE_COLLECTION_CAN_SKIP_THIS_END nsXPCWrappedJS* nsIXPConnectWrappedJS::AsXPCWrappedJS() { return static_cast(this); } nsresult nsIXPConnectWrappedJS::AggregatedQueryInterface(REFNSIID aIID, void** aInstancePtr) { MOZ_ASSERT(AsXPCWrappedJS()->IsAggregatedToNative(), "bad AggregatedQueryInterface call"); *aInstancePtr = nullptr; if (!AsXPCWrappedJS()->IsValid()) { return NS_ERROR_UNEXPECTED; } // Put this here rather that in DelegatedQueryInterface because it needs // to be in QueryInterface before the possible delegation to 'outer', but // we don't want to do this check twice in one call in the normal case: // once in QueryInterface and once in DelegatedQueryInterface. if (aIID.Equals(NS_GET_IID(nsIXPConnectWrappedJS))) { NS_ADDREF(this); *aInstancePtr = (void*)this; return NS_OK; } return AsXPCWrappedJS()->DelegatedQueryInterface(aIID, aInstancePtr); } NS_IMETHODIMP nsXPCWrappedJS::QueryInterface(REFNSIID aIID, void** aInstancePtr) { if (nullptr == aInstancePtr) { MOZ_ASSERT(false, "null pointer"); return NS_ERROR_NULL_POINTER; } *aInstancePtr = nullptr; if (aIID.Equals(NS_GET_IID(nsXPCOMCycleCollectionParticipant))) { *aInstancePtr = NS_CYCLE_COLLECTION_PARTICIPANT(nsXPCWrappedJS); return NS_OK; } if (aIID.Equals(NS_GET_IID(nsCycleCollectionISupports))) { *aInstancePtr = NS_CYCLE_COLLECTION_CLASSNAME(nsXPCWrappedJS)::Upcast(this); return NS_OK; } if (!IsValid()) { return NS_ERROR_UNEXPECTED; } if (aIID.Equals(NS_GET_IID(nsIXPConnectWrappedJSUnmarkGray))) { *aInstancePtr = nullptr; mJSObj.exposeToActiveJS(); // Just return some error value since one isn't supposed to use // nsIXPConnectWrappedJSUnmarkGray objects for anything. return NS_ERROR_FAILURE; } // Always check for this first so that our 'outer' can get this interface // from us without recurring into a call to the outer's QI! if (aIID.Equals(NS_GET_IID(nsIXPConnectWrappedJS))) { NS_ADDREF(this); *aInstancePtr = (void*)static_cast(this); return NS_OK; } nsISupports* outer = GetAggregatedNativeObject(); if (outer) { return outer->QueryInterface(aIID, aInstancePtr); } // else... return DelegatedQueryInterface(aIID, aInstancePtr); } // For a description of nsXPCWrappedJS lifetime and reference counting, see // the comment at the top of this file. MozExternalRefCountType nsXPCWrappedJS::AddRef(void) { MOZ_RELEASE_ASSERT(NS_IsMainThread(), "nsXPCWrappedJS::AddRef called off main thread"); MOZ_ASSERT(int32_t(mRefCnt) >= 0, "illegal refcnt"); nsISupports* base = NS_CYCLE_COLLECTION_CLASSNAME(nsXPCWrappedJS)::Upcast(this); nsrefcnt cnt = mRefCnt.incr(base); NS_LOG_ADDREF(this, cnt, "nsXPCWrappedJS", sizeof(*this)); if (2 == cnt && IsValid()) { GetJSObject(); // Unmark gray JSObject. // This WJS is no longer subject to finalization. if (isInList()) { remove(); } } return cnt; } MozExternalRefCountType nsXPCWrappedJS::Release(void) { MOZ_RELEASE_ASSERT(NS_IsMainThread(), "nsXPCWrappedJS::Release called off main thread"); MOZ_ASSERT(int32_t(mRefCnt) > 0, "dup release"); NS_ASSERT_OWNINGTHREAD(nsXPCWrappedJS); bool shouldDelete = false; nsISupports* base = NS_CYCLE_COLLECTION_CLASSNAME(nsXPCWrappedJS)::Upcast(this); nsrefcnt cnt = mRefCnt.decr(base, &shouldDelete); NS_LOG_RELEASE(this, cnt, "nsXPCWrappedJS"); if (0 == cnt) { if (MOZ_UNLIKELY(shouldDelete)) { mRefCnt.stabilizeForDeletion(); DeleteCycleCollectable(); } else { mRefCnt.incr(base); Destroy(); mRefCnt.decr(base); } } else if (1 == cnt) { // If we are not a root wrapper being used from a weak reference, // then the extra ref is not needed and we can let ourselves be // deleted. if (!HasWeakReferences()) { return Release(); } if (IsValid()) { XPCJSRuntime::Get()->AddSubjectToFinalizationWJS(this); } MOZ_ASSERT(IsRootWrapper(), "Only root wrappers should have weak references"); } return cnt; } NS_IMETHODIMP_(void) nsXPCWrappedJS::DeleteCycleCollectable(void) { delete this; } NS_IMETHODIMP nsXPCWrappedJS::GetWeakReference(nsIWeakReference** aInstancePtr) { if (!IsRootWrapper()) { return mRoot->GetWeakReference(aInstancePtr); } return nsSupportsWeakReference::GetWeakReference(aInstancePtr); } JSObject* nsXPCWrappedJS::GetJSObject() { return mJSObj; } JSObject* nsIXPConnectWrappedJS::GetJSObjectGlobal() { JSObject* obj = AsXPCWrappedJS()->mJSObj; if (js::IsCrossCompartmentWrapper(obj)) { JS::Compartment* comp = JS::GetCompartment(obj); return js::GetFirstGlobalInCompartment(comp); } return JS::GetNonCCWObjectGlobal(obj); } // static nsresult nsXPCWrappedJS::GetNewOrUsed(JSContext* cx, JS::HandleObject jsObj, REFNSIID aIID, nsXPCWrappedJS** wrapperResult) { // Do a release-mode assert against accessing nsXPCWrappedJS off-main-thread. MOZ_RELEASE_ASSERT(NS_IsMainThread(), "nsXPCWrappedJS::GetNewOrUsed called off main thread"); MOZ_RELEASE_ASSERT(js::GetContextCompartment(cx) == JS::GetCompartment(jsObj)); const nsXPTInterfaceInfo* info = GetInterfaceInfo(aIID); if (!info) { return NS_ERROR_FAILURE; } JS::RootedObject rootJSObj(cx, GetRootJSObject(cx, jsObj)); if (!rootJSObj) { return NS_ERROR_FAILURE; } xpc::CompartmentPrivate* rootComp = xpc::CompartmentPrivate::Get(rootJSObj); MOZ_ASSERT(rootComp); // Find any existing wrapper. RefPtr root = rootComp->GetWrappedJSMap()->Find(rootJSObj); MOZ_ASSERT_IF(root, !nsXPConnect::GetRuntimeInstance() ->GetMultiCompartmentWrappedJSMap() ->Find(rootJSObj)); if (!root) { root = nsXPConnect::GetRuntimeInstance() ->GetMultiCompartmentWrappedJSMap() ->Find(rootJSObj); } nsresult rv = NS_ERROR_FAILURE; if (root) { RefPtr wrapper = root->FindOrFindInherited(aIID); if (wrapper) { wrapper.forget(wrapperResult); return NS_OK; } } else if (rootJSObj != jsObj) { // Make a new root wrapper, because there is no existing // root wrapper, and the wrapper we are trying to make isn't // a root. const nsXPTInterfaceInfo* rootInfo = GetInterfaceInfo(NS_GET_IID(nsISupports)); if (!rootInfo) { return NS_ERROR_FAILURE; } root = new nsXPCWrappedJS(cx, rootJSObj, rootInfo, nullptr, &rv); if (NS_FAILED(rv)) { return rv; } } RefPtr wrapper = new nsXPCWrappedJS(cx, jsObj, info, root, &rv); if (NS_FAILED(rv)) { return rv; } wrapper.forget(wrapperResult); return NS_OK; } nsXPCWrappedJS::nsXPCWrappedJS(JSContext* cx, JSObject* aJSObj, const nsXPTInterfaceInfo* aInfo, nsXPCWrappedJS* root, nsresult* rv) : mJSObj(aJSObj), mInfo(aInfo), mRoot(root ? root : this), mNext(nullptr) { *rv = InitStub(mInfo->IID()); // Continue even in the failure case, so that our refcounting/Destroy // behavior works correctly. // There is an extra AddRef to support weak references to wrappers // that are subject to finalization. See the top of the file for more // details. NS_ADDREF_THIS(); if (IsRootWrapper()) { MOZ_ASSERT(!IsMultiCompartment(), "mNext is always nullptr here"); if (!xpc::CompartmentPrivate::Get(mJSObj)->GetWrappedJSMap()->Add(cx, this)) { *rv = NS_ERROR_OUT_OF_MEMORY; } } else { NS_ADDREF(mRoot); mNext = mRoot->mNext; mRoot->mNext = this; // We always start wrappers in the per-compartment table. If adding // this wrapper to the chain causes it to cross compartments, we need // to migrate the chain to the global table on the XPCJSContext. if (mRoot->IsMultiCompartment()) { xpc::CompartmentPrivate::Get(mRoot->mJSObj) ->GetWrappedJSMap() ->Remove(mRoot); auto destMap = nsXPConnect::GetRuntimeInstance()->GetMultiCompartmentWrappedJSMap(); if (!destMap->Add(cx, mRoot)) { *rv = NS_ERROR_OUT_OF_MEMORY; } } } mozilla::HoldJSObjects(this); } nsXPCWrappedJS::~nsXPCWrappedJS() { Destroy(); } void XPCJSRuntime::RemoveWrappedJS(nsXPCWrappedJS* wrapper) { AssertInvalidWrappedJSNotInTable(wrapper); if (!wrapper->IsValid()) { return; } // It is possible for the same JS XPCOM implementation object to be wrapped // with a different interface in multiple JS::Compartments. In this case, the // wrapper chain will contain references to multiple compartments. While we // always store single-compartment chains in the per-compartment wrapped-js // table, chains in the multi-compartment wrapped-js table may contain // single-compartment chains, if they have ever contained a wrapper in a // different compartment. Since removal requires a lookup anyway, we just do // the remove on both tables unconditionally. MOZ_ASSERT_IF( wrapper->IsMultiCompartment(), !xpc::CompartmentPrivate::Get(wrapper->GetJSObjectPreserveColor()) ->GetWrappedJSMap() ->HasWrapper(wrapper)); GetMultiCompartmentWrappedJSMap()->Remove(wrapper); xpc::CompartmentPrivate::Get(wrapper->GetJSObjectPreserveColor()) ->GetWrappedJSMap() ->Remove(wrapper); } #ifdef DEBUG static JS::CompartmentIterResult NotHasWrapperAssertionCallback( JSContext* cx, void* data, JS::Compartment* comp) { auto wrapper = static_cast(data); auto xpcComp = xpc::CompartmentPrivate::Get(comp); MOZ_ASSERT_IF(xpcComp, !xpcComp->GetWrappedJSMap()->HasWrapper(wrapper)); return JS::CompartmentIterResult::KeepGoing; } #endif void XPCJSRuntime::AssertInvalidWrappedJSNotInTable( nsXPCWrappedJS* wrapper) const { #ifdef DEBUG if (!wrapper->IsValid()) { MOZ_ASSERT(!GetMultiCompartmentWrappedJSMap()->HasWrapper(wrapper)); if (!mGCIsRunning) { JSContext* cx = XPCJSContext::Get()->Context(); JS_IterateCompartments(cx, wrapper, NotHasWrapperAssertionCallback); } } #endif } void nsXPCWrappedJS::Destroy() { MOZ_ASSERT(1 == int32_t(mRefCnt), "should be stabilized for deletion"); if (IsRootWrapper()) { nsXPConnect::GetRuntimeInstance()->RemoveWrappedJS(this); } Unlink(); } void nsXPCWrappedJS::Unlink() { nsXPConnect::GetRuntimeInstance()->AssertInvalidWrappedJSNotInTable(this); if (IsValid()) { XPCJSRuntime* rt = nsXPConnect::GetRuntimeInstance(); if (rt) { if (IsRootWrapper()) { rt->RemoveWrappedJS(this); } } mJSObj = nullptr; } if (IsRootWrapper()) { if (isInList()) { remove(); } ClearWeakReferences(); } else if (mRoot) { // unlink this wrapper nsXPCWrappedJS* cur = mRoot; while (1) { if (cur->mNext == this) { cur->mNext = mNext; break; } cur = cur->mNext; MOZ_ASSERT(cur, "failed to find wrapper in its own chain"); } // Note: unlinking this wrapper may have changed us from a multi- // compartment wrapper chain to a single-compartment wrapper chain. We // leave the wrapper in the multi-compartment table as it is likely to // need to be multi-compartment again in the future and, moreover, we // cannot get a JSContext here. // let the root go NS_RELEASE(mRoot); } if (mOuter) { XPCJSRuntime* rt = nsXPConnect::GetRuntimeInstance(); if (rt->GCIsRunning()) { DeferredFinalize(mOuter.forget().take()); } else { mOuter = nullptr; } } mozilla::DropJSObjects(this); } bool nsXPCWrappedJS::IsMultiCompartment() const { MOZ_ASSERT(IsRootWrapper()); JS::Compartment* compartment = Compartment(); nsXPCWrappedJS* next = mNext; while (next) { if (next->Compartment() != compartment) { return true; } next = next->mNext; } return false; } nsXPCWrappedJS* nsXPCWrappedJS::Find(REFNSIID aIID) { if (aIID.Equals(NS_GET_IID(nsISupports))) { return mRoot; } for (nsXPCWrappedJS* cur = mRoot; cur; cur = cur->mNext) { if (aIID.Equals(cur->GetIID())) { return cur; } } return nullptr; } // check if asking for an interface that some wrapper in the chain inherits from nsXPCWrappedJS* nsXPCWrappedJS::FindInherited(REFNSIID aIID) { MOZ_ASSERT(!aIID.Equals(NS_GET_IID(nsISupports)), "bad call sequence"); for (nsXPCWrappedJS* cur = mRoot; cur; cur = cur->mNext) { if (cur->mInfo->HasAncestor(aIID)) { return cur; } } return nullptr; } nsresult nsIXPConnectWrappedJS::GetInterfaceIID(nsIID** iid) { MOZ_ASSERT(iid, "bad param"); *iid = AsXPCWrappedJS()->GetIID().Clone(); return NS_OK; } void nsXPCWrappedJS::SystemIsBeingShutDown() { // XXX It turns out that it is better to leak here then to do any Releases // and have them propagate into all sorts of mischief as the system is being // shutdown. This was learned the hard way :( // mJSObj == nullptr is used to indicate that the wrapper is no longer valid // and that calls should fail without trying to use any of the // xpconnect mechanisms. 'IsValid' is implemented by checking this pointer. // Clear the contents of the pointer using unsafeGet() to avoid // triggering post barriers in shutdown, as this will access the chunk // containing mJSObj, which may have been freed at this point. This is safe // if we are not currently running an incremental GC. MOZ_ASSERT(!JS::IsIncrementalGCInProgress(xpc_GetSafeJSContext())); *mJSObj.unsafeGet() = nullptr; if (isInList()) { remove(); } // Notify other wrappers in the chain. if (mNext) { mNext->SystemIsBeingShutDown(); } } size_t nsXPCWrappedJS::SizeOfIncludingThis( mozilla::MallocSizeOf mallocSizeOf) const { // mJSObject is a JS pointer, so don't measure the object. mInfo is // not dynamically allocated. mRoot is not measured because it is // either |this| or we have already measured it. mOuter is rare and // probably not uniquely owned by this. size_t n = mallocSizeOf(this); n += nsAutoXPTCStub::SizeOfExcludingThis(mallocSizeOf); // Wrappers form a linked list via the mNext field, so include them all // in the measurement. Only root wrappers are stored in the map, so // everything will be measured exactly once. if (mNext) { n += mNext->SizeOfIncludingThis(mallocSizeOf); } return n; } /***************************************************************************/ nsresult nsIXPConnectWrappedJS::DebugDump(int16_t depth) { return AsXPCWrappedJS()->DebugDump(depth); } nsresult nsXPCWrappedJS::DebugDump(int16_t depth) { #ifdef DEBUG XPC_LOG_ALWAYS( ("nsXPCWrappedJS @ %p with mRefCnt = %" PRIuPTR, this, mRefCnt.get())); XPC_LOG_INDENT(); XPC_LOG_ALWAYS(("%s wrapper around JSObject @ %p", IsRootWrapper() ? "ROOT" : "non-root", mJSObj.get())); const char* name = mInfo->Name(); XPC_LOG_ALWAYS(("interface name is %s", name)); auto iid = mInfo->IID().ToString(); XPC_LOG_ALWAYS(("IID number is %s", iid.get())); XPC_LOG_ALWAYS(("nsXPTInterfaceInfo @ %p", mInfo)); if (!IsRootWrapper()) { XPC_LOG_OUTDENT(); } if (mNext) { if (IsRootWrapper()) { XPC_LOG_ALWAYS(("Additional wrappers for this object...")); XPC_LOG_INDENT(); } mNext->DebugDump(depth); if (IsRootWrapper()) { XPC_LOG_OUTDENT(); } } if (IsRootWrapper()) { XPC_LOG_OUTDENT(); } #endif return NS_OK; }