CVE-2026-106386
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
forthird_party/blink/renderer/modules/webaudio/audio_buffer.cc |
modified | |
whilethird_party/blink/renderer/modules/webaudio/offline_audio_destination_handler.cc |
modified | |
TEST_Fthird_party/blink/renderer/modules/webaudio/offline_audio_context_test.cc |
modified |
Files Changed
third_party/blink/renderer/modules/webaudio/offline_audio_context_test.ccthird_party/blink/renderer/modules/webaudio/audio_buffer.ccthird_party/blink/renderer/modules/webaudio/audio_buffer.hthird_party/blink/renderer/modules/webaudio/offline_audio_destination_handler.cc
Patch
From 341a2ce649d7971be32a549389dee5f24d3c2755 Mon Sep 17 00:00:00 2001
From: Michael Wilson <mjwilson@chromium.org>
Date: Fri, 04 Sep 2026 18:20:20 -0700
Subject: [PATCH] [WebAudio] Zero unrendered OfflineAudioContext frames on early finish
Ensure that OfflineAudioDestinationHandler::FinishOfflineRendering()
zero-fills any unrendered frames past `frames_processed_` before
resolving the startRendering() promise.
Also check HasAllocationFailed() on every iteration so failures abort
immediately into FinishOfflineRendering().
Fixed: 553156221
Change-Id: I68be8f17cf2fd547c16d37bf299a511bb0ba020f
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8349178
Commit-Queue: Michael Wilson <mjwilson@chromium.org>
Reviewed-by: Hongchan Choi <hongchan@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1692878}
---
diff --git a/third_party/blink/renderer/modules/webaudio/audio_buffer.cc b/third_party/blink/renderer/modules/webaudio/audio_buffer.cc
index bc4c0c6c..7a899b0 100644
--- a/third_party/blink/renderer/modules/webaudio/audio_buffer.cc
+++ b/third_party/blink/renderer/modules/webaudio/audio_buffer.cc
@@ -30,6 +30,7 @@
#include <memory>
+#include "base/check_op.h"
#include "base/compiler_specific.h"
#include "base/containers/span.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_audio_buffer_options.h"
@@ -336,9 +337,11 @@
}
}
-void SharedAudioBuffer::Zero() {
+void SharedAudioBuffer::Zero(size_t start_frame) {
+ DCHECK_LE(start_frame, length_);
for (auto& channel : channels_) {
- std::ranges::fill(channel.ByteSpan(), 0);
+ std::ranges::fill(channel.ByteSpan().subspan(start_frame * sizeof(float)),
+ 0);
}
}
diff --git a/third_party/blink/renderer/modules/webaudio/audio_buffer.h b/third_party/blink/renderer/modules/webaudio/audio_buffer.h
index 4bbf70b..0dd11f8 100644
--- a/third_party/blink/renderer/modules/webaudio/audio_buffer.h
+++ b/third_party/blink/renderer/modules/webaudio/audio_buffer.h
@@ -144,7 +144,7 @@
return channel_spans_[channel_index];
}
- void Zero();
+ void Zero(size_t start_frame = 0);
private:
float sample_rate_;
diff --git a/third_party/blink/renderer/modules/webaudio/offline_audio_destination_handler.cc b/third_party/blink/renderer/modules/webaudio/offline_audio_destination_handler.cc
index eb3a939..5015f110 100644
--- a/third_party/blink/renderer/modules/webaudio/offline_audio_destination_handler.cc
+++ b/third_party/blink/renderer/modules/webaudio/offline_audio_destination_handler.cc
@@ -171,17 +171,17 @@
"OfflineAudioDestinationHandler::DoOfflineRendering", "this",
reinterpret_cast<void*>(this));
- if (Context()->HasAllocationFailed()) {
- FinishOfflineRendering();
- return;
- }
-
unsigned number_of_channels = shared_render_target_->numberOfChannels();
// If there is more to process and there is no suspension at the moment,
// do continue to render quanta. Then calling OfflineAudioContext.resume()
// will pick up the render loop again from where it was suspended.
while (frames_to_process_ > 0) {
+ if (Context()->HasAllocationFailed()) {
+ FinishOfflineRendering();
+ return;
+ }
+
// Suspend the rendering if a scheduled suspend found at the current
// sample frame. Otherwise render one quantum.
if (RenderIfNotSuspended(render_bus_.get(),
@@ -227,6 +227,13 @@
void OfflineAudioDestinationHandler::FinishOfflineRendering() {
DCHECK(!IsMainThread());
+ if (shared_render_target_ &&
+ frames_processed_ < shared_render_target_->length()) {
+ // Rendering finished early, perhaps due to an allocation failure. Ensure
+ // that unrendered frames in the destination buffer are zeroed.
+ shared_render_target_->Zero(/*start_frame=*/frames_processed_);
+ }
+
// The actual rendering has been completed. Notify the context.
PostCrossThreadTask(
*main_thread_task_runner_, FROM_HERE,
diff --git a/third_party/blink/renderer/modules/webaudio/offline_audio_context_test.cc b/third_party/blink/renderer/modules/webaudio/offline_audio_context_test.cc
index 12db957..11c957347 100644
--- a/third_party/blink/renderer/modules/webaudio/offline_audio_context_test.cc
+++ b/third_party/blink/renderer/modules/webaudio/offline_audio_context_test.cc
@@ -6,13 +6,16 @@
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/platform/web_runtime_features.h"
+#include "third_party/blink/renderer/bindings/core/v8/script_promise_tester.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_offline_audio_context_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_union_audiocontextrendersizecategory_unsignedlong.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/testing/page_test_base.h"
+#include "third_party/blink/renderer/modules/webaudio/audio_buffer.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
+#include "third_party/blink/renderer/platform/bindings/script_wrappable.h"
namespace blink {
@@ -91,4 +94,57 @@
EXPECT_EQ(context->renderQuantumSize(), 256u);
}
+TEST_F(OfflineAudioContextTest, EarlyCompletionZeroesDestinationBuffer) {
+ V8TestingScope scope;
+
+ OfflineAudioContextOptions* options = OfflineAudioContextOptions::Create();
+ options->setNumberOfChannels(2);
+ options->setLength(256);
+ options->setSampleRate(44100.0f);
+ OfflineAudioContext* context = OfflineAudioContext::Create(
+ GetFrame().DomWindow(), options, ASSERT_NO_EXCEPTION);
+ ASSERT_TRUE(context);
+
+ // Suspend at frame 0.
+ ScriptPromise<IDLUndefined> suspend_promise =
+ context->suspendContext(scope.GetScriptState(), 0.0, ASSERT_NO_EXCEPTION);
+ ScriptPromiseTester suspend_tester(scope.GetScriptState(), suspend_promise);
+
+ ScriptPromise<AudioBuffer> render_promise = context->startOfflineRendering(
+ scope.GetScriptState(), ASSERT_NO_EXCEPTION);
+ ScriptPromiseTester render_tester(scope.GetScriptState(), render_promise);
+
+ // Wait for suspend to trigger.
+ suspend_tester.WaitUntilSettled();
+ EXPECT_TRUE(suspend_tester.IsFulfilled());
+
+ // Trigger allocation failure mid-render (while suspended).
+ context->SetAllocationFailed();
+
+ // Resume rendering.
+ ScriptPromise<IDLUndefined> resume_promise =
+ context->resumeContext(scope.GetScriptState(), ASSERT_NO_EXCEPTION);
+ ScriptPromiseTester resume_tester(scope.GetScriptState(), resume_promise);
+ resume_tester.WaitUntilSettled();
+ EXPECT_TRUE(resume_tester.IsFulfilled());
+
+ // Wait for rendering completion.
+ render_tester.WaitUntilSettled();
+ EXPECT_TRUE(render_tester.IsFulfilled());
+
+ auto* buffer = ToScriptWrappable<AudioBuffer>(
+ scope.GetIsolate(), render_tester.Value().V8Value().As<v8::Object>());
+ ASSERT_TRUE(buffer);
+ EXPECT_EQ(buffer->numberOfChannels(), 2u);
+ EXPECT_EQ(buffer->length(), 256u);
+
+ for (unsigned ch = 0; ch < buffer->numberOfChannels(); ++ch) {
+ auto array = buffer->getChannelData(ch);
+ ASSERT_TRUE(array);
+ for (float sample : array->AsSpan()) {
+ EXPECT_EQ(sample, 0.0f);
+ }
+ }
+}
+
} // namespace blink
Regression Test / PoC
diff --git a/third_party/blink/renderer/modules/webaudio/offline_audio_context_test.cc b/third_party/blink/renderer/modules/webaudio/offline_audio_context_test.cc
index 12db957..11c957347 100644
--- a/third_party/blink/renderer/modules/webaudio/offline_audio_context_test.cc
+++ b/third_party/blink/renderer/modules/webaudio/offline_audio_context_test.cc
@@ -6,13 +6,16 @@
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/public/platform/web_runtime_features.h"
+#include "third_party/blink/renderer/bindings/core/v8/script_promise_tester.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_offline_audio_context_options.h"
#include "third_party/blink/renderer/bindings/modules/v8/v8_union_audiocontextrendersizecategory_unsignedlong.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/testing/page_test_base.h"
+#include "third_party/blink/renderer/modules/webaudio/audio_buffer.h"
#include "third_party/blink/renderer/platform/bindings/exception_state.h"
+#include "third_party/blink/renderer/platform/bindings/script_wrappable.h"
namespace blink {
@@ -91,4 +94,57 @@
EXPECT_EQ(context->renderQuantumSize(), 256u);
}
+TEST_F(OfflineAudioContextTest, EarlyCompletionZeroesDestinationBuffer) {
+ V8TestingScope scope;
+
+ OfflineAudioContextOptions* options = OfflineAudioContextOptions::Create();
+ options->setNumberOfChannels(2);
+ options->setLength(256);
+ options->setSampleRate(44100.0f);
+ OfflineAudioContext* context = OfflineAudioContext::Create(
+ GetFrame().DomWindow(), options, ASSERT_NO_EXCEPTION);
+ ASSERT_TRUE(context);
+
+ // Suspend at frame 0.
+ ScriptPromise<IDLUndefined> suspend_promise =
+ context->suspendContext(scope.GetScriptState(), 0.0, ASSERT_NO_EXCEPTION);
+ ScriptPromiseTester suspend_tester(scope.GetScriptState(), suspend_promise);
+
+ ScriptPromise<AudioBuffer> render_promise = context->startOfflineRendering(
+ scope.GetScriptState(), ASSERT_NO_EXCEPTION);
+ ScriptPromiseTester render_tester(scope.GetScriptState(), render_promise);
+
+ // Wait for suspend to trigger.
+ suspend_tester.WaitUntilSettled();
+ EXPECT_TRUE(suspend_tester.IsFulfilled());
+
+ // Trigger allocation failure mid-render (while suspended).
+ context->SetAllocationFailed();
+
+ // Resume rendering.
+ ScriptPromise<IDLUndefined> resume_promise =
+ context->resumeContext(scope.GetScriptState(), ASSERT_NO_EXCEPTION);
+ ScriptPromiseTester resume_tester(scope.GetScriptState(), resume_promise);
+ resume_tester.WaitUntilSettled();
+ EXPECT_TRUE(resume_tester.IsFulfilled());
+
+ // Wait for rendering completion.
+ render_tester.WaitUntilSettled();
+ EXPECT_TRUE(render_tester.IsFulfilled());
+
+ auto* buffer = ToScriptWrappable<AudioBuffer>(
+ scope.GetIsolate(), render_tester.Value().V8Value().As<v8::Object>());
+ ASSERT_TRUE(buffer);
+ EXPECT_EQ(buffer->numberOfChannels(), 2u);
+ EXPECT_EQ(buffer->length(), 256u);
+
+ for (unsigned ch = 0; ch < buffer->numberOfChannels(); ++ch) {
+ auto array = buffer->getChannelData(ch);
+ ASSERT_TRUE(array);
+ for (float sample : array->AsSpan()) {
+ EXPECT_EQ(sample, 0.0f);
+ }
+ }
+}
+
} // namespace blink
Original Bug Report
Uninitialized heap disclosure in OfflineAudioContext on early render completion
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: OfflineAudioContext allocates its destination AudioBuffer uninitialized, relying on the background render loop to overwrite all frames before exposing it to JavaScript. If an allocation failure flag is set mid-render (e.g., during a scheduled suspension when node creation encounters memory pressure), the render loop aborts early without rendering or clearing remaining frames. The context then unconditionally resolves the startRendering() promise with the uninitialized buffer, potentially exposing recycled PartitionAlloc memory to web script.
Affected files:
third_party/blink/renderer/modules/webaudio/offline_audio_context.ccthird_party/blink/renderer/modules/webaudio/offline_audio_destination_handler.ccthird_party/blink/renderer/modules/webaudio/audio_buffer.ccthird_party/blink/renderer/modules/webaudio/audio_node_input.ccthird_party/blink/renderer/modules/webaudio/base_audio_context.h
Estimated timestamp from git blame: 2026-05-19
Summary
When OfflineAudioContext::startOfflineRendering() is called, it allocates its destination AudioBuffer uninitialized via AudioBuffer::CreateUninitialized(), expecting the audio rendering loop to overwrite every frame before exposing the buffer to web script. However, if the context’s has_allocation_failed_ flag is set mid-render (for example, during a scheduled suspension at frame 0 where node creation encounters memory pressure), OfflineAudioDestinationHandler::DoOfflineRendering() aborts early. OfflineAudioContext::FireCompletionEvent() then unconditionally resolves the startRendering() promise and dispatches completion events with the uninitialized AudioBuffer, potentially disclosing stale heap contents from Blink’s ArrayBufferPartition.
Root Cause Analysis
-
Uninitialized Destination Allocation: In
third_party/blink/renderer/modules/webaudio/offline_audio_context.cc:254-255:AudioBuffer* render_target = AudioBuffer::CreateUninitialized( number_of_channels, total_render_frames_, sample_rate);AudioBuffer::CreateUninitialized(audio_buffer.cc:131-148) constructs the underlyingDOMFloat32ArraywithInitializationPolicy::kDontInitialize, allocating memory fromPartitions::ArrayBufferPartition()withoutpartition_alloc::AllocFlags::kZeroFill(array_buffer_contents.cc:219-227). -
Early Termination on Allocation Failure Flag: At the beginning of the render loop (
third_party/blink/renderer/modules/webaudio/offline_audio_destination_handler.cc:174-177):if (Context()->HasAllocationFailed()) { FinishOfflineRendering(); return; }If
HasAllocationFailed()is true, rendering finishes immediately without rendering any frames or zeroing the buffer. -
Unconditional Exposure of Uninitialized Buffer:
FinishOfflineRendering()postsNotifyCompleteto the main thread (offline_audio_destination_handler.cc:227-235), invokingOfflineAudioContext::FireCompletionEvent()(offline_audio_context.cc:419-452):AudioBuffer* rendered_buffer = destinationNode()->DestinationBuffer(); DispatchEvent(*OfflineAudioCompletionEvent::Create(rendered_buffer)); complete_resolver_->Resolve(rendered_buffer);The method does not check
HasAllocationFailed(), does not verifyframes_processed_, and does not clear unrendered memory. -
Mid-Render Flag Setting via Graceful OOM: When an offline context is suspended at frame 0 via
ctx.suspend(0), the render thread suspends before writing any frames (frames_processed_ == 0). While suspended, JavaScript can create new nodes (e.g.ctx.createGain()). InAudioNodeInput::AudioNodeInput(audio_node_input.cc:39-47),AudioBus::TryCreate(1, RenderQuantumFrames())allocates viaPartitions::BufferTryAlignedZeroedMallocusingAllocFlags::kReturnNull. Under memory pressure, if this allocation fails gracefully,handler.Context()->SetAllocationFailed()setshas_allocation_failed_ = truepermanently on the context while falling back to a 0-frame bus without throwing a JavaScript exception. Whenctx.resume()is called,DoOfflineRenderingencounters the flag and triggers the early completion path.
Potential Steps to Trigger
Note: These steps are derived from static analysis of the Blink WebAudio subsystem; dynamic verification has not been performed by our automated tools.
- Instantiate an
OfflineAudioContextwith a large buffer size and configurable render quantum (e.g.,renderSizeHint: 262144). - Schedule a suspension at frame 0 using
ctx.suspend(0)and callp = ctx.startRendering(). - The render thread hits the suspension at frame 0 and resolves the
suspend(0)promise without rendering any audio data (frames_processed_ == 0). - In the
suspend(0)resolution callback, induce soft memory pressure in the Buffer partition (e.g., via large catchableArrayBufferallocations) and instantiate audio nodes (e.g.,ctx.createGain()orctx.createChannelMerger(32)). - When
AudioBus::TryCreate()fails gracefully insideAudioNodeInput,SetAllocationFailed()setshas_allocation_failed_ = trueon the context. - Release memory pressure and call
ctx.resume(). OfflineAudioDestinationHandler::DoOfflineRendering()detectsHasAllocationFailed()and immediately invokesFinishOfflineRendering().presolves with the uninitializedAudioBuffer. JavaScript can read raw, recycled heap bytes viarendered_buffer.getChannelData(0).
Impact
Potential disclosure of uninitialized ArrayBufferPartition heap memory (CWE-908) to web script in the renderer process. Recycled allocations in this partition can include stale data from prior typed arrays, WebAssembly memory instances, and PartitionAlloc freelist pointers.
Suggested Fix
- In
OfflineAudioDestinationHandler::FinishOfflineRendering()or whenContext()->HasAllocationFailed()is encountered, ensure any unrendered portions of the destination buffer ([frames_processed_, total_render_frames_)) are explicitly zeroed (e.g., callingshared_render_target_->Zero()). - Alternatively, in
OfflineAudioContext::FireCompletionEvent(), checkHasAllocationFailed()or verify whether rendering finished incompletely, and rejectcomplete_resolver_with a DOMException (e.g.,kNotSupportedError/kAbortError) rather than resolving with an uninitialized buffer.
Evaluated with Chrome root at commit: c6718ebbf2c677c6f63691e28c7801c563f6b19c
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
Data from false positives will be used to improve accuracy over time. And please feel free to reach out to me directly if you have concerns or feedback on the project.