CVE-2026-5864
Overview
Background
- `HandleNaNValues`
- a helper in the WebAudio param pipeline that scans a
base::span<float>and replaces anyNaNsample with a default value, using SIMD to process four floats at a time. - `UNSAFE_BUFFERS`
- a Chromium macro that wraps raw pointer arithmetic to opt a region out of hardened
-Wunsafe-buffer-usagechecking, signalling the author has manually proven bounds. - `_mm_loadu_ps` / `_mm_storeu_ps`
- x86 SSE intrinsics that read/write 16 bytes (four
floats) starting at a pointer;vld1q_f32/vst1q_f32are the NEON equivalents. - Render quantum sizing
- audio buffers are usually a multiple of four frames, but
values.size()is not guaranteed to be, which is the edge case this bug depends on.
Root Cause Analysis
The SIMD loop in HandleNaNValues was gated by if (values.size() >= 4) and then iterated with the condition for (k = 0; k < values.size(); k += 4). Because k advances in steps of four while the bound was the full values.size(), the final iteration ran whenever a multiple of four was still strictly less than the size — i.e. for any input whose length is not a multiple of four, the last pass read and wrote four floats starting past the valid tail. The _mm_loadu_ps/_mm_storeu_ps (and NEON vld1q_f32/vst1q_f32) calls, wrapped in UNSAFE_BUFFERS(values.data() + k), therefore touched up to three floats beyond the end of the heap buffer.
The fix computes truncated_size = values.size() & ~3 and loops k < truncated_size, so the SIMD body only ever processes complete four-element groups and the remainder is left to the serial tail loop that consumes the carried k. Switching the pointer math to values.subspan(k, 4u).data() additionally makes the span enforce the four-element window instead of trusting a hand-written bound.
k < values.size() rather than on the largest multiple of four at or below the size, so a non-multiple-of-four length let the vectorized store run one group too far; truncating the bound with & ~3 removes the overshoot.Attack Path
- Reach the param path
From a renderer, script drives a WebAudio graph whose
AudioParamautomation feedsHandleNaNValueswith a sample buffer whose length is not a multiple of four. - Trigger the trailing SIMD pass
With
values.size()not divisible by four, the old conditionk < values.size()admits one extra iteration beyond the last full group. - Overrun the buffer
_mm_storeu_ps(UNSAFE_BUFFERS(values.data() + k), ...)writes 16 bytes starting inside the final partial group, placing up to three floats past the end of the heap allocation (heap buffer overflow). - Downstream corruption
Adjacent heap data is overwritten with sanitized/default-merged float values; the specific object corrupted and any further primitive are not shown by the diff
(inferred).
Impact Assessment
(inferred).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
Audit Directions
- SIMD loop boundsFlag any vectorized loop that increments an index by the vector width but compares against the full element count instead of a width-truncated count (
& ~(N-1)); the tail iteration is the classic overrun. - `UNSAFE_BUFFERS` with runtime lengthsAudit every
UNSAFE_BUFFERS(ptr + k)whose safety argument rests on a loop condition, and confirm the condition actually boundsk + width, not justk. - Span conversion gapsLook for remaining raw-pointer SIMD/DSP code in
modules/webaudioand other media paths that still usesdata() + offsetrather thansubspan(offset, width).
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