CVE-2026-7905
Overview
Background
- `DecryptConfig`
- a media object describing a buffer’s
EncryptionScheme, key id, IV, optional encryption pattern and per-SubsampleEntryclear_bytes/cypher_bytessplit. - `EncryptionScheme::kUnencrypted`
- the scheme value meaning “not encrypted”, which the
DecryptConfigconstructor documents as invalid (“Unencrypted blocks should not have a DecryptConfig”,media/base/decrypt_config.cc:49). - TypeConverter
- the
mojoTypeConverterlayer inmedia/mojo/common/media_type_converters.ccthat turns renderer-suppliedmedia::mojom::DecryptConfigPtr/media::mojom::DecoderBufferPtrmessages intomedia::DecryptConfig/media::DecoderBufferobjects. - `QueueSecureInputBuffer`
- the
MediaCodecBridgeImplmethod that copies each subsample’s sizes intoint32_tarrays and passes them through JNI to the AndroidMediaCodec.
Root Cause Analysis
Verify before building a PoC
What the commit does not establish: Citations check out. DoSubsamplesMatch skip and codec_wrapper dispatch are absent from the material (correctly flagged). Mediaserver OOB is unshown. 1703fe calls itself speculative hardening; 878ee message claims the fix but only adds fail-closed and INT32_MAX bound.
1703fe61bee4), which an audit found was a preparatory/helper change, not the fix. This page now points to the commit whose diff removes the bug.TypeConverter<std::unique_ptr<media::DecryptConfig>, media::mojom::DecryptConfigPtr>::Convert (media/mojo/common/media_type_converters.cc:42-49 before 1703fe61bee4) passed the renderer-chosen input->encryption_scheme straight to std::make_unique<media::DecryptConfig> with no validation. The constructor’s only guard against kUnencrypted was DCHECK_NE(encryption_scheme_, EncryptionScheme::kUnencrypted) (media/base/decrypt_config.cc:50), and the “pattern only for cbcs” rule was also only a DCHECK (media/base/decrypt_config.cc:55); the report says these are compiled out of official release builds, so a renderer could produce a DecryptConfig labelled unencrypted. According to the report (unverified: media/base/decoder_buffer.cc and media/gpu/android/codec_wrapper.cc are not in the material), DecoderBuffer::DoSubsamplesMatch() skips the subsample-vs-size check for buffers that are not encrypted, while the Android codec wrapper sends any buffer that carries a DecryptConfig to QueueSecureInputBuffer. At that sink, only cypher_bytes was range-checked (media/base/android/media_codec_bridge_impl.cc:653) before native_clear_array[i] = subsamples.clear_bytes (media_codec_bridge_impl.cc:657) and the arrays were passed to Java (media_codec_bridge_impl.cc:684, :710), so subsample sizes that had never been checked against the buffer reached MediaCodec. The violated invariant is that a DecryptConfig built from IPC is never kUnencrypted, so subsample validation cannot be skipped for a buffer headed to the secure path. Commit 1703fe61bee4 (2026-03-30) is the first to close this: it adds converter-side checks that return nullptr for kUnencrypted and for a pattern on a non-kCbcs scheme, and turns both constructor DCHECKs into CHECKs. Commit 878ee241afee (2026-04-03) completes the fix: the DecoderBuffer converter now return nullptrs for the whole buffer when that DecryptConfig conversion fails (before this, media_type_converters.cc:186-190 passed the null result to set_decrypt_config, leaving a buffer with no DecryptConfig), and the JNI sink now also rejects clear_bytes > std::numeric_limits<int32_t>::max(). That bound is not a check against the buffer size: it does not reject the report’s example clear_bytes of 0x7FFFFFFF (equal to INT32_MAX) on a 10-byte buffer, so the real protection is the kUnencrypted rejection.EncryptionScheme::kUnencrypted for an IPC-supplied DecryptConfig because the only guard was a release-disabled DCHECK, and that label let the buffer skip subsample-size validation while still taking the secure JNI path. 1703fe61bee4 rejects such configs at deserialization and makes the constructor invariant a CHECK, and 878ee241afee then rejects the whole buffer and adds an INT32_MAX bound on clear_bytes.Attack Path
- Compromise a renderer
The attacker runs code in a renderer that can send
media::mojom::DecoderBuffermessages to the GPU process (inferred; from the report’s suggested steps). - Label the config unencrypted
They attach a
DecryptConfigwithencryption_schemeset tokUnencrypted, a valid-looking key id and IV, and aSubsampleEntrywhoseclear_bytesfar exceeds the buffer’sdata_size. Before1703fe61bee4the converter (media_type_converters.cc:46-48) built this config and nothing in a release build rejected it (decrypt_config.cc:50). - Skip subsample validation
Because the buffer is “not encrypted”, the subsample-vs-size check is skipped, but because it carries a
DecryptConfigit is sent toQueueSecureInputBuffer(inferred; from the report, sincedecoder_buffer.ccandcodec_wrapper.ccare not shown). - Hand bogus sizes to MediaCodec
QueueSecureInputBuffercopies the uncheckedclear_bytesintonative_clear_array(media_codec_bridge_impl.cc:657) and passes it through JNI, where the report says the Android media service may read or write past the shared buffer (inferred).
Impact Assessment
MediaCodec stack through the GPU process. The report says this could cause out-of-bounds access in the privileged Mediaserver and possibly a sandbox escape (inferred: the material contains no code beyond the JNI call). Preconditions are code execution in a renderer and the Android secure-decode path. The advisory rates this high severity and does not report in-the-wild exploitation.Changed Functions
| Function | Change | Notes |
|---|---|---|
Convertmedia/mojo/common/media_type_converters.cc |
modified | |
ifmedia/mojo/common/media_type_converters.cc |
modified | |
formedia/mojo/common/media_type_converters.cc |
modified |
Files Changed
media/base/decrypt_config.ccmedia/mojo/common/media_type_converters.cc
Audit Directions
- Release-disabled invariants on IPC inputFind constructors and converters whose only defense against renderer-chosen enum values is a
DCHECK. Validate those values in the Mojo deserializer, and make the constructor invariant aCHECK. - Validation keyed on a different predicate than dispatchWhere a size check is skipped based on one property (e.g. “is encrypted”) but routing to a sensitive sink depends on another (e.g. “has a
DecryptConfig”), make sure no input can satisfy one and not the other. - Fail-closed nested conversionWhen a sub-object converter can return
nullptr, the parent converter should reject the whole message, not quietly drop the field.
Patch
diff --git a/media/base/decrypt_config.cc b/media/base/decrypt_config.cc
index 2b889ba..d5b560b 100644
--- a/media/base/decrypt_config.cc
+++ b/media/base/decrypt_config.cc
@@ -47,12 +47,12 @@
subsamples_(subsamples),
encryption_pattern_(std::move(encryption_pattern)) {
// Unencrypted blocks should not have a DecryptConfig.
- DCHECK_NE(encryption_scheme_, EncryptionScheme::kUnencrypted);
+ CHECK_NE(encryption_scheme_, EncryptionScheme::kUnencrypted);
CHECK_GT(key_id_.size(), 0u);
CHECK_EQ(iv_.size(), static_cast<size_t>(DecryptConfig::kDecryptionKeySize));
// Pattern not allowed for non-'cbcs' schemes.
- DCHECK(encryption_scheme_ == EncryptionScheme::kCbcs || !encryption_pattern_);
+ CHECK(encryption_scheme_ == EncryptionScheme::kCbcs || !encryption_pattern_);
}
DecryptConfig::~DecryptConfig() = default;
diff --git a/media/mojo/common/media_type_converters.cc b/media/mojo/common/media_type_converters.cc
index ea13051..33cf431 100644
--- a/media/mojo/common/media_type_converters.cc
+++ b/media/mojo/common/media_type_converters.cc
@@ -43,6 +43,17 @@
TypeConverter<std::unique_ptr<media::DecryptConfig>,
media::mojom::DecryptConfigPtr>::
Convert(const media::mojom::DecryptConfigPtr& input) {
+ // Required invariants by the DecryptConfig constructor, to prevent a renderer
+ // from crashing the GPU we check them here as well and gracefully return
+ // nullptr instead.
+ if (input->encryption_scheme == media::EncryptionScheme::kUnencrypted) {
+ return nullptr;
+ }
+ // Pattern not allowed for non-'cbcs' schemes.
+ if (input->encryption_scheme != media::EncryptionScheme::kCbcs &&
+ input->encryption_pattern) {
+ return nullptr;
+ }
return std::make_unique<media::DecryptConfig>(
input->encryption_scheme, input->key_id, input->iv, input->subsamples,
input->encryption_pattern);
@@ -203,8 +214,10 @@
// `media::AudioBuffer`.
// `data_size()` refers to the amount of memory really used by the audio
// data. The rest is padding, which we don't need to copy.
- DCHECK_GT(input.data_size(), 0u);
- DCHECK_GE(input.data_size(), input.data_->span().size());
+ // Safe to CHECK here since this is into Mojo not From mojo (and thus not
+ // untrusted input).
+ CHECK_GT(input.data_size(), 0u);
+ CHECK_GE(input.data_size(), input.data_->span().size());
auto buffer_start = input.data_->span().begin();
auto buffer_end = buffer_start + input.data_size();
buffer->data.assign(buffer_start, buffer_end);
@@ -252,7 +265,8 @@
base::CheckMul(input->frame_count,
base::CheckMul(input->channel_count, bytes_per_channel))
.ValueOrDefault(0u);
- if (input->data.size() < min_data_size) {
+ if (input->data.size() < min_data_size ||
+ input->data.size() % input->channel_count != 0) {
DLOG(ERROR) << "Received invalid AudioBuffer, replace it with EOS.";
return media::AudioBuffer::CreateEOSBuffer();
}
@@ -261,7 +275,6 @@
// one in the case of interleaved data.
std::vector<const uint8_t*> channel_ptrs(input->channel_count, nullptr);
const size_t size_per_channel = input->data.size() / input->channel_count;
- DCHECK_EQ(0u, input->data.size() % input->channel_count);
for (int i = 0; i < input->channel_count; ++i) {
channel_ptrs[i] = UNSAFE_TODO(input->data.data() + i * size_per_channel);
}
Original Bug Report
Potential OOB Access in Android Mediaserver via DecryptConfig Validation Bypass
Project Fortify, an experimental security project, has identified the following potential security issue.
Overview: A compromised renderer can bypass subsample size validation by supplying a media::mojom::DecoderBuffer with an unencrypted DecryptConfig over Mojo. Due to a missing release-build check, this malicious configuration reaches the Android Mediaserver with an oversized clear_bytes value. This can result in an Out-of-Bounds memory access in the privileged system media service, potentially leading to a sandbox escape.
Affected files:
media/base/android/media_codec_bridge_impl.ccmedia/base/decoder_buffer.ccmedia/gpu/android/codec_wrapper.cc
Estimated timestamp from git blame: 2026-01-13
Description
There is a potential validation bypass in Chrome’s Android media stack that allows a compromised renderer to trigger an Out-of-Bounds (OOB) memory access in the Android Mediaserver process. By manipulating DecryptConfig with an EncryptionScheme::kUnencrypted scheme and a massive clear_bytes value, an attacker can bypass subsample validation. This large value is subsequently passed unchecked via JNI to the system’s MediaCodec service, potentially causing an OOB read or write and facilitating a sandbox escape.
Root Cause Analysis
The vulnerability is the result of a chain of missing checks and logical flaws across the Mojo IPC boundary and the Android media pipeline:
-
Insecure Mojo Deserialization: When the GPU process receives a
media::mojom::DecoderBuffer, it is deserialized usingmojo::TypeConverter. The converter forDecryptConfig(media/mojo/common/media_type_converters.cc) invokes themedia::DecryptConfigconstructor directly. The constructor only preventsEncryptionScheme::kUnencryptedvia aDCHECK_NE, which is compiled out in Official Release builds. Thus, an attacker can successfully instantiate aDecryptConfiglabeled as unencrypted. -
Validation Bypass: In
media/base/decoder_buffer.cc,DecoderBuffer::DoSubsamplesMatch()is responsible for verifying that the sum ofclear_bytesandcypher_bytesmatches the actual buffer size. However, it returns early if the buffer is unencrypted:if (!buffer.is_encrypted()) { return true; }Because
is_encrypted()returns false when the scheme iskUnencrypted, the function returnstrue, completely bypassing theVerifySubsamplesMatchSizecheck. -
Incorrect Dispatch Logic: In
media/gpu/android/codec_wrapper.cc, the code routes the buffer to the secure decoding path based solely on the presence of aDecryptConfigpointer, rather than whether the buffer is actually encrypted:const DecryptConfig* decrypt_config = buffer.decrypt_config(); if (decrypt_config) { result = codec_->QueueSecureInputBuffer(...); } -
Missing Bounds Check in JNI: In
media/base/android/media_codec_bridge_impl.cc,QueueSecureInputBufferprepares the subsample arrays for the Java layer. While it verifies thatsubsamples.cypher_bytesdoes not exceedINT32_MAX, it performs no validation whatsoever onsubsamples.clear_bytes. A massive value like0x7FFFFFFFeasily fits into the signed 32-bit array and is passed to Java, and subsequently to the AndroidMediaCodecsystem API.
Potential Attack Steps
Note: These are suggested steps for how an attacker might exploit this issue, as we do not yet have a working Proof of Concept.
- Gain code execution in the isolated renderer process.
- Establish a Mojo connection to the GPU process to access a video decoding interface (e.g.,
media::mojom::VideoDecoder). - Construct a malicious
media::mojom::DecoderBuffermessage. - Set the
data_sizeof the buffer payload to a small, valid size (e.g., 10 bytes). - Add a
media::mojom::DecryptConfigto the buffer. - Set the
encryption_schemeof theDecryptConfigtomedia::mojom::EncryptionScheme::kUnencrypted. - Add a
SubsampleEntryto thesubsamplesarray, settingclear_bytesto0x7FFFFFFFandcypher_bytesto0. - Set
key_idto a dummy non-empty array andivto a dummy 16-byte array to pass secondary release-buildCHECKs in the constructor. - Send the message to the GPU process. The buffer will bypass size validations, navigate the C++ to Java JNI layer, and reach the system’s
MediaCodecsubsystem. - The Android Mediaserver will attempt to parse
0x7FFFFFFFclear bytes from the shared memory buffer map, triggering an OOB read (and potentially OOB write depending on the underlying codec plugin’s memory operations).
Impact
Successful exploitation of this vulnerability causes memory corruption within the Android Mediaserver (or a specific hardware codec plugin). Given the privileges of the Mediaserver process, this could lead to Remote Code Execution (RCE) outside the Chromium sandbox, resulting in a full sandbox escape.
Evaluated with Chrome root at commit: 9760e6c70cd33a320713361f17c6dcca85648c0f
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. Please feel free to reach out to me if you have concerns or feedback.