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 <kmaheshb@google.com>
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 <mjwilson@chromium.org>
Reviewed-by: Hongchan Choi <hongchan@chromium.org>
Commit-Queue: Mahesh Kannan <kmaheshb@google.com>
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": [
+ "mjwilson@chromium.org",
+ "web-audio-team@google.com"
+ ],
+ "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": [
+ "mjwilson@chromium.org",
+ "web-audio-team@google.com"
+ ],
+ "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