High firefox Sandbox Escape 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionSandbox escape in the DOM: Workers component
ComponentDOM
Bug ClassSandbox Escape
Tracker2039873
Fix commit6e310738d31a (firefox) +39/-43
CISA KEVNot listed
CreditedQuy Pham
Disclosed2026-06-16

Background

Remote type
the string tag (e.g. SharedWebRemoteType, DEFAULT_REMOTE_TYPE, NOT_REMOTE_TYPE) that identifies which content-process category a document or worker is assigned to under process isolation.
Shared worker
a SharedWorker script shared across same-origin browsing contexts and hosted in a content process brokered by the parent-process SharedWorkerService.
`ThreadsafeContentParentHandle`
a parent-process handle that names the actual content process that sent a given IPC request, retrievable via BackgroundParent::GetContentParentHandle.
Fission
Gecko’s site-isolation architecture that places origins in distinct content processes, using each process’s remote type to enforce the boundary.

Root Cause Analysis

When a content process created a SharedWorker, SharedWorker::Constructor computed the target remote type in the content process and shipped it to the parent inside RemoteWorkerData, and the parent-side SharedWorkerService::GetOrCreateWorkerManagerOnMainThread recomputed it by calling RemoteWorkerManager::GetRemoteType, which for a shared worker derived its preferred remote type from ContentChild::GetSingleton()->GetRemoteType(). That derivation was wrong in the parent process, where ContentChild::GetSingleton() returns null, so the code silently fell back to the generic SharedWebRemoteType instead of the requesting process’s real remote type — meaning the parent’s decision was not anchored to the trusted identity of the process that actually asked. The violated invariant is that a shared worker must be placed in / matched to a content process consistent with the true remote type of the requesting process as known to the parent, never a value supplied or inferred from the content side.

The fix removes the ContentChild-based guess entirely and adds an explicit aCurrentRemoteType parameter to GetRemoteType, requiring each caller to pass an authoritative value. For shared workers the parent now derives that value from aContentParentHandle->GetRemoteType() — the trusted ThreadsafeContentParentHandle of the real sender — and the content-supplied copyData.remoteType() is no longer trusted or copied back. This works because the process-placement decision is now made in the parent using an unforgeable, correct source of the requester’s identity.

Key insight
The single mistake was letting the shared worker’s process-placement (remote type) be decided from an unreliable/content-influenced source — a ContentChild::GetSingleton() derivation that degrades to a generic default in the parent, plus a content-supplied remoteType() — instead of the parent’s trusted view of the sender. The fix threads the sender’s real remote type from ThreadsafeContentParentHandle::GetRemoteType() into GetRemoteType, so isolation is computed against the true process identity.

Attack Path

  1. Compromise a content process The attacker first achieves code execution inside a sandboxed content process, giving control over the IPC messages it emits.
  2. Craft a shared-worker request The compromised process issues a SharedWorker construction whose RemoteWorkerData (principal and self-reported remoteType()) is chosen to steer process selection.
  3. Exploit the mis-derived remote type In GetOrCreateWorkerManagerOnMainThread the parent recomputes the remote type using the generic SharedWebRemoteType fallback rather than the requester’s true remote type, so isolation is evaluated against the wrong process.
  4. Land the worker in an unintended process The shared worker is created in or matched to a content process that should not host that origin’s worker, breaching the Fission boundary.
  5. Cross the boundary Through the misplaced shared worker the attacker reaches state or execution context in a process outside its intended sandbox, escalating out of the origin’s isolation.

Impact Assessment

An attacker who already controls a sandboxed content process can cause a shared worker to be assigned to a content process it should not share, defeating Fission/site isolation and enabling a sandbox escape into another process context. This occurs in the parent-process SharedWorkerService path that brokers shared-worker placement across the content-process boundary. The precondition is prior compromise of a content process (or the ability to drive malicious shared-worker IPC), consistent with the SandboxEscape classification and high impact.

Changed Functions

FunctionChangeNotes
if
dom/workers/remoteworkers/RemoteWorkerManager.cpp
modified
GetOrCreateWorkerManagerRunnable
dom/workers/sharedworkers/SharedWorkerService.cpp
modified
Run
dom/workers/sharedworkers/SharedWorkerService.cpp
modified
RemoteWorkerData
dom/workers/sharedworkers/SharedWorkerService.h
modified
SharedWorkerManager
dom/workers/sharedworkers/SharedWorkerService.h
modified
SharedWorkerParent
dom/workers/sharedworkers/SharedWorkerService.h
modified
ThreadsafeContentParentHandle
dom/workers/sharedworkers/SharedWorkerService.h
modified
UniqueMessagePortId
dom/workers/sharedworkers/SharedWorkerService.h
modified

