Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInappropriate implementation in WebAudio
DescriptionInappropriate implementation in WebAudio
ComponentWebAudio
Bug ClassLogic Error
Tracker528276487
Fix commit1a17b7b8f3ab (chromium/src) +144/-11
CISA KEVNot listed
CreditedFound by XBOW and triaged by Brendan Dolan-Gavitt
Disclosed2026-07-21

Changed Functions

FunctionChangeNotes
TestProcessor
third_party/blink/renderer/modules/webaudio/audio_worklet_global_scope_test.cc
modified
constructor
third_party/blink/renderer/modules/webaudio/audio_worklet_global_scope_test.cc
modified
process
third_party/blink/renderer/modules/webaudio/audio_worklet_global_scope_test.cc
modified

Files Changed

  • third_party/blink/common/features.cc
  • third_party/blink/public/common/features.h
  • third_party/blink/renderer/core/workers/worker_backing_thread.cc
  • third_party/blink/renderer/modules/webaudio/audio_worklet_global_scope_test.cc
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 =
Loading diff…

Regression Test / PoC

shipped with the fix
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.
-
Loading diff…

Original Bug Report

reported by mo...@xbow.com

V8 optimizations make IEEE-only assumptions that are unsound with denormals disabled

VULNERABILITY DETAILS

Several V8 optimizing reductions are unsound when generated code runs with FTZ/DAZ enabled (as in the AudioWorklet isolate). This is a same-mode FTZ issue: compilation and execution are both under FTZ/DAZ. The bug is not about compiling in one FTZ mode and executing in another.

In each supported case, optimized code and unoptimized/baseline execution disagree about the same floating-point value: one side observes a signed zero and the other side preserves raw subnormal bits. The attached PoC feeds that disagreement into Object.is, integer range typing, and JSArray growth. The result is a stale JSArray length/backing-store mismatch that can be turned into a controlled native write.

PoC Structure

The d8 PoC uses V8 native syntax so that it can show the pre-optimization state, warm the function, request optimization with %OptimizeFunctionOnNextCall, and then run the optimized function. The selected optimization and the JSArray length-corruption sequence are in the same generated function.

The browser AudioWorklet demo cannot use V8 native optimization intrinsics. Instead, it repeatedly runs the same generated function in the AudioWorklet isolate so that Chrome’s normal tiering pipeline optimizes it. The query parameter root=... selects which of the eight optimization cases to run; the rest of the JavaScript and the command-execution code are shared.

The browser command-execution demo is intended for a local validation build with the V8 sandbox disabled. The latest validation used out/current-d8/args.gn with v8_enable_sandbox = false. The worklet also embeds two offsets into the tested chrome binary; rerun ./refresh_rce_offsets.sh /path/to/chrome after rebuilding Chrome. For the current validation binary, those offsets are CHROME_DTOR_OFFSET = 0x0000000006f3d340n and EXECVP_PLT_OFFSET = 0x000000000521c360n.

Affected Optimizations

Case ID: R001 DIV-ONE Reduction / optimization: Float64Div(x, 1.0) -> x. Unsound FTZ assumption: Runtime division under FTZ/DAZ materializes a subnormal input/result as signed zero; deleting the division preserves raw subnormal bits. V8 source locations: v8/src/compiler/machine-operator-reducer.cc:676; v8/src/compiler/turboshaft/machine-optimization-reducer.h:649; v8/src/maglev/maglev-reducer-inl.h:3456.

Case ID: R005 UNARY-MATH-MINMAX Reduction / optimization: One-argument Math.min(x) / Math.max(x) reduced to ToNumber(x) / identity-like numeric lowering. Unsound FTZ assumption: The unoptimized builtin path materializes the argument under FTZ; the optimized one-argument reduction passes the raw subnormal through. V8 source locations: v8/src/compiler/js-call-reducer.cc:3076; v8/src/compiler/js-call-reducer.cc:3090; v8/src/compiler/js-call-reducer.cc:5642.

