Medium watch CVSS 3.4 chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
3.4
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized resource in Media
DescriptionUninitialized resource in Media
ComponentMedia
Bug ClassUninitialized Memory
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
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.
Tracker496231550
Fix commita2581c3f7c0e (chromium/src) +133/-13
CWECWE-908
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-03-25 (per vendor advisory)
Fix landed2026-08-13 (public git)
Disclosed2026-09-08
Patch gap26 days — fix public in source before the advisory

Background

`MojoAudioEncoderService`
the GPU/utility-process implementation behind the mojom::AudioEncoder Mojo interface, whose Encode() receives audio buffers sent from a less-trusted renderer client.
Bitstream vs. PCM audio
platform audio encoders operate only on uncompressed PCM samples, whereas a bitstream AudioBuffer (e.g. kSampleFormatAc3) carries an opaque compressed payload whose frame_count is decoupled from its actual data size.
`AudioBuffer::WrapOrCopyToAudioBus()`
helper that produces an AudioBus from an AudioBuffer; for a bitstream buffer it allocates an AudioBus sized by frame_count without initializing per-channel sample memory.
`is_bitstream_format_`
an AudioBus flag that marks the bus as holding bitstream data, on which PCM sample operations like CopyPartialFramesTo are invalid.

Root Cause Analysis

MojoAudioEncoderService::Encode() converted the incoming mojom::AudioBufferPtr to an AudioBuffer and passed it straight to AudioBuffer::WrapOrCopyToAudioBus() without checking whether the buffer was end-of-stream or in a bitstream format. Because platform encoders only handle PCM, a bitstream buffer’s AudioBus is allocated with a large attacker-controlled frame_count but uninitialized channel memory (its real payload can be a single byte, as the WrapOrCopyToAudioBus_BitstreamDecoupledFrameCount test shows). If that bus is then processed as PCM — e.g. through CopyPartialFramesTo/CopyAndClipTo/Scale — it reads the uninitialized GPU-process heap, which could subsequently be encoded and returned, leaking process memory.

The fix rejects null, end_of_stream(), and IsBitstreamFormat() buffers up front with EncoderStatus::Codes::kInvalidInputFrame and mojo::ReportBadMessage, and hardens the PCM-only AudioBus methods by upgrading DCHECK(!is_bitstream_format_) to CHECK, so the invalid path now aborts safely even in release builds. A companion converter fix maps an empty-data bitstream buffer to an EOS buffer instead of a degenerate bitstream AudioBuffer.

Key insight
The service trusted the renderer-supplied sample format and never enforced the “PCM only” precondition before wrapping the buffer into an AudioBus, letting a bitstream buffer’s uninitialized, oversized allocation flow into PCM sample math; the fix enforces that precondition at the Mojo boundary and turns the debug-only invariant checks into release CHECKs.

Attack Path

  1. Reach the encoder service A compromised or malicious renderer holding the mojom::AudioEncoder remote initializes the encoder and calls Encode() with a crafted mojom::AudioBuffer.
  2. Supply a bitstream buffer The buffer sets a bitstream sample_format (e.g. kSampleFormatAc3) with a small (or empty) data payload but a large frame_count, so conversion yields an AudioBuffer whose IsBitstreamFormat() is true.
  3. Allocate uninitialized memory WrapOrCopyToAudioBus() builds an AudioBus sized by the inflated frame_count without initializing channel sample memory, leaving stale GPU-process heap contents in the bus.
  4. Process as PCM The platform encoder path treats the bus as PCM and invokes sample operations (CopyPartialFramesTo/CopyAndClipTo/Scale), reading the uninitialized bytes (inferred: the read result is subsequently encoded and returned to the client, disclosing heap memory).

Impact Assessment

An attacker controlling the renderer can cause the audio encoder service to allocate and process uninitialized heap memory, an information-disclosure primitive within the GPU/utility process that hosts MojoAudioEncoderService; the precondition is the ability to drive the mojom::AudioEncoder interface and submit a bitstream/EOS buffer. The advisory classifies this as an uninitialized-resource/information-leak issue in Media (Uninitialized resource in Media) and does not report in-the-wild exploitation. Any further escalation beyond leaking process memory is not demonstrated by the patch (inferred).

