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 <mjwilson@chromium.org>
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 <hongchan@chromium.org>
Commit-Queue: Michael Wilson <mjwilson@chromium.org>
Cr-Original-Commit-Position: refs/heads/main@{#1597152}
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7667367
Auto-Submit: Michael Wilson <mjwilson@chromium.org>
Commit-Queue: Hongchan Choi <hongchan@chromium.org>
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 \