Case ID: R006 SAMEVALUE-SIGNED-ZERO Reduction / optimization: Object.is / SameValue numeric lowering and signed-zero type reasoning. Unsound FTZ assumption: Optimized SameValue reasoning is not conservative for a negative subnormal that same-mode FTZ execution can observe as -0. V8 source locations: v8/src/compiler/js-call-reducer.cc:3581; v8/src/compiler/typed-optimization.cc:677; v8/src/compiler/simplified-lowering.cc:3770; v8/src/compiler/operation-typer.cc:1287.

Case ID: R009 MATH-ABS-FTZ-ZERO Reduction / optimization: Math.abs / NumberAbs lowered to raw Float64Abs / sign-bit clearing. Unsound FTZ assumption: The optimized path keeps raw subnormal-derived facts, while same-mode FTZ runtime observation can classify the value as zero. V8 source locations: v8/src/compiler/js-call-reducer.cc:5580; v8/src/compiler/js-call-reducer.cc:2982; v8/src/compiler/simplified-lowering.cc:3350; v8/src/compiler/simplified-lowering.cc:3382; v8/src/compiler/turboshaft/graph-builder.cc:707; v8/src/compiler/backend/x64/instruction-selector-x64.cc:3189; v8/src/maglev/maglev-reducer-inl.h:3945; v8/src/maglev/x64/maglev-ir-x64.cc:862.

Case ID: R010 SUBTRACT-FTZ-MINUSZERO-TYPING Reduction / optimization: OperationTyper::NumberSubtract and downstream SameValue typing for exact negative-subnormal subtraction results. Unsound FTZ assumption: The type excludes MinusZero, but under FTZ/DAZ an exact negative subnormal arithmetic result can be observed as -0. V8 source locations: v8/src/compiler/operation-typer.cc:682; v8/src/compiler/operation-typer.cc:692; v8/src/compiler/operation-typer.cc:719; v8/src/compiler/operation-typer.cc:1287; v8/src/compiler/typed-optimization.cc:657.

Case ID: R011 BINARY-MATH-MINMAX-COMPARE-SELECT Reduction / optimization: Two-argument Math.min / Math.max lowered to NumberMin / NumberMax and Float64 compare/select. Unsound FTZ assumption: Denormals can compare as zero under DAZ while still carrying raw sign/subnormal bits through selected optimized paths. V8 source locations: v8/src/compiler/js-call-reducer.cc:3076; v8/src/compiler/js-call-reducer.cc:3094; v8/src/compiler/operation-typer.cc:1092; v8/src/compiler/operation-typer.cc:1127; v8/src/compiler/simplified-lowering.cc:3463; v8/src/compiler/simplified-lowering.cc:3512; v8/src/compiler/representation-change.cc:1851.

Case ID: R018 FLOAT64MOD-ONE Reduction / optimization: % 1.0 / Float64Mod(x, 1.0) lowering and reducer handling. Unsound FTZ assumption: Source execution under FTZ flushes the subnormal before modulo; optimized lowering preserves the raw subnormal result used by the attached PoC. V8 source locations: v8/src/compiler/js-typed-lowering.cc:447; v8/src/compiler/machine-operator-reducer.cc:710; v8/src/compiler/representation-change.cc:1799; v8/src/compiler/representation-change.cc:1801; v8/src/compiler/simplified-lowering.cc:3173; v8/src/compiler/simplified-lowering.cc:3205; v8/src/maglev/maglev-graph-optimizer.cc:2921; v8/src/maglev/x64/maglev-ir-x64.cc:809; v8/src/maglev/x64/maglev-ir-x64.cc:816.