Files Changed

  • dom/serviceworkers/ServiceWorkerPrivate.cpp
  • dom/workers/remoteworkers/RemoteWorkerManager.cpp
  • dom/workers/remoteworkers/RemoteWorkerManager.h
  • dom/workers/sharedworkers/SharedWorker.cpp
  • dom/workers/sharedworkers/SharedWorkerService.cpp
  • dom/workers/sharedworkers/SharedWorkerService.h

Audit Directions

  • Process identity from the sender, not the payload
    In any parent/broker IPC handler, derive a process’s remote type or origin from the actor’s ThreadsafeContentParentHandle/ContentParent, never from a value carried in the message body.
  • `ContentChild::GetSingleton()` in shared code
    Flag logic that calls ContentChild::GetSingleton() (or similar child-only singletons) in code that can run in the parent process, where it returns null and silently degrades to a default.
  • Remote-type / isolation computations
    Review every call to GetRemoteType, IsolationOptionsForWorker, and MatchRemoteType to confirm the “current” remote type is an authoritative parent-side value and that content-supplied remoteType() fields are treated as untrusted.
diff --git a/dom/serviceworkers/ServiceWorkerPrivate.cpp b/dom/serviceworkers/ServiceWorkerPrivate.cpp
index eb1948f0005..5bda51baf72 100644
--- a/dom/serviceworkers/ServiceWorkerPrivate.cpp
+++ b/dom/serviceworkers/ServiceWorkerPrivate.cpp
@@ -714,7 +714,8 @@ nsresult ServiceWorkerPrivate::Initialize() {
   }
 
   auto remoteType = RemoteWorkerManager::GetRemoteType(
-      principal, WorkerKind::WorkerKindService);
+      principal, WorkerKind::WorkerKindService,
+      SharedWebRemoteType(principal->OriginAttributesRef()));
   if (NS_WARN_IF(remoteType.isErr())) {
     return remoteType.unwrapErr();
   }
