CVE-2026-5888
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifmedia/base/frame_buffer_pool.cc |
modified | |
formedia/base/frame_buffer_pool_unittest.cc |
modified | |
ifmedia/filters/dav1d_video_decoder.cc |
modified |
Files Changed
media/base/frame_buffer_pool.ccmedia/base/frame_buffer_pool_unittest.ccmedia/filters/dav1d_video_decoder.ccmedia/filters/vpx_video_decoder.cc
Patch
From c0244cac701a5bfadcd275dc10ee3d12ef593908 Mon Sep 17 00:00:00 2001
From: Eugene Zemtsov <eugene@chromium.org>
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 <dalecurtis@chromium.org>
Commit-Queue: Eugene Zemtsov <eugene@chromium.org>
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,
Regression Test / PoC
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();
}
Original Bug Report
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
-
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 inDecoderBuffer::side_data()->alpha_data(seemedia/formats/webm/webm_cluster_parser.ccandwebm_video_client.cc). -
Decoder: In
VpxVideoDecoder, the VP9 external frame buffer path allocates the alpha plane inFrameBufferPool::AllocateAlphaPlaneForFrameBuffer()(media/base/frame_buffer_pool.cc), which usesbase::UncheckedMalloc()and does not initialize the memory (comment: “the new array is purposely not initialized”). The alpha plane is then filled vialibyuv::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. -
VideoFrame: The resulting
VideoFramehascoded_size()from(vpx_image->w, vpx_image->d_h)andvisible_rect()from(d_w, d_h). Socoded_size.width()can exceedvisible_rect().width(). -
WebCodecs: In Blink,
VideoFrame.copyTo(options)validates the source rect against the frame’s coded size (ParseCopyToOptions→ToGfxRect(..., frame.coded_size(), ...)). For mappable frames,CopyMappablePlanes()copiesPlaneSize(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
Is the security issue related to a crash?
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