Medium chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

Medium
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized Use in WebCodecs
DescriptionUninitialized Use in WebCodecs
ComponentWebCodecs
Bug ClassUninitialized Memory
Tracker486506202
Fix commitc0244cac701a (chromium/src) +46/-36
CISA KEVNot listed
CreditedIdentified by the Octane Security Team: Giovanni Vignone, Paolo Gentry, Robert van Eijk
Disclosed2026-04-07

Changed Functions

FunctionChangeNotes
if
media/base/frame_buffer_pool.cc
modified
for
media/base/frame_buffer_pool_unittest.cc
modified
if
media/filters/dav1d_video_decoder.cc
modified

Files Changed

  • media/base/frame_buffer_pool.cc
  • media/base/frame_buffer_pool_unittest.cc
  • media/filters/dav1d_video_decoder.cc
  • media/filters/vpx_video_decoder.cc
From c0244cac701a5bfadcd275dc10ee3d12ef593908 Mon Sep 17 00:00:00 2001
From: Eugene Zemtsov <[email protected]>
Date: Mon, 02 Mar 2026 19:16:41 -0800
Subject: [PATCH] media: Zero-initialize frames in software decoders to prevent info leaks

Pass `zero_initialize_memory=true` when creating FrameBufferPools for
libvpx and dav1d decoders. Also update AllocateAlphaPlaneForFrameBuffer
to respect this flag. This prevents potential heap information
disclosure from uninitialized padding bytes.

We already do it for ffmpeg decoder.

Bug: 486506202
Change-Id: I5e88f827f7043cfebef140524092e5543d7d03a3
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7616614
Reviewed-by: Dale Curtis <[email protected]>
Commit-Queue: Eugene Zemtsov <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1592929}
---

diff --git a/media/base/frame_buffer_pool.cc b/media/base/frame_buffer_pool.cc
index e90f070..ceb0313c 100644
--- a/media/base/frame_buffer_pool.cc
+++ b/media/base/frame_buffer_pool.cc
@@ -63,6 +63,33 @@
   base::TimeTicks last_use_time;
 };
 
+namespace {
+
+BytesArray AllocateMemory(size_t min_size,
+                          bool zero_initialize,
+                          bool force_error) {
+  if (force_error) {
+    return {};
+  }
+
+  uint8_t* data = nullptr;
+  const bool result =
+      zero_initialize
+          ? base::UncheckedCalloc(1u, min_size, reinterpret_cast<void**>(&data))
+          : base::UncheckedMalloc(min_size, reinterpret_cast<void**>(&data));
+
+  // Unclear why, but the docs indicate both that `data` will be null on
+  // failure, and also that the return value must not be discarded.
+  if (!result || !data) {
+    return {};
+  }
+
+  // SAFETY: We have just allocated `min_size` of memory for `data`.
+  return UNSAFE_BUFFERS(BytesArray::FromOwningPointer(data, min_size));
+}
+
+}  // namespace
+
 FrameBufferPool::FrameBufferPool(bool zero_initialize_memory)
     : zero_initialize_memory_(zero_initialize_memory),
       tick_clock_(base::DefaultTickClock::GetInstance()) {}
@@ -98,35 +125,15 @@
   frame_buffer->held_by_library = true;
   if (frame_buffer->data.size() < min_size) {
     // Free the existing |data| first so that the memory can be reused,
-    // if possible. Note that the new array is purposely not initialized.
+    // if possible.
     frame_buffer->data = {};
+    frame_buffer->data = AllocateMemory(min_size, zero_initialize_memory_,
+                                        force_allocation_error_);
 
-    uint8_t* data = nullptr;
-    if (!force_allocation_error_) {
-      bool result = false;
-      if (zero_initialize_memory_) {
-        result = base::UncheckedCalloc(1u, min_size,
-                                       reinterpret_cast<void**>(&data));
-      } else {
-        result =
-            base::UncheckedMalloc(min_size, reinterpret_cast<void**>(&data));
-      }
-
-      // Unclear why, but the docs indicate both that `data` will be null on
-      // failure, and also that the return value must not be discarded.
-      if (!result) {
-        data = nullptr;
-      }
-    }
-
-    if (!data) {
+    if (frame_buffer->data.empty()) {
       frame_buffers_.erase(it);
       return {};
     }
-
-    // SAFETY: We have just allocated `min_size` of memory for `data`.
-    frame_buffer->data =
-        UNSAFE_BUFFERS(BytesArray::FromOwningPointer(data, min_size));
   }
 
   // Provide the client with a private identifier.
@@ -158,17 +165,10 @@
   DCHECK(IsUsedLocked(frame_buffer));
   if (frame_buffer->alpha_data.size() < min_size) {
     // Free the existing |alpha_data| first so that the memory can be reused,
-    // if possible. Note that the new array is purposely not initialized.
+    // if possible.
     frame_buffer->alpha_data = {};
-    uint8_t* data = nullptr;
-    if (force_allocation_error_ ||
-        !base::UncheckedMalloc(min_size, reinterpret_cast<void**>(&data)) ||
-        !data) {
-      return {};
-    }
-    // SAFETY: We have just allocated `min_size` of memory for `data`.
-    frame_buffer->alpha_data =
-        UNSAFE_BUFFERS(BytesArray::FromOwningPointer(data, min_size));
+    frame_buffer->alpha_data = AllocateMemory(min_size, zero_initialize_memory_,
+                                              force_allocation_error_);
   }
   return frame_buffer->alpha_data;
 }