Changed Functions

FunctionChangeNotes
if
media/base/audio_bus.cc
modified
for
media/base/audio_bus.cc
modified
TEST
media/base/audio_buffer_unittest.cc
modified
MojoAudioEncoderTest
media/mojo/clients/mojo_audio_encoder_unittest.cc
modified
TEST
media/mojo/clients/mojo_audio_encoder_unittest.cc
modified
BindLambdaForTesting
media/mojo/clients/mojo_audio_encoder_unittest.cc
modified
TEST
media/mojo/common/media_type_converters_unittest.cc
modified

Files Changed

  • media/mojo/common/media_type_converters_unittest.cc
  • media/base/audio_bus.cc
  • media/mojo/clients/mojo_audio_encoder_unittest.cc
  • media/mojo/services/mojo_audio_encoder_service.cc
  • media/mojo/common/media_type_converters.cc
  • media/base/audio_buffer_unittest.cc

Audit Directions

  • Format-precondition at Mojo boundaries
    Audit other media Mojo services that assume PCM (or any specific sample format) and confirm they validate IsBitstreamFormat()/end_of_stream() on renderer-supplied buffers before sample-level processing.
  • `frame_count` decoupled from `data` size
    Hunt for code that sizes buffers or busses by an attacker-controlled frame_count while the backing payload is separately sized, since that mismatch enables oversized uninitialized allocations.
  • `DCHECK`-guarded invariants on IPC-reachable paths
    Look for DCHECK-only guards protecting operations reachable from Mojo/IPC input; in release builds these do not fire and can let invalid state through, so they warrant upgrading to CHECK.
From a2581c3f7c0ee4feec2cfc7d4b8517a375825a6c Mon Sep 17 00:00:00 2001
From: Thomas Guilbert <tguilbert@chromium.org>
Date: Wed, 12 Aug 2026 18:48:50 -0700
Subject: [PATCH] Reject bitstream and EOS inputs in MojoAudioEncoderService

Platform audio encoders operate only on PCM audio. When receiving a
bitstream buffer, `WrapOrCopyToAudioBus()` allocates an uninitialized
AudioBus which could leak uninitialized GPU process heap memory if
subsequently processed as PCM.

This change:
- Rejects bitstream and end-of-stream buffers in
  MojoAudioEncoderService::Encode with kInvalidInputFrame.
- Upgrades DCHECK(!is_bitstream_format_) to CHECK in AudioBus methods
  (CopyAndClipTo, CopyPartialFramesTo, Scale, SwapChannels) to fail
  safely in release builds.
- Replaces empty bitstream buffers with EOS buffers
  during mojom AudioBuffer type conversions.
- Adds test coverage across media_unittests and media_mojo_unittests.

TAG=agy
CONV=52820452-9f78-43b7-a903-e2c9b7911cbe

