High patch-now ✔︎ verified CVSS 8.8 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.8
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactHeap buffer overflow in WebRTC
DescriptionHeap buffer overflow in WebRTC
ComponentWebRTC
Bug ClassOOB
Action tierpatch-now — corruption primitive, network-reachable in default config (confirmed) — factors set by independent review by AI
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.
Independent review by AIConfirmed
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Additionally cross-checked: a second, different model independently derived the same bug class, vulnerable function and fix from the commit.
Tracker502661101
Fix commitff1be2e8524f (src) +39/-37
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-04-17 (per vendor advisory)
Fix landed2026-04-16 (public git)
Disclosed2026-05-19
Patch gap33 days — fix public in source before the advisory

Background

NetEq `Normal`
the WebRTC NetEq DSP operation (renderer process) that produces normal-speech output and cross-fades from a prior Expand or Comfort Noise (CNG) frame.
`AudioMultiVector` / `AudioVector`
per-channel sample containers; Size() reports how many samples a channel actually holds.
`samples_per_ms_` / `kCngLength`
the per-millisecond sample count (48 at 48 kHz) and the fixed 48-sample CNG scratch length used to bound the cross-fade window.

Root Cause Analysis

In the pre-fix Normal::Process (modules/audio_coding/neteq/normal.cc:32), the CNG transition branch else if (last_mode == NetEq::Mode::kRfc3389Cng) at :159 sets win_length = samples_per_ms_ at :179 and then clamps it only against the scratch buffer with if (win_length > kCngLength) at :181-184 — it never clamps against the actual output size. The cross-fade loop at :186-191 writes (*output)[0][i] for i up to win_length - 1 (up to 47 at 48 kHz). Since output was filled from the decoded packet via PushBackInterleaved at :49, a small input packet leaves (*output)[0] shorter than win_length, so the loop writes out of bounds — a heap out-of-bounds write. (The report’s WrapIndex/DoRfc3389Cng-state theory is not what the code shows; the shown defect is a window length bounded by kCngLength rather than by the output vector — code wins.)

The fix adds a file-local Crossfade(from, to, win_length) that clamps the loop count to std::min({win_length, from.Size(), to.Size()}) and returns early when that is zero, then rewrites the CNG branch to copy cng_output into an AudioVector temp_vector(kCngLength) and call Crossfade(temp_vector, (*output)[0], samples_per_ms_); the Expand branch (which already clamped to output->Size() at pre-fix :144-147) is likewise routed through Crossfade.

The restored invariant: the cross-fade never writes more samples than the destination channel actually holds.

Key insight
The CNG cross-fade loop length was clamped only to the fixed kCngLength, not to the real output vector size, so a short packet overran the output; the new Crossfade helper clamps to std::min(win_length, from.Size(), to.Size()).

Attack Path

  1. Negotiate CNG + audio A peer negotiates a WebRTC audio stream that uses Comfort Noise so a decode can leave last_mode == NetEq::Mode::kRfc3389Cng. (inferred: the negotiation/trigger is from the report, not the shown source.)
  2. Deliver a short packet After the CNG frame, a very small normal-speech packet is delivered so (*output)[0] holds fewer samples than samples_per_ms_.
  3. Overrun on cross-fade Normal::Process enters the CNG branch and its loop at :186-191 writes (*output)[0][i] past the end of the undersized output channel, an out-of-bounds write on the renderer heap.

Impact Assessment

A remote peer can cause a heap out-of-bounds write in NetEq inside the sandboxed renderer process by following a CNG frame with an undersized packet. The advisory rates this high severity with no in-the-wild exploitation; escalation to memory corruption or code execution is (inferred).

Changed Functions

FunctionChangeNotes
if
modules/audio_coding/neteq/normal.cc
modified
for
modules/audio_coding/neteq/normal.cc
modified
statistics_
modules/audio_coding/neteq/normal.h
modified
TEST
modules/audio_coding/neteq/normal_unittest.cc
modified

Files Changed

  • modules/audio_coding/neteq/normal.cc
  • modules/audio_coding/neteq/normal.h
  • modules/audio_coding/neteq/normal_unittest.cc

Audit Directions

  • Clamp to the real buffer
    Any loop writing into a container must bound its index by that container’s actual Size(), not by a nominal/expected length constant.
  • Debug-only size asserts
    Treat RTC_DCHECK/DCHECK size or channel assertions as absent in production and add real clamps on attacker-influenced lengths.
  • Mode-transition paths
    Review DSP code that cross-fades between operating modes (Expand, CNG, PLC) for window lengths derived from sample rate rather than from the current frame size.