diff --git a/media/base/frame_buffer_pool_unittest.cc b/media/base/frame_buffer_pool_unittest.cc
index a5b7bff2..893e941 100644
--- a/media/base/frame_buffer_pool_unittest.cc
+++ b/media/base/frame_buffer_pool_unittest.cc
@@ -131,6 +131,14 @@
     nonzero |= !!buf[i];
   }
   EXPECT_FALSE(nonzero);
+
+  auto alpha_buf = pool->AllocateAlphaPlaneForFrameBuffer(kBufferSize, priv1);
+  nonzero = false;
+  for (size_t i = 0; i < kBufferSize; i++) {
+    nonzero |= !!alpha_buf[i];
+  }
+  EXPECT_FALSE(nonzero);
+
   pool->Shutdown();
 }
 
diff --git a/media/filters/dav1d_video_decoder.cc b/media/filters/dav1d_video_decoder.cc
index baef967..3456cd5 100644
--- a/media/filters/dav1d_video_decoder.cc
+++ b/media/filters/dav1d_video_decoder.cc
@@ -285,7 +285,8 @@
   }
 
   if (!frame_pool_) {
-    frame_pool_ = base::MakeRefCounted<FrameBufferPool>();
+    frame_pool_ =
+        base::MakeRefCounted<FrameBufferPool>(/*zero_initialize_memory=*/true);
   }
 
   // Clear any previously initialized decoder.
diff --git a/media/filters/vpx_video_decoder.cc b/media/filters/vpx_video_decoder.cc
index 14b4044..d197a8feb 100644
--- a/media/filters/vpx_video_decoder.cc
+++ b/media/filters/vpx_video_decoder.cc
@@ -245,7 +245,8 @@
            VPX_CODEC_CAP_EXTERNAL_FRAME_BUFFER);
 
     DCHECK(!memory_pool_);
-    memory_pool_ = base::MakeRefCounted<FrameBufferPool>();
+    memory_pool_ =
+        base::MakeRefCounted<FrameBufferPool>(/*zero_initialize_memory=*/true);
 
     if (vpx_codec_set_frame_buffer_functions(
             vpx_codec_.get(), &GetVP9FrameBuffer, &ReleaseVP9FrameBuffer,
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/media/base/frame_buffer_pool_unittest.cc b/media/base/frame_buffer_pool_unittest.cc
index a5b7bff2..893e941 100644
--- a/media/base/frame_buffer_pool_unittest.cc
+++ b/media/base/frame_buffer_pool_unittest.cc
@@ -131,6 +131,14 @@
     nonzero |= !!buf[i];
   }
   EXPECT_FALSE(nonzero);
+
+  auto alpha_buf = pool->AllocateAlphaPlaneForFrameBuffer(kBufferSize, priv1);
+  nonzero = false;
+  for (size_t i = 0; i < kBufferSize; i++) {
+    nonzero |= !!alpha_buf[i];
+  }
+  EXPECT_FALSE(nonzero);
+
   pool->Shutdown();
 }
Loading diff…

Original Bug Report

reported by [email protected]

Uninitialized alpha-plane padding in VP9-with-alpha external frame buffer decode causes renderer heap data disclosure via WebCodecs VideoFrame.copyTo(codedRect)


Report description

