Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Media
DescriptionInsufficient validation of untrusted input in Media
ComponentMedia
Bug ClassLogic Error
Tracker495259842
Fix commit1703fe61bee4 (chromium/src) +19/-6
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-05

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
From 1703fe61bee406496bc5af2b6c0af9fde8935b81 Mon Sep 17 00:00:00 2001
From: Stephen Nusko <[email protected]>
Date: Sun, 29 Mar 2026 23:45:15 -0700
Subject: [PATCH] [MiracleFix] Replace DCHECK with CHECK in media/base and media/mojo.

These assertions are critical for maintaining invariants and should
cause a crash in all build configurations if violated.

This is a speculative hardening based on the linked bug.

Bug: 495259842
Change-Id: I44509ce9b18f994d748bc42255ffa85ca3496abc
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7695435
Auto-Submit: Stephen Nusko <[email protected]>
Reviewed-by: Colin Blundell <[email protected]>
Reviewed-by: Takashi Toyoshima <[email protected]>
Commit-Queue: Stephen Nusko <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1606907}
---

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 [email protected]

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