Medium chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
—
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized resource in WebAudio
DescriptionUninitialized resource in WebAudio
ComponentWebAudio
Sub-componentWebAudio
Bug ClassUninitialized Memory
Tracker553156221
Fix commit341a2ce649d7 (chromium/src) +74/-8
CISA KEVNot listed
CreditedGoogle
Disclosed2026-10-06

Changed Functions

FunctionChangeNotes
for
third_party/blink/renderer/modules/webaudio/audio_buffer.cc
modified
while
third_party/blink/renderer/modules/webaudio/offline_audio_destination_handler.cc
modified
TEST_F
third_party/blink/renderer/modules/webaudio/offline_audio_context_test.cc
modified

Files Changed

  • third_party/blink/renderer/modules/webaudio/offline_audio_context_test.cc
  • third_party/blink/renderer/modules/webaudio/audio_buffer.cc
  • third_party/blink/renderer/modules/webaudio/audio_buffer.h
  • third_party/blink/renderer/modules/webaudio/offline_audio_destination_handler.cc
From 341a2ce649d7971be32a549389dee5f24d3c2755 Mon Sep 17 00:00:00 2001
From: Michael Wilson <[email protected]>
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 <[email protected]>
Reviewed-by: Hongchan Choi <[email protected]>
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
Loading diff…

Regression Test / PoC

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

Original Bug Report

reported by [email protected]

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.cc
  • third_party/blink/renderer/modules/webaudio/offline_audio_destination_handler.cc
  • third_party/blink/renderer/modules/webaudio/audio_buffer.cc
  • third_party/blink/renderer/modules/webaudio/audio_node_input.cc
  • third_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

  1. 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 underlying DOMFloat32Array with InitializationPolicy::kDontInitialize, allocating memory from Partitions::ArrayBufferPartition() without partition_alloc::AllocFlags::kZeroFill (array_buffer_contents.cc:219-227).

  2. 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.

  3. Unconditional Exposure of Uninitialized Buffer: FinishOfflineRendering() posts NotifyComplete to the main thread (offline_audio_destination_handler.cc:227-235), invoking OfflineAudioContext::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 verify frames_processed_, and does not clear unrendered memory.

  4. 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()). In AudioNodeInput::AudioNodeInput (audio_node_input.cc:39-47), AudioBus::TryCreate(1, RenderQuantumFrames()) allocates via Partitions::BufferTryAlignedZeroedMalloc using AllocFlags::kReturnNull. Under memory pressure, if this allocation fails gracefully, handler.Context()->SetAllocationFailed() sets has_allocation_failed_ = true permanently on the context while falling back to a 0-frame bus without throwing a JavaScript exception. When ctx.resume() is called, DoOfflineRendering encounters 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.

  1. Instantiate an OfflineAudioContext with a large buffer size and configurable render quantum (e.g., renderSizeHint: 262144).
  2. Schedule a suspension at frame 0 using ctx.suspend(0) and call p = ctx.startRendering().
  3. The render thread hits the suspension at frame 0 and resolves the suspend(0) promise without rendering any audio data (frames_processed_ == 0).
  4. In the suspend(0) resolution callback, induce soft memory pressure in the Buffer partition (e.g., via large catchable ArrayBuffer allocations) and instantiate audio nodes (e.g., ctx.createGain() or ctx.createChannelMerger(32)).
  5. When AudioBus::TryCreate() fails gracefully inside AudioNodeInput, SetAllocationFailed() sets has_allocation_failed_ = true on the context.
  6. Release memory pressure and call ctx.resume().
  7. OfflineAudioDestinationHandler::DoOfflineRendering() detects HasAllocationFailed() and immediately invokes FinishOfflineRendering().
  8. p resolves with the uninitialized AudioBuffer. JavaScript can read raw, recycled heap bytes via rendered_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

  1. In OfflineAudioDestinationHandler::FinishOfflineRendering() or when Context()->HasAllocationFailed() is encountered, ensure any unrendered portions of the destination buffer ([frames_processed_, total_render_frames_)) are explicitly zeroed (e.g., calling shared_render_target_->Zero()).
  2. Alternatively, in OfflineAudioContext::FireCompletionEvent(), check HasAllocationFailed() or verify whether rendering finished incompletely, and reject complete_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.

View on issue tracker