Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in WebAudio
DescriptionInappropriate implementation in WebAudio
ComponentWebAudio
Bug ClassLogic Error
Tracker484088917
Fix commitae053f4b6115 (chromium/src) +135/-18
CISA KEVNot listed
CreditedDavi Antônio Cruz
Disclosed2026-03-03

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/FFTSetupDatum on macOS, PFFFT/FFTSetup elsewhere) and allocates buffers sized to fft_size.
Configurable render quantum
a WebAudio feature that lets the render size hint vary, which can drive fft_size values 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.

Key insight
The single mistake was enforcing size and setup invariants with 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

  1. Reach WebAudio A malicious page creates an AudioContext and a node that internally builds an FFTFrame (for example a WaveShaperNode or convolver).
  2. Drive an out-of-range size Using the configurable render quantum / render size hint, the page induces an fft_size that falls outside [MinFFTSize(), MaxFFTSize()] or is otherwise not a registered power-of-two setup key.
  3. Bypass the absent guard In a release build the DCHECK bounds checks are compiled out, so construction proceeds with the invalid size instead of aborting.
  4. 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.
  5. 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

An attacker gains the ability to force WebAudio into an inconsistent FFT state with a size the code never validated, leading to out-of-bounds access or a corrupted setup in the renderer process. The precondition is only that the victim loads attacker-controlled web content that can create WebAudio nodes and steer 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

FunctionChangeNotes
if
third_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.cc
  • third_party/blink/renderer/platform/audio/pffft/fft_frame_pffft.cc
  • third_party/blink/web_tests/TestExpectations
  • third_party/blink/web_tests/VirtualTestSuites

Audit Directions

  • `DCHECK` guarding attacker-influenced sizes
    Search WebAudio and other Blink media code for DCHECK/DCHECK_EQ/DCHECK_GE that validate buffer sizes, indices, or table membership derived from page-controllable inputs, since these vanish in release builds and should be CHECKs.
  • Missing explicit range enforcement
    Look 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 guards
    Audit 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.
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",
Loading diff…

Regression Test / PoC

shipped with the fix
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>
Loading diff…

Original Bug Report

reported by da...@gmail.com

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

View on issue tracker