Bug: 496231550
Change-Id: I9e56cfbb161762670f0ad845f079e7d785ac59b5
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8218848
Reviewed-by: Nasko Oskov <nasko@chromium.org>
Commit-Queue: Thomas Guilbert <tguilbert@chromium.org>
Reviewed-by: Dale Curtis <dalecurtis@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1678514}
---
diff --git a/media/base/audio_bus.cc b/media/base/audio_bus.cc
index e6a2442..99cdad5e 100644
--- a/media/base/audio_bus.cc
+++ b/media/base/audio_bus.cc
@@ -304,7 +304,7 @@
 }
 
 void AudioBus::CopyAndClipTo(AudioBus* dest) const {
-  DCHECK(!is_bitstream_format_);
+  CHECK(!is_bitstream_format_);
   CHECK_EQ(channels(), dest->channels());
   CHECK_LE(frames(), dest->frames());
   for (auto [src_ch, dest_ch] :
@@ -317,7 +317,7 @@
                                    int frame_count,
                                    int dest_start_frame,
                                    AudioBus* dest) const {
-  DCHECK(!is_bitstream_format_);
+  CHECK(!is_bitstream_format_);
   CHECK_EQ(channels(), dest->channels());
 
   const size_t source_offset = base::checked_cast<size_t>(source_start_frame);
@@ -336,7 +336,7 @@
 }
 
 void AudioBus::Scale(float volume) {
-  DCHECK(!is_bitstream_format_);
+  CHECK(!is_bitstream_format_);
   if (volume > 0 && volume != 1) {
     for (auto channel : channel_data_) {
       vector_math::FMUL(channel, volume, channel);
@@ -347,7 +347,7 @@
 }
 
 void AudioBus::SwapChannels(int a, int b) {
-  DCHECK(!is_bitstream_format_);
+  CHECK(!is_bitstream_format_);
   DCHECK(a < channels() && a >= 0);
   DCHECK(b < channels() && b >= 0);
   DCHECK_NE(a, b);
diff --git a/media/mojo/common/media_type_converters.cc b/media/mojo/common/media_type_converters.cc
index d5d5b69..eb59c42 100644
--- a/media/mojo/common/media_type_converters.cc
+++ b/media/mojo/common/media_type_converters.cc
@@ -261,6 +261,11 @@
   }
 
   if (IsBitstream(input->sample_format)) {
+    if (input->data.empty()) {
+      DLOG(ERROR)
+          << "Received invalid bitstream AudioBuffer, replace it with EOS.";
+      return media::AudioBuffer::CreateEOSBuffer();
+    }
     return media::AudioBuffer::CopyBitstreamFrom(
         input->sample_format, input->channel_layout, input->channel_count,
         input->sample_rate, input->frame_count, input->data, input->timestamp);
diff --git a/media/mojo/services/mojo_audio_encoder_service.cc b/media/mojo/services/mojo_audio_encoder_service.cc
index 04877de..ac3babb 100644
--- a/media/mojo/services/mojo_audio_encoder_service.cc
+++ b/media/mojo/services/mojo_audio_encoder_service.cc
@@ -11,6 +11,7 @@
 #include "base/functional/callback_helpers.h"
 #include "base/logging.h"
 #include "media/mojo/common/media_type_converters.h"
+#include "mojo/public/cpp/bindings/message.h"
 
 namespace media {
 
@@ -49,6 +50,12 @@
   }
 
   auto audio_buffer = buffer.To<scoped_refptr<AudioBuffer>>();
+  if (!audio_buffer || audio_buffer->end_of_stream() ||
+      audio_buffer->IsBitstreamFormat()) {
+    std::move(callback).Run(EncoderStatus::Codes::kInvalidInputFrame);
+    mojo::ReportBadMessage("Invalid audio buffer passed to Encode().");
+    return;
+  }
   auto audio_bus = AudioBuffer::WrapOrCopyToAudioBus(audio_buffer);
   encoder_->Encode(std::move(audio_bus),
                    base::TimeTicks() + audio_buffer->timestamp(),
diff --git a/media/base/audio_buffer_unittest.cc b/media/base/audio_buffer_unittest.cc
index ba15377..4decc900 100644
--- a/media/base/audio_buffer_unittest.cc
+++ b/media/base/audio_buffer_unittest.cc
@@ -965,4 +965,30 @@
   b1 = nullptr;
 }
 
+TEST(AudioBufferTest, WrapOrCopyToAudioBus_BitstreamDecoupledFrameCount) {
+  constexpr ChannelLayout kChannelLayout = CHANNEL_LAYOUT_STEREO;
+  constexpr int kChannelCount = 2;
+  constexpr int kFrameCount = 100000;
+  constexpr uint8_t kTinyData[] = {0x42};
+
+  scoped_refptr<AudioBuffer> buffer = AudioBuffer::CopyBitstreamFrom(
+      kSampleFormatAc3, kChannelLayout, kChannelCount, kSampleRate, kFrameCount,
+      kTinyData, base::Microseconds(1));
+  ASSERT_TRUE(buffer);
+  EXPECT_EQ(kFrameCount, buffer->frame_count());
+  EXPECT_EQ(sizeof(kTinyData), buffer->data_size());
+  EXPECT_TRUE(buffer->IsBitstreamFormat());
+
+  std::unique_ptr<AudioBus> bus = AudioBuffer::WrapOrCopyToAudioBus(buffer);
+  ASSERT_TRUE(bus);
+  EXPECT_TRUE(bus->is_bitstream_format());
+  EXPECT_EQ(static_cast<size_t>(kFrameCount), bus->frames());
+  EXPECT_EQ(sizeof(kTinyData), bus->bitstream_data().size());
+  EXPECT_EQ(kFrameCount, bus->GetBitstreamFrames());
+
+  auto dest = AudioBus::Create(kChannelCount, kFrameCount);
+  EXPECT_DEATH_IF_SUPPORTED(
+      bus->CopyPartialFramesTo(0, kFrameCount, 0, dest.get()), "");
+}
+
 }  // namespace media
diff --git a/media/mojo/clients/mojo_audio_encoder_unittest.cc b/media/mojo/clients/mojo_audio_encoder_unittest.cc
index 390437e40..1f0b2d3 100644
--- a/media/mojo/clients/mojo_audio_encoder_unittest.cc
+++ b/media/mojo/clients/mojo_audio_encoder_unittest.cc
@@ -30,6 +30,7 @@
 #include "mojo/public/cpp/bindings/pending_receiver.h"
 #include "mojo/public/cpp/bindings/pending_remote.h"
 #include "mojo/public/cpp/bindings/self_owned_receiver.h"
+#include "mojo/public/cpp/system/functions.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 using ::base::test::RunOnceCallback;
@@ -41,6 +42,19 @@
 
 namespace media {
 
+namespace {
+
+AudioEncoder::Options MakeOptions() {
+  AudioEncoder::Options options;
+  options.codec = AudioCodec::kOpus;
+  options.bitrate = 128000;
+  options.channels = 2;
+  options.sample_rate = 44000;
+  return options;
+}
+
+}  // namespace
+
 // Tests MojoAudioEncoder (client) and MojoAudioEncoderService (service).
 class MojoAudioEncoderTest : public ::testing::Test {
  public:
@@ -95,15 +109,6 @@
     return result;
   }
 
-  AudioEncoder::Options MakeOptions() {
-    AudioEncoder::Options options;
-    options.codec = AudioCodec::kOpus;
-    options.bitrate = 128000;
-    options.channels = 2;
-    options.sample_rate = 44000;
-    return options;
-  }
-
   base::TimeTicks FromMilliseconds(int ms) {
     return base::TimeTicks() + base::Milliseconds(ms);
   }
@@ -475,4 +480,65 @@
   EXPECT_EQ(error_count, input_count);
 }
 
+TEST(MojoAudioEncoderServiceTest, ServiceRejectsInvalidInputs) {
+  base::test::SingleThreadTaskEnvironment task_environment;
+
+  // Helper to initialize a fresh service instance and verify that sending an
+  // invalid buffer triggers a mojo bad message report.
+  auto test_invalid_buffer = [&](mojom::AudioBufferPtr buffer) {
+    // Boilerplate service setup and initialization.
+    auto mock_encoder = std::make_unique<StrictMock<MockAudioEncoder>>();
+    EXPECT_CALL(*mock_encoder, Initialize(_, _, _))
+        .WillOnce(RunOnceCallback<2>(EncoderStatus::Codes::kOk));
+    EXPECT_CALL(*mock_encoder, OnDestruct());
+
+    auto service =
+        std::make_unique<MojoAudioEncoderService>(std::move(mock_encoder));
+    mojo::Remote<mojom::AudioEncoder> remote;
+    auto receiver = std::make_unique<mojo::Receiver<mojom::AudioEncoder>>(
+        service.get(), remote.BindNewPipeAndPassReceiver());
+
+    mojo::PendingAssociatedRemote<mojom::AudioEncoderClient> client_remote;
+    auto client_receiver = client_remote.InitWithNewEndpointAndPassReceiver();
+    base::RunLoop init_loop;
+    remote->Initialize(std::move(client_remote), MakeOptions(),
+                       base::BindLambdaForTesting([&](const EncoderStatus& s) {
+                         EXPECT_TRUE(s.is_ok());
+                         init_loop.Quit();
+                       }));
+    init_loop.Run();
+
+    // Intercept bad message reports.
+    bool bad_message_received = false;
+    base::RunLoop bad_message_loop;
+    mojo::SetDefaultProcessErrorHandler(
+        base::BindLambdaForTesting([&](const std::string& error) {
+          bad_message_received = true;
+          bad_message_loop.Quit();
+        }));
+
+    // Send the invalid buffer and verify a bad message is reported.
+    remote->Encode(std::move(buffer), base::DoNothing());
+    bad_message_loop.Run();
+    EXPECT_TRUE(bad_message_received);
+
+    mojo::SetDefaultProcessErrorHandler(base::NullCallback());
+  };
+
+  // Test bitstream format input.
+  auto bitstream_buffer = mojom::AudioBuffer::New();
+  bitstream_buffer->sample_format = SampleFormat::kSampleFormatAc3;
+  bitstream_buffer->channel_layout = CHANNEL_LAYOUT_STEREO;
+  bitstream_buffer->channel_count = 2;
+  bitstream_buffer->sample_rate = 44000;
+  bitstream_buffer->frame_count = 100;
+  bitstream_buffer->data = std::vector<uint8_t>(10);
+  test_invalid_buffer(std::move(bitstream_buffer));
+
+  // Test End-Of-Stream input.
+  auto eos_buffer = mojom::AudioBuffer::New();
+  eos_buffer->end_of_stream = true;
+  test_invalid_buffer(std::move(eos_buffer));
+}
+
 }  // namespace media
diff --git a/media/mojo/common/media_type_converters_unittest.cc b/media/mojo/common/media_type_converters_unittest.cc
index 34cb11b1..c8e1a8cb 100644
--- a/media/mojo/common/media_type_converters_unittest.cc
+++ b/media/mojo/common/media_type_converters_unittest.cc
@@ -387,4 +387,20 @@
   EXPECT_FALSE(decrypt_config);
 }
 
+TEST(MediaTypeConvertersTest, ConvertAudioBuffer_BitstreamEmptyData) {
+  auto ptr = mojom::AudioBuffer::New();
+  ptr->sample_format = SampleFormat::kSampleFormatAc3;
+  ptr->channel_layout = CHANNEL_LAYOUT_STEREO;
+  ptr->channel_count = 2;
+  ptr->sample_rate = 48000;
+  ptr->frame_count = 100;
+  // Explicitly provide empty data for a bitstream buffer.
+  ptr->data = std::vector<uint8_t>();
+
+  auto result = ptr.To<scoped_refptr<AudioBuffer>>();
+  // Bitstream buffers with empty data should be replaced with EOS.
+  ASSERT_TRUE(result);
+  EXPECT_TRUE(result->end_of_stream());
+}
+
 }  // namespace media
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/media/base/audio_buffer_unittest.cc b/media/base/audio_buffer_unittest.cc
index ba15377..4decc900 100644
--- a/media/base/audio_buffer_unittest.cc
+++ b/media/base/audio_buffer_unittest.cc
@@ -965,4 +965,30 @@
   b1 = nullptr;
 }
 
