CVE-2026-3540
Overview
Background
- `DCHECK`
- a debug-only assertion macro that verifies a condition in debug builds but compiles to nothing in release builds, so its check is absent in shipped Chrome.
- `CHECK`
- an always-on assertion macro that verifies a condition in every build and safely aborts the process if it fails, converting an invalid state into a controlled crash.
- `FFTFrame`
- the WebAudio Blink class that wraps a platform FFT backend (Apple
vDSP/FFTSetupDatumon macOS,PFFFT/FFTSetupelsewhere) and allocates buffers sized tofft_size. - Configurable render quantum
- a WebAudio feature that lets the render size hint vary, which can drive
fft_sizevalues that fall outside the previously assumed[MinFFTSize(), MaxFFTSize()]range.
Root Cause Analysis
The vulnerable path is FFTFrame construction in fft_frame_mac.cc and fft_frame_pffft.cc, where the FFT size and derived setup state were validated only with DCHECK-family macros that are compiled out of release builds. The intended invariant is that fft_size is a power of two lying within [MinFFTSize(), MaxFFTSize()] and that a matching precomputed platform FFT setup exists in the setup table; in release builds none of these were enforced. With the configurable render quantum feature able to push fft_size outside the assumed bounds, an out-of-range or unregistered size could reach vDSP_create_fftsetup/pffft_new_setup and the buffer allocations (real_data_, imag_data_, complex_data_, pffft_work_), producing an inconsistent or missing setup and mismatched buffer sizes that are later indexed during FFT computation.
The fix promotes every one of these guards to CHECK/CHECK_GE/CHECK_LE/CHECK_EQ and adds explicit CHECK_GE(fft_size, MinFFTSize()) / CHECK_LE(fft_size, MaxFFTSize()) bounds at both constructors, so an out-of-range size now deterministically aborts instead of proceeding into memory operations with a size the code never actually validated. This works because it closes the release-build gap between the assumed invariant and the enforced invariant.
DCHECKs that vanish in release builds, leaving no runtime guard once a configurable render quantum could feed fft_size values outside the assumed range; the fix converts them to always-on CHECKs and adds explicit MinFFTSize()/MaxFFTSize() bounds so any invalid size is caught before it reaches allocation or FFT computation.Attack Path
- Reach WebAudio
A malicious page creates an
AudioContextand a node that internally builds anFFTFrame(for example aWaveShaperNodeor convolver). - Drive an out-of-range size
Using the configurable render quantum / render size hint, the page induces an
fft_sizethat falls outside[MinFFTSize(), MaxFFTSize()]or is otherwise not a registered power-of-two setup key. - Bypass the absent guard
In a release build the
DCHECKbounds checks are compiled out, so construction proceeds with the invalid size instead of aborting. - Enter inconsistent FFT state The platform setup is created or looked up for a size that does not match the allocated buffers, yielding a null or mismatched setup and buffers sized inconsistently with subsequent FFT indexing.
- Trigger memory unsafety Later FFT computation indexes buffers using the mismatched size, reaching out-of-bounds or otherwise undefined behavior in the renderer.
Impact Assessment
fft_size via the configurable render quantum. After the fix the same input is contained as a deterministic CHECK abort rather than a memory-safety violation.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifthird_party/blink/renderer/platform/audio/mac/fft_frame_mac.cc |
modified | |
imag_data_third_party/blink/renderer/platform/audio/mac/fft_frame_mac.cc |
modified | |
pffft_work_third_party/blink/renderer/platform/audio/pffft/fft_frame_pffft.cc |
modified |
Files Changed
third_party/blink/renderer/platform/audio/mac/fft_frame_mac.ccthird_party/blink/renderer/platform/audio/pffft/fft_frame_pffft.ccthird_party/blink/web_tests/TestExpectationsthird_party/blink/web_tests/VirtualTestSuites
Audit Directions
- `DCHECK` guarding attacker-influenced sizesSearch WebAudio and other Blink media code for
DCHECK/DCHECK_EQ/DCHECK_GEthat validate buffer sizes, indices, or table membership derived from page-controllable inputs, since these vanish in release builds and should beCHECKs. - Missing explicit range enforcementLook for allocation or setup constructors that assume
[MinFFTSize(), MaxFFTSize()](or analogous bounds) without a hard runtime bound check, especially where a configurable render quantum or size hint can widen the input domain. - Setup-table lookups without existence guardsAudit lazy hash-map/vector setup tables keyed by size for release-build-absent
Contains/non-null assertions before dereference, which can turn an unregistered key into a null-pointer or out-of-bounds use.
Patch
From ae053f4b611578b24d6cc85f5fd82fd7b18dc06d Mon Sep 17 00:00:00 2001 From: Mahesh Bharadwaj Kannan <[email protected]> Date: Mon, 23 Feb 2026 14:47:10 -0800 Subject: [PATCH] Refactor DCHECKs to CHECKs for better error handling This CL betters the enforcement of range checks of configurable render quantum to ensure optimal performance Bug: 484088917 Change-Id: I4ce4882f37a472e5c965881d6de233239180c194 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7586734 Reviewed-by: Michael Wilson <[email protected]> Reviewed-by: Hongchan Choi <[email protected]> Commit-Queue: Mahesh Kannan <[email protected]> Cr-Commit-Position: refs/heads/main@{#1588956} --- diff --git a/third_party/blink/renderer/platform/audio/mac/fft_frame_mac.cc b/third_party/blink/renderer/platform/audio/mac/fft_frame_mac.cc index d3b4c0e..ec08cb7b 100644 --- a/third_party/blink/renderer/platform/audio/mac/fft_frame_mac.cc +++ b/third_party/blink/renderer/platform/audio/mac/fft_frame_mac.cc @@ -46,11 +46,11 @@ FFTFrame::FFTSetupDatum::FFTSetupDatum(unsigned log2fft_size) { // We only need power-of-two sized FFTS, so FFT_RADIX2. setup_ = vDSP_create_fftsetup(log2fft_size, FFT_RADIX2); - DCHECK(setup_); + CHECK(setup_); } FFTFrame::FFTSetupDatum::~FFTSetupDatum() { - DCHECK(setup_); + CHECK(setup_); vDSP_destroy_fftsetup(setup_); } @@ -63,7 +63,7 @@ if (first_call) { // Make sure we construct the fft_setups vector below on the main thread. // Once constructed, we can access it from any thread. - DCHECK(IsMainThread()); + CHECK(IsMainThread()); first_call = false; } @@ -82,7 +82,7 @@ // Make sure allocation of a new setup only occurs on the main thread so we // don't have a race condition with multiple threads trying to write to the // same element of the vector. - DCHECK(IsMainThread()); + CHECK(IsMainThread()); setup[log2fft_size] = std::make_unique<FFTSetupDatum>(log2fft_size); } @@ -94,8 +94,10 @@ log2fft_size_(static_cast<unsigned>(log2(fft_size))), real_data_(fft_size), imag_data_(fft_size) { + CHECK_GE(fft_size, MinFFTSize()); + CHECK_LE(fft_size, MaxFFTSize()); // We only allow power of two - DCHECK_EQ(1UL << log2fft_size_, fft_size_); + CHECK_EQ(1UL << log2fft_size_, fft_size_); // Initialize the PFFFT_Setup object here so that it will be ready when we // compute FFTs. diff --git a/third_party/blink/renderer/platform/audio/pffft/fft_frame_pffft.cc b/third_party/blink/renderer/platform/audio/pffft/fft_frame_pffft.cc index a39fed1..8295887 100644 --- a/third_party/blink/renderer/platform/audio/pffft/fft_frame_pffft.cc +++ b/third_party/blink/renderer/platform/audio/pffft/fft_frame_pffft.cc @@ -24,17 +24,17 @@ const unsigned kMinFFTPow2Size = 5; FFTFrame::FFTSetup::FFTSetup(unsigned fft_size) { - DCHECK_LE(fft_size, 1U << kMaxFFTPow2Size); - DCHECK_GE(fft_size, 1U << kMinFFTPow2Size); + CHECK_LE(fft_size, 1U << kMaxFFTPow2Size); + CHECK_GE(fft_size, 1U << kMinFFTPow2Size); // All FFTs we need are FFTs of real signals, and the inverse FFTs produce // real signals. Hence |PFFFT_REAL|. setup_ = pffft_new_setup(fft_size, PFFFT_REAL); - DCHECK(setup_); + CHECK(setup_); } FFTFrame::FFTSetup::~FFTSetup() { - DCHECK(setup_); + CHECK(setup_); pffft_destroy_setup(setup_); } @@ -56,14 +56,14 @@ // Make sure we construct the fft_setups vector below on the main thread. // Once constructed, we can access it from any thread. - DCHECK(IsMainThread()); + CHECK(IsMainThread()); first_call = false; base::AutoLock locker(setup_lock); // Initialize the hash map with all the possible keys (FFT sizes), with a // value of nullptr because we want to initialize the setup data lazily. The - // set of valid FFT sizes for PFFFT are of the form 2^k*3^m*5*n where k >= + // set of valid FFT sizes for PFFFT are of the form 2^k*3^m*5^n where k >= // 5, m >= 0, n >= 0. We only go up to a max size of 32768, because we need // at least an FFT size of 32768 for the convolver node. @@ -82,7 +82,7 @@ } // There should be 87 entries when we're done. - DCHECK_EQ(fft_setups.size(), 87u); + CHECK_EQ(fft_setups.size(), 87u); } return fft_setups; @@ -91,7 +91,7 @@ void FFTFrame::InitializeFFTSetupForSize(wtf_size_t fft_size) { auto& setup = FFTSetups(); - DCHECK(setup.Contains(fft_size)); + CHECK(setup.Contains(fft_size)); if (setup.find(fft_size)->value == nullptr) { DEFINE_STATIC_LOCAL(base::Lock, setup_lock, ()); @@ -99,7 +99,7 @@ // Make sure allocation of a new setup only occurs on the main thread so we // don't have a race condition with multiple threads trying to write to the // same element of the vector. - DCHECK(IsMainThread()); + CHECK(IsMainThread()); auto fft_data = std::make_unique<FFTSetup>(fft_size); base::AutoLock locker(setup_lock); @@ -110,8 +110,8 @@ PFFFT_Setup* FFTFrame::FFTSetupForSize(wtf_size_t fft_size) { auto& setup = FFTSetups(); - DCHECK(setup.Contains(fft_size)); - DCHECK(setup.find(fft_size)->value); + CHECK(setup.Contains(fft_size)); + CHECK(setup.find(fft_size)->value); return setup.find(fft_size)->value->GetSetup(); } @@ -123,6 +123,8 @@ imag_data_(fft_size / 2), complex_data_(fft_size), pffft_work_(fft_size) { + CHECK_GE(fft_size, MinFFTSize()); + CHECK_LE(fft_size, MaxFFTSize()); // Initialize the PFFFT_Setup object here so that it will be ready when we // compute FFTs. @@ -172,8 +174,8 @@ unsigned hrtf_fft_size = static_cast<unsigned>(HRTFPanner::FftSizeForSampleRate(sample_rate)); - DCHECK_GT(hrtf_fft_size, 1U << kMinFFTPow2Size); - DCHECK_LE(hrtf_fft_size, 1U << kMaxFFTPow2Size); + CHECK_GT(hrtf_fft_size, 1U << kMinFFTPow2Size); + CHECK_LE(hrtf_fft_size, 1U << kMaxFFTPow2Size); InitializeFFTSetupForSize(hrtf_fft_size); InitializeFFTSetupForSize(hrtf_fft_size / 2); diff --git a/third_party/blink/web_tests/TestExpectations b/third_party/blink/web_tests/TestExpectations index 81a3bb2..2ed4603f 100644 --- a/third_party/blink/web_tests/TestExpectations +++ b/third_party/blink/web_tests/TestExpectations @@ -9478,6 +9478,9 @@ # WebAudio AudioWorklet test failure in MSAN crbug.com/446563923 [ Linux ] external/wpt/webaudio/the-audio-api/the-audioworklet-interface/simple-input-output.https.html [ Crash Pass Timeout ] +# WebAudio configurable render quantum failing test +crbug.com/486238126 webaudio/WaveShaper/waveshaper-rendersizehint.html [ Crash ] + # Gardener 2025-10-10 crbug.com/450592015 [ Mac12-arm64 ] external/wpt/webxr/idlharness.https.window.html [ Failure ] crbug.com/450592015 [ Mac13-arm64 ] external/wpt/webxr/idlharness.https.window.html [ Failure ] diff --git a/third_party/blink/web_tests/VirtualTestSuites b/third_party/blink/web_tests/VirtualTestSuites index 6e41932e..4375b70 100644 --- a/third_party/blink/web_tests/VirtualTestSuites +++ b/third_party/blink/web_tests/VirtualTestSuites @@ -6226,5 +6226,25 @@ "--enable-features=DurableMessages:max_global_buffer_size/20000" ], "expires": "Dec 31, 2026" + }, + { + "prefix": "web-audio-configurable-render-quantum", + "owners": [ + "[email protected]", + "[email protected]" + ], + "platforms": [ + "Linux", + "Mac", + "Win",
Regression Test / PoC
diff --git a/third_party/blink/web_tests/TestExpectations b/third_party/blink/web_tests/TestExpectations
index 81a3bb2..2ed4603f 100644
--- a/third_party/blink/web_tests/TestExpectations
+++ b/third_party/blink/web_tests/TestExpectations
@@ -9478,6 +9478,9 @@
# WebAudio AudioWorklet test failure in MSAN
crbug.com/446563923 [ Linux ] external/wpt/webaudio/the-audio-api/the-audioworklet-interface/simple-input-output.https.html [ Crash Pass Timeout ]
+# WebAudio configurable render quantum failing test
+crbug.com/486238126 webaudio/WaveShaper/waveshaper-rendersizehint.html [ Crash ]
+
# Gardener 2025-10-10
crbug.com/450592015 [ Mac12-arm64 ] external/wpt/webxr/idlharness.https.window.html [ Failure ]
crbug.com/450592015 [ Mac13-arm64 ] external/wpt/webxr/idlharness.https.window.html [ Failure ]
diff --git a/third_party/blink/web_tests/VirtualTestSuites b/third_party/blink/web_tests/VirtualTestSuites
index 6e41932e..4375b70 100644
--- a/third_party/blink/web_tests/VirtualTestSuites
+++ b/third_party/blink/web_tests/VirtualTestSuites
@@ -6226,5 +6226,25 @@
"--enable-features=DurableMessages:max_global_buffer_size/20000"
],
"expires": "Dec 31, 2026"
+ },
+ {
+ "prefix": "web-audio-configurable-render-quantum",
+ "owners": [
+ "[email protected]",
+ "[email protected]"
+ ],
+ "platforms": [
+ "Linux",
+ "Mac",
+ "Win",
+ "Android"
+ ],
+ "bases": [
+ "webaudio/WaveShaper/waveshaper-rendersizehint.html"
+ ],
+ "args": [
+ "--enable-features=WebAudioConfigurableRenderQuantum"
+ ],
+ "expires": "never"
}
]
diff --git a/third_party/blink/web_tests/virtual/web-audio-configurable-render-quantum/README.md b/third_party/blink/web_tests/virtual/web-audio-configurable-render-quantum/README.md
new file mode 100644
index 0000000..05ef1820
--- /dev/null
+++ b/third_party/blink/web_tests/virtual/web-audio-configurable-render-quantum/README.md
@@ -0,0 +1,7 @@
+# Configurable Render Quantum Size Tests
+
+This virtual test suite runs with
+"--enable-features=WebAudioConfigurableRenderQuantum" set. This allows testing
+that running at different render quantum sizes produces the same output.
+TODO(crbug.com/40637820) Remove this test suite once the feature is shipped to
+stable.
\ No newline at end of file
diff --git a/third_party/blink/web_tests/webaudio/WaveShaper/waveshaper-rendersizehint.html b/third_party/blink/web_tests/webaudio/WaveShaper/waveshaper-rendersizehint.html
new file mode 100644
index 0000000..ff93b29
--- /dev/null
+++ b/third_party/blink/web_tests/webaudio/WaveShaper/waveshaper-rendersizehint.html
@@ -0,0 +1,83 @@
+<!DOCTYPE html>
+<title>WaveShaper: Configurable Render Quantum Size Support</title>
+<script src="../../resources/testharness.js"></script>
+<script src="../../resources/testharnessreport.js"></script>
+<script>
+test(() => {
+ const powerOfTwoHint = 4096;
+ const context = new AudioContext({
+ renderSizeHint: powerOfTwoHint,
+ latencyHint: 'playback',
+ });
+ assert_true(
+ context instanceof AudioContext,
+ 'AudioContext with power-of-two renderSizeHint should be created.',
+ );
+
+ const shaper = context.createWaveShaper();
+ shaper.connect(context.destination);
+ shaper.oversample = '4x';
+},
+'Constructing AudioContext with a power-of-two renderSizeHint (4096) ' +
+'and using WaveShaper oversampling.');
+
+test(() => {
+ const nonPowerOfTwoHint = 57456;
+ const context = new AudioContext({
+ renderSizeHint: nonPowerOfTwoHint,
+ latencyHint: 'playback',
+ });
+ assert_true(
+ context instanceof AudioContext,
+ 'AudioContext with non-power-of-two renderSizeHint should be created.',
+ );
+
+ const shaper = context.createWaveShaper();
+ shaper.connect(context.destination);
+ shaper.oversample = '4x';
+},
+'Constructing AudioContext with a non-power-of-two renderSizeHint (57456) ' +
+'and using WaveShaper oversampling.');
+
+test(() => {
+ const minSampleRate = 3000;
+ const minRenderSizeHint = 1;
+ const context = new AudioContext({
+ sampleRate: minSampleRate,
+ renderSizeHint: minRenderSizeHint,
+ latencyHint: 'playback',
+ });
+ assert_true(
+ context instanceof AudioContext,
+ 'AudioContext with minimum sampleRate and ' +
+ 'renderSizeHint should be created.',
+ );
+
+ const shaper = context.createWaveShaper();
+ shaper.connect(context.destination);
+ shaper.oversample = '4x';
+},
+'Constructing AudioContext with minimum sampleRate ' +
+'renderSizeHint (1) and using WaveShaper oversampling.');
+
+test(() => {
+ const maxSampleRate = 768000;
+ const maxRenderSizeHint = 768000 * 6;
+ const context = new AudioContext({
+ sampleRate: maxSampleRate,
+ renderSizeHint: maxRenderSizeHint,
+ latencyHint: 'playback',
+ });
+ assert_true(
+ context instanceof AudioContext,
+ 'AudioContext with maximum sampleRate and ' +
+ 'renderSizeHint should be created.',
+ );
+
+ const shaper = context.createWaveShaper();
+ shaper.connect(context.destination);
+ shaper.oversample = '4x';
+},
+'Constructing AudioContext with maximum sampleRate and ' +
+'renderSizeHint and using WaveShaper oversampling.');
+</script>
Original Bug Report
Heap-buffer-overflow in blink::FFTFrame 146.0.7670.0
VULNERABILITY DETAILS
Heap-buffer-overflow in blink::FFTFrame via WaveShaperNode oversampling with non-power-of-2 renderSizeHint
VERSION Chrome Version:146.0.7670.0
Operating System: Linux, Debian
REPRODUCTION CASE
<!DOCTYPE html>
<!--
./chrome --enable-features=WebAudioConfigurableRenderQuantum
-->
<script>
try {
const ctx = new AudioContext({
renderSizeHint: 57456,
latencyHint: 'playback'
});
const shaper = ctx.createWaveShaper();
shaper.connect(ctx.destination);
shaper.oversample = '4x';
} catch (e) {
console.error(e);
}
</script>
Type of crash: renderer
Crash State: asan.txt (attachments), also llvm.txt generated by llvm-symbolizer --obj=/home/davi/chromium-asan/chrome 0x3b84dc3d 0x3ba46be4 0x3ba47adf 0x3ba3fb38 0x3ba3edbb 0x3ba3c651 0x3a5b6b58 0x166a18be 0x166a00ac 0x17836e1c 0x1783dae4 0x178317c6 0x1702fb49 0x17046d82 0x1b5c5f75 0x1b514829 0x1b5115db 0x1b51132a 0x16a1b716 0x16a1de13 0x16572898
CREDIT INFORMATION
Reporter credit: Davi Antônio Cruz