CVE-2026-5864
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 4e9562ca7b4214fe359b5f3b885443e301bc5f74 Mon Sep 17 00:00:00 2001 From: Michael Wilson <[email protected]> Date: Tue, 10 Mar 2026 10:08:27 -0700 Subject: [PATCH] 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. Bug: 401184803 Bug: 490642831 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-Commit-Position: refs/heads/main@{#1597152} --- 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
OOB Read in AudioParamHandler::CalculateFinalValues
Summary
AudioParamHandler::CalculateFinalValues forwards a render-quantum-sized span into HandleNaNValues, but HandleNaNValues processes the span in 4-float SIMD chunks using k < values.size() instead of truncating to a multiple of 4. Once WebAudioConfigurableRenderQuantummakes odd render quanta such as133script-reachable, the last SIMD iteration reads and writes beyond theAudioParam` buffer and produces OOB.
> NOTE: this issue’s root cause is difference with the previous reported issues 487357842, 487357842
Details
The current WebAudio implementation allows script to choose a non-default render quantum through BaseAudioContext.renderQuantumSize and validates that value in AudioContext::Create. That value flows into audio-rate AudioParam processing, where AudioParamHandler::CalculateFinalValues calls `HandleNaNValues(values, DefaultValue()) on a span whose length is the active render quantum.
In HandleNaNValues, the issue is that the SIMD loop advances by four but is bounded only by k < values.size(). For a 133-frame quantum, the final iteration executes with k == 132, so the _mm_loadu_ps/_mm_storeu_pspair touches elements132..135even though only element132is inside the span. After that,values.subspan(k) is evaluated with k == 136, which is also beyond the span length:
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) {
__m128 v = _mm_loadu_ps(values.data() + k);
__m128 isnan = _mm_cmpunord_ps(v, v);
__m128 result = _mm_and_ps(isnan, defaults);
result = _mm_or_ps(_mm_andnot_ps(isnan, v), result);
_mm_storeu_ps(values.data() + k, result);
}
}
#endif
std::ranges::replace_if(values.subspan(k),
[](float value) { return std::isnan(value); },
default_value);
}
Bisection
This issue is introduced by the commit https://chromium-review.googlesource.com/c/chromium/src/+/6804197, which makes this issue became script-reachable when renderSizeHint is propagated into the CalculateFinalValues and HandleNaNValues.
Reproduction
Download the chromium from https://storage.googleapis.com/chromium-browser-asan/linux-release/asan-linux-release-1595156.zip
Run with
./chrome --no-sandbox --enable-experimental-web-platform-features poc.html
You would observe the OOB crash shown in asan.txt
Suggested Fix
Clamp HandleNaNValues to a SIMD-safe prefix before issuing 4-wide loads and stores. We may compute simd_end = values.size() & ~3u, iterate while k < simd_end, and keep the scalar tail in the `replace_if(values.subspan(k), …) path.
- https://chromium-review.googlesource.com/c/chromium/src/+/6804197
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/modules/webaudio/audio_context.cc;l=484
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/modules/webaudio/audio_param_handler.cc;l=339
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/modules/webaudio/audio_param_handler.cc;l=51
- https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/modules/webaudio/base_audio_context.idl;l=25
- https://storage.googleapis.com/chromium-browser-asan/linux-release/asan-linux-release-1595156.zip