/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=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 "LSObject.h" // Local includes #include "ActorsChild.h" #include "LSDatabase.h" #include "LSObserver.h" // Global includes #include #include "MainThreadUtils.h" #include "mozilla/BasePrincipal.h" #include "mozilla/ErrorResult.h" #include "mozilla/MacroForEach.h" #include "mozilla/OriginAttributes.h" #include "mozilla/Preferences.h" #include "mozilla/RemoteLazyInputStreamThread.h" #include "mozilla/ScopeExit.h" #include "mozilla/SpinEventLoopUntil.h" #include "mozilla/StaticMutex.h" #include "mozilla/StaticPrefs_dom.h" #include "mozilla/StorageAccess.h" #include "mozilla/Unused.h" #include "mozilla/dom/ClientInfo.h" #include "mozilla/dom/Document.h" #include "mozilla/dom/LocalStorageCommon.h" #include "mozilla/dom/PBackgroundLSRequest.h" #include "mozilla/dom/PBackgroundLSSharedTypes.h" #include "mozilla/dom/quota/QuotaManager.h" #include "mozilla/ipc/BackgroundChild.h" #include "mozilla/ipc/BackgroundUtils.h" #include "mozilla/ipc/PBackgroundChild.h" #include "mozilla/ipc/PBackgroundSharedTypes.h" #include "nsCOMPtr.h" #include "nsContentUtils.h" #include "nsDebug.h" #include "nsError.h" #include "nsIEventTarget.h" #include "nsIPrincipal.h" #include "nsIRunnable.h" #include "nsIScriptObjectPrincipal.h" #include "nsISerialEventTarget.h" #include "nsITimer.h" #include "nsPIDOMWindow.h" #include "nsString.h" #include "nsTArray.h" #include "nsTStringRepr.h" #include "nsThread.h" #include "nsThreadUtils.h" #include "nsXULAppAPI.h" #include "nscore.h" /** * Automatically cancel and abort synchronous LocalStorage requests (for example * datastore preparation) if they take this long. We've chosen a value that is * long enough that it is unlikely for the problem to be falsely triggered by * slow system I/O. We've also chosen a value long enough so that automated * tests should time out and fail if LocalStorage hangs. Also, this value is * long enough so that testers can notice the (content process) hang; we want to * know about the hangs, not hide them. On the other hand this value is less * than 60 seconds which is used by nsTerminator to crash a hung main process. */ #define FAILSAFE_CANCEL_SYNC_OP_MS 50000 namespace mozilla::dom { namespace { class RequestHelper; StaticMutex gRequestHelperMutex; nsISerialEventTarget* gSyncLoopEventTarget = nullptr; /** * Tracks whether a sync message has been received to the main thread but not * yet processed. Used by RequestHelper logic to abort effectively synchronous * calls if a sync IPC message is received which could result in deadlock. * This is a boolean because, by definition, the parent can only send one sync * message to the child at a time. */ bool gPendingSyncMessage = false; /* * Wrapper for the pushed event queue. The wrapper automatically dispatches * runnables to the main thread when pushed event queue is no longer active. * This exists because the event loop spinning can be aborted. */ class NestedEventTargetWrapper final : public nsISerialEventTarget { nsCOMPtr mNestedEventTarget; bool mDisconnected; public: explicit NestedEventTargetWrapper(nsISerialEventTarget* aNestedEventTarget) : mNestedEventTarget(aNestedEventTarget), mDisconnected(false) {} private: ~NestedEventTargetWrapper() = default; NS_DECL_THREADSAFE_ISUPPORTS NS_DECL_NSIEVENTTARGET_FULL }; /** * Main-thread helper that implements the blocking logic required by * LocalStorage's synchronous semantics. StartAndReturnResponse pushes an * event queue which is a new event target and spins its nested event loop until * a result is received or an abort is necessary due to a PContent-managed sync * IPC message being received. Note that because the event queue is its own * event target, there is no re-entrancy. Normal main-thread runnables will not * get a chance to run. See StartAndReturnResponse() for info on this choice. * * The normal life-cycle of this method looks like: * - Main Thread: LSObject::DoRequestSynchronously creates a RequestHelper and * invokes StartAndReturnResponse(). It pushes the event queue and Dispatches * the RequestHelper to the RemoteLazyInputStream thread. * - RemoteLazyInputStream Thread: RequestHelper::Run is called, invoking * Start() which invokes LSObject::StartRequest, which gets-or-creates the * PBackground actor if necessary (which may dispatch a runnable to the nested * event queue on the main thread), sends LSRequest constructor which is * provided with a callback reference to the RequestHelper. State advances to * ResponsePending. * - RemoteLazyInputStreamThread: LSRequestChild::Recv__delete__ is received, * which invokes RequestHelepr::OnResponse, advancing the state to Finishing * and dispatching RequestHelper to its own nested event target. * - Main Thread: RequestHelper::Run is called, invoking Finish() which advances * the state to Complete and sets mWaiting to false, allowing the nested event * loop being spun by StartAndReturnResponse to cease spinning and return the * received response. * * See LocalStorageCommon.h for high-level context and method comments for * low-level details. */ class RequestHelper final : public Runnable, public LSRequestChildCallback { enum class State { /** * The RequestHelper has been created and dispatched to the * RemoteLazyInputStream Thread. */ Initial, /** * Start() has been invoked on the RemoteLazyInputStream Thread and * LSObject::StartRequest has been invoked from there, sending an IPC * message to PBackground to service the request. We stay in this state * until a response is received. */ ResponsePending, /** * A response has been received and RequestHelper has been dispatched back * to the nested event loop to call Finish(). */ Finishing, /** * Finish() has been called on the main thread. The nested event loop will * terminate imminently and the received response returned to the caller of * StartAndReturnResponse. */ Complete }; // The object we are issuing a request on behalf of. Present because of the // need to invoke LSObject::StartRequest off the main thread. Dropped on // return to the main-thread in Finish(). RefPtr mObject; // The thread the RequestHelper was created on. This should be the main // thread. nsCOMPtr mOwningEventTarget; // The pushed event queue that we use to spin the event loop without // processing any of the events dispatched at the mOwningEventTarget (which // would result in re-entrancy and violate LocalStorage semantics). nsCOMPtr mNestedEventTarget; // The wrapper for the pushed event queue. The wrapper automatically // dispatches runnables to the main thread when pushed event queue is no // longer active. This exists because the event loop spinning can be aborted. nsCOMPtr mNestedEventTargetWrapper; // The IPC actor handling the request with standard IPC allocation rules. // Our reference is nulled in OnResponse which corresponds to the actor's // __destroy__ method. LSRequestChild* mActor; const LSRequestParams mParams; LSRequestResponse mResponse; nsresult mResultCode; State mState; // Control flag for the nested event loop; once set to false, the loop ends. bool mWaiting; bool mCancelled; public: RequestHelper(LSObject* aObject, const LSRequestParams& aParams) : Runnable("dom::RequestHelper"), mObject(aObject), mOwningEventTarget(GetCurrentEventTarget()), mActor(nullptr), mParams(aParams), mResultCode(NS_OK), mState(State::Initial), mWaiting(true), mCancelled(false) {} bool IsOnOwningThread() const { MOZ_ASSERT(mOwningEventTarget); bool current; return NS_SUCCEEDED(mOwningEventTarget->IsOnCurrentThread(¤t)) && current; } void AssertIsOnOwningThread() const { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(IsOnOwningThread()); } nsresult StartAndReturnResponse(LSRequestResponse& aResponse); private: ~RequestHelper() = default; nsresult Start(); void Finish(); NS_DECL_ISUPPORTS_INHERITED NS_DECL_NSIRUNNABLE // LSRequestChildCallback void OnResponse(const LSRequestResponse& aResponse) override; }; } // namespace LSObject::LSObject(nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal, nsIPrincipal* aStoragePrincipal) : Storage(aWindow, aPrincipal, aStoragePrincipal), mPrivateBrowsingId(0), mInExplicitSnapshot(false) { AssertIsOnOwningThread(); MOZ_ASSERT(NextGenLocalStorageEnabled()); } LSObject::~LSObject() { AssertIsOnOwningThread(); DropObserver(); } // static void LSObject::Initialize() { MOZ_ASSERT(NS_IsMainThread()); nsCOMPtr domFileThread = RemoteLazyInputStreamThread::GetOrCreate(); if (NS_WARN_IF(!domFileThread)) { return; } RefPtr runnable = NS_NewRunnableFunction("LSObject::Initialize", []() { AssertIsOnDOMFileThread(); mozilla::ipc::PBackgroundChild* backgroundActor = mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread(); if (NS_WARN_IF(!backgroundActor)) { return; } }); if (NS_WARN_IF( NS_FAILED(domFileThread->Dispatch(runnable, NS_DISPATCH_NORMAL)))) { return; } } // static nsresult LSObject::CreateForWindow(nsPIDOMWindowInner* aWindow, Storage** aStorage) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aWindow); MOZ_ASSERT(aStorage); MOZ_ASSERT(NextGenLocalStorageEnabled()); MOZ_ASSERT(StorageAllowedForWindow(aWindow) != StorageAccess::eDeny); nsCOMPtr sop = do_QueryInterface(aWindow); MOZ_ASSERT(sop); nsCOMPtr principal = sop->GetPrincipal(); if (NS_WARN_IF(!principal)) { return NS_ERROR_FAILURE; } nsCOMPtr storagePrincipal = sop->GetEffectiveStoragePrincipal(); if (NS_WARN_IF(!storagePrincipal)) { return NS_ERROR_FAILURE; } if (principal->IsSystemPrincipal()) { return NS_ERROR_NOT_AVAILABLE; } // localStorage is not available on some pages on purpose, for example // about:home. Match the old implementation by using GenerateOriginKey // for the check. nsCString originAttrSuffix; nsCString originKey; nsresult rv = storagePrincipal->GetStorageOriginKey(originKey); storagePrincipal->OriginAttributesRef().CreateSuffix(originAttrSuffix); if (NS_FAILED(rv)) { return NS_ERROR_NOT_AVAILABLE; } auto principalInfo = MakeUnique(); rv = PrincipalToPrincipalInfo(principal, principalInfo.get()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(principalInfo->type() == PrincipalInfo::TContentPrincipalInfo); auto storagePrincipalInfo = MakeUnique(); rv = PrincipalToPrincipalInfo(storagePrincipal, storagePrincipalInfo.get()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(storagePrincipalInfo->type() == PrincipalInfo::TContentPrincipalInfo); if (NS_WARN_IF( !quota::QuotaManager::IsPrincipalInfoValid(*storagePrincipalInfo))) { return NS_ERROR_FAILURE; } #ifdef DEBUG LS_TRY_INSPECT( const auto& quotaInfo, quota::QuotaManager::GetInfoFromPrincipal(storagePrincipal.get())); MOZ_ASSERT(originAttrSuffix == quotaInfo.mSuffix); const auto& origin = quotaInfo.mOrigin; #else LS_TRY_INSPECT( const auto& origin, quota::QuotaManager::GetOriginFromPrincipal(storagePrincipal.get())); #endif uint32_t privateBrowsingId; rv = storagePrincipal->GetPrivateBrowsingId(&privateBrowsingId); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } Maybe clientInfo = aWindow->GetClientInfo(); if (clientInfo.isNothing()) { return NS_ERROR_FAILURE; } Maybe clientId = Some(clientInfo.ref().Id()); nsString documentURI; if (nsCOMPtr doc = aWindow->GetExtantDoc()) { rv = doc->GetDocumentURI(documentURI); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } } RefPtr object = new LSObject(aWindow, principal, storagePrincipal); object->mPrincipalInfo = std::move(principalInfo); object->mStoragePrincipalInfo = std::move(storagePrincipalInfo); object->mPrivateBrowsingId = privateBrowsingId; object->mClientId = clientId; object->mOrigin = origin; object->mOriginKey = originKey; object->mDocumentURI = documentURI; object.forget(aStorage); return NS_OK; } // static nsresult LSObject::CreateForPrincipal(nsPIDOMWindowInner* aWindow, nsIPrincipal* aPrincipal, nsIPrincipal* aStoragePrincipal, const nsAString& aDocumentURI, bool aPrivate, LSObject** aObject) { MOZ_ASSERT(NS_IsMainThread()); MOZ_ASSERT(aPrincipal); MOZ_ASSERT(aStoragePrincipal); MOZ_ASSERT(aObject); nsCString originAttrSuffix; nsCString originKey; nsresult rv = aStoragePrincipal->GetStorageOriginKey(originKey); aStoragePrincipal->OriginAttributesRef().CreateSuffix(originAttrSuffix); if (NS_FAILED(rv)) { return NS_ERROR_NOT_AVAILABLE; } auto principalInfo = MakeUnique(); rv = PrincipalToPrincipalInfo(aPrincipal, principalInfo.get()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(principalInfo->type() == PrincipalInfo::TContentPrincipalInfo || principalInfo->type() == PrincipalInfo::TSystemPrincipalInfo); auto storagePrincipalInfo = MakeUnique(); rv = PrincipalToPrincipalInfo(aStoragePrincipal, storagePrincipalInfo.get()); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT( storagePrincipalInfo->type() == PrincipalInfo::TContentPrincipalInfo || storagePrincipalInfo->type() == PrincipalInfo::TSystemPrincipalInfo); if (NS_WARN_IF( !quota::QuotaManager::IsPrincipalInfoValid(*storagePrincipalInfo))) { return NS_ERROR_FAILURE; } #ifdef DEBUG LS_TRY_INSPECT( const auto& quotaInfo, ([&storagePrincipalInfo, &aPrincipal]() -> Result { if (storagePrincipalInfo->type() == PrincipalInfo::TSystemPrincipalInfo) { return quota::QuotaManager::GetInfoForChrome(); } LS_TRY_RETURN(quota::QuotaManager::GetInfoFromPrincipal(aPrincipal)); }())); MOZ_ASSERT(originAttrSuffix == quotaInfo.mSuffix); const auto& origin = quotaInfo.mOrigin; #else LS_TRY_INSPECT( const auto& origin, ([&storagePrincipalInfo, &aPrincipal]() -> Result { if (storagePrincipalInfo->type() == PrincipalInfo::TSystemPrincipalInfo) { return nsAutoCString{quota::QuotaManager::GetOriginForChrome()}; } LS_TRY_RETURN(quota::QuotaManager::GetOriginFromPrincipal(aPrincipal)); }())); #endif Maybe clientId; if (aWindow) { Maybe clientInfo = aWindow->GetClientInfo(); if (clientInfo.isNothing()) { return NS_ERROR_FAILURE; } clientId = Some(clientInfo.ref().Id()); } else if (Preferences::GetBool("dom.storage.client_validation")) { return NS_ERROR_FAILURE; } RefPtr object = new LSObject(aWindow, aPrincipal, aStoragePrincipal); object->mPrincipalInfo = std::move(principalInfo); object->mStoragePrincipalInfo = std::move(storagePrincipalInfo); object->mPrivateBrowsingId = aPrivate ? 1 : 0; object->mClientId = clientId; object->mOrigin = origin; object->mOriginKey = originKey; object->mDocumentURI = aDocumentURI; object.forget(aObject); return NS_OK; } // namespace dom // static already_AddRefed LSObject::GetSyncLoopEventTarget() { MOZ_ASSERT(XRE_IsParentProcess()); nsCOMPtr target; { StaticMutexAutoLock lock(gRequestHelperMutex); target = gSyncLoopEventTarget; } return target.forget(); } // static void LSObject::OnSyncMessageReceived() { nsCOMPtr target; { StaticMutexAutoLock lock(gRequestHelperMutex); target = gSyncLoopEventTarget; gPendingSyncMessage = true; } if (target) { RefPtr runnable = NS_NewRunnableFunction("LSObject::CheckFlagRunnable", []() {}); MOZ_ALWAYS_SUCCEEDS( target->Dispatch(runnable.forget(), NS_DISPATCH_NORMAL)); } } // static void LSObject::OnSyncMessageHandled() { StaticMutexAutoLock lock(gRequestHelperMutex); gPendingSyncMessage = false; } LSRequestChild* LSObject::StartRequest(nsIEventTarget* aMainEventTarget, const LSRequestParams& aParams, LSRequestChildCallback* aCallback) { AssertIsOnDOMFileThread(); mozilla::ipc::PBackgroundChild* backgroundActor = XRE_IsParentProcess() ? mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread( aMainEventTarget) : mozilla::ipc::BackgroundChild::GetForCurrentThread(); if (NS_WARN_IF(!backgroundActor)) { return nullptr; } LSRequestChild* actor = new LSRequestChild(); if (!backgroundActor->SendPBackgroundLSRequestConstructor(actor, aParams)) { return nullptr; } // Must set callback after calling SendPBackgroundLSRequestConstructor since // it can be called synchronously when SendPBackgroundLSRequestConstructor // fails. actor->SetCallback(aCallback); return actor; } Storage::StorageType LSObject::Type() const { AssertIsOnOwningThread(); return eLocalStorage; } bool LSObject::IsForkOf(const Storage* aStorage) const { AssertIsOnOwningThread(); MOZ_ASSERT(aStorage); if (aStorage->Type() != eLocalStorage) { return false; } return static_cast(aStorage)->mOrigin == mOrigin; } int64_t LSObject::GetOriginQuotaUsage() const { AssertIsOnOwningThread(); // It's not necessary to return an actual value here. This method is // implemented only because the SessionStore currently needs it to cap the // amount of data it persists to disk (via nsIDOMWindowUtils.getStorageUsage). // Any callers that want to know about storage usage should be asking // QuotaManager directly. // // Note: This may change as LocalStorage is repurposed to be the new // SessionStorage backend. return 0; } uint32_t LSObject::GetLength(nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) { AssertIsOnOwningThread(); if (!CanUseStorage(aSubjectPrincipal)) { aError.Throw(NS_ERROR_DOM_SECURITY_ERR); return 0; } nsresult rv = EnsureDatabase(); if (NS_WARN_IF(NS_FAILED(rv))) { aError.Throw(rv); return 0; } uint32_t result; rv = mDatabase->GetLength(this, &result); if (NS_WARN_IF(NS_FAILED(rv))) { aError.Throw(rv); return 0; } return result; } void LSObject::Key(uint32_t aIndex, nsAString& aResult, nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) { AssertIsOnOwningThread(); if (!CanUseStorage(aSubjectPrincipal)) { aError.Throw(NS_ERROR_DOM_SECURITY_ERR); return; } nsresult rv = EnsureDatabase(); if (NS_WARN_IF(NS_FAILED(rv))) { aError.Throw(rv); return; } nsString result; rv = mDatabase->GetKey(this, aIndex, result); if (NS_WARN_IF(NS_FAILED(rv))) { aError.Throw(rv); return; } aResult = result; } void LSObject::GetItem(const nsAString& aKey, nsAString& aResult, nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) { AssertIsOnOwningThread(); if (!CanUseStorage(aSubjectPrincipal)) { aError.Throw(NS_ERROR_DOM_SECURITY_ERR); return; } nsresult rv = EnsureDatabase(); if (NS_WARN_IF(NS_FAILED(rv))) { aError.Throw(rv); return; } nsString result; rv = mDatabase->GetItem(this, aKey, result); if (NS_WARN_IF(NS_FAILED(rv))) { aError.Throw(rv); return; } aResult = result; } void LSObject::GetSupportedNames(nsTArray& aNames) { AssertIsOnOwningThread(); if (!CanUseStorage(*nsContentUtils::SubjectPrincipal())) { // Return just an empty array. aNames.Clear(); return; } nsresult rv = EnsureDatabase(); if (NS_WARN_IF(NS_FAILED(rv))) { return; } rv = mDatabase->GetKeys(this, aNames); if (NS_WARN_IF(NS_FAILED(rv))) { return; } } void LSObject::SetItem(const nsAString& aKey, const nsAString& aValue, nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) { AssertIsOnOwningThread(); if (!CanUseStorage(aSubjectPrincipal)) { aError.Throw(NS_ERROR_DOM_SECURITY_ERR); return; } nsresult rv = EnsureDatabase(); if (NS_WARN_IF(NS_FAILED(rv))) { aError.Throw(rv); return; } LSNotifyInfo info; rv = mDatabase->SetItem(this, aKey, aValue, info); if (rv == NS_ERROR_FILE_NO_DEVICE_SPACE) { rv = NS_ERROR_DOM_QUOTA_EXCEEDED_ERR; } if (NS_WARN_IF(NS_FAILED(rv))) { aError.Throw(rv); return; } if (info.changed()) { OnChange(aKey, info.oldValue(), aValue); } } void LSObject::RemoveItem(const nsAString& aKey, nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) { AssertIsOnOwningThread(); if (!CanUseStorage(aSubjectPrincipal)) { aError.Throw(NS_ERROR_DOM_SECURITY_ERR); return; } nsresult rv = EnsureDatabase(); if (NS_WARN_IF(NS_FAILED(rv))) { aError.Throw(rv); return; } LSNotifyInfo info; rv = mDatabase->RemoveItem(this, aKey, info); if (NS_WARN_IF(NS_FAILED(rv))) { aError.Throw(rv); return; } if (info.changed()) { OnChange(aKey, info.oldValue(), VoidString()); } } void LSObject::Clear(nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) { AssertIsOnOwningThread(); if (!CanUseStorage(aSubjectPrincipal)) { aError.Throw(NS_ERROR_DOM_SECURITY_ERR); return; } nsresult rv = EnsureDatabase(); if (NS_WARN_IF(NS_FAILED(rv))) { aError.Throw(rv); return; } LSNotifyInfo info; rv = mDatabase->Clear(this, info); if (NS_WARN_IF(NS_FAILED(rv))) { aError.Throw(rv); return; } if (info.changed()) { OnChange(VoidString(), VoidString(), VoidString()); } } void LSObject::Open(nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) { AssertIsOnOwningThread(); if (!CanUseStorage(aSubjectPrincipal)) { aError.Throw(NS_ERROR_DOM_SECURITY_ERR); return; } nsresult rv = EnsureDatabase(); if (NS_WARN_IF(NS_FAILED(rv))) { aError.Throw(rv); return; } } void LSObject::Close(nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) { AssertIsOnOwningThread(); if (!CanUseStorage(aSubjectPrincipal)) { aError.Throw(NS_ERROR_DOM_SECURITY_ERR); return; } DropDatabase(); } void LSObject::BeginExplicitSnapshot(nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) { AssertIsOnOwningThread(); if (!CanUseStorage(aSubjectPrincipal)) { aError.Throw(NS_ERROR_DOM_SECURITY_ERR); return; } if (mInExplicitSnapshot) { aError.Throw(NS_ERROR_ALREADY_INITIALIZED); return; } nsresult rv = EnsureDatabase(); if (NS_WARN_IF(NS_FAILED(rv))) { aError.Throw(rv); return; } rv = mDatabase->BeginExplicitSnapshot(this); if (NS_WARN_IF(NS_FAILED(rv))) { aError.Throw(rv); return; } mInExplicitSnapshot = true; } void LSObject::EndExplicitSnapshot(nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) { AssertIsOnOwningThread(); if (!CanUseStorage(aSubjectPrincipal)) { aError.Throw(NS_ERROR_DOM_SECURITY_ERR); return; } if (!mInExplicitSnapshot) { aError.Throw(NS_ERROR_NOT_INITIALIZED); return; } nsresult rv = EndExplicitSnapshotInternal(); if (NS_WARN_IF(NS_FAILED(rv))) { aError.Throw(rv); return; } } bool LSObject::GetHasActiveSnapshot(nsIPrincipal& aSubjectPrincipal, ErrorResult& aError) { AssertIsOnOwningThread(); if (!CanUseStorage(aSubjectPrincipal)) { aError.Throw(NS_ERROR_DOM_SECURITY_ERR); return 0; } if (mDatabase && mDatabase->HasActiveSnapshot()) { MOZ_ASSERT(!mDatabase->IsAllowedToClose()); return true; } return false; } NS_IMPL_ADDREF_INHERITED(LSObject, Storage) NS_IMPL_RELEASE_INHERITED(LSObject, Storage) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(LSObject) NS_INTERFACE_MAP_END_INHERITING(Storage) NS_IMPL_CYCLE_COLLECTION_CLASS(LSObject) NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(LSObject, Storage) tmp->AssertIsOnOwningThread(); NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(LSObject, Storage) tmp->AssertIsOnOwningThread(); tmp->DropDatabase(); NS_IMPL_CYCLE_COLLECTION_UNLINK_END nsresult LSObject::DoRequestSynchronously(const LSRequestParams& aParams, LSRequestResponse& aResponse) { // We don't need this yet, but once the request successfully finishes, it's // too late to initialize PBackground child on the owning thread, because // it can fail and parent would keep an extra strong ref to the datastore or // observer. mozilla::ipc::PBackgroundChild* backgroundActor = mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread(); if (NS_WARN_IF(!backgroundActor)) { return NS_ERROR_FAILURE; } RefPtr helper = new RequestHelper(this, aParams); // This will start and finish the request on the RemoteLazyInputStream thread. // The owning thread is synchronously blocked while the request is // asynchronously processed on the RemoteLazyInputStream thread. nsresult rv = helper->StartAndReturnResponse(aResponse); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } if (aResponse.type() == LSRequestResponse::Tnsresult) { nsresult errorCode = aResponse.get_nsresult(); if (errorCode == NS_ERROR_FILE_NO_DEVICE_SPACE) { errorCode = NS_ERROR_DOM_QUOTA_EXCEEDED_ERR; } return errorCode; } return NS_OK; } nsresult LSObject::EnsureDatabase() { AssertIsOnOwningThread(); if (mDatabase && !mDatabase->IsAllowedToClose()) { return NS_OK; } mDatabase = LSDatabase::Get(mOrigin); if (mDatabase) { MOZ_ASSERT(!mDatabase->IsAllowedToClose()); return NS_OK; } // We don't need this yet, but once the request successfully finishes, it's // too late to initialize PBackground child on the owning thread, because // it can fail and parent would keep an extra strong ref to the datastore. mozilla::ipc::PBackgroundChild* backgroundActor = mozilla::ipc::BackgroundChild::GetOrCreateForCurrentThread(); if (NS_WARN_IF(!backgroundActor)) { return NS_ERROR_FAILURE; } LSRequestCommonParams commonParams; commonParams.principalInfo() = *mPrincipalInfo; commonParams.storagePrincipalInfo() = *mStoragePrincipalInfo; commonParams.originKey() = mOriginKey; LSRequestPrepareDatastoreParams params; params.commonParams() = commonParams; params.clientId() = mClientId; LSRequestResponse response; nsresult rv = DoRequestSynchronously(params, response); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(response.type() == LSRequestResponse::TLSRequestPrepareDatastoreResponse); const LSRequestPrepareDatastoreResponse& prepareDatastoreResponse = response.get_LSRequestPrepareDatastoreResponse(); uint64_t datastoreId = prepareDatastoreResponse.datastoreId(); // The datastore is now ready on the parent side (prepared by the asynchronous // request on the RemoteLazyInputStream thread). // Let's create a direct connection to the datastore (through a database // actor) from the owning thread. // Note that we now can't error out, otherwise parent will keep an extra // strong reference to the datastore. RefPtr database = new LSDatabase(mOrigin); LSDatabaseChild* actor = new LSDatabaseChild(database); MOZ_ALWAYS_TRUE(backgroundActor->SendPBackgroundLSDatabaseConstructor( actor, *mStoragePrincipalInfo, mPrivateBrowsingId, datastoreId)); database->SetActor(actor); mDatabase = std::move(database); return NS_OK; } void LSObject::DropDatabase() { AssertIsOnOwningThread(); if (mInExplicitSnapshot) { nsresult rv = EndExplicitSnapshotInternal(); Unused << NS_WARN_IF(NS_FAILED(rv)); } mDatabase = nullptr; } nsresult LSObject::EnsureObserver() { AssertIsOnOwningThread(); if (mObserver) { return NS_OK; } mObserver = LSObserver::Get(mOrigin); if (mObserver) { return NS_OK; } LSRequestPrepareObserverParams params; params.principalInfo() = *mPrincipalInfo; params.storagePrincipalInfo() = *mStoragePrincipalInfo; params.clientId() = mClientId; LSRequestResponse response; nsresult rv = DoRequestSynchronously(params, response); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } MOZ_ASSERT(response.type() == LSRequestResponse::TLSRequestPrepareObserverResponse); const LSRequestPrepareObserverResponse& prepareObserverResponse = response.get_LSRequestPrepareObserverResponse(); uint64_t observerId = prepareObserverResponse.observerId(); // The obsserver is now ready on the parent side (prepared by the asynchronous // request on the RemoteLazyInputStream thread). // Let's create a direct connection to the observer (through an observer // actor) from the owning thread. // Note that we now can't error out, otherwise parent will keep an extra // strong reference to the observer. mozilla::ipc::PBackgroundChild* backgroundActor = mozilla::ipc::BackgroundChild::GetForCurrentThread(); MOZ_ASSERT(backgroundActor); RefPtr observer = new LSObserver(mOrigin); LSObserverChild* actor = new LSObserverChild(observer); MOZ_ALWAYS_TRUE( backgroundActor->SendPBackgroundLSObserverConstructor(actor, observerId)); observer->SetActor(actor); mObserver = std::move(observer); return NS_OK; } void LSObject::DropObserver() { AssertIsOnOwningThread(); if (mObserver) { mObserver = nullptr; } } void LSObject::OnChange(const nsAString& aKey, const nsAString& aOldValue, const nsAString& aNewValue) { AssertIsOnOwningThread(); NotifyChange(/* aStorage */ this, StoragePrincipal(), aKey, aOldValue, aNewValue, /* aStorageType */ kLocalStorageType, mDocumentURI, /* aIsPrivate */ !!mPrivateBrowsingId, /* aImmediateDispatch */ false); } nsresult LSObject::EndExplicitSnapshotInternal() { AssertIsOnOwningThread(); // Can be only called if the mInExplicitSnapshot flag is true. // An explicit snapshot must have been created. MOZ_ASSERT(mInExplicitSnapshot); // If an explicit snapshot have been created then mDatabase must be not null. // DropDatabase could be called in the meatime, but that would set // mInExplicitSnapshot to false. EnsureDatabase could be called in the // meantime too, but that can't set mDatabase to null or to a new value. See // the comment below. MOZ_ASSERT(mDatabase); // Existence of a snapshot prevents the database from allowing to close. See // LSDatabase::RequestAllowToClose and LSDatabase::NoteFinishedSnapshot. // If the database is not allowed to close then mDatabase could not have been // nulled out or set to a new value. See EnsureDatabase. MOZ_ASSERT(!mDatabase->IsAllowedToClose()); nsresult rv = mDatabase->EndExplicitSnapshot(this); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } mInExplicitSnapshot = false; return NS_OK; } void LSObject::LastRelease() { AssertIsOnOwningThread(); DropDatabase(); } NS_IMPL_ISUPPORTS(NestedEventTargetWrapper, nsIEventTarget, nsISerialEventTarget); NS_IMETHODIMP_(bool) NestedEventTargetWrapper::IsOnCurrentThreadInfallible() { MOZ_CRASH( "IsOnCurrentThreadInfallible should never be called on " "NestedEventTargetWrapper"); } NS_IMETHODIMP NestedEventTargetWrapper::IsOnCurrentThread(bool* aResult) { MOZ_CRASH( "IsOnCurrentThread should never be called on " "NestedEventTargetWrapper"); } NS_IMETHODIMP NestedEventTargetWrapper::Dispatch(already_AddRefed aEvent, uint32_t aFlags) { MOZ_ASSERT(mNestedEventTarget); if (mDisconnected) { MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(std::move(aEvent), aFlags)); return NS_OK; } nsCOMPtr event(aEvent); nsresult rv = mNestedEventTarget->Dispatch(event, aFlags); if (rv == NS_ERROR_UNEXPECTED) { mDisconnected = true; // Dispatch leaked the event object on the NS_ERROR_UNEXPECTED failure, so // we explicitly release this object once for that. event.get()->Release(); MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(event.forget(), aFlags)); } else if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_OK; } NS_IMETHODIMP NestedEventTargetWrapper::DispatchFromScript(nsIRunnable* aEvent, uint32_t aFlags) { MOZ_CRASH( "DispatchFromScript should never be called on " "NestedEventTargetWrapper"); } NS_IMETHODIMP NestedEventTargetWrapper::DelayedDispatch(already_AddRefed aEvent, uint32_t aDelayMs) { MOZ_CRASH( "DelayedDispatch should never be called on " "NestedEventTargetWrapper"); } nsresult RequestHelper::StartAndReturnResponse(LSRequestResponse& aResponse) { AssertIsOnOwningThread(); // Normally, we would use the standard way of blocking the thread using // a monitor. // The problem is that BackgroundChild::GetOrCreateForCurrentThread() // called on the RemoteLazyInputStream thread may dispatch a runnable to the // main thread to finish initialization of PBackground. A monitor would block // the main thread and the runnable would never get executed causing the // helper to be stuck in a wait loop. // However, BackgroundChild::GetOrCreateForCurrentThread() supports passing // a custom main event target, so we can create a nested event target and // spin the event loop. Nothing can dispatch to the nested event target // except BackgroundChild::GetOrCreateForCurrentThread(), so spinning of the // event loop can't fire any other events. // This way the thread is synchronously blocked in a safe manner and the // runnable gets executed. { auto thread = static_cast(NS_GetCurrentThread()); const nsLocalExecutionGuard localExecution(thread->EnterLocalExecution()); mNestedEventTarget = localExecution.GetEventTarget(); MOZ_ASSERT(mNestedEventTarget); mNestedEventTargetWrapper = new NestedEventTargetWrapper(mNestedEventTarget); nsCOMPtr domFileThread = XRE_IsParentProcess() ? RemoteLazyInputStreamThread::GetOrCreate() : RemoteLazyInputStreamThread::Get(); if (NS_WARN_IF(!domFileThread)) { return NS_ERROR_FAILURE; } nsresult rv; { { StaticMutexAutoLock lock(gRequestHelperMutex); if (StaticPrefs::dom_storage_abort_on_sync_parent_to_child_messages() && NS_WARN_IF(gPendingSyncMessage)) { return NS_ERROR_FAILURE; } gSyncLoopEventTarget = mNestedEventTargetWrapper; } auto autoClearSyncLoopEventTarget = mozilla::MakeScopeExit([&] { StaticMutexAutoLock lock(gRequestHelperMutex); gSyncLoopEventTarget = nullptr; }); rv = domFileThread->Dispatch(this, NS_DISPATCH_NORMAL); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } nsCOMPtr timer = NS_NewTimer(); MOZ_ALWAYS_SUCCEEDS(timer->SetTarget(mNestedEventTarget)); MOZ_ALWAYS_SUCCEEDS(timer->InitWithNamedFuncCallback( [](nsITimer* aTimer, void* aClosure) { auto helper = static_cast(aClosure); helper->mCancelled = true; }, this, FAILSAFE_CANCEL_SYNC_OP_MS, nsITimer::TYPE_ONE_SHOT, "RequestHelper::StartAndReturnResponse::SpinEventLoopTimer")); MOZ_ALWAYS_TRUE(SpinEventLoopUntil( [&]() { if (mCancelled) { return true; } if (!mWaiting) { return true; } { StaticMutexAutoLock lock(gRequestHelperMutex); if (StaticPrefs:: dom_storage_abort_on_sync_parent_to_child_messages() && NS_WARN_IF(gPendingSyncMessage)) { return true; } } return false; }, thread)); MOZ_ALWAYS_SUCCEEDS(timer->Cancel()); } // If mWaiting is still set to true, it means that the event loop spinning // was aborted and we need to cancel the request in the parent since we // don't care about the result anymore. // We can check mWaiting here because it's only ever touched on the main // thread. if (NS_WARN_IF(mWaiting)) { // Don't touch mResponse, mResultCode or mState here! The // RemoteLazyInputStream Thread may be accessing them at the same moment. RefPtr self = this; RefPtr runnable = NS_NewRunnableFunction("RequestHelper::SendCancelRunnable", [self]() { LSRequestChild* actor = self->mActor; // Start() could fail or it hasn't had a chance to run yet, so we // need to check if actor is not null. // The actor can also be in the final (finishing) state, in that // case we are not allowed to send the cancel message and it // wouldn't make sense because the request is about to be destroyed // anyway. if (actor && !actor->Finishing()) { actor->SendCancel(); } }); rv = domFileThread->Dispatch(runnable, NS_DISPATCH_NORMAL); if (NS_WARN_IF(NS_FAILED(rv))) { return rv; } return NS_ERROR_FAILURE; } // localExecution will be destructed when we leave this scope. If the event // loop spinning was aborted and other threads dispatched new runnables to // the nested event queue, they will be moved to the main event queue here // and later asynchronusly processed. So nothing will be lost. } if (NS_WARN_IF(NS_FAILED(mResultCode))) { return mResultCode; } aResponse = std::move(mResponse); return NS_OK; } nsresult RequestHelper::Start() { AssertIsOnDOMFileThread(); MOZ_ASSERT(mState == State::Initial); mState = State::ResponsePending; LSRequestChild* actor = mObject->StartRequest(mNestedEventTargetWrapper, mParams, this); if (NS_WARN_IF(!actor)) { return NS_ERROR_FAILURE; } mActor = actor; return NS_OK; } void RequestHelper::Finish() { AssertIsOnOwningThread(); MOZ_ASSERT(mState == State::Finishing); mObject = nullptr; mWaiting = false; mState = State::Complete; } NS_IMPL_ISUPPORTS_INHERITED0(RequestHelper, Runnable) NS_IMETHODIMP RequestHelper::Run() { nsresult rv; switch (mState) { case State::Initial: rv = Start(); break; case State::Finishing: Finish(); return NS_OK; default: MOZ_CRASH("Bad state!"); } if (NS_WARN_IF(NS_FAILED(rv)) && mState != State::Finishing) { if (NS_SUCCEEDED(mResultCode)) { mResultCode = rv; } mState = State::Finishing; if (IsOnOwningThread()) { Finish(); } else { MOZ_ALWAYS_SUCCEEDS( mNestedEventTargetWrapper->Dispatch(this, NS_DISPATCH_NORMAL)); } } return NS_OK; } void RequestHelper::OnResponse(const LSRequestResponse& aResponse) { AssertIsOnDOMFileThread(); MOZ_ASSERT(mState == State::ResponsePending); mActor = nullptr; mResponse = aResponse; mState = State::Finishing; MOZ_ALWAYS_SUCCEEDS( mNestedEventTargetWrapper->Dispatch(this, NS_DISPATCH_NORMAL)); } } // namespace mozilla::dom