CVE-2026-16420
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
TestProcessorthird_party/blink/renderer/modules/webaudio/audio_worklet_global_scope_test.cc |
modified | |
constructorthird_party/blink/renderer/modules/webaudio/audio_worklet_global_scope_test.cc |
modified | |
processthird_party/blink/renderer/modules/webaudio/audio_worklet_global_scope_test.cc |
modified |
Files Changed
third_party/blink/common/features.ccthird_party/blink/public/common/features.hthird_party/blink/renderer/core/workers/worker_backing_thread.ccthird_party/blink/renderer/modules/webaudio/audio_worklet_global_scope_test.cc
Patch
From 1a17b7b8f3ab8f274c24279ef5ac0de15b9800ef Mon Sep 17 00:00:00 2001
From: Hongchan Choi <hongchan@google.com>
Date: Thu, 16 Jul 2026 10:06:45 -0700
Subject: [PATCH] [M150] [WebAudio] Disable FTZ/DAZ during JavaScript AudioWorklet execution
Original change's description:
> [WebAudio] Disable FTZ/DAZ during JavaScript AudioWorklet execution
>
> This CL disables the Float-to-Zero (FTZ) and Denormals-are-Zero (DAZ)
> FPU features when running JavaScript code within AudioWorklets.
>
> Historically, worker backing threads for AudioWorklets enabled FTZ/DAZ
> to avoid microcode performance overheads during real-time processing.
> However, V8 compiler optimization assumes strict IEEE-754 floating
> point semantics. Running JIT-optimized code under non-standard FPU
> behavior causes compiler and runtime execution divergences, which
> leads to incorrect type assertions and memory safety issues.
>
> To resolve this alignment issue, this change:
> 1. Prevents AudioWorklet backing threads from turning on FTZ/DAZ at
> startup, ensuring that the V8 Isolate initializes in standard
> IEEE-754 mode.
> 2. Instantiates DenormalEnabler inside AudioWorkletProcessor::Process()
> to temporarily disable FTZ/DAZ during JS execution. This guarantees
> standard floating point behavior for user scripts while leaving
> native WebAudio DSP nodes to run with FTZ/DAZ enabled.
>
> Bug: 528276487, 527930356
> Test: blink_unittests --gtest_filter="*DenormalProcessing*"
>
> TAG=agy
> CONV=9d8e1d6c-f19f-4ec3-adae-3a4328b455ca
>
> Change-Id: I7f012fbb7e50fe4bd5369968a94d6531bfb12df1
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8021606
> Reviewed-by: Michael Lippautz <mlippautz@chromium.org>
> Commit-Queue: Hongchan Choi <hongchan@chromium.org>
> Cr-Commit-Position: refs/heads/main@{#1659058}
(cherry picked from commit d3b91ccae2842f11bad90ce73e600e1dd771b8a7)
Bug: 535458979,528276487,527930356
Change-Id: I7f012fbb7e50fe4bd5369968a94d6531bfb12df1
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8108657
Commit-Queue: rubber-stamper@appspot.gserviceaccount.com <rubber-stamper@appspot.gserviceaccount.com>
Auto-Submit: chrome-cherry-picker@chops-service-accounts.iam.gserviceaccount.com <chrome-cherry-picker@chops-service-accounts.iam.gserviceaccount.com>
Bot-Commit: rubber-stamper@appspot.gserviceaccount.com <rubber-stamper@appspot.gserviceaccount.com>
Cr-Commit-Position: refs/branch-heads/7871@{#3539}
Cr-Branched-From: f542126b8c1b3e80104b26bb05ec830bd1206f29-refs/heads/main@{#1639810}
---
diff --git a/third_party/blink/common/features.cc b/third_party/blink/common/features.cc
index d210598df..1c03e395 100644
--- a/third_party/blink/common/features.cc
+++ b/third_party/blink/common/features.cc
@@ -51,6 +51,12 @@
"ad-auction-signals-max-size-bytes",
10000);
+// Controls whether JavaScript execution inside AudioWorkletProcessor::Process()
+// runs under strict IEEE-754 floating-point semantics (disabling FTZ/DAZ).
+// Enabled by default as a remote kill-switch.
+BASE_FEATURE(kAudioWorkletJSDenormalEnabler,
+ base::FEATURE_ENABLED_BY_DEFAULT);
+
#if BUILDFLAG(IS_ANDROID)
// If enabled, then use desktop page webprefs for Android devices that have
// large displays, specifically tablets and desktops.
diff --git a/third_party/blink/public/common/features.h b/third_party/blink/public/common/features.h
index 7083f57..1818562 100644
--- a/third_party/blink/public/common/features.h
+++ b/third_party/blink/public/common/features.h
@@ -48,6 +48,8 @@
BLINK_COMMON_EXPORT BASE_DECLARE_FEATURE_PARAM(int,
kAdAuctionSignalsMaxSizeBytes);
+BLINK_COMMON_EXPORT BASE_DECLARE_FEATURE(kAudioWorkletJSDenormalEnabler);
+
// Avoids copying ResourceRequest::TrustedParams when possible.
BLINK_COMMON_EXPORT BASE_DECLARE_FEATURE(kAvoidTrustedParamsCopies);
diff --git a/third_party/blink/renderer/core/workers/worker_backing_thread.cc b/third_party/blink/renderer/core/workers/worker_backing_thread.cc
index 5997bed3..345cc9c 100644
--- a/third_party/blink/renderer/core/workers/worker_backing_thread.cc
+++ b/third_party/blink/renderer/core/workers/worker_backing_thread.cc
@@ -93,11 +93,13 @@
}
bool IsDenormalDisabledThreadType(ThreadType type) {
- // Disable denormals on WebAudio threads for performance reasons. See:
- // https://esdiscuss.org/topic/float-denormal-issue-in-javascript-processor-node-in-web-audio-api
- return type == ThreadType::kOfflineAudioWorkletThread ||
- type == ThreadType::kRealtimeAudioWorkletThread ||
- type == ThreadType::kSemiRealtimeAudioWorkletThread;
+ if (type == ThreadType::kOfflineAudioWorkletThread ||
+ type == ThreadType::kRealtimeAudioWorkletThread ||
+ type == ThreadType::kSemiRealtimeAudioWorkletThread) {
+ return !base::FeatureList::IsEnabled(
+ blink::features::kAudioWorkletJSDenormalEnabler);
+ }
+ return false;
}
} // namespace
diff --git a/third_party/blink/renderer/modules/webaudio/audio_worklet_global_scope_test.cc b/third_party/blink/renderer/modules/webaudio/audio_worklet_global_scope_test.cc
index 8506f32..99b5306f 100644
--- a/third_party/blink/renderer/modules/webaudio/audio_worklet_global_scope_test.cc
+++ b/third_party/blink/renderer/modules/webaudio/audio_worklet_global_scope_test.cc
@@ -8,8 +8,10 @@
#include "base/compiler_specific.h"
#include "base/synchronization/waitable_event.h"
+#include "base/test/scoped_feature_list.h"
#include "media/base/audio_bus.h"
#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/v8_cache_options.mojom-blink.h"
#include "third_party/blink/public/platform/task_type.h"
#include "third_party/blink/public/platform/web_url_request.h"
@@ -42,6 +44,7 @@
#include "third_party/blink/renderer/modules/webaudio/audio_worklet_processor_definition.h"
#include "third_party/blink/renderer/modules/webaudio/offline_audio_worklet_thread.h"
#include "third_party/blink/renderer/platform/audio/audio_bus.h"
+#include "third_party/blink/renderer/platform/audio/denormal_disabler.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
#include "third_party/blink/renderer/platform/bindings/source_location.h"
#include "third_party/blink/renderer/platform/bindings/v8_object_constructor.h"
@@ -125,6 +128,17 @@
waitable_event.Wait();
}
+ void RunDenormalProcessTest(WorkerThread* thread, bool expect_denormals) {
+ base::WaitableEvent waitable_event;
+ PostCrossThreadTask(
+ *thread->GetTaskRunner(TaskType::kInternalTest), FROM_HERE,
+ CrossThreadBindOnce(
+ &AudioWorkletGlobalScopeTest::RunDenormalProcessTestOnWorkletThread,
+ CrossThreadUnretained(this), CrossThreadUnretained(thread),
+ expect_denormals, CrossThreadUnretained(&waitable_event)));
+ waitable_event.Wait();
+ }
+
void RunParsingTest(WorkerThread* thread) {
base::WaitableEvent waitable_event;
PostCrossThreadTask(
@@ -346,6 +360,79 @@
wait_event->Signal();
}
+ void RunDenormalProcessTestOnWorkletThread(WorkerThread* thread,
+ bool expect_denormals,
+ base::WaitableEvent* wait_event) {
+ EXPECT_TRUE(thread->IsCurrentThread());
+
+ auto* global_scope = To<AudioWorkletGlobalScope>(thread->GlobalScope());
+ ScriptState* script_state =
+ global_scope->ScriptController()->GetScriptState();
+
+ ScriptState::Scope scope(script_state);
+ v8::Isolate* isolate = script_state->GetIsolate();
+ EXPECT_TRUE(isolate);
+ V8DoNotRunMicrotasksScope microtasks_scope(script_state);
+
+ String source_code =
+ R"JS(
+ class TestProcessor extends AudioWorkletProcessor {
+ constructor () { super(); }
+ process (inputs, outputs) {
+ let f64 = new Float64Array(1);
+ // The minimum positive normal 64-bit float is
+ // 2.225e-308. Therefore, 1.0e-309 is a denormal
+ // double. If FTZ/DAZ is enabled, it is treated
+ // as zero or flushed to zero, making f64[0]
+ // equal to 0.0. If disabled, the division
+ // computes 1.0e-310 (a valid denormal double
+ // > 0.0).
+ let denorm = 1.0e-309;
+ f64[0] = denorm / 10.0;
+ let outputChannel = outputs[0][0];
+ outputChannel[0] = f64[0] > 0.0 ? 1.0 : 0.0;
+ }
+ }
+ registerProcessor('testProcessor', TestProcessor);
+ )JS";
+ ExpectEvaluateScriptModule(global_scope, source_code, true);
+
+ auto* channel = MakeGarbageCollected<MessageChannel>(thread->GlobalScope());
+ MessagePortChannel dummy_port_channel = channel->port2()->Disentangle();
+ AudioWorkletProcessor* processor =
+ global_scope->CreateProcessor("testProcessor",
+ dummy_port_channel,
+ SerializedScriptValue::NullValue());
+ EXPECT_TRUE(processor);
+
+ Vector<scoped_refptr<AudioBus>> input_buses;
+ Vector<scoped_refptr<AudioBus>> output_buses;
+ HashMap<String, std::unique_ptr<AudioFloatArray>> param_data_map;
+ scoped_refptr<AudioBus> input_bus =
+ AudioBus::Create(1, kRenderQuantumFrames);
+ scoped_refptr<AudioBus> output_bus =
Regression Test / PoC
diff --git a/third_party/blink/renderer/modules/webaudio/audio_worklet_global_scope_test.cc b/third_party/blink/renderer/modules/webaudio/audio_worklet_global_scope_test.cc
index 8506f32..99b5306f 100644
--- a/third_party/blink/renderer/modules/webaudio/audio_worklet_global_scope_test.cc
+++ b/third_party/blink/renderer/modules/webaudio/audio_worklet_global_scope_test.cc
@@ -8,8 +8,10 @@
#include "base/compiler_specific.h"
#include "base/synchronization/waitable_event.h"
+#include "base/test/scoped_feature_list.h"
#include "media/base/audio_bus.h"
#include "testing/gtest/include/gtest/gtest.h"
+#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/v8_cache_options.mojom-blink.h"
#include "third_party/blink/public/platform/task_type.h"
#include "third_party/blink/public/platform/web_url_request.h"
@@ -42,6 +44,7 @@
#include "third_party/blink/renderer/modules/webaudio/audio_worklet_processor_definition.h"
#include "third_party/blink/renderer/modules/webaudio/offline_audio_worklet_thread.h"
#include "third_party/blink/renderer/platform/audio/audio_bus.h"
+#include "third_party/blink/renderer/platform/audio/denormal_disabler.h"
#include "third_party/blink/renderer/platform/bindings/script_state.h"
#include "third_party/blink/renderer/platform/bindings/source_location.h"
#include "third_party/blink/renderer/platform/bindings/v8_object_constructor.h"
@@ -125,6 +128,17 @@
waitable_event.Wait();
}
+ void RunDenormalProcessTest(WorkerThread* thread, bool expect_denormals) {
+ base::WaitableEvent waitable_event;
+ PostCrossThreadTask(
+ *thread->GetTaskRunner(TaskType::kInternalTest), FROM_HERE,
+ CrossThreadBindOnce(
+ &AudioWorkletGlobalScopeTest::RunDenormalProcessTestOnWorkletThread,
+ CrossThreadUnretained(this), CrossThreadUnretained(thread),
+ expect_denormals, CrossThreadUnretained(&waitable_event)));
+ waitable_event.Wait();
+ }
+
void RunParsingTest(WorkerThread* thread) {
base::WaitableEvent waitable_event;
PostCrossThreadTask(
@@ -346,6 +360,79 @@
wait_event->Signal();
}
+ void RunDenormalProcessTestOnWorkletThread(WorkerThread* thread,
+ bool expect_denormals,
+ base::WaitableEvent* wait_event) {
+ EXPECT_TRUE(thread->IsCurrentThread());
+
+ auto* global_scope = To<AudioWorkletGlobalScope>(thread->GlobalScope());
+ ScriptState* script_state =
+ global_scope->ScriptController()->GetScriptState();
+
+ ScriptState::Scope scope(script_state);
+ v8::Isolate* isolate = script_state->GetIsolate();
+ EXPECT_TRUE(isolate);
+ V8DoNotRunMicrotasksScope microtasks_scope(script_state);
+
+ String source_code =
+ R"JS(
+ class TestProcessor extends AudioWorkletProcessor {
+ constructor () { super(); }
+ process (inputs, outputs) {
+ let f64 = new Float64Array(1);
+ // The minimum positive normal 64-bit float is
+ // 2.225e-308. Therefore, 1.0e-309 is a denormal
+ // double. If FTZ/DAZ is enabled, it is treated
+ // as zero or flushed to zero, making f64[0]
+ // equal to 0.0. If disabled, the division
+ // computes 1.0e-310 (a valid denormal double
+ // > 0.0).
+ let denorm = 1.0e-309;
+ f64[0] = denorm / 10.0;
+ let outputChannel = outputs[0][0];
+ outputChannel[0] = f64[0] > 0.0 ? 1.0 : 0.0;
+ }
+ }
+ registerProcessor('testProcessor', TestProcessor);
+ )JS";
+ ExpectEvaluateScriptModule(global_scope, source_code, true);
+
+ auto* channel = MakeGarbageCollected<MessageChannel>(thread->GlobalScope());
+ MessagePortChannel dummy_port_channel = channel->port2()->Disentangle();
+ AudioWorkletProcessor* processor =
+ global_scope->CreateProcessor("testProcessor",
+ dummy_port_channel,
+ SerializedScriptValue::NullValue());
+ EXPECT_TRUE(processor);
+
+ Vector<scoped_refptr<AudioBus>> input_buses;
+ Vector<scoped_refptr<AudioBus>> output_buses;
+ HashMap<String, std::unique_ptr<AudioFloatArray>> param_data_map;
+ scoped_refptr<AudioBus> input_bus =
+ AudioBus::Create(1, kRenderQuantumFrames);
+ scoped_refptr<AudioBus> output_bus =
+ AudioBus::Create(1, kRenderQuantumFrames);
+ AudioChannel* output_channel = output_bus->Channel(0);
+
+ input_buses.push_back(input_bus.get());
+ output_buses.push_back(output_bus.get());
+ output_bus->Zero();
+
+ // Simulate the audio thread rendering stack by instantiating
+ // DenormalDisabler.
+ DenormalDisabler scoped_disabler;
+
+ // processor->Process() internally instantiates DenormalEnabler, which
+ // disables FTZ/DAZ during V8 execution if enabled.
+ processor->Process(input_buses, output_buses, param_data_map);
+
+ // Verify that the JS execution was affected by the outer
+ // DenormalDisabler only if the feature is disabled.
+ EXPECT_EQ(output_channel->Span()[0], expect_denormals ? 1.0f : 0.0f);
+
+ wait_event->Signal();
+ }
+
void RunParsingParameterDescriptorTestOnWorkletThread(
WorkerThread* thread,
base::WaitableEvent* wait_event) {
@@ -420,6 +507,30 @@
thread->WaitForShutdownForTesting();
}
+TEST_F(AudioWorkletGlobalScopeTest, DenormalProcessing_FeatureEnabled) {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndEnableFeature(
+ blink::features::kAudioWorkletJSDenormalEnabler);
+
+ std::unique_ptr<OfflineAudioWorkletThread> thread =
+ CreateAudioWorkletThread();
+ RunDenormalProcessTest(thread.get(), /*expect_denormals=*/true);
+ thread->Terminate();
+ thread->WaitForShutdownForTesting();
+}
+
+TEST_F(AudioWorkletGlobalScopeTest, DenormalProcessing_FeatureDisabled) {
+ base::test::ScopedFeatureList feature_list;
+ feature_list.InitAndDisableFeature(
+ blink::features::kAudioWorkletJSDenormalEnabler);
+
+ std::unique_ptr<OfflineAudioWorkletThread> thread =
+ CreateAudioWorkletThread();
+ RunDenormalProcessTest(thread.get(), /*expect_denormals=*/false);
+ thread->Terminate();
+ thread->WaitForShutdownForTesting();
+}
+
TEST_F(AudioWorkletGlobalScopeTest, ParsingParameterDescriptor) {
std::unique_ptr<OfflineAudioWorkletThread> thread
= CreateAudioWorkletThread();
diff --git a/third_party/blink/web_tests/external/wpt/webaudio/the-audio-api/the-audioworklet-interface/audioworklet-denormals.https.window-expected.txt b/third_party/blink/web_tests/external/wpt/webaudio/the-audio-api/the-audioworklet-interface/audioworklet-denormals.https.window-expected.txt
deleted file mode 100644
index 443943d..0000000
--- a/third_party/blink/web_tests/external/wpt/webaudio/the-audio-api/the-audioworklet-interface/audioworklet-denormals.https.window-expected.txt
+++ /dev/null
@@ -1,5 +0,0 @@
-This is a testharness.js-based test.
-[FAIL] Test denormal behavior in AudioWorkletGlobalScope
- assert_true: The denormals should be non-zeros in AudioWorkletGlobalScope. expected true got false
-Harness: the test ran to completion.
-
Original Bug Report
DCHECK RangeType::New failure due to unsound-under-FTZ V8 optimizations
VULNERABILITY DETAILS
Several V8 optimizing reductions and materialization boundaries are unsound when generated code runs with FTZ/DAZ enabled. This is the same class of issue as crbug.com/528276487 ; the affected optimizations reported here are those that we have shown can reach the DCHECK described below.
The attached script contains cases where the listed V8 optimization itself
produces an optimized negative Float64 subnormal. The script then stores that
optimized value in a JavaScript variable and passes it to a one-argument
Math.max(v) function. In a DCHECK build, optimizing that Math.max(v)
function reaches:
Debug check failed: IsInteger(lim.min) && IsInteger(lim.max)
The failing code is TurboFan range construction for Math.max(v). The case IDs
below identify different optimizations that can produce the bad input value;
they are not intended to claim independent RangeType bugs.
The following case IDs reproduce without experimental V8 feature flags: R002,
R003, R004, R007, R008, R012, R022, R023, R024, R025,
R033, R048, R049, R050, R052, R053, R054, R060, R061,
R063, R069, P002, P003.
One additional case, R064, requires experimental phi-untagging flags:
--maglev-future --maglev-untagged-phis --maglev-licm --maglev-range-analysis. This case is included only as feature-gated evidence
and should not be treated as enabled in the default V8 configuration.
For R063, the simple literal-object form no longer produces the needed
negative subnormal. The attached script uses a loop-write object-field form that
still exercises the same object-field Float64 materialization issue.
For R008 and R064, the command line asks the script to optimize the
value-producing function with Maglev. The separate Math.max(v) function that
hits RangeType::New is then optimized normally by TurboFan.
For R053, the attached script uses a DataView unary-negation operation
followed by setFloat64/getFloat64. Raw DataView copy-only variants are not
claimed in this report because they do not by themselves demonstrate the same
JavaScript-number value flowing into Math.max(v).
I have included details of the affected optimizations as the attachment
AFFECTED_OPTIMIZATIONS.md, as the list is too long to include inline.
VERSION
Chrome Version: 151.0.7915.0 dev build.
Operating System: Ubuntu 24.04.4 LTS x86_64
(Linux 6.17.0-1010-aws).
Validated against:
- Chromium checkout:
6f813967910885b4b8dafaf6a72b7a5c80fba157. - V8 checkout:
0021d325c3a448f4e26e21ef4e0db1bea1e39fda. d8/ V8 version:15.1.192.- DCHECK validation build:
dcheck_always_on = true.
REPRODUCTION CASE
Attached file: dcheck_poc.js.
List supported cases:
DCHECK_D8=${DCHECK_D8:-/home/moyix/chromium-src-codex/src/out/dcheck-d8/d8}
"$DCHECK_D8" --allow-natives-syntax --flush-denormals \
dcheck_poc.js -- list
Representative JavaScript case:
"$DCHECK_D8" --allow-natives-syntax --flush-denormals \
dcheck_poc.js -- r002_mul_one_neg captured-dcheck
Representative Maglev case:
"$DCHECK_D8" --allow-natives-syntax --flush-denormals \
dcheck_poc.js -- r008_maglev_min_daz_tie_neg captured-dcheck maglev
Feature-gated Maglev phi-untagging case:
"$DCHECK_D8" --allow-natives-syntax --flush-denormals \
--maglev --maglev-future --maglev-untagged-phis \
--maglev-licm --maglev-range-analysis \
dcheck_poc.js -- r064_phi_branch_neg captured-dcheck maglev
The R064 command intentionally does not use --maglev-as-top-tier or
--optimize-on-next-call-optimizes-to-maglev: the value-producing function is
optimized with Maglev via the script argument, while the Math.max(v) function
that reaches RangeType::New is optimized normally by TurboFan.
Representative Wasm case:
"$DCHECK_D8" --allow-natives-syntax --flush-denormals \
--wasm-sync-tier-up --no-wasm-native-module-cache \
dcheck_poc.js -- r069_f64_floor_neg captured-dcheck
Use captured-range instead of captured-dcheck to print the optimized value
and the Math.max(v) result without intentionally aborting. captured-dcheck
retries the Math.max(v) function up to five times because successful
reproductions abort while that function is being optimized.
FOR CRASHES, PLEASE INCLUDE THE FOLLOWING ADDITIONAL INFORMATION
Type of crash: d8 DCHECK abort in a dcheck_always_on = true validation
build.
Crash State:
PRODUCER r002_mul_one_neg R002 base=0x8000000000000000 opt=0x8004df9f8cb29569 baseNegSub=false optNegSub=true status=41
#
# Fatal error in ../../v8/src/compiler/turbofan-types.h, line 378
# Debug check failed: IsInteger(lim.min) && IsInteger(lim.max).
#
==== C stack trace ===============================
v8::internal::compiler::RangeType::New(v8::internal::compiler::RangeType::Limits, v8::internal::Zone*)+0x191
v8::internal::compiler::Type::Intersect(v8::internal::compiler::Type, v8::internal::compiler::Type, v8::internal::Zone*)+0x191
v8::internal::compiler::OperationTyper::ToNumber(v8::internal::compiler::Type)+0x14a
v8::internal::compiler::Typer::Visitor::Reduce(v8::internal::compiler::Node*)+0x1b
v8::internal::compiler::GraphReducer::Reduce(v8::internal::compiler::Node*)+0xc6
v8::internal::compiler::Typer::Run(...)+0xd0
v8::internal::compiler::PipelineImpl::OptimizeTurbofanGraph(...)+0xe3
CREDIT INFORMATION
Reporter credit: Found by XBOW and triaged by Brendan Dolan-Gavitt