CVE-2026-4459
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forthird_party/blink/renderer/modules/webaudio/audio_param_handler.cc |
modified |
Files Changed
third_party/blink/renderer/modules/webaudio/audio_param_handler.cc
Patch
From d18fd084c4611b3897687aacb6f4af5368fcdf81 Mon Sep 17 00:00:00 2001 From: Michael Wilson <[email protected]> Date: Tue, 17 Mar 2026 12:47:03 -0700 Subject: [PATCH] [M146] Replace UNSAFE_BUFFERS in AudioParamHandler with safe operations This should cause no functional change. Also do a small optimization in HandleNaNValues for inputs that are larger than 4 by skipping the first if check, since we expect longer inputs in general. (cherry picked from commit 4e9562ca7b4214fe359b5f3b885443e301bc5f74) Bug: 401184803 Bug: 490642831 Bug: 490246422 Change-Id: I1044267b7e6370af4793fe973fe36224630ced95 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7649387 Reviewed-by: Hongchan Choi <[email protected]> Commit-Queue: Michael Wilson <[email protected]> Cr-Original-Commit-Position: refs/heads/main@{#1597152} Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7667367 Auto-Submit: Michael Wilson <[email protected]> Commit-Queue: Hongchan Choi <[email protected]> Cr-Commit-Position: refs/branch-heads/7680@{#2744} Cr-Branched-From: 76b7d80e5cda23fe6537eed26d68c92e995c7f39-refs/heads/main@{#1582197} --- diff --git a/third_party/blink/renderer/modules/webaudio/audio_param_handler.cc b/third_party/blink/renderer/modules/webaudio/audio_param_handler.cc index b3b95dd..88a8f39 100644 --- a/third_party/blink/renderer/modules/webaudio/audio_param_handler.cc +++ b/third_party/blink/renderer/modules/webaudio/audio_param_handler.cc @@ -51,40 +51,35 @@ void HandleNaNValues(base::span<float> values, float default_value) { unsigned k = 0; #if defined(ARCH_CPU_X86_FAMILY) - if (values.size() >= 4) { - __m128 defaults = _mm_set1_ps(default_value); - for (k = 0; k < values.size(); k += 4) { - // SAFETY: The for loop condition has been checked k < values.size(). - __m128 v = _mm_loadu_ps(UNSAFE_BUFFERS(values.data() + k)); - // cmpuord returns all 1's if v is NaN for each elmeent of v. - __m128 isnan = _mm_cmpunord_ps(v, v); - // Replace NaN parts with default. - __m128 result = _mm_and_ps(isnan, defaults); - // Merge in the parts that aren't NaN - result = _mm_or_ps(_mm_andnot_ps(isnan, v), result); - // SAFETY: The for loop condition has been checked k < values.size(). - _mm_storeu_ps(UNSAFE_BUFFERS(values.data() + k), result); - } + // Truncate to the next-lowest multiple of 4. + const size_t truncated_size = values.size() & ~3; + __m128 defaults = _mm_set1_ps(default_value); + for (k = 0; k < truncated_size; k += 4) { + __m128 v = _mm_loadu_ps(values.subspan(k, 4u).data()); + // cmpuord returns all 1's if v is NaN for each elmeent of v. + __m128 isnan = _mm_cmpunord_ps(v, v); + // Replace NaN parts with default. + __m128 result = _mm_and_ps(isnan, defaults); + // Merge in the parts that aren't NaN + result = _mm_or_ps(_mm_andnot_ps(isnan, v), result); + _mm_storeu_ps(values.subspan(k, 4u).data(), result); } #elif defined(CPU_ARM_NEON) - if (values.size() >= 4) { - uint32x4_t defaults = - reinterpret_cast<uint32x4_t>(vdupq_n_f32(default_value)); - for (k = 0; k < values.size(); k += 4) { - // SAFETY: The for loop condition has been checked k < values.size(). - float32x4_t v = vld1q_f32(UNSAFE_BUFFERS(values.data() + k)); - // Returns true (all ones) if v is not NaN - uint32x4_t is_not_nan = vceqq_f32(v, v); - // Get the parts that are not NaN - uint32x4_t result = - vandq_u32(is_not_nan, reinterpret_cast<uint32x4_t>(v)); - // Replace the parts that are NaN with the default and merge with previous - // result. (Note: vbic_u32(x, y) = x and not y) - result = vorrq_u32(result, vbicq_u32(defaults, is_not_nan)); - // SAFETY: The for loop condition has been checked k < values.size(). - vst1q_f32(UNSAFE_BUFFERS(values.data() + k), - reinterpret_cast<float32x4_t>(result)); - } + // Truncate to the next-lowest multiple of 4. + const size_t truncated_size = values.size() & ~3; + uint32x4_t defaults = + reinterpret_cast<uint32x4_t>(vdupq_n_f32(default_value)); + for (k = 0; k < truncated_size; k += 4) { + float32x4_t v = vld1q_f32(values.subspan(k, 4u).data()); + // Returns true (all ones) if v is not NaN + uint32x4_t is_not_nan = vceqq_f32(v, v); + // Get the parts that are not NaN + uint32x4_t result = vandq_u32(is_not_nan, reinterpret_cast<uint32x4_t>(v)); + // Replace the parts that are NaN with the default and merge with previous + // result. (Note: vbic_u32(x, y) = x and not y) + result = vorrq_u32(result, vbicq_u32(defaults, is_not_nan)); + vst1q_f32(values.subspan(k, 4u).data(), + reinterpret_cast<float32x4_t>(result)); } #endif @@ -1831,10 +1826,7 @@ // Process 4 loop steps. for (; write_index < fill_to_frame_trunc; write_index += 4) { - // SAFETY: DCHECK previously checked that `fill_to_frame_trunc < - // values.size()`. In the for loop, `write_index < fill_to_frame_trunc` so - // this is safe. - _mm_storeu_ps(UNSAFE_BUFFERS(values.data() + write_index), v_value); + _mm_storeu_ps(values.subspan(write_index, 4u).data(), v_value); v_value = _mm_add_ps(v_value, v_inc); } } @@ -2020,10 +2012,7 @@ v_value = _mm_set_ps1(value); v_result = _mm_add_ps(v_value, _mm_mul_ps(v_delta, v_c)); - // SAFETY: DCHECK previously checked that `fill_to_frame_trunc < - // values.size()`. In the for loop, `write_index < fill_to_frame_trunc` - // so this is safe. - _mm_storeu_ps(UNSAFE_BUFFERS(values.data() + write_index), v_result); + _mm_storeu_ps(values.subspan(write_index, 4u).data(), v_result); // Update value for next iteration. value += delta * c3; @@ -2184,10 +2173,7 @@ __m128 v_value = _mm_add_ps(v_c0, _mm_mul_ps(_mm_sub_ps(v_c1, v_c0), v_delta)); - // SAFETY: DCHECK previously checked that `fill_to_frame_trunc < - // values.size()`. In the for loop, `write_index < fill_to_frame_trunc` so - // this is safe. - _mm_storeu_ps(UNSAFE_BUFFERS(values.data() + write_index), v_value); + _mm_storeu_ps(values.subspan(write_index, 4u).data(), v_value); } // Pass along k to the serial loop. k = truncated_steps;
Original Bug Report
HandleNaNValues SIMD OOB read+write via non-aligned renderSizeHint in WebAudio AudioParam
Steps to reproduce the problem
- Build ASan Chrome for Linux (dcheck_always_on=false)
- Save the attached PoC as
poc.html - Run:
./chrome --enable-blink-features=WebAudioConfigurableRenderQuantum \ --no-sandbox poc.html
Problem Description
Root Cause
HandleNaNValues() in audio_param_handler.cc:51-94 has a hand-rolled SIMD loop that processes 4 floats at a time using SSE/NEON intrinsics:
void HandleNaNValues(base::span<float> values, float default_value) {
unsigned k = 0;
#if defined(ARCH_CPU_X86_FAMILY)
if (values.size() >= 4) {
for (k = 0; k < values.size(); k += 4) { // BUG: k+3 can exceed size()
__m128 v = _mm_loadu_ps(values.data() + k); // reads [k..k+3]
// ... NaN replacement logic ...
_mm_storeu_ps(values.data() + k, result); // writes [k..k+3]
}
}
#endif
std::ranges::replace_if(values.subspan(k), ...); // subspan(k) where k > size() => CHECK crash
}
The loop condition k < values.size() only guarantees k is within bounds, NOT k + 3. When values.size() % 4 != 0, the last iteration reads and writes up to 3 floats (12 bytes) past the end of the buffer. The “SAFETY” comments on lines 57 and 65 are incorrect – they claim the loop condition is sufficient, but _mm_loadu_ps accesses 4 consecutive floats.
After the loop, k exceeds values.size(), and values.subspan(k) triggers a hardened bounds CHECK crash in base::span.
How It Triggers
Any AudioParam with an audio-rate rendering connection (another node’s output connected to the param) will call HandleNaNValues() during CalculateFinalValues(). The values span size equals render_quantum_frames (from renderSizeHint). When this is not a multiple of 4 (e.g., 5, 6, 7, 9, 10, 11, …), the SIMD overflow occurs.
The GainNode PoC:
OfflineAudioContext({renderSizeHint: 5})– creates context with 5-frame render quantumcreateGain()– GainHandler allocatessample_accurate_gain_values_AudioFloatArray of 5 floats (20 bytes)oscillator.connect(gainNode.gain)– creates audio-rate rendering connection to gain param- During rendering, GainHandler::Process calls gain_->CalculateSampleAccurateValues(span of 5)
- Inside CalculateFinalValues, NumberOfRenderingConnections() > 0 triggers HandleNaNValues(span of 5, defaultValue)
- SSE loop: k=0 processes [0..3] OK, k=4 processes [4..7] – 12 bytes OOB read+write
- subspan(8) on 5-element span – CHECK(8 <= 5) – crash
Summary
HandleNaNValues SIMD OOB read+write via non-aligned renderSizeHint in WebAudio AudioParam
Custom Questions
Type of crash:
renderer
Crash state:
heap-buffer-overflow
Reporter credit:
Jihyeon Jeong (Compsec Lab, Seoul National University / Research Intern)
Additional Data
Category: Security
Chrome Channel: Not sure
Regression: N/A \