+TEST(AudioBufferTest, WrapOrCopyToAudioBus_BitstreamDecoupledFrameCount) {
+  constexpr ChannelLayout kChannelLayout = CHANNEL_LAYOUT_STEREO;
+  constexpr int kChannelCount = 2;
+  constexpr int kFrameCount = 100000;
+  constexpr uint8_t kTinyData[] = {0x42};
+
+  scoped_refptr<AudioBuffer> buffer = AudioBuffer::CopyBitstreamFrom(
+      kSampleFormatAc3, kChannelLayout, kChannelCount, kSampleRate, kFrameCount,
+      kTinyData, base::Microseconds(1));
+  ASSERT_TRUE(buffer);
+  EXPECT_EQ(kFrameCount, buffer->frame_count());
+  EXPECT_EQ(sizeof(kTinyData), buffer->data_size());
+  EXPECT_TRUE(buffer->IsBitstreamFormat());
+
+  std::unique_ptr<AudioBus> bus = AudioBuffer::WrapOrCopyToAudioBus(buffer);
+  ASSERT_TRUE(bus);
+  EXPECT_TRUE(bus->is_bitstream_format());
+  EXPECT_EQ(static_cast<size_t>(kFrameCount), bus->frames());
+  EXPECT_EQ(sizeof(kTinyData), bus->bitstream_data().size());
+  EXPECT_EQ(kFrameCount, bus->GetBitstreamFrames());
+
+  auto dest = AudioBus::Create(kChannelCount, kFrameCount);
+  EXPECT_DEATH_IF_SUPPORTED(
+      bus->CopyPartialFramesTo(0, kFrameCount, 0, dest.get()), "");
+}
+
 }  // namespace media