Uninitialized alpha-plane padding in VP9-with-alpha external frame buffer decode causes renderer heap data disclosure via WebCodecs VideoFrame.copyTo(codedRect)


Bug location

Where do you want to report your vulnerability?

Chrome VRP – Report security issues affecting the Chrome browser. See program rules

Which URL (or repository) have you found the vulnerability in?

https://github.com/chromium/chromium


The problem

Please describe the technical details of the vulnerability

When decoding WebM VP9 content with alpha enabled, Chromium allocates an uninitialized alpha-plane buffer and fills only the visible-width alpha pixels. If the decoded frame’s coded width is greater than its visible width (e.g. due to stride alignment), the per-row padding bytes in the alpha plane are never written and remain uninitialized. WebCodecs VideoFrame.copyTo() allows JavaScript to request a copy using a rectangle bounded by the frame’s coded size. For CPU-backed (mappable) frames, the Blink copy path copies src_rect.width() bytes per row from each plane and does not clamp to the visible rect. By calling copyTo() with a rect that spans the full coded width and height, an attacker can copy and then read the uninitialized alpha-plane padding bytes from JavaScript, resulting in renderer heap information disclosure.

Root cause chain

  1. WebM / demuxer: When the track has AlphaMode = 1 and a block has block-additional data whose first 8 bytes (BlockAddID, big endian) equal 1, the remainder is stored in DecoderBuffer::side_data()->alpha_data (see media/formats/webm/webm_cluster_parser.cc and webm_video_client.cc).

  2. Decoder: In VpxVideoDecoder, the VP9 external frame buffer path allocates the alpha plane in FrameBufferPool::AllocateAlphaPlaneForFrameBuffer() (media/base/frame_buffer_pool.cc), which uses base::UncheckedMalloc() and does not initialize the memory (comment: “the new array is purposely not initialized”). The alpha plane is then filled via libyuv::CopyPlane() with width = vpx_image_alpha->d_w (visible width) and height = d_h. The allocation size is stride × height (e.g. vpx_image_alpha->stride[VPX_PLANE_Y] * d_h). When stride (coded width) > d_w, the bytes at the end of each row are never written.

  3. VideoFrame: The resulting VideoFrame has coded_size() from (vpx_image->w, vpx_image->d_h) and visible_rect() from (d_w, d_h). So coded_size.width() can exceed visible_rect().width().

  4. WebCodecs: In Blink, VideoFrame.copyTo(options) validates the source rect against the frame’s coded size (ParseCopyToOptionsToGfxRect(..., frame.coded_size(), ...)). For mappable frames, CopyMappablePlanes() copies PlaneSize(src_rect.width(), ...) bytes per row from each plane (third_party/blink/renderer/modules/webcodecs/video_frame.cc). No clamping to the visible rect is applied, so a rect covering the full coded size causes the uninitialized padding to be copied into the destination buffer and exposed to script.

Precondition: The decoded frame has coded width > visible width (e.g. odd or non-aligned width such as 9 or 17). When this holds, the read is deterministic; the content of the leaked bytes depends on allocator state (prior heap usage).

Impact analysis

Who can exploit it

Any attacker who can run JavaScript in the renderer can exploit this, for example by hosting a page that the victim visits (e.g. via link, ad, or compromised site), or by utilizing an XSS primitive or other bug that allows execution of script in a Chrome tab. No special permissions or user interaction beyond loading the page are required.

What they gain

The attacker gains read access to uninitialized renderer heap memory (the alpha-plane padding). That can include pointers (e.g. heap addresses), which can support ASLR bypass or heap layout inference, leftover data from previous allocations (strings, structures, etc.), and stable information disclosure when the same allocation patterns are repeated (e.g. repeated decoding with the same or similar frame sizes). By itself, it does not escape the renderer sandbox or escalate privileges, but it turns an information-disclosure primitive into javascript-readable data that can support further exploitation (e.g. heap grooming, pointer leaks for a separate bug).

Severity Analysis

Given the Severity Guidelines for Security Issues, a medium severity classification is well-supported for this finding.


The cause

What version of Chrome have you found the security issue in?

Current Live Stable Release: 145.0.7632.110 and 145.0.7632.103

No, it is not related to a crash.

Choose the type of vulnerability

Information Leak

How would you like to be publicly acknowledged for your report?

Identified by the Octane Security Team: Giovanni Vignone, Paolo Gentry, Robert van Eijk

View on issue tracker