High watch ~ plausible CVSS 8.3 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Media
DescriptionInsufficient validation of untrusted input in Media
ComponentMedia
Sub-componentMiracleFix
Bug ClassInput Validation
Action tierwatch — logic/policy bug — security-relevant, no memory-unsafety — factors set by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIPlausible
Reviewer notes: 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.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker495259842
Fix commit1703fe61bee4 (chromium/src) +19/-6
Related commits878ee241afee candidate · chromium/src
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-03-23 (per vendor advisory)
Fix landed2026-03-30 (public git)
Disclosed2026-05-05
Patch gap36 days — fix public in source before the advisory

Background

`DecryptConfig`
a media object describing a buffer’s EncryptionScheme, key id, IV, optional encryption pattern and per-SubsampleEntry clear_bytes/cypher_bytes split.
`EncryptionScheme::kUnencrypted`
the scheme value meaning “not encrypted”, which the DecryptConfig constructor documents as invalid (“Unencrypted blocks should not have a DecryptConfig”, media/base/decrypt_config.cc:49).
TypeConverter
the mojo TypeConverter layer in media/mojo/common/media_type_converters.cc that turns renderer-supplied media::mojom::DecryptConfigPtr/media::mojom::DecoderBufferPtr messages into media::DecryptConfig/media::DecoderBuffer objects.
`QueueSecureInputBuffer`
the MediaCodecBridgeImpl method that copies each subsample’s sizes into int32_t arrays and passes them through JNI to the Android MediaCodec.

Root Cause Analysis

Verify before building a PoC

This analysis is consistent with the fixing commit but not fully provable from it: part of the vulnerable path lives in code the fix commit does not touch. Confirm the trigger and the sink against the source before relying on it for exploit development.
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.
Re-mapped on 2026-10-03: our pipeline had picked the earliest commit citing this bug (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.
The renderer-to-GPU deserializer 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.
Key insight
A renderer could choose 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

  1. Compromise a renderer The attacker runs code in a renderer that can send media::mojom::DecoderBuffer messages to the GPU process (inferred; from the report’s suggested steps).
  2. Label the config unencrypted They attach a DecryptConfig with encryption_scheme set to kUnencrypted, a valid-looking key id and IV, and a SubsampleEntry whose clear_bytes far exceeds the buffer’s data_size. Before 1703fe61bee4 the converter (media_type_converters.cc:46-48) built this config and nothing in a release build rejected it (decrypt_config.cc:50).
  3. Skip subsample validation Because the buffer is “not encrypted”, the subsample-vs-size check is skipped, but because it carries a DecryptConfig it is sent to QueueSecureInputBuffer (inferred; from the report, since decoder_buffer.cc and codec_wrapper.cc are not shown).
  4. Hand bogus sizes to MediaCodec QueueSecureInputBuffer copies the unchecked clear_bytes into native_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

A compromised renderer could pass subsample sizes that do not match the buffer into the Android 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

FunctionChangeNotes
Convert
media/mojo/common/media_type_converters.cc
modified
if
media/mojo/common/media_type_converters.cc
modified
for
media/mojo/common/media_type_converters.cc
modified

Files Changed

  • media/base/decrypt_config.cc
  • media/mojo/common/media_type_converters.cc

Audit Directions

  • Release-disabled invariants on IPC input
    Find 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 a CHECK.
  • Validation keyed on a different predicate than dispatch
    Where 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 conversion
    When a sub-object converter can return nullptr, the parent converter should reject the whole message, not quietly drop the field.
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);
   }
Loading diff…

Original Bug Report

reported by rj...@google.com

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.cc
  • media/base/decoder_buffer.cc
  • media/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:

  1. Insecure Mojo Deserialization: When the GPU process receives a media::mojom::DecoderBuffer, it is deserialized using mojo::TypeConverter. The converter for DecryptConfig (media/mojo/common/media_type_converters.cc) invokes the media::DecryptConfig constructor directly. The constructor only prevents EncryptionScheme::kUnencrypted via a DCHECK_NE, which is compiled out in Official Release builds. Thus, an attacker can successfully instantiate a DecryptConfig labeled as unencrypted.

  2. Validation Bypass: In media/base/decoder_buffer.cc, DecoderBuffer::DoSubsamplesMatch() is responsible for verifying that the sum of clear_bytes and cypher_bytes matches 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 is kUnencrypted, the function returns true, completely bypassing the VerifySubsamplesMatchSize check.

  3. 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 a DecryptConfig pointer, rather than whether the buffer is actually encrypted:

    const DecryptConfig* decrypt_config = buffer.decrypt_config();
    if (decrypt_config) {
      result = codec_->QueueSecureInputBuffer(...);
    }
    
  4. Missing Bounds Check in JNI: In media/base/android/media_codec_bridge_impl.cc, QueueSecureInputBuffer prepares the subsample arrays for the Java layer. While it verifies that subsamples.cypher_bytes does not exceed INT32_MAX, it performs no validation whatsoever on subsamples.clear_bytes. A massive value like 0x7FFFFFFF easily fits into the signed 32-bit array and is passed to Java, and subsequently to the Android MediaCodec system 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.

  1. Gain code execution in the isolated renderer process.
  2. Establish a Mojo connection to the GPU process to access a video decoding interface (e.g., media::mojom::VideoDecoder).
  3. Construct a malicious media::mojom::DecoderBuffer message.
  4. Set the data_size of the buffer payload to a small, valid size (e.g., 10 bytes).
  5. Add a media::mojom::DecryptConfig to the buffer.
  6. Set the encryption_scheme of the DecryptConfig to media::mojom::EncryptionScheme::kUnencrypted.
  7. Add a SubsampleEntry to the subsamples array, setting clear_bytes to 0x7FFFFFFF and cypher_bytes to 0.
  8. Set key_id to a dummy non-empty array and iv to a dummy 16-byte array to pass secondary release-build CHECKs in the constructor.
  9. 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 MediaCodec subsystem.
  10. The Android Mediaserver will attempt to parse 0x7FFFFFFF clear 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.

View on issue tracker