From ff1be2e8524fe63069fc2406d5e3f3ddccafee1a Mon Sep 17 00:00:00 2001
From: Jakob Ivarsson <jakobi@webrtc.org>
Date: Thu, 16 Apr 2026 10:35:57 +0000
Subject: [PATCH] Check vector sizes when crossfading from CNG/expand to normal.

This fixes a potential out-of-bounds write.

Bug: chromium:502661101
Change-Id: I6f03b522643d7a55040d7f5403f342b32d47f0c7
Reviewed-on: https://webrtc-review.googlesource.com/c/src/+/464500
Commit-Queue: Jakob Ivarsson‎ <jakobi@webrtc.org>
Reviewed-by: Henrik Lundin <henrik.lundin@webrtc.org>
Cr-Commit-Position: refs/heads/main@{#47447}
---

diff --git a/modules/audio_coding/neteq/normal.cc b/modules/audio_coding/neteq/normal.cc
index 117c480..be727fb 100644
--- a/modules/audio_coding/neteq/normal.cc
+++ b/modules/audio_coding/neteq/normal.cc
@@ -22,12 +22,32 @@
 #include "common_audio/signal_processing/include/spl_inl.h"
 #include "modules/audio_coding/codecs/cng/webrtc_cng.h"
 #include "modules/audio_coding/neteq/audio_multi_vector.h"
+#include "modules/audio_coding/neteq/audio_vector.h"
 #include "modules/audio_coding/neteq/background_noise.h"
 #include "modules/audio_coding/neteq/decoder_database.h"
 #include "modules/audio_coding/neteq/expand.h"
 #include "rtc_base/checks.h"
 
 namespace webrtc {
+namespace {
+
+void Crossfade(const AudioVector& from, AudioVector& to, size_t win_length) {
+  const size_t win_length_clamped =
+      std::min({win_length, from.Size(), to.Size()});
+  if (win_length_clamped == 0) {
+    return;
+  }
+  int16_t win_slope_Q14 = (1 << 14) / static_cast<int16_t>(win_length_clamped);
+  int16_t win_up_Q14 = 0;
+  for (size_t i = 0; i < win_length_clamped; i++) {
+    win_up_Q14 += win_slope_Q14;
+    to[i] =
+        (win_up_Q14 * to[i] + ((1 << 14) - win_up_Q14) * from[i] + (1 << 13)) >>
+        14;
+  }
+}
+
+}  // namespace
 
 int Normal::Process(const int16_t* input,
                     size_t length,
@@ -138,23 +158,7 @@
 
       // Interpolate the expanded data into the new vector.
       // (NB/WB/SWB32/SWB48 8/16/32/48 samples.)
-      size_t win_length = samples_per_ms_;
-      int16_t win_slope_Q14 = default_win_slope_Q14_;
-      RTC_DCHECK_LT(channel_ix, output->Channels());
-      if (win_length > output->Size()) {
-        win_length = output->Size();
-        win_slope_Q14 = (1 << 14) / static_cast<int16_t>(win_length);
-      }
-      int16_t win_up_Q14 = 0;
-      for (size_t i = 0; i < win_length; i++) {
-        win_up_Q14 += win_slope_Q14;
-        (*output)[channel_ix][i] =
-            (win_up_Q14 * (*output)[channel_ix][i] +
-             ((1 << 14) - win_up_Q14) * expanded[channel_ix][i] + (1 << 13)) >>
-            14;
-      }
-      RTC_DCHECK_GT(win_up_Q14,
-                    (1 << 14) - 32);  // Worst case rouding is a length of 34
+      Crossfade(expanded[channel_ix], (*output)[channel_ix], samples_per_ms_);
     }
   } else if (last_mode == NetEq::Mode::kRfc3389Cng) {
     RTC_DCHECK_EQ(output->Channels(), 1);  // Not adapted for multi-channel yet.
@@ -176,22 +180,9 @@
     }
     // Interpolate the CNG into the new vector.
     // (NB/WB/SWB32/SWB48 8/16/32/48 samples.)
-    size_t win_length = samples_per_ms_;
-    int16_t win_slope_Q14 = default_win_slope_Q14_;
-    if (win_length > kCngLength) {
-      win_length = kCngLength;
-      win_slope_Q14 = (1 << 14) / static_cast<int16_t>(win_length);
-    }
-    int16_t win_up_Q14 = 0;
-    for (size_t i = 0; i < win_length; i++) {
-      win_up_Q14 += win_slope_Q14;
-      (*output)[0][i] =
-          (win_up_Q14 * (*output)[0][i] +
-           ((1 << 14) - win_up_Q14) * cng_output[i] + (1 << 13)) >>
-          14;
-    }
-    RTC_DCHECK_GT(win_up_Q14,
-                  (1 << 14) - 32);  // Worst case rouding is a length of 34
+    AudioVector temp_vector(kCngLength);
+    temp_vector.OverwriteAt(cng_output, kCngLength, 0);
+    Crossfade(temp_vector, (*output)[0], samples_per_ms_);
   }
 
   return static_cast<int>(length);
