CVE-2026-9119
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifmodules/audio_coding/neteq/normal.cc |
modified | |
formodules/audio_coding/neteq/normal.cc |
modified | |
statistics_modules/audio_coding/neteq/normal.h |
modified | |
TESTmodules/audio_coding/neteq/normal_unittest.cc |
modified |
Files Changed
modules/audio_coding/neteq/normal.ccmodules/audio_coding/neteq/normal.hmodules/audio_coding/neteq/normal_unittest.cc
Patch
From ff1be2e8524fe63069fc2406d5e3f3ddccafee1a Mon Sep 17 00:00:00 2001 From: Jakob Ivarsson <[email protected]> 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 <[email protected]> Reviewed-by: Henrik Lundin <[email protected]> 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
Regression Test / PoC
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
Original Bug Report
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.ccthird_party/webrtc/modules/audio_coding/neteq/neteq_impl.ccthird_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:
- 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::SetSampleRateAndChannelsallocates analgorithm_buffer_containing anAudioVectorfor each channel. EachAudioVectoris initialized with a default capacity of 11 elements (int16_t). - State Desynchronization: The attacker sends a CNG SID packet.
NetEqImpl::DoRfc3389Cngcallscomfort_noise_->Generate, which logs an error and returnskMultiChannelNotSupportedbecause CNG is only supported for mono streams. However,DoRfc3389Cngunconditionally updates thelast_mode_tokRfc3389Cngand ignores thekMultiChannelNotSupportederror. The buffer remains empty with a capacity of 11. - Transition to Normal Speech: The attacker sends a very small multi-channel L16 packet (e.g., 10 samples per channel).
NetEqImpl::DoNormalis called. The 10 samples are pushed into thealgorithm_buffer_, and since 10 fits within the default capacity of 11, no reallocation occurs. - Out-of-Bounds Access:
Normal::Processexecutes a cross-fade loop to transition from CNG to the new speech frame becauselast_mode_ == kRfc3389Cng:At 48 kHz,// 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; }win_lengthis 48. The code accesses the array up toi = 47. - Insufficient Wrapping: The
AudioVectorindexing logic uses an inlineWrapIndexmethod which performs a single subtraction instead of a modulo operation:For// 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; }i = 47andcapacity = 11,ixbecomes36, which is significantly out-of-bounds for the 11-element array. In release builds whereRTC_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
- State Consistency: In
NetEqImpl::DoRfc3389Cng, handleComfortNoise::kMultiChannelNotSupportedproperly and prevent updatinglast_mode_tokRfc3389Cngif CNG generation fails. - Index Wrapping: Update
AudioVector::WrapIndexto use modulo (ix %= capacity) instead of a single subtraction, or enforce tighter checks if indices are never expected to exceed2 * 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.