Medium watch CVSS 8.8 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactHeap buffer overflow in Media
DescriptionHeap buffer overflow in Media
ComponentMedia
Sub-componentFuzzer
Bug ClassOOB
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
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.
Tracker476461867
Fix commitec1030c9323c (chromium/src) +176/-19
CWECWE-122 (Heap buffer overflow)
CVSS vectorCVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-01-18 (per vendor advisory)
Fix landed2026-02-06 (public git)
Disclosed2026-02-18
Patch gap12 days — fix public in source before the advisory

Background

`ChannelLayout`
Chromium’s enum describing a speaker arrangement (e.g. CHANNEL_LAYOUT_STEREO, CHANNEL_LAYOUT_5_1), from which ChannelLayoutToChannelCount() derives the number of channels a buffer is expected to hold.
Channel mask
a bitmask in which each set bit marks one present speaker; the patch defines ChannelMask as uint32_t and pins each Channels enum value (LEFT = 0 … SIDE_RIGHT = 10) to its bit position, matching the Windows WAVEFORMATEXTENSIBLE order.
Symphonia
a Rust audio decoding library; per the commit message, the decoder built on it can return buffers whose channel count (reported as a channel bitmask) differs from the stream’s initial config.
`media::AudioBuffer`
Chromium’s container for decoded PCM, which the decoder fills by copying the decoded samples; the commit names its constructor as where the overflow shows up.

Root Cause Analysis

The commit message says a decoder built on Symphonia could emit audio buffers whose channel count differed from the configured ChannelLayout. When the decoder then copied that output into a media::AudioBuffer, it expected the amount of data implied by the old layout rather than what it actually received, which overflowed the heap buffer inside media::AudioBuffer::AudioBuffer. The broken invariant is that the ChannelLayout (and so the channel count) used to size and copy the output must match the decoded frame’s real channel set.

The fix’s stated approach is to recompute the layout from Symphonia’s channel bitmask whenever the channel count changes. However, the diff shown here is cut off. It contains only the supporting infrastructure in media/base/channel_layout.cc and media/base/channel_layout.h: a new ChannelMaskToLayout() lookup, the compile-time ComputeChannelMask() helper and kChannelMaskToLayoutMap table, and explicit Channels enum values. The decoder-side change that calls ChannelMaskToLayout() when the channel count changes, and the AudioBuffer copy path itself, are not in the provided diff, so the exact sink and the guard placement there cannot be verified from this material.

Key insight
The decoder trusted the channel layout from the stream config even though Symphonia could report a different channel set for a given buffer, so the copy into AudioBuffer used a mismatched size. The fix adds a mask-to-layout mapping (ChannelMaskToLayout(), which returns CHANNEL_LAYOUT_DISCRETE for unknown masks) so the layout can be re-derived from the actual channel bitmask whenever the count changes.

Attack Path

  1. Supply crafted media An attacker serves an audio stream decoded through the Symphonia-based decoder, whose decoded buffers report a channel count different from the initial config (per the commit message; the exact formats routed to this decoder are not shown in the diff).
  2. Channel count changes mid-stream The decoder gets a buffer whose channel bitmask does not match the configured ChannelLayout, and before the fix it does not update the layout.
  3. Mismatched copy into `AudioBuffer` When the decoder copies samples into the output media::AudioBuffer, the expected and actual data sizes disagree, causing a heap buffer overflow during construction (per the commit subject and message).
  4. Heap corruption Writing past the buffer could corrupt neighbouring heap data in the decoding process (inferred).

Impact Assessment

An attacker who can get crafted audio decoded by the Symphonia-based decoder can trigger a heap buffer overflow in media::AudioBuffer construction, in whichever process runs that decoder (renderer or a media/utility process; not shown in the diff). The diff does not show whether this decoder is enabled in a stock build or sits behind a feature flag or specific platform, so whether ordinary web content can reach it by default cannot be confirmed from this material. The vendor rates it medium severity, and no exploitation in the wild is reported; any escalation to code execution is (inferred).

Changed Functions

FunctionChangeNotes
for
media/base/channel_layout.cc
modified
if
media/base/channel_layout.cc
modified

Files Changed

  • media/base/BUILD.gn
  • media/base/channel_layout.cc
  • media/base/channel_layout.h
  • media/base/channel_layout_unittest.cc