diff --git a/media/mojo/clients/mojo_audio_encoder_unittest.cc b/media/mojo/clients/mojo_audio_encoder_unittest.cc
index 390437e40..1f0b2d3 100644
--- a/media/mojo/clients/mojo_audio_encoder_unittest.cc
+++ b/media/mojo/clients/mojo_audio_encoder_unittest.cc
@@ -30,6 +30,7 @@
 #include "mojo/public/cpp/bindings/pending_receiver.h"
 #include "mojo/public/cpp/bindings/pending_remote.h"
 #include "mojo/public/cpp/bindings/self_owned_receiver.h"
+#include "mojo/public/cpp/system/functions.h"
 #include "testing/gtest/include/gtest/gtest.h"
 
 using ::base::test::RunOnceCallback;
@@ -41,6 +42,19 @@
 
 namespace media {
 
+namespace {
+
+AudioEncoder::Options MakeOptions() {
+  AudioEncoder::Options options;
+  options.codec = AudioCodec::kOpus;
+  options.bitrate = 128000;
+  options.channels = 2;
+  options.sample_rate = 44000;
+  return options;
+}
+
+}  // namespace
+
 // Tests MojoAudioEncoder (client) and MojoAudioEncoderService (service).
 class MojoAudioEncoderTest : public ::testing::Test {
  public:
@@ -95,15 +109,6 @@
     return result;
   }
 
-  AudioEncoder::Options MakeOptions() {
-    AudioEncoder::Options options;
-    options.codec = AudioCodec::kOpus;
-    options.bitrate = 128000;
-    options.channels = 2;
-    options.sample_rate = 44000;
-    return options;
-  }
-
   base::TimeTicks FromMilliseconds(int ms) {
     return base::TimeTicks() + base::Milliseconds(ms);
   }
@@ -475,4 +480,65 @@
   EXPECT_EQ(error_count, input_count);
 }
 
+TEST(MojoAudioEncoderServiceTest, ServiceRejectsInvalidInputs) {
+  base::test::SingleThreadTaskEnvironment task_environment;
+
+  // Helper to initialize a fresh service instance and verify that sending an
+  // invalid buffer triggers a mojo bad message report.
+  auto test_invalid_buffer = [&](mojom::AudioBufferPtr buffer) {
+    // Boilerplate service setup and initialization.
+    auto mock_encoder = std::make_unique<StrictMock<MockAudioEncoder>>();
+    EXPECT_CALL(*mock_encoder, Initialize(_, _, _))
+        .WillOnce(RunOnceCallback<2>(EncoderStatus::Codes::kOk));
+    EXPECT_CALL(*mock_encoder, OnDestruct());
+
+    auto service =
+        std::make_unique<MojoAudioEncoderService>(std::move(mock_encoder));
+    mojo::Remote<mojom::AudioEncoder> remote;
+    auto receiver = std::make_unique<mojo::Receiver<mojom::AudioEncoder>>(
+        service.get(), remote.BindNewPipeAndPassReceiver());
+
+    mojo::PendingAssociatedRemote<mojom::AudioEncoderClient> client_remote;
+    auto client_receiver = client_remote.InitWithNewEndpointAndPassReceiver();
+    base::RunLoop init_loop;
+    remote->Initialize(std::move(client_remote), MakeOptions(),
+                       base::BindLambdaForTesting([&](const EncoderStatus& s) {
+                         EXPECT_TRUE(s.is_ok());
+                         init_loop.Quit();
+                       }));
+    init_loop.Run();
+
+    // Intercept bad message reports.
+    bool bad_message_received = false;
+    base::RunLoop bad_message_loop;
+    mojo::SetDefaultProcessErrorHandler(
+        base::BindLambdaForTesting([&](const std::string& error) {
+          bad_message_received = true;
+          bad_message_loop.Quit();
+        }));
+
+    // Send the invalid buffer and verify a bad message is reported.
+    remote->Encode(std::move(buffer), base::DoNothing());
+    bad_message_loop.Run();
+    EXPECT_TRUE(bad_message_received);
+
+    mojo::SetDefaultProcessErrorHandler(base::NullCallback());
+  };
+
+  // Test bitstream format input.
+  auto bitstream_buffer = mojom::AudioBuffer::New();
+  bitstream_buffer->sample_format = SampleFormat::kSampleFormatAc3;
+  bitstream_buffer->channel_layout = CHANNEL_LAYOUT_STEREO;
+  bitstream_buffer->channel_count = 2;
+  bitstream_buffer->sample_rate = 44000;
+  bitstream_buffer->frame_count = 100;
+  bitstream_buffer->data = std::vector<uint8_t>(10);
+  test_invalid_buffer(std::move(bitstream_buffer));
+
+  // Test End-Of-Stream input.
+  auto eos_buffer = mojom::AudioBuffer::New();
+  eos_buffer->end_of_stream = true;
+  test_invalid_buffer(std::move(eos_buffer));
+}
+
 }  // namespace media
diff --git a/media/mojo/common/media_type_converters_unittest.cc b/media/mojo/common/media_type_converters_unittest.cc
index 34cb11b1..c8e1a8cb 100644
--- a/media/mojo/common/media_type_converters_unittest.cc
+++ b/media/mojo/common/media_type_converters_unittest.cc
@@ -387,4 +387,20 @@
   EXPECT_FALSE(decrypt_config);
 }
 
+TEST(MediaTypeConvertersTest, ConvertAudioBuffer_BitstreamEmptyData) {
+  auto ptr = mojom::AudioBuffer::New();
+  ptr->sample_format = SampleFormat::kSampleFormatAc3;
+  ptr->channel_layout = CHANNEL_LAYOUT_STEREO;
+  ptr->channel_count = 2;
+  ptr->sample_rate = 48000;
+  ptr->frame_count = 100;
+  // Explicitly provide empty data for a bitstream buffer.
+  ptr->data = std::vector<uint8_t>();
+
+  auto result = ptr.To<scoped_refptr<AudioBuffer>>();
+  // Bitstream buffers with empty data should be replaced with EOS.
+  ASSERT_TRUE(result);
+  EXPECT_TRUE(result->end_of_stream());
+}
+
 }  // namespace media
Loading diff…

Original Bug Report

reported by vi...@google.com

Potential GPU process uninitialized heap memory disclosure via crafted bitstream AudioBuffer

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A compromised renderer can bypass data size validation in media_type_converters.cc by sending a bitstream AudioBuffer with a massive frame count and tiny payload. This causes the GPU process to allocate a large, uninitialized AudioBus which is subsequently processed as PCM audio by Mac and Android platform encoders in release builds. The encoder compresses this uninitialized heap memory and returns it to the renderer, leaking sensitive GPU process data.

Affected files:

  • media/base/audio_bus.cc
  • media/mojo/services/mojo_audio_encoder_service.cc
  • media/base/audio_buffer.cc
  • media/mojo/common/media_type_converters.cc

Estimated timestamp from git blame: 2025-01-24

Description

There is a potential uninitialized heap memory disclosure vulnerability in the GPU process’s audio encoding pipeline. A compromised renderer can craft a malicious mojom::AudioBuffer that bypasses size validation during Mojo type conversion. This results in the allocation of an AudioBus backed by largely uninitialized heap memory. Due to the reliance on DCHECK instead of CHECK for bitstream format validation in the audio FIFO path, platform encoders on Mac and Android process this uninitialized memory as PCM audio data, compress it, and send it back to the renderer.

Technical Details

  1. Validation Bypass: In media/mojo/common/media_type_converters.cc, the TypeConverter for mojom::AudioBufferPtr checks if (IsBitstream(input->sample_format)) on line 235 and returns early if true. Crucially, this early return completely bypasses the min_data_size calculation and validation (input->data.size() < min_data_size) that occurs later on line 251. This allows an attacker to specify a massive frame_count (e.g., 100,000) with a tiny data.size() (e.g., 1 byte).
  2. Uninitialized Allocation: When MojoAudioEncoderService::Encode receives this buffer, it calls AudioBuffer::WrapOrCopyToAudioBus. Since bitstream formats cannot be directly wrapped, it invokes AudioBus::Create(channels, frames), allocating memory for the massive frame_count. The AudioBus constructor uses base::AlignedUninit<float> to allocate this backing memory, leaving it completely uninitialized.
  3. Partial Initialization: The subsequent call to AudioBuffer::ReadFrames copies only the tiny provided payload (1 byte) into the AudioBus, leaving the remaining 99,999+ frames worth of float data uninitialized, containing residual GPU process heap data.
  4. DCHECK Bypass in Release Builds: The AudioBus is then passed to the platform encoder. On Mac (AudioToolboxAudioEncoder) and Android (NdkAudioEncoder), the encoder uses ConvertingAudioFifo to manage frame chunking. The FIFO pulls data by calling AudioBus::CopyPartialFramesTo. This method contains a DCHECK(!is_bitstream_format_) to prevent reading bitstream memory as PCM frames. However, because DCHECKs are removed in Release builds, the check is silently bypassed.
  5. Information Leak: CopyPartialFramesTo copies the uninitialized heap data into an output buffer, which is fed into the underlying AAC encoder. The encoder compresses this uninitialized memory and returns it to the renderer via client_->OnEncodedBufferReady, resulting in a substantial heap disclosure.

Note: Windows is not affected because MFAudioEncoder uses ToInterleaved(), which invokes channel() containing a mandatory CHECK(!is_bitstream_format_) that safely crashes the process.

Potential Attacker Steps

Note: These are suggested/potential steps as our setup does not currently have the ability to run code or verify a live Proof of Concept.

  1. Compromise a renderer process (e.g., via a v8 exploit).
  2. Connect to the media::mojom::InterfaceFactory and create an AudioEncoder bound to the GPU process.
  3. Initialize the encoder with an AAC codec.
  4. Optionally, perform heap grooming in the GPU process to position sensitive targets (like IPC handles or cryptographic tokens) into predictable allocations matching the AudioBus sizing.
  5. Craft a media::mojom::AudioBuffer with a bitstream sample_format (e.g., kSampleFormatAc3), a large frame_count, and a minimal 1-byte data payload.
  6. Send this buffer via the Encode Mojo method.
  7. Receive the encoded AAC buffers sent back from the GPU process.
  8. Decode the AAC audio and use signal analysis to recover the uninitialized GPU heap memory, aiding in a sandbox escape.

Suggested Fix

  1. Enforce strict validation: In media/mojo/common/media_type_converters.cc, move the min_data_size calculation and check so that it also applies to bitstream formats before CopyBitstreamFrom is called. Ensure the validation accurately reflects bitstream sizing requirements.
  2. Harden AudioBus accessors: Upgrade the DCHECK(!is_bitstream_format_) in AudioBus::CopyPartialFramesTo and related methods (like channel(), Scale(), SwapChannels()) to a mandatory CHECK(!is_bitstream_format_). This will ensure the process safely terminates in Release builds if anomalous bitstream processing occurs, mirroring the safety of the Windows ToInterleaved() path.

Evaluated with Chrome root at commit: bb48272cafb7e24c93f55ef40da398cd206ee651


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