Medium firefox Logic Error 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impactmoderate
DescriptionSite isolation issue in the Graphics component
ComponentCore
Bug ClassLogic Error
Tracker2048345
Fix commit8d21c36c0b90 (firefox) +91/-35
CISA KEVNot listed
CreditedSteven Julian
Disclosed2026-07-21

Background

`RemoteSurfaceAllocator`
GeckoView’s GPU/parent-process implementation of the ISurfaceAllocator AIDL interface that hands out GeckoSurfaceTexture handles to child processes.
`ISurfaceAllocator`
The Binder (AIDL) interface a content process uses to remotely acquireSurface, releaseSurface, configureSync, and sync surfaces in the compositor process.
Surface handle
A 64-bit identifier (mAllocatorId << 32 | counter) naming a specific GeckoSurfaceTexture, passed across the Binder boundary to reference an allocated surface.
`IBinder.DeathRecipient`
An Android callback interface whose binderDied fires when a linked remote Binder (here, a client process) dies, letting the allocator reclaim that client’s resources.

Root Cause Analysis

RemoteSurfaceAllocator was a process-wide singleton obtained via getInstance, so every content process talking to the GPU (or parent) process shared one allocator with no record of which client owned which surface handle. Because releaseSurface, configureSync, and sync accepted any caller-supplied handle and simply looked it up in the global GeckoSurfaceTexture table, one content process could name a surface handle belonging to a different content process and act on it, violating the site-isolation invariant that a process may only manipulate surfaces it allocated.

The fix replaces the singleton with a per-client instance created via create(allocatorId, client), records every allocated handle in a per-instance mOwnedHandles list at acquireSurface time, and adds an ensureOwned guard that throws SecurityException when an operation targets a handle this session did not allocate. It also links to the client’s Binder death (linkToDeath/binderDied) so a client’s handles are released when it disconnects, and marks the mutating methods synchronized to keep the ownership set consistent. This works because handle ownership is now bound to the specific client Binder that requested the surface, so cross-client references are rejected before they reach the shared texture table.

Key insight
The core mistake was treating the surface allocator as a single shared, unauthenticated endpoint that trusted any caller-supplied handle, so surfaces were addressable across process boundaries with no ownership check. The fix binds each allocator to one client Binder and validates via ensureOwned that every mutated handle was allocated by that same client.

Attack Path

  1. Obtain the shared allocator A malicious or compromised content process acquires the singleton ISurfaceAllocator from the GPU/parent process just as any legitimate content process would.
  2. Learn or guess a foreign handle The attacker references a surface handle allocated by a different content process (handles were globally scoped monotonic counters with no per-client namespace enforcement).
  3. Invoke a mutating call on the foreign handle The attacker calls releaseSurface, configureSync, or sync with that handle, which the pre-fix code executed without checking ownership.
  4. Manipulate another process's surface The GPU/parent process performs the operation against the victim’s GeckoSurfaceTexture, e.g. releasing it or reconfiguring its snapshot target, affecting a surface belonging to another origin/process.

Impact Assessment

An attacker in one content process could act on graphics surfaces owned by another content process, breaking site isolation for the Graphics component on Android/GeckoView. The affected context is the compositor (GPU or parent) process that hosts the shared allocator, and the precondition is control of a content child process able to reach the ISurfaceAllocator interface and reference another client’s handle. Consistent with the metadata this is a moderate logic/site-isolation error rather than a demonstrated memory-corruption primitive.

Changed Functions

FunctionChangeNotes
if
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RemoteSurfaceAllocator.java
modified
if
mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceAllocator.java
modified
if
mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessManager.java
modified

Files Changed

  • mobile/android/geckoview/src/main/aidl/org/mozilla/gecko/process/IChildProcess.aidl
  • mobile/android/geckoview/src/main/aidl/org/mozilla/gecko/process/IProcessManager.aidl
  • mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RemoteSurfaceAllocator.java
  • mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceAllocator.java
  • mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessManager.java
  • mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceChildProcess.java
  • mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceGpuProcess.java

