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 /dom/localstorage/LSObject.cpp | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'dom/localstorage/LSObject.cpp')
-rw-r--r-- | dom/localstorage/LSObject.cpp | 1384 |
1 files changed, 1384 insertions, 0 deletions
diff --git a/dom/localstorage/LSObject.cpp b/dom/localstorage/LSObject.cpp new file mode 100644 index 0000000000..ca019e5bb2 --- /dev/null +++ b/dom/localstorage/LSObject.cpp @@ -0,0 +1,1384 @@ +/* -*- 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 <utility> +#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<nsISerialEventTarget> 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<LSObject> mObject; + // The thread the RequestHelper was created on. This should be the main + // thread. + nsCOMPtr<nsIEventTarget> 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<nsISerialEventTarget> 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<nsISerialEventTarget> 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<nsIEventTarget> domFileThread = + RemoteLazyInputStreamThread::GetOrCreate(); + if (NS_WARN_IF(!domFileThread)) { + return; + } + + RefPtr<Runnable> 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<nsIScriptObjectPrincipal> sop = do_QueryInterface(aWindow); + MOZ_ASSERT(sop); + + nsCOMPtr<nsIPrincipal> principal = sop->GetPrincipal(); + if (NS_WARN_IF(!principal)) { + return NS_ERROR_FAILURE; + } + + nsCOMPtr<nsIPrincipal> 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<PrincipalInfo>(); + rv = PrincipalToPrincipalInfo(principal, principalInfo.get()); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + MOZ_ASSERT(principalInfo->type() == PrincipalInfo::TContentPrincipalInfo); + + auto storagePrincipalInfo = MakeUnique<PrincipalInfo>(); + 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> clientInfo = aWindow->GetClientInfo(); + if (clientInfo.isNothing()) { + return NS_ERROR_FAILURE; + } + + Maybe<nsID> clientId = Some(clientInfo.ref().Id()); + + nsString documentURI; + if (nsCOMPtr<Document> doc = aWindow->GetExtantDoc()) { + rv = doc->GetDocumentURI(documentURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + RefPtr<LSObject> 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<PrincipalInfo>(); + 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<PrincipalInfo>(); + 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<quota::QuotaInfo, nsresult> { + 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<nsAutoCString, nsresult> { + if (storagePrincipalInfo->type() == + PrincipalInfo::TSystemPrincipalInfo) { + return nsAutoCString{quota::QuotaManager::GetOriginForChrome()}; + } + + LS_TRY_RETURN(quota::QuotaManager::GetOriginFromPrincipal(aPrincipal)); + }())); +#endif + + Maybe<nsID> clientId; + if (aWindow) { + Maybe<ClientInfo> 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<LSObject> 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<nsISerialEventTarget> LSObject::GetSyncLoopEventTarget() { + MOZ_ASSERT(XRE_IsParentProcess()); + + nsCOMPtr<nsISerialEventTarget> target; + + { + StaticMutexAutoLock lock(gRequestHelperMutex); + target = gSyncLoopEventTarget; + } + + return target.forget(); +} + +// static +void LSObject::OnSyncMessageReceived() { + nsCOMPtr<nsISerialEventTarget> target; + + { + StaticMutexAutoLock lock(gRequestHelperMutex); + target = gSyncLoopEventTarget; + gPendingSyncMessage = true; + } + + if (target) { + RefPtr<Runnable> 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<const LSObject*>(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<nsString>& 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<RequestHelper> 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<LSDatabase> 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<LSObserver> 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<nsIRunnable> aEvent, + uint32_t aFlags) { + MOZ_ASSERT(mNestedEventTarget); + + if (mDisconnected) { + MOZ_ALWAYS_SUCCEEDS(NS_DispatchToMainThread(std::move(aEvent), aFlags)); + return NS_OK; + } + + nsCOMPtr<nsIRunnable> 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<nsIRunnable> 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<nsThread*>(NS_GetCurrentThread()); + + const nsLocalExecutionGuard localExecution(thread->EnterLocalExecution()); + mNestedEventTarget = localExecution.GetEventTarget(); + MOZ_ASSERT(mNestedEventTarget); + + mNestedEventTargetWrapper = + new NestedEventTargetWrapper(mNestedEventTarget); + + nsCOMPtr<nsIEventTarget> 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<nsITimer> timer = NS_NewTimer(); + + MOZ_ALWAYS_SUCCEEDS(timer->SetTarget(mNestedEventTarget)); + + MOZ_ALWAYS_SUCCEEDS(timer->InitWithNamedFuncCallback( + [](nsITimer* aTimer, void* aClosure) { + auto helper = static_cast<RequestHelper*>(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<RequestHelper> self = this; + + RefPtr<Runnable> 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 |