Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in Media
DescriptionOut of bounds read in Media
ComponentMedia
Bug ClassOOB
Tracker486536242
Fix commit0ff4ac1d54ba (chromium/src) +39/-2
CISA KEVNot listed
CreditedAmeen Basha M K
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
TEST_F
media/cast/openscreen/remoting_proto_utils_unittest.cc
modified

Files Changed

  • media/cast/openscreen/remoting_proto_utils.cc
  • media/cast/openscreen/remoting_proto_utils_unittest.cc
From 0ff4ac1d54bad0476ad3a570a4da393726bf9eb1 Mon Sep 17 00:00:00 2001
From: Jordan Bayles <[email protected]>
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 <[email protected]>
Commit-Queue: Jordan Bayles <[email protected]>
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();
Loading diff…

Regression Test / PoC

shipped with the fix
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();
Loading diff…

Original Bug Report

reported by [email protected]

Global-Buffer-Overflow in GetDecoderName() via Unchecked static_cast in Cast Remoting

Steps to reproduce the problem

  1. Add and compile the below attached fuzzer
  2. execute the fuzzer with attached file
  3. 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=8888
  • poc_pipeline_stats_maxint.bin — AudioDecoderType=INT_MAX
  • poc_pipeline_stats_neg.bin — AudioDecoderType=-1
  • poc_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 \

View on issue tracker