Case ID: R062 JSARRAY-DOUBLE-ELEMENT-MATERIALIZATION Reduction / optimization: JSArray PACKED_DOUBLE_ELEMENTS / HOLEY_DOUBLE_ELEMENTS store/load materialization and direct double-element forwarding. Unsound FTZ assumption: Baseline FTZ stores/reloads the element as signed zero; optimized double-element paths can forward raw subnormal bits into the SameValue and range-typing sequence used by the attached PoC. V8 source locations: v8/src/compiler/js-native-context-specialization.cc:3795; v8/src/compiler/js-native-context-specialization.cc:4140; v8/src/compiler/access-builder.cc:1399; v8/src/compiler/turboshaft/machine-lowering-reducer-inl.h:3407; v8/src/compiler/turboshaft/machine-lowering-reducer-inl.h:3475; v8/src/maglev/maglev-graph-builder.cc:4361; v8/src/maglev/maglev-graph-builder.cc:4374; v8/src/maglev/maglev-graph-builder.cc:5672; v8/src/maglev/maglev-graph-builder.cc:5904; v8/src/maglev/maglev-ir.h:8858; v8/src/maglev/maglev-ir.h:8925; v8/src/compiler/turboshaft/turbolev-graph-builder.cc:3479; v8/src/compiler/turboshaft/turbolev-graph-builder.cc:3617.

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.
  • Browser RCE validation build: v8_enable_sandbox = false.

REPRODUCTION CASE

Attached files:

  • controlled release crash / stale-array state: rce_controlled_crash.js;
  • browser AudioWorklet RCE demo launching /usr/bin/xcalc: rce_poc.html;
  • browser RCE worklet: rce_worklet.js;
  • RCE offset-refresh script: refresh_rce_offsets.sh.

Controlled crash state without performing the marker write:

D8=${D8:-/path/to/d8}
"$D8" --allow-natives-syntax --flush-denormals \
  rce_controlled_crash.js -- R010

Expected pre-crash state for a vulnerable build includes:

CONTROLLED_STATE i=0,length=65,first[4] aliases second.length

To perform the marker write and crash the vulnerable build:

"$D8" --allow-natives-syntax --flush-denormals \
  rce_controlled_crash.js -- R010 trigger

The same script supports all release-memory-corruption cases:

"$D8" --allow-natives-syntax --flush-denormals \
  rce_controlled_crash.js -- list

For the browser RCE path, serve the directory containing rce_poc.html and rce_worklet.js over HTTP and open:

rce_poc.html?root=R010&action=run&cmd=/usr/bin/xcalc&seconds=60&stay=1

The launcher accepts root=R001, R005, R006, R009, R010, R011, R018, or R062. That parameter selects the affected optimization to exercise. If cmd is omitted, the demo defaults to /usr/bin/xcalc; any other command can be supplied with the cmd query parameter.

CRASH INFORMATION

Type of crash: V8 process crash in d8 for the controlled native-write trigger; browser AudioWorklet command-execution demo in a local Chrome validation build with the V8 sandbox disabled.

Crash State:

ROOT R010 SUBTRACT-FTZ-MINUSZERO-TYPING
BASELINE d=0x8000000000000000,b=true,i=64,len=65
OPT_STATE d=0x8000000000000000,b=true,n=-1,o=4294967296,o_=0,i=0,first.length=65,first[4]=3,second.length=3,second[0]=1,status=41
CONTROLLED_STATE i=0,length=65,first[4] aliases second.length
Received signal 11 SEGV_ACCERR 21de4141413f

==== C stack trace ===============================

/home/moyix/chromium-src-codex/src/out/current-d8/d8(+0x1f39676)[0x6040e41c3676]
/lib/x86_64-linux-gnu/libc.so.6(+0x45330)[0x7f3c96c45330]
/home/moyix/chromium-src-codex/src/out/current-d8/d8(+0xa0172e)[0x6040e2c8b72e]
/home/moyix/chromium-src-codex/src/out/current-d8/d8(+0xa04b4e)[0x6040e2c8eb4e]
/home/moyix/chromium-src-codex/src/out/current-d8/d8(+0x1dbfdb6)[0x6040e4049db6]

CREDIT INFORMATION

Reporter credit: Found by XBOW and triaged by Brendan Dolan-Gavitt

View on issue tracker