High firefox Logic Error 🔧 Commit mapped

Overview

High
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
Impacthigh
DescriptionIncorrect boundary conditions in the Audio/Video: Playback component
ComponentDOM
Bug ClassLogic Error
Tracker2047802
Fix commita336a4e2973a (firefox) +34/-14
CISA KEVNot listed
CreditedSteven Julian
Disclosed2026-07-21

Background

`SampleBuffer`
A GeckoView Java class wrapping a shared-memory region used to shuttle encoded/decoded media samples between the codec and Gecko native code.
`nativeCopy`
A @WrapForJNI method that memcpys bytes out of the shared-memory buffer into a raw native destination pointer supplied by C++.
`NativeCopy`
The Gecko C++ side of the JNI binding through which AndroidDataEncoder and RemoteDataDecoder request a copy into a heap buffer they allocated.
`checkBounds`
A new helper added by the fix that validates offset/size against both source capacity and destination capacity using 64-bit arithmetic.

Root Cause Analysis

The pre-fix nativeCopy accepted a destination pointer dest, an offset, and a size, and validated only that offset + size did not exceed mSharedMem.getSize() — the source shared-memory size — before calling nativeMemcpy(dest, src, size). It never checked whether size bytes actually fit in the caller-provided destination buffer, so the destination-capacity invariant was entirely absent, and the offset + size comparison was done in 32-bit int, allowing integer overflow to defeat even the source check. Because the shared-memory contents (including offset/size metadata) originate from the out-of-process media codec, a malformed sample could drive a copy that overran the destination heap allocation.

The fix routes every path through checkBounds, which recomputes inEnd = (long) offset + size in 64-bit to prevent overflow and rejects the copy when inEnd > inCapacity (source) or size > outCapacity (destination). Each C++ caller was updated to pass the true destination capacity (config->Length(), writer->Size(), writer->Size() - prependSize, audio.Length() * sampleSize), and null-pointer guards on dest and the mapped src were added.

Key insight
The single core mistake was validating the copy length only against the source shared-memory size while trusting the caller-supplied destination pointer to be large enough; the fix threads the destination capacity into nativeCopy and enforces size <= destCapacity (plus overflow-safe source bounds) in the shared checkBounds helper.

Attack Path

  1. Deliver crafted media The victim loads a page or app that decodes/encodes attacker-controlled audio or video through the Android media stack.
  2. Poison sample metadata The codec-side shared-memory buffer is populated with offset/size values (or an oversized size) that exceed the destination heap allocation.
  3. Trigger the copy Gecko C++ calls NativeCopy, invoking Java nativeCopy, whose only check compares offset + size against the source size and can be bypassed via 32-bit overflow.
  4. Overflow the destination nativeMemcpy writes size bytes past the end of the MediaByteBuffer/MediaRawData heap buffer that C++ allocated.
  5. Corrupt adjacent memory The out-of-bounds write clobbers neighboring heap data, enabling potential memory corruption.

Impact Assessment

An attacker gains an out-of-bounds heap write whose length and offset derive from untrusted media sample metadata, corrupting memory in the Gecko process performing the media copy. Exploitation requires the victim to process attacker-controlled encoded audio/video on Android (GeckoView/Firefox for Android). This is a high-severity boundary-condition flaw that could lead to memory corruption and potentially further compromise.

Changed Functions

FunctionChangeNotes
if
dom/media/platforms/android/AndroidDataEncoder.cpp
modified
if
mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/SampleBuffer.java
modified

Files Changed

  • dom/media/platforms/android/AndroidDataEncoder.cpp
  • dom/media/platforms/android/RemoteDataDecoder.cpp
  • mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/SampleBuffer.java

Audit Directions

  • Length-only copy validation
    Flag any native/JNI copy helper that checks the requested size only against the source and trusts the caller’s destination pointer; require the destination capacity to be passed and enforced.
  • 32-bit boundary arithmetic
    Search for offset + size (or similar) compared against a limit in int arithmetic; promote to 64-bit before comparison to prevent overflow bypass.
  • Cross-process metadata trust
    Treat offset/size and other framing values sourced from shared memory or out-of-process codecs as untrusted, and verify them against both endpoints at the trust boundary.