Audit Directions

  • Config-vs-frame channel mismatch
    Check every audio decoder wrapper (FFmpeg, Symphonia, platform decoders) for paths where per-frame channel count or layout can change mid-stream while output buffers are still sized from the initial AudioDecoderConfig.
  • Copies sized from metadata instead of the source
    Look for copies into AudioBuffer and similar containers whose length comes from a layout or frame count rather than from the actual size of the decoder’s output.
  • Rust/C++ FFI boundaries in media
    Check that values reported across Rust decoder bindings (channel counts, masks, frame lengths) are validated against the C++ side’s allocation assumptions before any copy.
From ec1030c9323c82affd75cc3facbc9c602859e3bb Mon Sep 17 00:00:00 2001
From: Jordan Bayles <jophba@chromium.org>
Date: Fri, 06 Feb 2026 13:38:00 -0800
Subject: [PATCH] [Fuzzer] Fix overflow in media::AudioBuffer::AudioBuffer

Fuzzing discovered a bug in which audio streams that return buffers
with a different channel count than the config has can result in an
overflow since, when the decoder copies to the output buffer, it is
expecting a different amount of data than it actually received.

This patch resolves this by updating the channel layout to match
the channel bitmask returned by Symphonia whenever the channel
count changes.

Bug: 476461867

Change-Id: Id14fc6e49bdfee06c5190ba4b00b8731fcc447b7
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7499455
Reviewed-by: Thomas Guilbert <tguilbert@chromium.org>
Commit-Queue: Jordan Bayles <jophba@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1581099}
---

diff --git a/media/base/BUILD.gn b/media/base/BUILD.gn
index 3a00a32..2b72ebdc 100644
--- a/media/base/BUILD.gn
+++ b/media/base/BUILD.gn
@@ -630,6 +630,7 @@
     "callback_registry_unittest.cc",
     "callback_timeout_helpers_unittest.cc",
     "capture_version_unittest.cc",
+    "channel_layout_unittest.cc",
     "channel_mixer_unittest.cc",
     "channel_mixing_matrix_unittest.cc",
     "container_names_unittest.cc",
diff --git a/media/base/channel_layout.cc b/media/base/channel_layout.cc
index d7e4a024..1c023c72 100644
--- a/media/base/channel_layout.cc
+++ b/media/base/channel_layout.cc
@@ -6,6 +6,7 @@
 
 #include <stddef.h>
 
+#include <algorithm>
 #include <array>
 
 #include "base/check_op.h"