diff --git a/dom/workers/remoteworkers/RemoteWorkerManager.cpp b/dom/workers/remoteworkers/RemoteWorkerManager.cpp
index 781c1330e73..24467b59e35 100644
--- a/dom/workers/remoteworkers/RemoteWorkerManager.cpp
+++ b/dom/workers/remoteworkers/RemoteWorkerManager.cpp
@@ -120,7 +120,8 @@ bool RemoteWorkerManager::MatchRemoteType(const nsACString& processRemoteType,
 
 // static
 Result<nsCString, nsresult> RemoteWorkerManager::GetRemoteType(
-    const nsCOMPtr<nsIPrincipal>& aPrincipal, WorkerKind aWorkerKind) {
+    const nsCOMPtr<nsIPrincipal>& aPrincipal, WorkerKind aWorkerKind,
+    const nsACString& aCurrentRemoteType) {
   AssertIsOnMainThread();
 
   MOZ_ASSERT_IF(aWorkerKind == WorkerKind::WorkerKindService,
@@ -133,20 +134,8 @@ Result<nsCString, nsresult> RemoteWorkerManager::GetRemoteType(
     return NOT_REMOTE_TYPE;
   }
 
-  nsCString preferredRemoteType =
-      SharedWebRemoteType(aPrincipal->OriginAttributesRef());
-  if (aWorkerKind == WorkerKind::WorkerKindShared) {
-    if (auto* contentChild = ContentChild::GetSingleton()) {
-      // For a shared worker set the preferred remote type to the content
-      // child process remote type.
-      preferredRemoteType = contentChild->GetRemoteType();
-    } else if (aPrincipal->IsSystemPrincipal()) {
-      preferredRemoteType = NOT_REMOTE_TYPE;
-    }
-  }
-
   auto result = IsolationOptionsForWorker(
-      aPrincipal, aWorkerKind, preferredRemoteType, FissionAutostart());
+      aPrincipal, aWorkerKind, aCurrentRemoteType, FissionAutostart());
   if (NS_WARN_IF(result.isErr())) {
     LOG(("GetRemoteType Abort: IsolationOptionsForWorker failed"));
     return Err(NS_ERROR_DOM_ABORT_ERR);
@@ -161,7 +150,7 @@ Result<nsCString, nsresult> RemoteWorkerManager::GetRemoteType(
         ("GetRemoteType workerType=%s, principal=%s, "
          "preferredRemoteType=%s, selectedRemoteType=%s",
          aWorkerKind == WorkerKind::WorkerKindService ? "service" : "shared",
-         principalOrigin.get(), preferredRemoteType.get(),
+         principalOrigin.get(), PromiseFlatCString(aCurrentRemoteType).get(),
          options.mRemoteType.get()));
   }
 
diff --git a/dom/workers/remoteworkers/RemoteWorkerManager.h b/dom/workers/remoteworkers/RemoteWorkerManager.h
index 1fb5debdcda..d61e120ab15 100644
--- a/dom/workers/remoteworkers/RemoteWorkerManager.h
+++ b/dom/workers/remoteworkers/RemoteWorkerManager.h
@@ -46,7 +46,8 @@ class RemoteWorkerManager final {
    * launched.
    */
   static Result<nsCString, nsresult> GetRemoteType(
-      const nsCOMPtr<nsIPrincipal>& aPrincipal, WorkerKind aWorkerKind);
+      const nsCOMPtr<nsIPrincipal>& aPrincipal, WorkerKind aWorkerKind,
+      const nsACString& aCurrentRemoteType);
 
   static bool HasExtensionPrincipal(const RemoteWorkerData& aData);
 
diff --git a/dom/workers/sharedworkers/SharedWorker.cpp b/dom/workers/sharedworkers/SharedWorker.cpp
index a74d9258b77..5ea65c62e66 100644
--- a/dom/workers/sharedworkers/SharedWorker.cpp
+++ b/dom/workers/sharedworkers/SharedWorker.cpp
@@ -250,13 +250,6 @@ already_AddRefed<SharedWorker> SharedWorker::Constructor(
   MOZ_ASSERT(loadInfo.mCookieJarSettings);
   net::CookieJarSettings::Cast(loadInfo.mCookieJarSettings)->Serialize(cjsData);
 
-  auto remoteType = RemoteWorkerManager::GetRemoteType(
-      loadInfo.mPrincipal, WorkerKind::WorkerKindShared);
-  if (NS_WARN_IF(remoteType.isErr())) {
-    aRv.Throw(remoteType.unwrapErr());
-    return nullptr;
-  }
-
   Maybe<RFPTargetSet> overriddenFingerprintingSettingsArg;
   if (loadInfo.mOverriddenFingerprintingSettings.isSome()) {
     overriddenFingerprintingSettingsArg.emplace(
@@ -273,7 +266,7 @@ already_AddRefed<SharedWorker> SharedWorker::Constructor(
       loadInfo.mIsOn3PCBExceptionList,
       OriginTrials::FromWindow(nsGlobalWindowInner::Cast(window)),
       void_t() /* OptionalServiceWorkerData */, agentClusterId,
-      remoteType.unwrap());
+      DEFAULT_REMOTE_TYPE /* ignored */);
 
   PSharedWorkerChild* pActor = actorChild->SendPSharedWorkerConstructor(
       remoteWorkerData, loadInfo.mWindowID, portIdentifier.release());
diff --git a/dom/workers/sharedworkers/SharedWorkerService.cpp b/dom/workers/sharedworkers/SharedWorkerService.cpp
index 4d536ea9cf2..6e2a874df4e 100644
--- a/dom/workers/sharedworkers/SharedWorkerService.cpp
+++ b/dom/workers/sharedworkers/SharedWorkerService.cpp
@@ -8,6 +8,7 @@
 #include "mozilla/ClearOnShutdown.h"
 #include "mozilla/SchedulerGroup.h"
 #include "mozilla/StaticMutex.h"
+#include "mozilla/dom/ContentParent.h"
 #include "mozilla/dom/MessagePort.h"
 #include "mozilla/dom/RemoteWorkerManager.h"  // RemoteWorkerManager::GetRemoteType
 #include "mozilla/dom/RemoteWorkerTypes.h"
@@ -31,14 +32,15 @@ StaticRefPtr<SharedWorkerService> sSharedWorkerService;
 
 class GetOrCreateWorkerManagerRunnable final : public Runnable {
  public:
-  GetOrCreateWorkerManagerRunnable(SharedWorkerService* aService,
-                                   SharedWorkerParent* aActor,
-                                   const RemoteWorkerData& aData,
-                                   uint64_t aWindowID,
-                                   const MessagePortIdentifier& aPortIdentifier)
+  GetOrCreateWorkerManagerRunnable(
+      SharedWorkerService* aService,
+      ThreadsafeContentParentHandle* aContentParentHandle,
+      SharedWorkerParent* aActor, const RemoteWorkerData& aData,
+      uint64_t aWindowID, const MessagePortIdentifier& aPortIdentifier)
       : Runnable("GetOrCreateWorkerManagerRunnable"),
         mBackgroundEventTarget(GetCurrentSerialEventTarget()),
         mService(aService),
+        mContentParentHandle(aContentParentHandle),
         mActor(aActor),
         mData(aData),
         mWindowID(aWindowID),
@@ -47,7 +49,8 @@ class GetOrCreateWorkerManagerRunnable final : public Runnable {
   NS_IMETHOD
   Run() {
     mService->GetOrCreateWorkerManagerOnMainThread(
-        mBackgroundEventTarget, mActor, mData, mWindowID, mPortIdentifier);
+        mBackgroundEventTarget, mContentParentHandle, mActor, mData, mWindowID,
+        mPortIdentifier);
 
     return NS_OK;
   }
@@ -55,6 +58,7 @@ class GetOrCreateWorkerManagerRunnable final : public Runnable {
  private:
   nsCOMPtr<nsIEventTarget> mBackgroundEventTarget;
   RefPtr<SharedWorkerService> mService;
+  RefPtr<ThreadsafeContentParentHandle> mContentParentHandle;
   RefPtr<SharedWorkerParent> mActor;
   RemoteWorkerData mData;
   uint64_t mWindowID;
@@ -154,19 +158,23 @@ void SharedWorkerService::GetOrCreateWorkerManager(
     uint64_t aWindowID, const MessagePortIdentifier& aPortIdentifier) {
   AssertIsOnBackgroundThread();
 
+  RefPtr<ThreadsafeContentParentHandle> contentParentHandle =
+      BackgroundParent::GetContentParentHandle(aActor->Manager());
+
   // The real check happens on main-thread.
   RefPtr<GetOrCreateWorkerManagerRunnable> r =
-      new GetOrCreateWorkerManagerRunnable(this, aActor, aData, aWindowID,
-                                           aPortIdentifier);
+      new GetOrCreateWorkerManagerRunnable(this, contentParentHandle, aActor,
+                                           aData, aWindowID, aPortIdentifier);
 
   nsresult rv = SchedulerGroup::Dispatch(r.forget());
   (void)NS_WARN_IF(NS_FAILED(rv));
 }
 
 void SharedWorkerService::GetOrCreateWorkerManagerOnMainThread(
-    nsIEventTarget* aBackgroundEventTarget, SharedWorkerParent* aActor,
-    const RemoteWorkerData& aData, uint64_t aWindowID,
-    UniqueMessagePortId& aPortIdentifier) {
+    nsIEventTarget* aBackgroundEventTarget,
+    ThreadsafeContentParentHandle* aContentParentHandle,
+    SharedWorkerParent* aActor, const RemoteWorkerData& aData,
+    uint64_t aWindowID, UniqueMessagePortId& aPortIdentifier) {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(aBackgroundEventTarget);
   MOZ_ASSERT(aActor);
@@ -180,17 +188,19 @@ void SharedWorkerService::GetOrCreateWorkerManagerOnMainThread(
   }
 
   nsCOMPtr<nsIPrincipal> principal = principalOrErr.unwrap();
+
+  nsCString currentRemoteType = aContentParentHandle
+                                    ? aContentParentHandle->GetRemoteType()
+                                    : NOT_REMOTE_TYPE;
   auto remoteType = RemoteWorkerManager::GetRemoteType(
-      principal, WorkerKind::WorkerKindShared);
+      principal, WorkerKind::WorkerKindShared, currentRemoteType);
   if (NS_WARN_IF(remoteType.isErr())) {
     ErrorPropagationOnMainThread(aBackgroundEventTarget, aActor,
                                  remoteType.unwrapErr());
     return;
   }
 
-  if (!remoteType.unwrap().Equals(copyData.remoteType())) {
-    copyData.remoteType() = remoteType.unwrap();
-  }
+  copyData.remoteType() = remoteType.unwrap();
 
   auto partitionedPrincipalOrErr =
       PrincipalInfoToPrincipal(copyData.partitionedPrincipalInfo());
diff --git a/dom/workers/sharedworkers/SharedWorkerService.h b/dom/workers/sharedworkers/SharedWorkerService.h
index 3f76c25badf..6b6d18826db 100644
--- a/dom/workers/sharedworkers/SharedWorkerService.h
+++ b/dom/workers/sharedworkers/SharedWorkerService.h
@@ -23,6 +23,7 @@ class MessagePortIdentifier;
 class RemoteWorkerData;
 class SharedWorkerManager;
 class SharedWorkerParent;
+class ThreadsafeContentParentHandle;
 class UniqueMessagePortId;
 
 /**
@@ -48,9 +49,10 @@ class SharedWorkerService final {
                                 const MessagePortIdentifier& aPortIdentifier);
 
   void GetOrCreateWorkerManagerOnMainThread(
-      nsIEventTarget* aBackgroundEventTarget, SharedWorkerParent* aActor,
-      const RemoteWorkerData& aData, uint64_t aWindowID,
-      UniqueMessagePortId& aPortIdentifier);
+      nsIEventTarget* aBackgroundEventTarget,
+      ThreadsafeContentParentHandle* aContentParentHandle,
+      SharedWorkerParent* aActor, const RemoteWorkerData& aData,
+      uint64_t aWindowID, UniqueMessagePortId& aPortIdentifier);
 
   void RemoveWorkerManagerOnMainThread(SharedWorkerManager* aManager);
 
Loading diff…