Audit Directions

  • Shared singleton IPC endpoints
    Flag any cross-process service exposed as a single global instance that operates on caller-supplied identifiers without recording which client created them.
  • Handle authorization
    For every AIDL/Binder method that takes an opaque handle or ID, verify an ownership or capability check runs before the handle is dereferenced or mutated.
  • Resource lifetime on client death
    Confirm remotely allocated resources are linked to the client Binder via linkToDeath so a disconnecting or crashing process cannot leak or leave dangling surfaces in the compositor process.
diff --git a/mobile/android/geckoview/src/main/aidl/org/mozilla/gecko/process/IChildProcess.aidl b/mobile/android/geckoview/src/main/aidl/org/mozilla/gecko/process/IChildProcess.aidl
index 2e76c9eac77..9060795fecf 100644
--- a/mobile/android/geckoview/src/main/aidl/org/mozilla/gecko/process/IChildProcess.aidl
+++ b/mobile/android/geckoview/src/main/aidl/org/mozilla/gecko/process/IChildProcess.aidl
@@ -9,6 +9,7 @@ import org.mozilla.gecko.gfx.ISurfaceAllocator;
 import org.mozilla.gecko.process.IProcessManager;
 
 import android.os.Bundle;
+import android.os.IBinder;
 import android.os.ParcelFileDescriptor;
 
 interface IChildProcess {
@@ -39,6 +40,9 @@ interface IChildProcess {
      * consumed by the GPU process. Must only be called for a GPU child process type.
      * @param allocatorId A unique ID used to identify the GPU process instance the allocator
      *     belongs to.
+     * @param client An IBinder identifying the process connecting to the surface allocator. For
+     *     content processes this will have been passed from the content process through the parent,
+     *     and finally to the GPU process here.
      */
-    ISurfaceAllocator getSurfaceAllocator(int allocatorId);
+    ISurfaceAllocator getSurfaceAllocator(int allocatorId, in IBinder client);
 }
diff --git a/mobile/android/geckoview/src/main/aidl/org/mozilla/gecko/process/IProcessManager.aidl b/mobile/android/geckoview/src/main/aidl/org/mozilla/gecko/process/IProcessManager.aidl
index b75f3171242..224df64fc82 100644
--- a/mobile/android/geckoview/src/main/aidl/org/mozilla/gecko/process/IProcessManager.aidl
+++ b/mobile/android/geckoview/src/main/aidl/org/mozilla/gecko/process/IProcessManager.aidl
@@ -7,8 +7,10 @@ package org.mozilla.gecko.process;
 import org.mozilla.gecko.IGeckoEditableChild;
 import org.mozilla.gecko.gfx.ISurfaceAllocator;
 
+import android.os.IBinder;
+
 interface IProcessManager {
     void getEditableParent(in IGeckoEditableChild child, long contentId, long tabId);
     // Returns the interface that child processes should use to allocate Surfaces.
-    ISurfaceAllocator getSurfaceAllocator();
+    ISurfaceAllocator getSurfaceAllocator(in IBinder client);
 }
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RemoteSurfaceAllocator.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RemoteSurfaceAllocator.java
index 31b5999100c..2d9b08e3b4f 100644
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RemoteSurfaceAllocator.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/RemoteSurfaceAllocator.java
@@ -4,39 +4,60 @@
 
 package org.mozilla.gecko.gfx;
 
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.LongSparseArray;
 import java.util.concurrent.atomic.AtomicInteger;
 import org.mozilla.gecko.GeckoThread;
 
-public final class RemoteSurfaceAllocator extends ISurfaceAllocator.Stub {
+public final class RemoteSurfaceAllocator extends ISurfaceAllocator.Stub
+    implements IBinder.DeathRecipient {
   private static final String LOGTAG = "RemoteSurfaceAllocator";
 
-  private static RemoteSurfaceAllocator mInstance;
-
+  /// Unique ID identifying the process this instance belongs to, which must be 0 for the parent
+  /// process. This is used so that following a GPU process shutdown, surface handles allocated by
+  /// the new compositor process (either a new GPU process, or the parent process) do not clash with
+  /// surface handles allocated by a previous GPU process.
   private final int mAllocatorId;
   /// Monotonically increasing counter used to generate unique handles
   /// for each SurfaceTexture by combining with mAllocatorId.
   private static AtomicInteger sNextHandle = new AtomicInteger(1);
 
+  ///  List of Surface handles owned by this instance.
+  private final LongSparseArray<Boolean> mOwnedHandles = new LongSparseArray<Boolean>();
+  ///  Whether the client is still connected to this allocator.
+  private boolean mClientConnected = true;
+
   /**
-   * Retrieves the singleton allocator instance for this process.
+   * Retrieves the allocator instance for the provided client.
    *
    * @param allocatorId A unique ID identifying the process this instance belongs to, which must be
-   *     0 for the parent process instance.
+   *     0 for a parent process instance.
+   * @param client An IBinder identifying the process for which we will be allocating surfaces.
    */
-  public static synchronized RemoteSurfaceAllocator getInstance(final int allocatorId) {
-    if (mInstance == null && GeckoThread.isStateAtLeast(GeckoThread.State.JNI_READY)) {
-      mInstance = new RemoteSurfaceAllocator(allocatorId);
+  public static RemoteSurfaceAllocator create(final int allocatorId, final IBinder client) {
+    if (GeckoThread.isStateAtLeast(GeckoThread.State.JNI_READY)) {
+      try {
+        return new RemoteSurfaceAllocator(allocatorId, client);
+      } catch (final RemoteException ignored) {
+      }
     }
-    return mInstance;
+    return null;
   }
 
-  private RemoteSurfaceAllocator(final int allocatorId) {
+  private RemoteSurfaceAllocator(final int allocatorId, final IBinder client)
+      throws RemoteException {
     mAllocatorId = allocatorId;
+    client.linkToDeath(this, 0);
   }
 
   @Override
-  public GeckoSurface acquireSurface(
+  public synchronized GeckoSurface acquireSurface(
       final int width, final int height, final boolean singleBufferMode) {
+    if (!mClientConnected) {
+      return null;
+    }
+
     final long handle = ((long) mAllocatorId << 32) | sNextHandle.getAndIncrement();
     final GeckoSurfaceTexture gst = GeckoSurfaceTexture.acquire(singleBufferMode, handle);
 
@@ -48,11 +69,15 @@ public final class RemoteSurfaceAllocator extends ISurfaceAllocator.Stub {
       gst.setDefaultBufferSize(width, height);
     }
 
+    mOwnedHandles.put(handle, true);
     return new GeckoSurface(gst);
   }
 
   @Override
-  public void releaseSurface(final long handle) {
+  public synchronized void releaseSurface(final long handle) {
+    ensureOwned(handle);
+    mOwnedHandles.remove(handle);
+
     final GeckoSurfaceTexture gst = GeckoSurfaceTexture.lookup(handle);
     if (gst != null) {
       gst.decrementUse();
@@ -60,7 +85,9 @@ public final class RemoteSurfaceAllocator extends ISurfaceAllocator.Stub {
   }
 
   @Override
-  public void configureSync(final SyncConfig config) {
+  public synchronized void configureSync(final SyncConfig config) {
+    ensureOwned(config.sourceTextureHandle);
+
     final GeckoSurfaceTexture gst = GeckoSurfaceTexture.lookup(config.sourceTextureHandle);
     if (gst != null) {
       gst.configureSnapshot(config.targetSurface, config.width, config.height);
@@ -68,10 +95,30 @@ public final class RemoteSurfaceAllocator extends ISurfaceAllocator.Stub {
   }
 
   @Override
-  public void sync(final long handle) {
+  public synchronized void sync(final long handle) {
+    ensureOwned(handle);
+
     final GeckoSurfaceTexture gst = GeckoSurfaceTexture.lookup(handle);
     if (gst != null) {
       gst.takeSnapshot();
     }
   }
+
+  @Override
+  public synchronized void binderDied() {
+    mClientConnected = false;
+    for (int i = 0; i < mOwnedHandles.size(); i++) {
+      final GeckoSurfaceTexture gst = GeckoSurfaceTexture.lookup(mOwnedHandles.keyAt(i));
+      if (gst != null) {
+        gst.decrementUse();
+      }
+    }
+    mOwnedHandles.clear();
+  }
+
+  private void ensureOwned(final long handle) {
+    if (mOwnedHandles.indexOfKey(handle) < 0) {
+      throw new SecurityException("Surface handle is not owned by this allocator session");
+    }
+  }
 }
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceAllocator.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceAllocator.java
index d4da49a0ad5..63d5f91cb03 100644
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceAllocator.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/SurfaceAllocator.java
@@ -4,6 +4,7 @@
 
 package org.mozilla.gecko.gfx;
 
+import android.os.Binder;
 import android.os.IBinder;
 import android.os.RemoteException;
 import android.util.Log;
@@ -17,6 +18,7 @@ import org.mozilla.gecko.process.GeckoServiceChildProcess;
   private static final String LOGTAG = "SurfaceAllocator";
 
   private static ISurfaceAllocator sAllocator;
+  private static final IBinder sClient = new Binder();
 
   // Keep a reference to all allocated Surfaces, so that we can release them if we lose the
   // connection to the allocator service.
@@ -30,9 +32,9 @@ import org.mozilla.gecko.process.GeckoServiceChildProcess;
 
     try {
       if (GeckoAppShell.isParentProcess()) {
-        sAllocator = GeckoProcessManager.getInstance().getSurfaceAllocator();
+        sAllocator = GeckoProcessManager.getInstance().getSurfaceAllocator(sClient);
       } else {
-        sAllocator = GeckoServiceChildProcess.getSurfaceAllocator();
+        sAllocator = GeckoServiceChildProcess.getSurfaceAllocator(sClient);
       }
 
       if (sAllocator == null) {
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessManager.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessManager.java
index 5d73a0386ed..2d02a89e004 100644
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessManager.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoProcessManager.java
@@ -70,11 +70,12 @@ public final class GeckoProcessManager extends IProcessManager.Stub {
   }
 
   /**
-   * Returns the surface allocator interface to be used by child processes to allocate Surfaces. The
-   * service bound to the returned interface may live in either the GPU process or parent process.
+   * Returns the surface allocator interface to be used by the client process to allocate Surfaces.
+   * The service bound to the returned interface may live in either the GPU process or parent
+   * process.
    */
   @Override // IProcessManager
-  public ISurfaceAllocator getSurfaceAllocator() {
+  public ISurfaceAllocator getSurfaceAllocator(final IBinder client) {
     final boolean gpuEnabled = GeckoAppShell.isGpuProcessEnabled();
 
     try {
@@ -87,19 +88,19 @@ public final class GeckoProcessManager extends IProcessManager.Stub {
               final GpuProcessConnection conn =
                   (GpuProcessConnection) INSTANCE.mConnections.getExistingConnection(selector);
               if (conn != null) {
-                allocator.complete(conn.getSurfaceAllocator());
+                allocator.complete(conn.getSurfaceAllocator(client));
               } else {
                 // If we cannot find a GPU process, it has probably been killed and not yet
                 // restarted. Return null here, and allow the caller to try again later.
-                // We definitely do *not* want to return the parent process allocator instead, as
+                // We definitely do *not* want to return a parent process allocator instead, as
                 // that will result in surfaces being allocated in the parent process, which
                 // therefore won't be usable when the GPU process is eventually launched.
                 allocator.complete(null);
               }
             });
       } else {
-        // The GPU process is disabled, so return the parent process allocator instance.
-        allocator.complete(RemoteSurfaceAllocator.getInstance(0));
+        // The GPU process is disabled, so return a parent process allocator instance.
+        allocator.complete(RemoteSurfaceAllocator.create(0, client));
       }
       return allocator.poll(100);
     } catch (final Throwable e) {
@@ -337,7 +338,6 @@ public final class GeckoProcessManager extends IProcessManager.Stub {
 
   private static final class GpuProcessConnection extends NonContentConnection {
     private CompositorSurfaceManager mCompositorSurfaceManager;
-    private ISurfaceAllocator mSurfaceAllocator;
 
     // Unique ID used to identify each GPU process instance. Will always be non-zero,
     // and unlike the process' pid cannot be the same value for successive instances.
@@ -367,14 +367,14 @@ public final class GeckoProcessManager extends IProcessManager.Stub {
       return mCompositorSurfaceManager;
     }
 
-    public ISurfaceAllocator getSurfaceAllocator() {
-      if (mSurfaceAllocator == null && getChild() != null) {
+    public ISurfaceAllocator getSurfaceAllocator(final IBinder client) {
+      if (getChild() != null) {
         try {
-          mSurfaceAllocator = getChild().getSurfaceAllocator(mUniqueGpuProcessId);
+          return getChild().getSurfaceAllocator(mUniqueGpuProcessId, client);
         } catch (final RemoteException ignored) {
         }
       }
-      return mSurfaceAllocator;
+      return null;
     }
   }
 
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceChildProcess.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceChildProcess.java
index 57eed95bf17..f50ea854d1d 100644
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceChildProcess.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceChildProcess.java
@@ -168,7 +168,7 @@ public class GeckoServiceChildProcess extends Service {
     }
 
     @Override
-    public ISurfaceAllocator getSurfaceAllocator(final int allocatorId) {
+    public ISurfaceAllocator getSurfaceAllocator(final int allocatorId, final IBinder client) {
       Log.e(LOGTAG, "Invalid call to IChildProcess.getSurfaceAllocator for non-GPU process");
       throw new AssertionError(
           "Invalid call to IChildProcess.getSurfaceAllocator for non-GPU process.");
@@ -210,7 +210,7 @@ public class GeckoServiceChildProcess extends Service {
    * Returns the surface allocator interface that should be used by this process to allocate
    * Surfaces, for consumption in either the GPU process or parent process.
    */
-  public static ISurfaceAllocator getSurfaceAllocator() throws RemoteException {
-    return sProcessManager.getSurfaceAllocator();
+  public static ISurfaceAllocator getSurfaceAllocator(final IBinder client) throws RemoteException {
+    return sProcessManager.getSurfaceAllocator(client);
   }
 }
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceGpuProcess.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceGpuProcess.java
index 83c85288593..a7e9c1a8e3a 100644
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceGpuProcess.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/process/GeckoServiceGpuProcess.java
@@ -5,6 +5,7 @@
 package org.mozilla.gecko.process;
 
 import android.os.Binder;
+import android.os.IBinder;
 import android.util.SparseArray;
 import android.view.Surface;
 import org.mozilla.gecko.annotation.WrapForJNI;
@@ -22,8 +23,8 @@ public class GeckoServiceGpuProcess extends GeckoServiceChildProcess {
     }
 
     @Override
-    public ISurfaceAllocator getSurfaceAllocator(final int allocatorId) {
-      return RemoteSurfaceAllocator.getInstance(allocatorId);
+    public ISurfaceAllocator getSurfaceAllocator(final int allocatorId, final IBinder client) {
+      return RemoteSurfaceAllocator.create(allocatorId, client);
     }
   }
 
Loading diff…