Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactHeap buffer overflow in Chromecast
DescriptionHeap buffer overflow in Chromecast
ComponentChromecast
Bug ClassOOB
Tracker499048914
Fix commit44d93683f0a9 (chromium/src) +43/-2
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-30

Changed Functions

FunctionChangeNotes
if
chromecast/media/cma/base/decoder_buffer_adapter.cc
modified
TEST
chromecast/media/cma/base/decoder_buffer_adapter_unittest.cc
modified

Files Changed

  • chromecast/media/cma/base/decoder_buffer_adapter.cc
  • chromecast/media/cma/base/decoder_buffer_adapter.h
  • chromecast/media/cma/base/decoder_buffer_adapter_unittest.cc
From 44d93683f0a98116dfce72bd382eee3241ff22c5 Mon Sep 17 00:00:00 2001
From: Simeon Anfinrud <sanfin@chromium.org>
Date: Thu, 07 May 2026 16:32:14 -0700
Subject: [PATCH] [chromecast] Fix Potential OOB Heap Read/Write in CMA via Unvalidated DecryptConfig Subsamples

The CMA pipeline did not validate that the sum of media::DecryptConfig
subsamples matches the size of the DecoderBuffer. This lack of validation
could pass corrupt configurations to vendor decryption backends, leading
to OOB reads/writes. This commit adds a check in DecoderBufferAdapter
to ensure subsamples match the buffer data size, and rejects invalid
buffers by converting them to EOS buffers.

