Files
Eden Chuang d61a14089e Bug 2026301 - Synchronize ThreadSafeRequestHandle::mRunnable to fix cross-thread use-after-free r=dom-worker-reviewers,asuth
mRunnable was a plain RefPtr read on the main thread (GetCacheCreator and the
other accessors) and cleared on the worker thread by ReleaseRequest() with no
synchronization. When the worker's store dropped the last reference to the
shared ScriptLoaderRunnable, the main thread could dereference the already-freed
pointer, producing a heap-use-after-free.

Guard mRunnable with a Mutex (enforced via MOZ_GUARDED_BY). ReleaseRequest() and
the new SetRunnable() mutate it under the lock, releasing the runnable outside
the lock so it is never destroyed while the mutex is held. Each main-thread
accessor copies mRunnable into a local strong reference under the lock and bails
if it was already released. GetCacheCreator() now returns a strong CacheCreator
reference taken under the lock, so the caller keeps it alive across DeleteCache()
even if the runnable is released on the worker thread afterwards.

Differential Revision: https://phabricator.services.mozilla.com/D307211
2026-06-30 13:52:46 +00:00

144 lines
4.4 KiB
C++

/* 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 "WorkerLoadContext.h"
#include "CacheLoadHandler.h" // CacheCreator
#include "js/loader/ScriptLoadRequest.h"
#include "mozilla/dom/workerinternals/ScriptLoader.h"
namespace mozilla {
namespace dom {
WorkerLoadContext::WorkerLoadContext(
Kind aKind, const Maybe<ClientInfo>& aClientInfo,
workerinternals::loader::WorkerScriptLoader* aScriptLoader,
bool aOnlyExistingCachedResourcesAllowed)
: JS::loader::LoadContextBase(JS::loader::ContextKind::Worker),
mKind(aKind),
mClientInfo(aClientInfo),
mScriptLoader(aScriptLoader),
mOnlyExistingCachedResourcesAllowed(aOnlyExistingCachedResourcesAllowed) {
};
bool WorkerLoadContext::IsTopLevel() {
return mRequest->IsTopLevel() && (mKind == Kind::MainScript);
};
ThreadSafeRequestHandle::ThreadSafeRequestHandle(
JS::loader::ScriptLoadRequest* aRequest, nsISerialEventTarget* aSyncTarget)
: mRequest(aRequest), mOwningEventTarget(aSyncTarget) {}
WorkerLoadContext* ThreadSafeRequestHandle::GetContext() {
return mRequest->GetWorkerLoadContext();
}
void ThreadSafeRequestHandle::SetRunnable(
workerinternals::loader::ScriptLoaderRunnable* aRunnable) {
MutexAutoLock lock(mMutex);
mRunnable = aRunnable;
}
already_AddRefed<JS::loader::ScriptLoadRequest>
ThreadSafeRequestHandle::ReleaseRequest() {
RefPtr<JS::loader::ScriptLoadRequest> request;
RefPtr<workerinternals::loader::ScriptLoaderRunnable> runnable;
{
MutexAutoLock lock(mMutex);
mRequest.swap(request);
mRunnable.swap(runnable);
}
// Drop the last reference to the runnable, if any, outside of the lock so it
// is never destroyed while mMutex is held.
return request.forget();
}
nsresult ThreadSafeRequestHandle::OnStreamComplete(nsresult aStatus) {
RefPtr<workerinternals::loader::ScriptLoaderRunnable> runnable;
{
MutexAutoLock lock(mMutex);
runnable = mRunnable;
}
if (!runnable) {
return NS_OK;
}
return runnable->OnStreamComplete(this, aStatus);
}
void ThreadSafeRequestHandle::LoadingFinished(nsresult aRv) {
RefPtr<workerinternals::loader::ScriptLoaderRunnable> runnable;
{
MutexAutoLock lock(mMutex);
runnable = mRunnable;
}
if (!runnable) {
return;
}
runnable->LoadingFinished(this, aRv);
}
void ThreadSafeRequestHandle::MaybeExecuteFinishedScripts() {
RefPtr<workerinternals::loader::ScriptLoaderRunnable> runnable;
{
MutexAutoLock lock(mMutex);
runnable = mRunnable;
}
if (!runnable) {
return;
}
runnable->MaybeExecuteFinishedScripts(this);
}
bool ThreadSafeRequestHandle::IsCancelled() {
RefPtr<workerinternals::loader::ScriptLoaderRunnable> runnable;
{
MutexAutoLock lock(mMutex);
runnable = mRunnable;
}
return runnable && runnable->IsCancelled();
}
nsresult ThreadSafeRequestHandle::GetCancelResult() {
RefPtr<workerinternals::loader::ScriptLoaderRunnable> runnable;
{
MutexAutoLock lock(mMutex);
runnable = mRunnable;
}
return runnable ? runnable->GetCancelResult() : NS_OK;
}
already_AddRefed<workerinternals::loader::CacheCreator>
ThreadSafeRequestHandle::GetCacheCreator() {
AssertIsOnMainThread();
MutexAutoLock lock(mMutex);
if (!mRunnable) {
return nullptr;
}
// Return a strong reference so the CacheCreator stays alive for the caller
// even if the runnable is released on the worker thread once we drop the
// lock.
RefPtr<workerinternals::loader::CacheCreator> cacheCreator =
mRunnable->GetCacheCreator();
return cacheCreator.forget();
}
ThreadSafeRequestHandle::~ThreadSafeRequestHandle() {
// Normally we only touch mStrongRef on the owning thread. This is safe,
// however, because when we do use mStrongRef on the owning thread we are
// always holding a strong ref to the ThreadsafeHandle via the owning
// runnable. So we cannot run the ThreadsafeHandle destructor simultaneously.
if (!mRequest || mOwningEventTarget->IsOnCurrentThread()) {
return;
}
// Dispatch in NS_ProxyRelease is guaranteed to succeed here because we block
// shutdown until all Contexts have been destroyed. Therefore it is ok to have
// MOZ_ALWAYS_SUCCEED here.
MOZ_ALWAYS_SUCCEEDS(NS_ProxyRelease("ThreadSafeRequestHandle::mRequest",
mOwningEventTarget, mRequest.forget()));
}
} // namespace dom
} // namespace mozilla