CVE-2026-13798
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifchromecast/media/cma/base/decoder_buffer_adapter.cc |
modified | |
TESTchromecast/media/cma/base/decoder_buffer_adapter_unittest.cc |
modified |
Files Changed
chromecast/media/cma/base/decoder_buffer_adapter.ccchromecast/media/cma/base/decoder_buffer_adapter.hchromecast/media/cma/base/decoder_buffer_adapter_unittest.cc
Patch
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 =
Regression Test / PoC
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 =
Original Bug Report
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.ccchromecast/media/cma/pipeline/av_pipeline_impl.ccchromecast/media/cma/backend/backend_decryptor.ccchromecast/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.
- Deserialization & Allocation: In the browser process,
MojoDecoderBufferReaderdeserializes this buffer usingmojo::TypeConverter(media/mojo/common/media_type_converters.cc). The backingbase::HeapArrayis allocated based on the attacker-controlledmojo_buffer->data_size. However, theTypeConverterforDecryptConfigblindly accepts the providedsubsamplesarray without verifying if the total subsample size exceedsdata_size. - CMA Pipeline Ingestion: The
media::DecoderBufferenters the CMA pipeline atDemuxerStreamAdapter::OnNewBuffer. It is wrapped in achromecast::media::DecoderBufferAdapter. The adapter’s constructor (chromecast/media/cma/base/decoder_buffer_adapter.cc) blindly copies the malicious subsamples into aCastDecryptConfigImpl. - Missing Validation: Crucially, Chromium provides
media::VerifySubsamplesMatchSize()andmedia::DecoderBuffer::DoSubsamplesMatch()to catch this exact mismatch, but neither is called during Mojo deserialization or within the Chromecast CMA pipeline. - Vendor Backend Execution: The buffer is passed to
BackendDecryptor::Decrypt()(chromecast/media/cma/pipeline/backend_decryptor.cc), which calls the vendor backend viadecryptor_->PushBufferForDecrypt(buffer.get(), buffer->writable_data()). - Contract Violation: The interface contract (
chromecast/public/media/media_pipeline_backend.h) mandates that the output buffermust 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 thebuffer->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.
- Compromise a renderer process on a Chromecast device.
- Establish an encrypted media session, controlling the AES decryption key and IV.
- Hook the renderer’s
mojom::DemuxerStreamimplementation. - Send a crafted
mojom::DecoderBufferto the browser process with:- A small
data_size(e.g., 16 bytes). - A
DecryptConfigcontaining a singleSubsampleEntrywithclear_bytes= 0 andcypher_bytes= 65536 (64KB).
- A small
- 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.