Bug: 499048914
Test: Compiled and passed unit tests.
Change-Id: I5a1693ac70c051695407def0667012e48de9dce2
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7765502
Auto-Submit: Simeon Anfinrud <sanfin@chromium.org>
Commit-Queue: Simeon Anfinrud <sanfin@chromium.org>
Reviewed-by: Shawn Quereshi <shawnq@google.com>
Cr-Commit-Position: refs/heads/main@{#1627330}
---

diff --git a/chromecast/media/cma/base/decoder_buffer_adapter.cc b/chromecast/media/cma/base/decoder_buffer_adapter.cc
index 1c0ce2f..0852eef 100644
--- a/chromecast/media/cma/base/decoder_buffer_adapter.cc
+++ b/chromecast/media/cma/base/decoder_buffer_adapter.cc
@@ -4,7 +4,9 @@
 
 #include "chromecast/media/cma/base/decoder_buffer_adapter.h"
 
+#include "base/logging.h"
 #include "base/notreached.h"
+#include "base/numerics/checked_math.h"
 #include "chromecast/media/cma/base/cast_decrypt_config_impl.h"
 #include "chromecast/public/media/cast_decrypt_config.h"
 #include "media/base/decoder_buffer.h"
@@ -44,8 +46,21 @@
       buffer_->end_of_stream() ? nullptr : buffer_->decrypt_config();
   if (decrypt_config) {
     std::vector<SubsampleEntry> subsamples;
+    base::CheckedNumeric<size_t> total_subsample_size = 0;
     for (const auto& sample : decrypt_config->subsamples()) {
       subsamples.emplace_back(sample.clear_bytes, sample.cypher_bytes);
+      total_subsample_size += sample.clear_bytes;
+      total_subsample_size += sample.cypher_bytes;
+    }
+    if (!subsamples.empty() &&
+        (!total_subsample_size.IsValid() ||
+         total_subsample_size.ValueOrDie() != buffer_->size())) {
+      LOG(ERROR) << "Invalid DecryptConfig: total_subsample_size="
+                 << static_cast<size_t>(total_subsample_size.ValueOrDefault(0))
+                 << " vs buffer size=" << buffer_->size();
+      // Invalid DecryptConfig, reject the buffer to prevent OOB read/write.
+      buffer_ = ::media::DecoderBuffer::CreateEOSBuffer();
+      return;
     }
     if (subsamples.empty()) {
       // DecryptConfig may contain 0 subsamples if all content is encrypted.
diff --git a/chromecast/media/cma/base/decoder_buffer_adapter.h b/chromecast/media/cma/base/decoder_buffer_adapter.h
index 27b93e3c..57a63b9 100644
--- a/chromecast/media/cma/base/decoder_buffer_adapter.h
+++ b/chromecast/media/cma/base/decoder_buffer_adapter.h
@@ -48,7 +48,7 @@
   ~DecoderBufferAdapter() override;
 
   StreamId stream_id_;
-  scoped_refptr<::media::DecoderBuffer> const buffer_;
+  scoped_refptr<::media::DecoderBuffer> buffer_;
   std::unique_ptr<CastDecryptConfig> decrypt_config_;
 };
 
diff --git a/chromecast/media/cma/base/decoder_buffer_adapter_unittest.cc b/chromecast/media/cma/base/decoder_buffer_adapter_unittest.cc
index 190dbd14..983ec205 100644
--- a/chromecast/media/cma/base/decoder_buffer_adapter_unittest.cc
+++ b/chromecast/media/cma/base/decoder_buffer_adapter_unittest.cc
@@ -125,7 +125,10 @@
         ::media::DecryptConfig::CreateCencConfig(kKeyId, kIV, subsamples);
     EXPECT_TRUE(decrypt_config);
 
-    scoped_refptr<::media::DecoderBuffer> buffer = MakeDecoderBuffer();
+    // Make a buffer that matches the subsamples size
+    std::vector<uint8_t> dummy_data(37, 0);
+    scoped_refptr<::media::DecoderBuffer> buffer =
+        ::media::DecoderBuffer::CopyFrom(dummy_data);
     buffer->set_decrypt_config(std::move(decrypt_config));
     scoped_refptr<DecoderBufferAdapter> buffer_adapter(
         new DecoderBufferAdapter(buffer));
@@ -152,6 +155,29 @@
   EXPECT_EQ(nullptr, buffer_adapter->decrypt_config());
 }
 
+TEST(DecoderBufferAdapterTest, RejectInvalidDecryptConfig) {
+  uint32_t kClearBytes[] = {10, 15};
+  uint32_t kCypherBytes[] = {5, 7};
+  std::vector<::media::SubsampleEntry> subsamples;
+  subsamples.emplace_back(kClearBytes[0], kCypherBytes[0]);
+  subsamples.emplace_back(kClearBytes[1], kCypherBytes[1]);
+
+  // Make a buffer that is too small for these subsamples
+  scoped_refptr<::media::DecoderBuffer> buffer =
+      ::media::DecoderBuffer::CopyFrom(
+          UNSAFE_BUFFERS(base::span<const uint8_t>(kBufferData, 5u)));
+
+  std::unique_ptr<::media::DecryptConfig> decrypt_config =
+      ::media::DecryptConfig::CreateCencConfig("key-id", kIv, subsamples);
+  buffer->set_decrypt_config(std::move(decrypt_config));
+
+  scoped_refptr<DecoderBufferAdapter> buffer_adapter(
+      new DecoderBufferAdapter(buffer));
+
+  // Should be converted to an EOS buffer
+  EXPECT_TRUE(buffer_adapter->end_of_stream());
+}
+
 TEST(DecoderBufferAdapterTest, SetsEncryptionSchemeOfCencDecryptConfig) {
   scoped_refptr<::media::DecoderBuffer> buffer = MakeDecoderBuffer();
   std::unique_ptr<::media::DecryptConfig> decrypt_config =
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/chromecast/media/cma/base/decoder_buffer_adapter_unittest.cc b/chromecast/media/cma/base/decoder_buffer_adapter_unittest.cc
index 190dbd14..983ec205 100644
--- a/chromecast/media/cma/base/decoder_buffer_adapter_unittest.cc
+++ b/chromecast/media/cma/base/decoder_buffer_adapter_unittest.cc
@@ -125,7 +125,10 @@
         ::media::DecryptConfig::CreateCencConfig(kKeyId, kIV, subsamples);
     EXPECT_TRUE(decrypt_config);
 
-    scoped_refptr<::media::DecoderBuffer> buffer = MakeDecoderBuffer();
+    // Make a buffer that matches the subsamples size
+    std::vector<uint8_t> dummy_data(37, 0);
+    scoped_refptr<::media::DecoderBuffer> buffer =
+        ::media::DecoderBuffer::CopyFrom(dummy_data);
     buffer->set_decrypt_config(std::move(decrypt_config));
     scoped_refptr<DecoderBufferAdapter> buffer_adapter(
         new DecoderBufferAdapter(buffer));
@@ -152,6 +155,29 @@
   EXPECT_EQ(nullptr, buffer_adapter->decrypt_config());
 }
 
+TEST(DecoderBufferAdapterTest, RejectInvalidDecryptConfig) {
+  uint32_t kClearBytes[] = {10, 15};
+  uint32_t kCypherBytes[] = {5, 7};
+  std::vector<::media::SubsampleEntry> subsamples;
+  subsamples.emplace_back(kClearBytes[0], kCypherBytes[0]);
+  subsamples.emplace_back(kClearBytes[1], kCypherBytes[1]);
+
+  // Make a buffer that is too small for these subsamples
+  scoped_refptr<::media::DecoderBuffer> buffer =
+      ::media::DecoderBuffer::CopyFrom(
+          UNSAFE_BUFFERS(base::span<const uint8_t>(kBufferData, 5u)));
+
+  std::unique_ptr<::media::DecryptConfig> decrypt_config =
+      ::media::DecryptConfig::CreateCencConfig("key-id", kIv, subsamples);
+  buffer->set_decrypt_config(std::move(decrypt_config));
+
+  scoped_refptr<DecoderBufferAdapter> buffer_adapter(
+      new DecoderBufferAdapter(buffer));
+
+  // Should be converted to an EOS buffer
+  EXPECT_TRUE(buffer_adapter->end_of_stream());
+}
+
 TEST(DecoderBufferAdapterTest, SetsEncryptionSchemeOfCencDecryptConfig) {
   scoped_refptr<::media::DecoderBuffer> buffer = MakeDecoderBuffer();
   std::unique_ptr<::media::DecryptConfig> decrypt_config =
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential OOB Heap Read/Write in Chromecast CMA via Unvalidated DecryptConfig Subsamples

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the security team.

Overview: The Chromecast CMA media pipeline fails to validate that DecryptConfig subsamples match the size of the associated DecoderBuffer. A compromised renderer can exploit this to trigger massive out-of-bounds heap reads and writes in the browser process when the buffer is passed to vendor decryption backends. This can be leveraged for a sandbox escape.

Affected files:

  • chromecast/media/cma/base/decoder_buffer_adapter.cc
  • chromecast/media/cma/pipeline/av_pipeline_impl.cc
  • chromecast/media/cma/backend/backend_decryptor.cc
  • chromecast/media/cma/base/decrypt_util.cc

Estimated timestamp from git blame: 2024-10-10

Summary

A potential vulnerability exists in the Chromecast Cast Media Architecture (CMA) pipeline that allows a compromised renderer process to trigger an out-of-bounds (OOB) heap read and write in the browser process. The issue stems from a failure to validate media::DecryptConfig subsamples against the actual allocated size of the media::DecoderBuffer data. Because the Chromecast CMA pipeline (running in the browser process) implicitly trusts these unvalidated sizes, vendor-implemented decryption backends are tricked into performing OOB memory accesses.

Technical Details

When a renderer responds to a mojom::DemuxerStream::Read request, it provides a mojom::DecoderBuffer.

  1. Deserialization & Allocation: In the browser process, MojoDecoderBufferReader deserializes this buffer using mojo::TypeConverter (media/mojo/common/media_type_converters.cc). The backing base::HeapArray is allocated based on the attacker-controlled mojo_buffer->data_size. However, the TypeConverter for DecryptConfig blindly accepts the provided subsamples array without verifying if the total subsample size exceeds data_size.
  2. CMA Pipeline Ingestion: The media::DecoderBuffer enters the CMA pipeline at DemuxerStreamAdapter::OnNewBuffer. It is wrapped in a chromecast::media::DecoderBufferAdapter. The adapter’s constructor (chromecast/media/cma/base/decoder_buffer_adapter.cc) blindly copies the malicious subsamples into a CastDecryptConfigImpl.
  3. Missing Validation: Crucially, Chromium provides media::VerifySubsamplesMatchSize() and media::DecoderBuffer::DoSubsamplesMatch() to catch this exact mismatch, but neither is called during Mojo deserialization or within the Chromecast CMA pipeline.
  4. Vendor Backend Execution: The buffer is passed to BackendDecryptor::Decrypt() (chromecast/media/cma/pipeline/backend_decryptor.cc), which calls the vendor backend via decryptor_->PushBufferForDecrypt(buffer.get(), buffer->writable_data()).
  5. Contract Violation: The interface contract (chromecast/public/media/media_pipeline_backend.h) mandates that the output buffer must be long enough to hold clear data. Because Chromium skipped validation, this contract is violated. The vendor backend iterates over the malicious subsamples and performs decryption starting at the buffer->writable_data() pointer, reading and writing far past the bounds of the small heap allocation.

Impact

An attacker can use AES-CTR (Counter mode) decryption as a highly controlled XOR bit-flipping primitive. By controlling the AES key (e.g., via EME), the IV, and the subsample offsets, the attacker can XOR arbitrary chosen patterns against adjacent heap memory. Overwriting security-critical objects (such as vtable pointers or Mojo receiver state) on the browser process heap leads directly to a sandbox escape and arbitrary Remote Code Execution (RCE).

Potential Reproduction Steps

Note: These are suggested steps based on code analysis; our tooling agent does not run code.

  1. Compromise a renderer process on a Chromecast device.
  2. Establish an encrypted media session, controlling the AES decryption key and IV.
  3. Hook the renderer’s mojom::DemuxerStream implementation.
  4. Send a crafted mojom::DecoderBuffer to the browser process with:
    • A small data_size (e.g., 16 bytes).
    • A DecryptConfig containing a single SubsampleEntry with clear_bytes = 0 and cypher_bytes = 65536 (64KB).
  5. The browser process allocates a 16-byte heap buffer but instructs the vendor backend to decrypt 64KB of data, resulting in a massive OOB heap write.

Suggested Fix

Add robust validation during Mojo deserialization to ensure subsample sizes match the buffer size. In media/mojo/common/media_type_converters.cc, the TypeConverter for DecoderBufferPtr (or the callers of MojoDecoderBufferReader) should explicitly call media::VerifySubsamplesMatchSize() and discard the buffer if the validation fails. Alternatively, use the newer ValidateAndConvertMojoDecoderBuffer utility which may include stricter checks, or enforce DecoderBuffer::DoSubsamplesMatch() upon entry to the CMA pipeline in DemuxerStreamAdapter.

Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker