CVE-2026-87456
Overview
Background
- `MojoAudioEncoderService`
- the GPU/utility-process implementation behind the
mojom::AudioEncoderMojo interface, whoseEncode()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 whoseframe_countis decoupled from its actual data size. - `AudioBuffer::WrapOrCopyToAudioBus()`
- helper that produces an
AudioBusfrom anAudioBuffer; for a bitstream buffer it allocates anAudioBussized byframe_countwithout initializing per-channel sample memory. - `is_bitstream_format_`
- an
AudioBusflag that marks the bus as holding bitstream data, on which PCM sample operations likeCopyPartialFramesToare 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.
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
- Reach the encoder service
A compromised or malicious renderer holding the
mojom::AudioEncoderremote initializes the encoder and callsEncode()with a craftedmojom::AudioBuffer. - Supply a bitstream buffer
The buffer sets a bitstream
sample_format(e.g.kSampleFormatAc3) with a small (or empty)datapayload but a largeframe_count, so conversion yields anAudioBufferwhoseIsBitstreamFormat()is true. - Allocate uninitialized memory
WrapOrCopyToAudioBus()builds anAudioBussized by the inflatedframe_countwithout initializing channel sample memory, leaving stale GPU-process heap contents in the bus. - 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
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
| Function | Change | Notes |
|---|---|---|
ifmedia/base/audio_bus.cc |
modified | |
formedia/base/audio_bus.cc |
modified | |
TESTmedia/base/audio_buffer_unittest.cc |
modified | |
MojoAudioEncoderTestmedia/mojo/clients/mojo_audio_encoder_unittest.cc |
modified | |
TESTmedia/mojo/clients/mojo_audio_encoder_unittest.cc |
modified | |
BindLambdaForTestingmedia/mojo/clients/mojo_audio_encoder_unittest.cc |
modified | |
TESTmedia/mojo/common/media_type_converters_unittest.cc |
modified |
Files Changed
media/mojo/common/media_type_converters_unittest.ccmedia/base/audio_bus.ccmedia/mojo/clients/mojo_audio_encoder_unittest.ccmedia/mojo/services/mojo_audio_encoder_service.ccmedia/mojo/common/media_type_converters.ccmedia/base/audio_buffer_unittest.cc
Audit Directions
- Format-precondition at Mojo boundariesAudit 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` sizeHunt for code that sizes buffers or busses by an attacker-controlled
frame_countwhile the backing payload is separately sized, since that mismatch enables oversized uninitialized allocations. - `DCHECK`-guarded invariants on IPC-reachable pathsLook 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 toCHECK.
Patch
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
Regression Test / PoC
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
Original Bug Report
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.ccmedia/mojo/services/mojo_audio_encoder_service.ccmedia/base/audio_buffer.ccmedia/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
- Validation Bypass: In
media/mojo/common/media_type_converters.cc, theTypeConverterformojom::AudioBufferPtrchecksif (IsBitstream(input->sample_format))on line 235 and returns early if true. Crucially, this early return completely bypasses themin_data_sizecalculation and validation (input->data.size() < min_data_size) that occurs later on line 251. This allows an attacker to specify a massiveframe_count(e.g., 100,000) with a tinydata.size()(e.g., 1 byte). - Uninitialized Allocation: When
MojoAudioEncoderService::Encodereceives this buffer, it callsAudioBuffer::WrapOrCopyToAudioBus. Since bitstream formats cannot be directly wrapped, it invokesAudioBus::Create(channels, frames), allocating memory for the massiveframe_count. TheAudioBusconstructor usesbase::AlignedUninit<float>to allocate this backing memory, leaving it completely uninitialized. - Partial Initialization: The subsequent call to
AudioBuffer::ReadFramescopies only the tiny provided payload (1 byte) into theAudioBus, leaving the remaining 99,999+ frames worth of float data uninitialized, containing residual GPU process heap data. - DCHECK Bypass in Release Builds: The
AudioBusis then passed to the platform encoder. On Mac (AudioToolboxAudioEncoder) and Android (NdkAudioEncoder), the encoder usesConvertingAudioFifoto manage frame chunking. The FIFO pulls data by callingAudioBus::CopyPartialFramesTo. This method contains aDCHECK(!is_bitstream_format_)to prevent reading bitstream memory as PCM frames. However, becauseDCHECKs are removed in Release builds, the check is silently bypassed. - Information Leak:
CopyPartialFramesTocopies 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 viaclient_->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.
- Compromise a renderer process (e.g., via a v8 exploit).
- Connect to the
media::mojom::InterfaceFactoryand create anAudioEncoderbound to the GPU process. - Initialize the encoder with an AAC codec.
- Optionally, perform heap grooming in the GPU process to position sensitive targets (like IPC handles or cryptographic tokens) into predictable allocations matching the
AudioBussizing. - Craft a
media::mojom::AudioBufferwith a bitstreamsample_format(e.g.,kSampleFormatAc3), a largeframe_count, and a minimal 1-bytedatapayload. - Send this buffer via the
EncodeMojo method. - Receive the encoded AAC buffers sent back from the GPU process.
- Decode the AAC audio and use signal analysis to recover the uninitialized GPU heap memory, aiding in a sandbox escape.
Suggested Fix
- Enforce strict validation: In
media/mojo/common/media_type_converters.cc, move themin_data_sizecalculation and check so that it also applies to bitstream formats beforeCopyBitstreamFromis called. Ensure the validation accurately reflects bitstream sizing requirements. - Harden AudioBus accessors: Upgrade the
DCHECK(!is_bitstream_format_)inAudioBus::CopyPartialFramesToand related methods (likechannel(),Scale(),SwapChannels()) to a mandatoryCHECK(!is_bitstream_format_). This will ensure the process safely terminates in Release builds if anomalous bitstream processing occurs, mirroring the safety of the WindowsToInterleaved()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.