CVE-2026-16364
Overview
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
@WrapForJNImethod thatmemcpys 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
AndroidDataEncoderandRemoteDataDecoderrequest 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.
nativeCopy and enforces size <= destCapacity (plus overflow-safe source bounds) in the shared checkBounds helper.Attack Path
- Deliver crafted media The victim loads a page or app that decodes/encodes attacker-controlled audio or video through the Android media stack.
- Poison sample metadata
The codec-side shared-memory buffer is populated with
offset/sizevalues (or an oversizedsize) that exceed the destination heap allocation. - Trigger the copy
Gecko C++ calls
NativeCopy, invoking JavanativeCopy, whose only check comparesoffset + sizeagainst the source size and can be bypassed via 32-bit overflow. - Overflow the destination
nativeMemcpywritessizebytes past the end of theMediaByteBuffer/MediaRawDataheap buffer that C++ allocated. - Corrupt adjacent memory The out-of-bounds write clobbers neighboring heap data, enabling potential memory corruption.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifdom/media/platforms/android/AndroidDataEncoder.cpp |
modified | |
ifmobile/android/geckoview/src/main/java/org/mozilla/gecko/media/SampleBuffer.java |
modified |
Files Changed
dom/media/platforms/android/AndroidDataEncoder.cppdom/media/platforms/android/RemoteDataDecoder.cppmobile/android/geckoview/src/main/java/org/mozilla/gecko/media/SampleBuffer.java
Audit Directions
- Length-only copy validationFlag 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 arithmeticSearch for
offset + size(or similar) compared against a limit inintarithmetic; promote to 64-bit before comparison to prevent overflow bypass. - Cross-process metadata trustTreat
offset/sizeand other framing values sourced from shared memory or out-of-process codecs as untrusted, and verify them against both endpoints at the trust boundary.
Patch
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);
}