Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read and write in WebAudio
DescriptionOut of bounds read and write in WebAudio
ComponentWebAudio
Bug ClassOOB
Tracker490246422
Fix commitd18fd084c461 (chromium/src) +30/-44
CISA KEVNot listed
CreditedJihyeon Jeong (Compsec Lab, Seoul National University / Research Intern)
Disclosed2026-03-18

Changed Functions

FunctionChangeNotes
for
third_party/blink/renderer/modules/webaudio/audio_param_handler.cc
modified

Files Changed

  • third_party/blink/renderer/modules/webaudio/audio_param_handler.cc
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;
Loading diff…

Original Bug Report

reported by [email protected]

HandleNaNValues SIMD OOB read+write via non-aligned renderSizeHint in WebAudio AudioParam

Steps to reproduce the problem

  1. Build ASan Chrome for Linux (dcheck_always_on=false)
  2. Save the attached PoC as poc.html
  3. 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:

  1. OfflineAudioContext({renderSizeHint: 5}) – creates context with 5-frame render quantum
  2. createGain() – GainHandler allocates sample_accurate_gain_values_ AudioFloatArray of 5 floats (20 bytes)
  3. oscillator.connect(gainNode.gain) – creates audio-rate rendering connection to gain param
  4. During rendering, GainHandler::Process calls gain_->CalculateSampleAccurateValues(span of 5)
  5. Inside CalculateFinalValues, NumberOfRenderingConnections() > 0 triggers HandleNaNValues(span of 5, defaultValue)
  6. SSE loop: k=0 processes [0..3] OK, k=4 processes [4..7] – 12 bytes OOB read+write
  7. 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 \

View on issue tracker