diff --git a/modules/audio_coding/neteq/normal.h b/modules/audio_coding/neteq/normal.h
index e6c9187..d0dfc2f 100644
--- a/modules/audio_coding/neteq/normal.h
+++ b/modules/audio_coding/neteq/normal.h
@@ -17,7 +17,6 @@
 #include "api/neteq/neteq.h"
 #include "modules/audio_coding/neteq/statistics_calculator.h"
 #include "rtc_base/checks.h"
-#include "rtc_base/numerics/safe_conversions.h"
 
 namespace webrtc {
 
@@ -42,8 +41,6 @@
         background_noise_(background_noise),
         expand_(expand),
         samples_per_ms_(CheckedDivExact(fs_hz_, 1000)),
-        default_win_slope_Q14_(
-            dchecked_cast<uint16_t>((1 << 14) / samples_per_ms_)),
         statistics_(statistics) {}
 
   virtual ~Normal() {}
@@ -68,7 +65,6 @@
   const BackgroundNoise& background_noise_;
   Expand* expand_;
   const size_t samples_per_ms_;
-  const int16_t default_win_slope_Q14_;
   StatisticsCalculator* const statistics_;
 };
 
diff --git a/modules/audio_coding/neteq/normal_unittest.cc b/modules/audio_coding/neteq/normal_unittest.cc
index 272869d..c29a2c3 100644
--- a/modules/audio_coding/neteq/normal_unittest.cc
+++ b/modules/audio_coding/neteq/normal_unittest.cc
@@ -14,6 +14,7 @@
 
 #include <cstddef>
 #include <cstdint>
+#include <vector>
 
 #include "api/neteq/neteq.h"
 #include "api/neteq/tick_timer.h"
@@ -147,6 +148,20 @@
   EXPECT_CALL(expand, Die());  // Called when `expand` goes out of scope.
 }
 
+TEST(Normal, LastModeRfc3389CngSmallInput) {
+  constexpr size_t kChannels = 1;
+  constexpr size_t kInputFrames = 10;
+  MockDecoderDatabase db;
+  Normal normal(/*fs_hz=*/48000, /*decoder_database=*/&db,
+                /*background_noise=*/BackgroundNoise(kChannels),
+                /*expand=*/nullptr, /*statistics=*/nullptr);
+  AudioMultiVector output(kChannels);
+  std::vector<int16_t> input(kChannels * kInputFrames, 0);
+  EXPECT_EQ(normal.Process(input.data(), input.size(), NetEq::Mode::kRfc3389Cng,
+                           &output),
+            static_cast<int>(input.size()));
+}
+
 // TODO(hlundin): Write more tests.
 
 }  // namespace webrtc
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/modules/audio_coding/neteq/normal_unittest.cc b/modules/audio_coding/neteq/normal_unittest.cc
index 272869d..c29a2c3 100644
--- a/modules/audio_coding/neteq/normal_unittest.cc
+++ b/modules/audio_coding/neteq/normal_unittest.cc
@@ -14,6 +14,7 @@
 
 #include <cstddef>
 #include <cstdint>
+#include <vector>
 
 #include "api/neteq/neteq.h"
 #include "api/neteq/tick_timer.h"
@@ -147,6 +148,20 @@
   EXPECT_CALL(expand, Die());  // Called when `expand` goes out of scope.
 }
 
+TEST(Normal, LastModeRfc3389CngSmallInput) {
+  constexpr size_t kChannels = 1;
+  constexpr size_t kInputFrames = 10;
+  MockDecoderDatabase db;
+  Normal normal(/*fs_hz=*/48000, /*decoder_database=*/&db,
+                /*background_noise=*/BackgroundNoise(kChannels),
+                /*expand=*/nullptr, /*statistics=*/nullptr);
+  AudioMultiVector output(kChannels);
+  std::vector<int16_t> input(kChannels * kInputFrames, 0);
+  EXPECT_EQ(normal.Process(input.data(), input.size(), NetEq::Mode::kRfc3389Cng,
+                           &output),
+            static_cast<int>(input.size()));
+}
+
 // TODO(hlundin): Write more tests.
 
 }  // namespace webrtc
Loading diff…

Original Bug Report

reported by rj...@google.com