diff --git a/dom/media/platforms/android/AndroidDataEncoder.cpp b/dom/media/platforms/android/AndroidDataEncoder.cpp
index 73d9e910b1e..01d98d729a6 100644
--- a/dom/media/platforms/android/AndroidDataEncoder.cpp
+++ b/dom/media/platforms/android/AndroidDataEncoder.cpp
@@ -276,8 +276,8 @@ static RefPtr<MediaByteBuffer> ExtractCodecConfig(
   auto config = MakeRefPtr<MediaByteBuffer>(aSize);
   config->SetLength(aSize);
   NS_ENSURE_SUCCESS(
-      aBuffer->NativeCopy(reinterpret_cast<jlong>(config->Elements()), aOffset,
-                          aSize),
+      aBuffer->NativeCopy(reinterpret_cast<jlong>(config->Elements()),
+                          config->Length(), aOffset, aSize),
       nullptr);
   if (!aAsAVCC) {
     return config;
@@ -385,7 +385,7 @@ RefPtr<MediaRawData> AndroidDataEncoder::GetOutputData(
   }
 
   NS_ENSURE_SUCCESS(aBuffer->NativeCopy(reinterpret_cast<jlong>(writer->Data()),
-                                        aOffset, aSize),
+                                        writer->Size(), aOffset, aSize),
                     nullptr);
   output->mKeyframe = aIsKeyFrame;
 
@@ -421,7 +421,7 @@ RefPtr<MediaRawData> AndroidDataEncoder::GetOutputDataH264(
 
   NS_ENSURE_SUCCESS(
       aBuffer->NativeCopy(reinterpret_cast<jlong>(writer->Data() + prependSize),
-                          aOffset, aSize),
+                          writer->Size() - prependSize, aOffset, aSize),
       nullptr);
 
   if (asAVCC && !AnnexB::ConvertSampleToAVCC(output, avccHeader)) {
diff --git a/dom/media/platforms/android/RemoteDataDecoder.cpp b/dom/media/platforms/android/RemoteDataDecoder.cpp
index 01d57d39aae..9e0a1453558 100644
--- a/dom/media/platforms/android/RemoteDataDecoder.cpp
+++ b/dom/media/platforms/android/RemoteDataDecoder.cpp
@@ -842,8 +842,9 @@ class RemoteAudioDecoder final : public RemoteDataDecoder {
         LOG("OOM while allocating temporary output buffer");
         return;
       }
-      nsresult rv = aBuffer->NativeCopy(reinterpret_cast<jlong>(audio.get()),
-                                        offset, size);
+      nsresult rv =
+          aBuffer->NativeCopy(reinterpret_cast<jlong>(audio.get()),
+                              audio.Length() * sampleSize, offset, size);
       if (NS_FAILED(rv)) {
         LOG("Fail to copy audio buffer");
         Error(MediaResult(rv, __func__));
diff --git a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/SampleBuffer.java b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/SampleBuffer.java
index 62a9c38a559..e9d7cb60031 100644
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/SampleBuffer.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/media/SampleBuffer.java
@@ -50,11 +50,27 @@ public final class SampleBuffer implements Parcelable {
     return mSharedMem != null ? mSharedMem.getSize() : 0;
   }
 
+  private void checkBounds(
+      final int offset, final int size, final int inCapacity, final int outCapacity)
+      throws IOException {
+    if (mSharedMem == null || !mSharedMem.isValid()) {
+      throw new IOException("Invalid state.");
+    }
+    if (offset < 0 || size < 0) {
+      throw new IOException("Illegal source offset/size");
+    }
+    final long inEnd = (long) offset + size;
+    if (inEnd > inCapacity || size > outCapacity) {
+      throw new IOException("Out-of-bound: buffer too small.");
+    }
+  }
+
   public void readFromByteBuffer(final ByteBuffer src, final int offset, final int size)
       throws IOException {
     if (!src.isDirect()) {
       throw new IOException("SharedMemBuffer only support reading from direct byte buffer.");
     }
+    checkBounds(offset, size, src.capacity(), capacity());
     try {
       nativeReadFromDirectBuffer(src, mSharedMem.getPointer(), offset, size);
       mSharedMem.flush();
@@ -72,6 +88,7 @@ public final class SampleBuffer implements Parcelable {
     if (!dest.isDirect()) {
       throw new IOException("SharedMemBuffer only support writing to direct byte buffer.");
     }
+    checkBounds(offset, size, capacity(), dest.capacity());
     try {
       nativeWriteToDirectBuffer(mSharedMem.getPointer(), dest, offset, size);
     } catch (final NullPointerException e) {
@@ -83,16 +100,18 @@ public final class SampleBuffer implements Parcelable {
       long src, ByteBuffer dest, int offset, int size);
 
   @WrapForJNI(exceptionMode = "nsresult")
-  public void nativeCopy(final long dest, final int offset, final int size) throws IOException {
-    if (mSharedMem == null || !mSharedMem.isValid()) {
-      throw new IOException("Invalid state.");
-    }
-    if (offset + size > mSharedMem.getSize()) {
-      throw new IOException("Out-of-bound: buffer too small.");
+  public void nativeCopy(final long dest, final int destCapacity, final int offset, final int size)
+      throws IOException {
+    if (dest == 0) {
+      throw new IOException("Null destination pointer.");
     }
+    checkBounds(offset, size, capacity(), destCapacity);
     try {
-      final long src = mSharedMem.getPointer() + offset;
-      nativeMemcpy(dest, src, size);
+      final long src = mSharedMem.getPointer();
+      if (src == 0) {
+        throw new IOException("Shared memory not mapped.");
+      }
+      nativeMemcpy(dest, src + offset, size);
     } catch (final NullPointerException e) {
       throw new IOException(e);
     }
Loading diff…