CVE-2026-10998
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TEST_Fmedia/cast/openscreen/remoting_proto_utils_unittest.cc |
modified |
Files Changed
media/cast/openscreen/remoting_proto_utils.ccmedia/cast/openscreen/remoting_proto_utils_unittest.cc
Patch
From 0ff4ac1d54bad0476ad3a570a4da393726bf9eb1 Mon Sep 17 00:00:00 2001
From: Jordan Bayles <jophba@chromium.org>
Date: Fri, 10 Apr 2026 18:05:37 -0700
Subject: [PATCH] Fix Global-Buffer-Overflow in Cast Remoting proto handling
When converting openscreen::cast::PipelineStatistics to
media::PipelineStatistics, the decoder_type fields for audio and video
were being static_cast directly to media::AudioDecoderType and
media::VideoDecoderType from the attacker-controlled int64 proto values.
Since standard enums do not natively validate bounds, an out-of-bounds
value would cause a global-buffer-overflow (OOB read) when propagating
to the GetDecoderName() switch table in media/base/decoder.cc.
This CL introduces a templated helper function SafeCastAsContiguousEnum
(guarded by a C++20 requires clause to enforce kMaxValue and kUnknown
properties) to ensure the proto integer safely falls within [kUnknown, kMaxValue].
If the value is out of bounds, it gracefully defaults to the kUnknown
enum state instead of triggering a crash.
Bug: 486536242
Change-Id: I72a4d52a8d9081249a1c367c44d56bc1b6a92a94
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7747040
Reviewed-by: Mark Foltz <mfoltz@chromium.org>
Commit-Queue: Jordan Bayles <jophba@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1613207}
---
diff --git a/media/cast/openscreen/remoting_proto_utils.cc b/media/cast/openscreen/remoting_proto_utils.cc
index e4c2e431..9a9e0fa 100644
--- a/media/cast/openscreen/remoting_proto_utils.cc
+++ b/media/cast/openscreen/remoting_proto_utils.cc
@@ -24,6 +24,23 @@
constexpr size_t kProtoBufferHeaderSize = sizeof(uint16_t);
constexpr size_t kDataBufferHeaderSize = sizeof(uint32_t);
+// Helper method for Chromium enums that are contiguous, meaning all integers in
+// [kUnknown, kMaxValue] are valid (where kUnknown is typically zero but not a
+// requirement for this function).
+template <typename Enum>
+ requires requires {
+ Enum::kMaxValue;
+ Enum::kUnknown;
+ }
+Enum SafeCastAsContiguousEnum(int64_t value) {
+ static_assert(Enum::kUnknown <= Enum::kMaxValue);
+ if (value >= static_cast<int64_t>(Enum::kUnknown) &&
+ value <= static_cast<int64_t>(Enum::kMaxValue)) {
+ return static_cast<Enum>(value);
+ }
+ return Enum::kUnknown;
+}
+
scoped_refptr<media::DecoderBuffer> ConvertProtoToDecoderBuffer(
const openscreen::cast::DecoderBuffer& buffer_message,
scoped_refptr<media::DecoderBuffer> buffer) {
@@ -335,7 +352,8 @@
if (stats_message.has_audio_decoder_info()) {
auto audio_info = stats_message.audio_decoder_info();
stats->audio_pipeline_info.decoder_type =
- static_cast<media::AudioDecoderType>(audio_info.decoder_type());
+ SafeCastAsContiguousEnum<media::AudioDecoderType>(
+ audio_info.decoder_type());
stats->audio_pipeline_info.is_platform_decoder =
audio_info.is_platform_decoder();
stats->audio_pipeline_info.has_decrypting_demuxer_stream = false;
@@ -344,7 +362,8 @@
if (stats_message.has_video_decoder_info()) {
auto video_info = stats_message.video_decoder_info();
stats->video_pipeline_info.decoder_type =
- static_cast<media::VideoDecoderType>(video_info.decoder_type());
+ SafeCastAsContiguousEnum<media::VideoDecoderType>(
+ video_info.decoder_type());
stats->video_pipeline_info.is_platform_decoder =
video_info.is_platform_decoder();
stats->video_pipeline_info.has_decrypting_demuxer_stream = false;
diff --git a/media/cast/openscreen/remoting_proto_utils_unittest.cc b/media/cast/openscreen/remoting_proto_utils_unittest.cc
index d666c3b..8bb1319a 100644
--- a/media/cast/openscreen/remoting_proto_utils_unittest.cc
+++ b/media/cast/openscreen/remoting_proto_utils_unittest.cc
@@ -173,6 +173,24 @@
EXPECT_EQ(original, converted);
}
+TEST_F(ProtoUtilsTest, PipelineStatisticsConversionOutOfBoundsTest) {
+ openscreen::cast::PipelineStatistics pb_stats;
+ auto* pb_video_info = pb_stats.mutable_video_decoder_info();
+ auto* pb_audio_info = pb_stats.mutable_audio_decoder_info();
+
+ // Set out-of-bounds decoder types.
+ pb_video_info->set_decoder_type(9999);
+ pb_audio_info->set_decoder_type(9999);
+
+ media::PipelineStatistics converted;
+ ConvertProtoToPipelineStatistics(pb_stats, &converted);
+
+ EXPECT_EQ(converted.audio_pipeline_info.decoder_type,
+ media::AudioDecoderType::kUnknown);
+ EXPECT_EQ(converted.video_pipeline_info.decoder_type,
+ media::VideoDecoderType::kUnknown);
+}
+
TEST_F(ProtoUtilsTest, VideoDecoderConfigConversionTest) {
const media::VideoDecoderConfig video_config =
media::TestVideoConfig::Normal();
Regression Test / PoC
diff --git a/media/cast/openscreen/remoting_proto_utils_unittest.cc b/media/cast/openscreen/remoting_proto_utils_unittest.cc
index d666c3b..8bb1319a 100644
--- a/media/cast/openscreen/remoting_proto_utils_unittest.cc
+++ b/media/cast/openscreen/remoting_proto_utils_unittest.cc
@@ -173,6 +173,24 @@
EXPECT_EQ(original, converted);
}
+TEST_F(ProtoUtilsTest, PipelineStatisticsConversionOutOfBoundsTest) {
+ openscreen::cast::PipelineStatistics pb_stats;
+ auto* pb_video_info = pb_stats.mutable_video_decoder_info();
+ auto* pb_audio_info = pb_stats.mutable_audio_decoder_info();
+
+ // Set out-of-bounds decoder types.
+ pb_video_info->set_decoder_type(9999);
+ pb_audio_info->set_decoder_type(9999);
+
+ media::PipelineStatistics converted;
+ ConvertProtoToPipelineStatistics(pb_stats, &converted);
+
+ EXPECT_EQ(converted.audio_pipeline_info.decoder_type,
+ media::AudioDecoderType::kUnknown);
+ EXPECT_EQ(converted.video_pipeline_info.decoder_type,
+ media::VideoDecoderType::kUnknown);
+}
+
TEST_F(ProtoUtilsTest, VideoDecoderConfigConversionTest) {
const media::VideoDecoderConfig video_config =
media::TestVideoConfig::Normal();
Original Bug Report
Global-Buffer-Overflow in GetDecoderName() via Unchecked static_cast in Cast Remoting
Steps to reproduce the problem
- Add and compile the below attached fuzzer
- execute the fuzzer with attached file
- you can see the Global-BOF
Problem Description
ConvertProtoToPipelineStatistics() in media/cast/openscreen/remoting_proto_utils.cc (lines 335, 344) uses bare static_cast to convert attacker-controlled int64 protobuf fields to media::AudioDecoderType and media::VideoDecoderType enum types without any bounds validation.
// remoting_proto_utils.cc:335 — NO bounds check
stats->audio_pipeline_info.decoder_type =
static_cast<media::AudioDecoderType>(audio_info.decoder_type());
// remoting_proto_utils.cc:344 — NO bounds check
stats->video_pipeline_info.decoder_type =
static_cast<media::VideoDecoderType>(video_info.decoder_type());
The proto definition (remoting.proto) declares decoder_type as int64:
message AudioDecoderInfo {
optional int64 decoder_type = 1; // Attacker-controlled, full int64 range
optional bool is_platform_decoder = 2;
};
Meanwhile, media::AudioDecoderType is enum class : int with valid values 0-10 (kMaxValue = kSymphonia = 10), and media::VideoDecoderType has valid values 0-19 (kMaxValue = kVideoToolbox = 19).
When the tainted enum value propagates to GetDecoderName() in media/base/decoder.cc, the switch statement has no default case. The compiler (Clang with -O2) compiles this as a jump table in .rodata. An out-of-range value indexes beyond the table boundaries, causing a global-buffer-overflow — an OOB read of the jump table that lands in a poisoned ASan redzone around adjacent global variables.
Contrast with safe code in the same file: Every other enum conversion in remoting_proto_utils.cc uses the ToMedia*() functions from remoting_proto_enum_utils.cc, which return std::optional with a default: return std::nullopt; case. The ConvertProtoToPipelineStatistics function is the only one that bypasses this safety pattern.
Vulnerable Function — remoting_proto_utils.cc:319-357
void ConvertProtoToPipelineStatistics(
const openscreen::cast::PipelineStatistics& stats_message,
media::PipelineStatistics* stats) {
// ... safe field assignments ...
if (stats_message.has_audio_decoder_info()) {
auto audio_info = stats_message.audio_decoder_info();
stats->audio_pipeline_info.decoder_type =
static_cast<media::AudioDecoderType>(audio_info.decoder_type()); // ← BUG: no bounds check
stats->audio_pipeline_info.is_platform_decoder =
audio_info.is_platform_decoder();
// ...
}
if (stats_message.has_video_decoder_info()) {
auto video_info = stats_message.video_decoder_info();
stats->video_pipeline_info.decoder_type =
static_cast<media::VideoDecoderType>(video_info.decoder_type()); // ← BUG: no bounds check
stats->video_pipeline_info.is_platform_decoder =
video_info.is_platform_decoder();
// ...
}
}
Crash Site — decoder.cc:62-63
const char* GetDecoderName(AudioDecoderType type) {
switch (type) { // ← Compiled as jump table, NO default case
case AudioDecoderType::kUnknown: return "Unknown Audio Decoder";
case AudioDecoderType::kFFmpeg: return "FFmpegAudioDecoder";
// ... cases 0-10 ...
case AudioDecoderType::kSymphonia: return "SymphoniaAudioDecoder";
}
// ← No return statement — UB if value not in 0-10
}
std::ostream& operator<<(std::ostream& out, AudioDecoderType type) {
return out << GetDecoderName(type); // ← Crash: OOB read of jump table
}
Reproduction Steps
1. Build Configuration
GN args (out/ASan/args.gn):
is_asan = true
is_debug = false
use_libfuzzer = true
is_component_build = false
use_remoteexec = false
dcheck_always_on = false
treat_warnings_as_errors = false
2. Build Command
autoninja -C out/ASan remoting_proto_utils_fuzzer
3. Generate PoC
python3 AI_Fuzzer/gen_poc_pipeline_stats_oob.py
This generates:
poc_pipeline_stats_oob.bin— AudioDecoderType=9999, VideoDecoderType=8888poc_pipeline_stats_maxint.bin— AudioDecoderType=INT_MAXpoc_pipeline_stats_neg.bin— AudioDecoderType=-1poc_pipeline_stats_video_oob.bin— VideoDecoderType=5000
4. Run PoC
./out/ASan/remoting_proto_utils_fuzzer AI_Fuzzer/poc_pipeline_stats_oob.bin
Summary
Global-Buffer-Overflow in GetDecoderName() via Unchecked static_cast in Cast Remoting
Custom Questions
Crash state:
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 3115684938
INFO: Loaded 1 modules (1291389 inline 8-bit counters): 1291389 [0x57f680bb27a0, 0x57f680cedc1d),
INFO: Loaded 1 PC tables (1291389 PCs): 1291389 [0x57f680cedc20,0x57f6820a23f0),
./out/ASan/remoting_proto_utils_fuzzer: Running 1 inputs 1 time(s) each.
Running: AI_Fuzzer/poc_pipeline_stats_oob.bin
=================================================================
==163459==ERROR: AddressSanitizer: global-buffer-overflow on address 0x57f67feb04f8 at pc 0x57f677e77bf2 bp 0x7ffc1c687790 sp 0x7ffc1c687788
READ of size 8 at 0x57f67feb04f8 thread T0
#0 0x57f677e77bf1 in media::operator<<(std::__Cr::basic_ostream<char, std::__Cr::char_traits<char>>&, media::AudioDecoderType) media/base/decoder.cc:63:3
#1 0x57f6774aaef5 in LLVMFuzzerTestOneInput AI_Fuzzer/remoting_proto_utils_fuzzer.cc:102:13
#2 0x57f6774f2e76 in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) third_party/libFuzzer/src/FuzzerLoop.cpp:619:13
#3 0x57f6774c44dd in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) third_party/libFuzzer/src/FuzzerDriver.cpp:329:6
#4 0x57f6774cd4d0 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) third_party/libFuzzer/src/FuzzerDriver.cpp:864:9
#5 0x57f6774b4205 in main third_party/libFuzzer/src/FuzzerMain.cpp:20:10
#6 0x79bf23c2a1c9 in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16
#7 0x79bf23c2a28a in __libc_start_main csu/../csu/libc-start.c:360:3
#8 0x57f6773cde29 in _start (/home/basha/Desktop/chromefuzz/chromium/src/out/ASan/remoting_proto_utils_fuzzer+0x47fce29) (BuildId: 8cfb48960df82d09)
0x57f67feb04f8 is located 40 bytes before global variable 'vtable for (anonymous namespace)::ImageFromPictureRec' defined in '../../third_party/skia/src/shaders/SkPictureShader.cpp' (0x57f67feb0520) of size 80
0x57f67feb04f8 is located 0 bytes after global variable 'vtable for SkPictureShader' defined in '../../third_party/skia/src/shaders/SkPictureShader.cpp' (0x57f67feb0460) of size 152
SUMMARY: AddressSanitizer: global-buffer-overflow media/base/decoder.cc:63:3 in media::operator<<(std::__Cr::basic_ostream<char, std::__Cr::char_traits<char>>&, media::AudioDecoderType)
Shadow bytes around the buggy address:
0x57f67feb0200: f9 f9 f9 f9 00 00 00 00 00 00 00 00 00 00 00 00
0x57f67feb0280: 00 00 00 00 00 00 00 f9 f9 f9 f9 f9 00 00 00 00
0x57f67feb0300: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 f9
0x57f67feb0380: f9 f9 f9 f9 00 00 00 00 00 00 00 00 00 00 00 00
0x57f67feb0400: 00 00 00 00 00 00 00 f9 f9 f9 f9 f9 00 00 00 00
=>0x57f67feb0480: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00[f9]
0x57f67feb0500: f9 f9 f9 f9 00 00 00 00 00 00 00 00 00 00 f9 f9
0x57f67feb0580: f9 f9 f9 f9 00 00 00 00 00 00 00 00 00 00 00 00
0x57f67feb0600: 00 00 00 00 00 00 00 f9 f9 f9 f9 f9 00 00 00 00
0x57f67feb0680: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 f9
0x57f67feb0700: f9 f9 f9 f9 00 00 00 00 00 00 f9 f9 f9 f9 f9 f9
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==163459==ABORTING
Reporter credit:
Ameen Basha M K & Mohammed Yasar B
Additional Data
Category: Security
Chrome Channel: Not sure
Regression: N/A \