Potential heap-buffer-overflow in WebRTC NetEq Normal::Process via multi-channel CNG

Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the Chrome Security team. Please see go/chrome-ai-generated-security-bugs-faq for more information.

Overview: A potential heap buffer overflow occurs in WebRTC’s NetEq module during a transition from Comfort Noise (CNG) to normal speech on multi-channel streams. A logical flaw in index wrapping combined with unhandled CNG errors leads to out-of-bounds access, potentially allowing RCE in the renderer process.

Affected files:

  • third_party/webrtc/modules/audio_coding/neteq/normal.cc
  • third_party/webrtc/modules/audio_coding/neteq/neteq_impl.cc
  • third_party/webrtc/modules/audio_coding/neteq/audio_vector.h

Estimated timestamp from git blame: 2022-01-24

Technical Analysis

A potential heap-buffer-overflow (read-modify-write) exists in webrtc::Normal::Process due to a failure to correctly manage buffer sizes and index wrapping when handling multi-channel audio streams during a transition from Comfort Noise (CNG) back to normal speech.

The Vulnerability

The issue is triggered when a WebRTC session alternates between multi-channel L16 audio and CNG. The steps below outline a potential path an attacker could use to reach the vulnerability:

  1. Initialization: An attacker establishes a WebRTC connection negotiating a multi-channel L16 audio stream (e.g., 48 kHz, 8 channels) and CNG. When the first packet arrives, NetEqImpl::SetSampleRateAndChannels allocates an algorithm_buffer_ containing an AudioVector for each channel. Each AudioVector is initialized with a default capacity of 11 elements (int16_t).
  2. State Desynchronization: The attacker sends a CNG SID packet. NetEqImpl::DoRfc3389Cng calls comfort_noise_->Generate, which logs an error and returns kMultiChannelNotSupported because CNG is only supported for mono streams. However, DoRfc3389Cng unconditionally updates the last_mode_ to kRfc3389Cng and ignores the kMultiChannelNotSupported error. The buffer remains empty with a capacity of 11.
  3. Transition to Normal Speech: The attacker sends a very small multi-channel L16 packet (e.g., 10 samples per channel). NetEqImpl::DoNormal is called. The 10 samples are pushed into the algorithm_buffer_, and since 10 fits within the default capacity of 11, no reallocation occurs.
  4. Out-of-Bounds Access: Normal::Process executes a cross-fade loop to transition from CNG to the new speech frame because last_mode_ == kRfc3389Cng:
    // third_party/webrtc/modules/audio_coding/neteq/normal.cc
    for (size_t i = 0; i < win_length; i++) {
      win_up_Q14 += win_slope_Q14;
      (*output)[0][i] = (win_up_Q14 * (*output)[0][i] + ... ) >> 14;
    }
    
    At 48 kHz, win_length is 48. The code accesses the array up to i = 47.
  5. Insufficient Wrapping: The AudioVector indexing logic uses an inline WrapIndex method which performs a single subtraction instead of a modulo operation:
    // third_party/webrtc/modules/audio_coding/neteq/audio_vector.h
    inline size_t WrapIndex(size_t index, size_t begin_index, size_t capacity) {
      size_t ix = index + begin_index;
      if (ix >= capacity) {
        ix -= capacity;
      }
      return ix;
    }
    
    For i = 47 and capacity = 11, ix becomes 36, which is significantly out-of-bounds for the 11-element array. In release builds where RTC_DCHECK_EQ(output->Channels(), 1) is compiled out, this leads to an out-of-bounds read-modify-write.

Impact

The overflow occurs in small PartitionAlloc buckets (e.g., 32-byte bucket). Due to how AudioMultiVector allocates its channels, the overflow is likely to corrupt adjacent AudioVector structures or their array_ pointers. By overwriting an AudioVector pointer, an attacker could achieve an arbitrary read/write primitive during subsequent audio processing, potentially leading to arbitrary remote code execution within the sandboxed renderer process.

(Note: These are potential exploit steps based on static analysis; our tooling agent does not yet have the ability to run code to produce a working proof of concept.)

Suggested Fix

  1. State Consistency: In NetEqImpl::DoRfc3389Cng, handle ComfortNoise::kMultiChannelNotSupported properly and prevent updating last_mode_ to kRfc3389Cng if CNG generation fails.
  2. Index Wrapping: Update AudioVector::WrapIndex to use modulo (ix %= capacity) instead of a single subtraction, or enforce tighter checks if indices are never expected to exceed 2 * capacity.

Evaluated with Chrome root at commit: c0eb5541aebfa4ea08806eaf6e94bcc69f87ab2f


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. And please feel free to reach out to me directly if you have concerns or feedback on the project.

View on issue tracker