@@ -15,6 +16,8 @@
 
 namespace media {
 
+namespace {
+
 constexpr auto kLayoutToChannels = std::to_array<int>({
     0,  // CHANNEL_LAYOUT_NONE
     0,  // CHANNEL_LAYOUT_UNSUPPORTED
@@ -60,8 +63,7 @@
 // surround sound channel in FFmpeg's 5.1 layout is in the 5th position (because
 // the order is L, R, C, LFE, LS, RS), so
 // kChannelOrderings[CHANNEL_LAYOUT_5_1][SIDE_LEFT] = 4;
-const std::array<std::array<const int, CHANNELS_MAX + 1>,
-                 CHANNEL_LAYOUT_MAX + 1>
+constexpr std::array<std::array<int, CHANNELS_MAX + 1>, CHANNEL_LAYOUT_MAX + 1>
     kChannelOrderings = {{
         // FL | FR | FC | LFE | BL | BR | FLofC | FRofC | BC | SL | SR
 
@@ -180,6 +182,30 @@
         {0, 1, -1, 2, -1, -1, -1, -1, 3, -1, -1},
     }};
 
+// Helper to compute bitmask for a layout at compile-time.
+constexpr ChannelMask ComputeChannelMask(ChannelLayout layout) {
+  ChannelMask mask = 0;
+  for (int c = 0; c <= Channels::CHANNELS_MAX; ++c) {
+    if (kChannelOrderings[layout][c] != -1) {
+      mask |= 1ULL << c;
+    }
+  }
+  return mask;
+}
+
+// Map of all channel layouts to their respective masks.
+constexpr auto kChannelMaskToLayoutMap = []() {
+  std::array<std::pair<ChannelMask, ChannelLayout>, CHANNEL_LAYOUT_MAX + 1>
+      entries;
+  for (int i = 0; i <= CHANNEL_LAYOUT_MAX; ++i) {
+    ChannelLayout layout = static_cast<ChannelLayout>(i);
+    entries[i] = {ComputeChannelMask(layout), layout};
+  }
+  return entries;
+}();
+
+}  // namespace
+
 int ChannelLayoutToChannelCount(ChannelLayout layout) {
   DCHECK_LT(static_cast<size_t>(layout), std::size(kLayoutToChannels));
   DCHECK_LE(kLayoutToChannels[layout], kMaxConcurrentChannels);
@@ -216,6 +242,17 @@
   return CHANNEL_LAYOUT_UNSUPPORTED;
 }
 
+ChannelLayout ChannelMaskToLayout(ChannelMask channel_mask) {
+  for (const auto& entry : kChannelMaskToLayoutMap) {
+    if (entry.first == channel_mask) {
+      return entry.second;
+    }
+  }
+  // If we don't find a standard ChannelLayout associated with the mask, return
+  // a DISCRETE layout so that we can still handle the raw channel data.
+  return CHANNEL_LAYOUT_DISCRETE;
+}
+
 int ChannelOrder(ChannelLayout layout, Channels channel) {
   DCHECK_LT(static_cast<size_t>(layout), std::size(kChannelOrderings));
   DCHECK_LT(static_cast<size_t>(channel), std::size(kChannelOrderings[0]));
diff --git a/media/base/channel_layout.h b/media/base/channel_layout.h
index f3ae4e8f..63294aa 100644
--- a/media/base/channel_layout.h
+++ b/media/base/channel_layout.h
@@ -5,6 +5,8 @@
 #ifndef MEDIA_BASE_CHANNEL_LAYOUT_H_
 #define MEDIA_BASE_CHANNEL_LAYOUT_H_
 
+#include <stdint.h>
+
 #include "media/base/media_export.h"
 
 namespace media {
@@ -132,21 +134,30 @@
   CHANNEL_LAYOUT_MAX = CHANNEL_LAYOUT_3_1_BACK
 };
 
+// The channel order matches the order of the bitmask in the Windows
+// WAVEFORMATEXTENSIBLE format. The value of the enum corresponds to the bit
+// position in the mask (e.g. LEFT is bit 0, RIGHT is bit 1, etc.).
+//
+// This standard is used by Windows (WASAPI), FFmpeg (legacy layouts), and
+// SMPTE.
+//
 // Note: Do not reorder or reassign these values; other code depends on their
-// ordering to operate correctly. E.g., CoreAudio channel layout computations.
+// ordering to operate correctly. E.g., CoreAudio channel layout computations
+// and ChannelMaskToLayout().
 enum Channels {
   LEFT = 0,
-  RIGHT,
-  CENTER,
-  LFE,
-  BACK_LEFT,
-  BACK_RIGHT,
-  LEFT_OF_CENTER,
-  RIGHT_OF_CENTER,
-  BACK_CENTER,
-  SIDE_LEFT,
-  SIDE_RIGHT,
-  CHANNELS_MAX = SIDE_RIGHT, // Must always equal the largest value ever logged.
+  RIGHT = 1,
+  CENTER = 2,
+  LFE = 3,
+  BACK_LEFT = 4,
+  BACK_RIGHT = 5,
+  LEFT_OF_CENTER = 6,
+  RIGHT_OF_CENTER = 7,
+  BACK_CENTER = 8,
+  SIDE_LEFT = 9,
+  SIDE_RIGHT = 10,
+  CHANNELS_MAX =
+      SIDE_RIGHT,  // Must always equal the largest value ever logged.
 };
 
 // The maximum number of concurrently active channels for all possible layouts.
@@ -170,6 +181,15 @@
 // or return CHANNEL_LAYOUT_UNSUPPORTED if there is no good match.
 MEDIA_EXPORT ChannelLayout GuessChannelLayout(int channels);
 
+// Returns the channel layout for a given channel mask. This code assumes that
+// the mask uses the Channels enum as the position of each channel, e.g.
+// a `LEFT` channel would be represented as `1 << Channels::LEFT` or `0b1`.
+//
+// Returns CHANNEL_LAYOUT_DISCRETE if the bitmask does not match any known
+// channel layout.
+using ChannelMask = uint32_t;
+MEDIA_EXPORT ChannelLayout ChannelMaskToLayout(ChannelMask channel_mask);
+
 // Returns a string representation of the channel layout.
 MEDIA_EXPORT const char* ChannelLayoutToString(ChannelLayout layout);
 
diff --git a/media/base/channel_layout_unittest.cc b/media/base/channel_layout_unittest.cc
new file mode 100644
index 0000000..dad0438
--- /dev/null
+++ b/media/base/channel_layout_unittest.cc
@@ -0,0 +1,57 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "media/base/channel_layout.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/media/base/channel_layout_unittest.cc b/media/base/channel_layout_unittest.cc
new file mode 100644
index 0000000..dad0438
--- /dev/null
+++ b/media/base/channel_layout_unittest.cc
@@ -0,0 +1,57 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "media/base/channel_layout.h"
+
+#include "testing/gtest/include/gtest/gtest.h"
+
+namespace media {
+
+TEST(ChannelLayoutTest, ChannelMaskToLayout_StandardLayouts) {
+  // Test standard layouts.
+  EXPECT_EQ(CHANNEL_LAYOUT_MONO, ChannelMaskToLayout(1 << CENTER));
+  EXPECT_EQ(CHANNEL_LAYOUT_STEREO,
+            ChannelMaskToLayout((1 << LEFT) | (1 << RIGHT)));
+  EXPECT_EQ(
+      CHANNEL_LAYOUT_5_1,
+      ChannelMaskToLayout((1 << LEFT) | (1 << RIGHT) | (1 << CENTER) |
+                          (1 << LFE) | (1 << SIDE_LEFT) | (1 << SIDE_RIGHT)));
+}
+
+TEST(ChannelLayoutTest, ChannelMaskToLayout_DuplicateLayouts) {
+  // Layouts with a lower index should win.
+
+  // CHANNEL_LAYOUT_STEREO (3) vs CHANNEL_LAYOUT_STEREO_DOWNMIX (16)
+  EXPECT_EQ(CHANNEL_LAYOUT_STEREO,
+            ChannelMaskToLayout((1 << LEFT) | (1 << RIGHT)));
+
+  // CHANNEL_LAYOUT_5_1 (10) vs CHANNEL_LAYOUT_5_1_4_DOWNMIX (33)
+  EXPECT_EQ(CHANNEL_LAYOUT_5_1,
+            ChannelMaskToLayout(1 << LEFT | 1 << RIGHT | 1 << CENTER |
+                                1 << LFE | 1 << SIDE_LEFT | 1 << SIDE_RIGHT));
+  // CHANNEL_LAYOUT_SURROUND (5) vs CHANNEL_LAYOUT_STEREO_AND_KEYBOARD_MIC (30)
+  EXPECT_EQ(CHANNEL_LAYOUT_SURROUND,
+            ChannelMaskToLayout((1 << LEFT) | (1 << RIGHT) | (1 << CENTER)));
+}
+
+TEST(ChannelLayoutTest, ChannelMaskToLayout_NonstandardLayouts) {
+  EXPECT_EQ(CHANNEL_LAYOUT_DISCRETE, ChannelMaskToLayout(1UL << 31));
+  EXPECT_EQ(CHANNEL_LAYOUT_NONE, ChannelMaskToLayout(0));
+}
+
+TEST(ChannelLayoutTest, ChannelMaskToLayout_UnknownChannelsReturnDiscrete) {
+  constexpr uint32_t kUnknownSpeaker = 31;
+  // Ensure that the speaker type is not currently supported.
+  static_assert(kUnknownSpeaker > CHANNELS_MAX);
+
+  EXPECT_EQ(
+      CHANNEL_LAYOUT_DISCRETE,
+      ChannelMaskToLayout((1 << LEFT) | (1 << RIGHT) | (1 << kUnknownSpeaker)));
+
+  EXPECT_EQ(CHANNEL_LAYOUT_DISCRETE,
+            ChannelMaskToLayout((1 << LEFT) | (1 << RIGHT) | (1 << CENTER) |
+                                (1 << LFE) | (1 << SIDE_LEFT) |
+                                (1 << SIDE_RIGHT) | 1 << kUnknownSpeaker));
+}
+}  // namespace media
Loading diff…

Original Bug Report

reported by 24...@project.gserviceaccount.com

media_pipeline_integration_fuzzer: Heap-buffer-overflow in media::AudioBuffer::AudioBuffer

Detailed Report: https://clusterfuzz.com/testcase?key=6619123802898432

Fuzzing Engine: libFuzzer Fuzz Target: media_pipeline_integration_fuzzer Job Type: chromeos_libfuzzer_chrome_asan Platform Id: linux

Crash Type: Heap-buffer-overflow READ {*} Crash Address: 0x7aa1e8107d00 Crash State: media::AudioBuffer::AudioBuffer media::AudioBuffer::CopyFrom media::SymphoniaAudioDecoder::ToMediaAudioBuffer

Sanitizer: address (ASAN)

Recommended Security Severity: Medium

Regressed: https://clusterfuzz.com/revisions?job=chromeos_libfuzzer_chrome_asan&range=1560045:1560111

Reproducer Testcase: https://clusterfuzz.com/download?testcase_id=6619123802898432

Issue filed automatically.

See https://chromium.googlesource.com/chromium/src/+/master/testing/libfuzzer/reproducing.md for instructions on reproducing this bug locally.

View on issue tracker