CVE-2026-4677
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forthird_party/blink/renderer/platform/audio/cpu/arm/delay_neon.cc |
modified |
Files Changed
third_party/blink/renderer/modules/webaudio/delay_handler.ccthird_party/blink/renderer/platform/audio/cpu/arm/delay_neon.ccthird_party/blink/renderer/platform/audio/cpu/x86/delay_sse2.cc
Patch
From b05c9128c8aa84f028bc336d899ac6c852622429 Mon Sep 17 00:00:00 2001 From: Michael Wilson <[email protected]> Date: Tue, 10 Mar 2026 16:19:05 -0700 Subject: [PATCH] Replace UNSAFE_TODO in Delay with safe operations This should cause no functional change. It also required updating call sites to use span instead of pointers. Bug: 401184803 Bug: 490533968 Change-Id: I13fd424ea4fa7cc679a5206ecb5ee7dd67c025e1 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7649841 Reviewed-by: Hongchan Choi <[email protected]> Commit-Queue: Michael Wilson <[email protected]> Cr-Commit-Position: refs/heads/main@{#1597393} --- diff --git a/third_party/blink/renderer/modules/webaudio/delay_handler.cc b/third_party/blink/renderer/modules/webaudio/delay_handler.cc index 3a2393a..6be84f1 100644 --- a/third_party/blink/renderer/modules/webaudio/delay_handler.cc +++ b/third_party/blink/renderer/modules/webaudio/delay_handler.cc @@ -73,16 +73,16 @@ CHECK(delay_time_->IsAudioRate()); delay_time_->CalculateSampleAccurateValues( kernels_[i]->DelayTimes().first(frames_to_process)); - kernels_[i]->ProcessARate(source_bus->Channel(i)->Data(), - destination_bus->Channel(i)->MutableData(), + kernels_[i]->ProcessARate(source_bus->Channel(i)->Span(), + destination_bus->Channel(i)->MutableSpan(), frames_to_process); } } else { for (unsigned i = 0; i < kernels_.size(); ++i) { CHECK(!delay_time_->IsAudioRate()); kernels_[i]->SetDelayTime(delay_time_->FinalValue()); - kernels_[i]->ProcessKRate(source_bus->Channel(i)->Data(), - destination_bus->Channel(i)->MutableData(), + kernels_[i]->ProcessKRate(source_bus->Channel(i)->Span(), + destination_bus->Channel(i)->MutableSpan(), frames_to_process); } } diff --git a/third_party/blink/renderer/platform/audio/cpu/arm/delay_neon.cc b/third_party/blink/renderer/platform/audio/cpu/arm/delay_neon.cc index 8d95504..7239918 100644 --- a/third_party/blink/renderer/platform/audio/cpu/arm/delay_neon.cc +++ b/third_party/blink/renderer/platform/audio/cpu/arm/delay_neon.cc @@ -5,6 +5,7 @@ #include <arm_neon.h> #include <algorithm> +#include <array> #include "base/compiler_specific.h" #include "build/build_config.h" @@ -51,16 +52,14 @@ reinterpret_cast<uint32x4_t>(v_buffer_length), cmp))); } -std::tuple<unsigned, int> Delay::ProcessARateVector( - float* destination, - uint32_t frames_to_process) const { - const int buffer_length = buffer_.size(); - const float* buffer = buffer_.Data(); +std::tuple<size_t, size_t> Delay::ProcessARateVector( + base::span<float> destination, + size_t frames_to_process) const { + const size_t buffer_length = buffer_.size(); const float sample_rate = sample_rate_; - const float* delay_times = delay_times_.Data(); - int w_index = write_index_; + size_t w_index = write_index_; const float32x4_t v_sample_rate = vdupq_n_f32(sample_rate); const float32x4_t v_all_zeros = vdupq_n_f32(0); @@ -74,25 +73,26 @@ const int32x4_t v_incr = vdupq_n_s32(4); // Temp arrays for storing the samples needed for interpolation - float sample1[4] __attribute((aligned(16))); - float sample2[4] __attribute((aligned(16))); + std::array<float, 4> sample1 __attribute((aligned(16))); + std::array<float, 4> sample2 __attribute((aligned(16))); // Temp array for holding the indices so we can access them // individually. - int read_index1[4] __attribute((aligned(16))); - int read_index2[4] __attribute((aligned(16))); + std::array<int, 4> read_index1 __attribute((aligned(16))); + std::array<int, 4> read_index2 __attribute((aligned(16))); // Initialize the write index vector, and wrap the values if needed. - int32x4_t v_write_index = {w_index + 0, w_index + 1, w_index + 2, - w_index + 3}; + int32x4_t v_write_index = { + static_cast<int32_t>(w_index + 0), static_cast<int32_t>(w_index + 1), + static_cast<int32_t>(w_index + 2), static_cast<int32_t>(w_index + 3)}; v_write_index = WrapIndexVector(v_write_index, v_buffer_length_int); int number_of_loops = frames_to_process / 4; - int k = 0; + size_t k = 0; for (int n = 0; n < number_of_loops; ++n, k += 4) { - const float32x4_t v_delay_time = - vmaxq_f32(UNSAFE_TODO(vld1q_f32(delay_times + k)), v_all_zeros); + const float32x4_t v_delay_time = vmaxq_f32( + vld1q_f32(delay_times_.as_span().subspan(k, 4u).data()), v_all_zeros); const float32x4_t v_desired_delay_frames = vmulq_f32(v_delay_time, v_sample_rate); @@ -115,16 +115,16 @@ // Save indices so we can access the components individually for // getting the aamples from the buffer. - vst1q_s32(read_index1, v_read_index1); - vst1q_s32(read_index2, v_read_index2); + vst1q_s32(read_index1.data(), v_read_index1); + vst1q_s32(read_index2.data(), v_read_index2); for (int m = 0; m < 4; ++m) { - UNSAFE_TODO(sample1[m]) = UNSAFE_TODO(buffer[read_index1[m])]; - UNSAFE_TODO(sample2[m]) = UNSAFE_TODO(buffer[read_index2[m])]; + sample1[m] = buffer_[read_index1[m]]; + sample2[m] = buffer_[read_index2[m]]; } - const float32x4_t v_sample1 = vld1q_f32(sample1); - const float32x4_t v_sample2 = vld1q_f32(sample2); + const float32x4_t v_sample1 = vld1q_f32(sample1.data()); + const float32x4_t v_sample2 = vld1q_f32(sample2.data()); v_write_index = vaddq_s32(v_write_index, v_incr); v_write_index = WrapIndexVector(v_write_index, v_buffer_length_int); @@ -133,7 +133,7 @@ const float32x4_t sample = vaddq_f32( v_sample1, vmulq_f32(interpolation_factor, vsubq_f32(v_sample2, v_sample1))); - UNSAFE_TODO(vst1q_f32(destination + k, sample)); + vst1q_f32(destination.subspan(k, 4u).data(), sample); } // Update |w_index| based on how many frames we processed here, wrapping @@ -146,8 +146,8 @@ return std::make_tuple(k, w_index); } -void Delay::HandleNaN(float* delay_times, - uint32_t frames_to_process, +void Delay::HandleNaN(base::span<float> delay_times, + size_t frames_to_process, float max_time) { unsigned k = 0; int number_of_loops = frames_to_process / 4; @@ -156,7 +156,7 @@ // This is approximately 4 times faster than the scalar version. for (int loop = 0; loop < number_of_loops; ++loop, k += 4) { - float32x4_t x = UNSAFE_TODO(vld1q_f32(delay_times + k)); + float32x4_t x = vld1q_f32(delay_times.subspan(k, 4u).data()); // x == x only fails when x is NaN. Then cmp is set to 0. Otherwise // 0xffffffff uint32x4_t cmp = vceqq_f32(x, x); @@ -180,14 +180,14 @@ xint = vorrq_u32(xint, cmp); // Finally, save the float result. - UNSAFE_TODO( - vst1q_f32(delay_times + k, reinterpret_cast<float32x4_t>(xint))); + vst1q_f32(delay_times.subspan(k, 4u).data(), + reinterpret_cast<float32x4_t>(xint)); } // Handle any frames not done in the loop above. for (; k < frames_to_process; ++k) { - if (std::isnan(UNSAFE_TODO(delay_times[k]))) { - UNSAFE_TODO(delay_times[k]) = max_time; + if (std::isnan(delay_times[k])) { + delay_times[k] = max_time; } } } diff --git a/third_party/blink/renderer/platform/audio/cpu/x86/delay_sse2.cc b/third_party/blink/renderer/platform/audio/cpu/x86/delay_sse2.cc index ed7fb615..436dc31 100644 --- a/third_party/blink/renderer/platform/audio/cpu/x86/delay_sse2.cc +++ b/third_party/blink/renderer/platform/audio/cpu/x86/delay_sse2.cc @@ -52,15 +52,13 @@ return _mm_sub_ps(v_position, _mm_and_ps(v_buffer_length, cmp)); } -std::tuple<unsigned, int> Delay::ProcessARateVector( - float* destination, - uint32_t frames_to_process) const { - const int buffer_length = buffer_.size(); - const float* buffer = buffer_.Data(); +std::tuple<size_t, size_t> Delay::ProcessARateVector( + base::span<float> destination, + size_t frames_to_process) const {
Original Bug Report
NaN poisoning in k-rate SetValueCurve leads to out-of-bounds read in DelayNode
NaN poisoning in k-rate SetValueCurve leads to out-of-bounds read in DelayNode
Summary
A crafted setValueCurveAtTime call on a k-rate DelayNode.delayTime AudioParam can produce a NaN delay value that propagates unchecked through Delay::ProcessKRate, causing static_cast<int>(NaN) to yield INT_MIN on x86/x64. This results in a massive out-of-bounds memory read (approximately 8 GB before the ring buffer), crashing the renderer process. The vulnerability affects all desktop platforms (Linux, macOS, Windows) on x86/x64 architectures.
Root Cause
When AudioParam::setValueCurveAtTime(curve, startTime, duration) is called, the event creation code pre-computes the curve sampling rate as a per-second value:
// audio_param_handler.cc — CreateSetValueCurveEvent
double curve_points = (curve.size() - 1) / duration;
This computation runs on the main thread, where IEEE 754 subnormal handling is standard. If duration is a normal double just above DBL_MIN (approximately 2.225e-308) and the curve has enough points, the division overflows to positive infinity. For instance, a 6-element curve with duration = 2.3e-308 produces 5 / 2.3e-308 ≈ 2.17e308, which exceeds DBL_MAX and becomes +inf. The event stores this infinite CurvePointsPerSecond without any validation; the only checks on the event fields are DCHECK(std::isfinite(...)) assertions that are compiled out in release builds.
The vulnerability manifests during audio rendering on the audio thread, which runs with x86 MXCSR flags DAZ (Denormals Are Zero) and FTZ (Flush To Zero) enabled for performance. The key property of the chosen duration is that it must be a normal (non-subnormal) double so that the audio thread does not flush it to zero when computing frame coverage. With duration = 2.3e-308, the product sampleRate * duration = 48000 * 2.3e-308 ≈ 1.1e-303 is a normal double, and ceil(1.1e-303) = 1, meaning the SetValueCurve event covers exactly one audio frame. This allows the event’s processing loop to execute.
Inside ProcessSetValueCurve, the scalar fallback path computes a virtual curve index for each frame:
// audio_param_handler.cc — ProcessSetValueCurve, scalar loop
double current_virtual_index =
curve_virtual_index + k * curve_points_per_frame;
On the first iteration (k = 0) with curve_virtual_index = 0 and curve_points_per_frame = +inf, IEEE 754 arithmetic produces 0 * inf = NaN. The subsequent clamping of the curve index uses comparison operators that propagate NaN:
// audio_param_handler.cc — ProcessSetValueCurve, scalar loop
double delta = std::min(current_virtual_index - curve_index0, 1.0);
Since std::min uses operator< and NaN comparisons return false, the NaN survives as delta. The interpolated value c0 + (c1 - c0) * NaN evaluates to NaN because any arithmetic with NaN produces NaN.
The a-rate (sample-accurate) path is protected against NaN through HandleNaNValues() in CalculateFinalValues, but the k-rate path through FinalValue() and ValueForContextTime() lacks this protection entirely. After ValuesForFrameRangeImpl returns the NaN value, it passes through Vclip with a single-element span, which takes the scalar code path using ClampTo(). The ClampTo implementation in math_extras.h relies on comparison operators:
// math_extras.h — ClampToDirectComparison
if (value >= max) return max;
if (value <= min) return min;
return value;
Both comparisons against NaN evaluate to false, so the NaN value is returned unchanged. A DCHECK(!__builtin_isnan(...)) guard exists but is absent in release builds.
The NaN delay value then enters Delay::ProcessKRate, where it passes through another ClampTo call unimpeded, and is converted to a buffer index:
// delay.cc — ProcessKRate
double delay_time = DelayTime(sample_rate);
delay_time = ClampTo(delay_time, 0.0, max_time);
double desired_delay_frames = delay_time * sample_rate;
double read_position = w_index + buffer_length - desired_delay_frames;
int read_index1 = static_cast<int>(read_position);
float* read_pointer = &buffer[read_index1];
memcpy(sample1, read_pointer, sizeof(*sample1) * std::min(frames_to_process, remainder));
On x86/x64, static_cast<int>(NaN) compiles to cvttsd2si, which returns INT_MIN (0x80000000) for NaN inputs. This produces read_index1 = -2147483648, causing the subsequent pointer arithmetic and memcpy to read approximately 8 GB before the start of the ring buffer. The access lands in unmapped memory, producing a SEGV_MAPERR signal.
Reproduce
To reproduce this issue, check out Chromium at commit e256102970bf347f2cc827935dbcb09ee18a3b60 and configure an ASAN release build. Place the following in out/asan-release/args.gn:
is_debug = false
is_asan = true
is_component_build = true
symbol_level = 1
Build Chrome with autoninja -C out/asan-release chrome. No source patches are required.
Copy poc.html from this directory to the Chromium source root ~/chromium/src/poc.html, then launch Chrome with ASAN:
ASAN_OPTIONS=detect_odr_violation=0 \
xvfb-run -a out/asan-release/chrome \
--no-sandbox --disable-gpu \
--user-data-dir=/tmp/poc-test \
poc.html
On headless servers, xvfb-run -a provides a virtual display. The renderer process will crash within a few seconds with Received signal 11 SEGV_MAPERR, confirming an out-of-bounds memory read from inside blink::Delay::ProcessKRate. The crash address will be approximately 8 GB before the delay line ring buffer, which is the result of static_cast<int>(NaN) producing INT_MIN (0x80000000) as a buffer index on x86/x64. The crash log in asan.log contains the full stack trace and register dump.
Crash Log
Received signal 11 SEGV_MAPERR 7b9950013800
#0 0x55f29ae6f046 ___interceptor_backtrace
#1 0x7f9bdad5f3e2 base::debug::CollectStackTrace base/debug/stack_trace_posix.cc:1048
#2 0x7f9bdad04c53 base::debug::StackTrace::StackTrace base/debug/stack_trace.cc:280
#3 0x7f9bdad5e67b base::debug::StackDumpSignalHandler base/debug/stack_trace_posix.cc:483
#4 0x7f9b6a242520 (signal handler)
#5 0x7f9b6a2c4881 memcpy memmove-vec-unaligned-erms.S:220
#6 0x55f29aec727c __asan_memcpy
#7 0x7f9b7f23d4cc blink::Delay::ProcessKRate delay.cc:268
#8 0x7f9b72903999 blink::DelayHandler::Process delay_handler.cc:84
#9 0x7f9b7283a8fe blink::AudioHandler::ProcessIfNecessary audio_handler.cc:331
#10 0x7f9b7285c7c6 blink::AudioNodeOutput::Pull audio_node_output.cc:135
#11 0x7f9b7285995c blink::AudioNodeInput::SumAllConnections audio_node_input.cc:132
#12 0x7f9b72859de6 blink::AudioNodeInput::Pull audio_node_input.cc:162
#13 0x7f9b7293900c blink::OfflineAudioDestinationHandler::RenderIfNotSuspended offline_audio_destination_handler.cc:304
#14 0x7f9b72937b02 blink::OfflineAudioDestinationHandler::DoOfflineRendering offline_audio_destination_handler.cc:188
#15 0x7f9b7293a2ad base::internal::Invoker<...>::RunOnce bind_internal.h:740
#16 0x7f9bdab614f3 base::TaskAnnotator::RunTaskImpl callback.h:155
#17 0x7f9bdabe29df ThreadControllerWithMessagePumpImpl::DoWorkImpl task_annotator.h:112
#18 0x7f9bdabe19b7 ThreadControllerWithMessagePumpImpl::DoWork thread_controller_with_message_pump_impl.cc:346
#19 0x7f9bdaa03592 base::MessagePumpDefault::Run message_pump_default.cc:42
#20 0x7f9bdabe4059 ThreadControllerWithMessagePumpImpl::Run thread_controller_with_message_pump_impl.cc:650
#21 0x7f9bdaacbb53 base::RunLoop::Run run_loop.cc:135
#22 0x7f9b8007000d blink::scheduler::NonMainThreadImpl::SimpleThreadImpl::Run non_main_thread_impl.cc:178
#23 0x7f9bdacde6fd base::ThreadFunc platform_thread_posix.cc:102
#24 0x55f29aec7137 asan_thread_start
#25 0x7f9b6a294ac3 start_thread pthread_create.c:442
#26 0x7f9b6a326850 clone3 clone3.S:81
r8: 00000f9feb863b30 r9: 0000000000000008 r10: 0000000000000008 r11: 0000000000000000
r12: 00007b9950013800 r13: 00000000000001bf r14: 0000000000000200 r15: 00007b9950013800
di: 00007cfb5c35d780 si: 00007b9950013800 bp: 00007b9a92d7ad30 bx: 00007cfb5c35d780
dx: 0000000000000200 ax: 00007cfb5c35d780 cx: 00000f9feb863af0 sp: 00007b9a92d7a4e8
ip: 00007f9b6a2c4881 efl: 0000000000010206 cgf: 002b000000000033 erf: 0000000000000004
trp: 000000000000000e msk: 0000000000000000 cr2: 00007b9950013800
[end of stack trace]
Credit
Please use c6eed09fc8b174b0f3eebedcceb1e792 as the credit for this vulnerability. Thank you.