CVE-2026-5888
Overview
Background
- `FrameBufferPool`
- A
mediahelper that allocates and recycles backing buffers for decoded video frames, shared by the software video decoders. - `zero_initialize_memory_`
- A per-pool boolean that selects whether new buffers are allocated with
base::UncheckedCalloc(zeroed) orbase::UncheckedMalloc(uninitialized). - Alpha plane
- An optional extra buffer that holds per-pixel transparency for a decoded frame, allocated via
AllocateAlphaPlaneForFrameBuffer. - Padding bytes
- Bytes inside a reused/over-sized buffer that the decoder writes pixel data into but may not fully overwrite, leaving residual heap contents.
Root Cause Analysis
The dav1d (AV1) and libvpx (VP8/VP9) software decoders constructed their FrameBufferPool with the default constructor, which left zero_initialize_memory_ false, so the main frame-buffer path allocated memory with base::UncheckedMalloc and the removed comment explicitly noted “the new array is purposely not initialized.” Because a decoded frame’s strides, visible region, and padding need not cover every allocated byte, any bytes the decoder failed to write retained whatever heap contents previously lived there, and those frames are observable to web content (e.g. drawn to a canvas and read back).
The fix passes /*zero_initialize_memory=*/true when the dav1d and vpx decoders create their pools, and it also routes both the frame-buffer and the alpha-plane allocations through a new shared AllocateMemory helper so the alpha plane now honors the same flag; previously AllocateAlphaPlaneForFrameBuffer unconditionally used base::UncheckedMalloc and never zeroed. The invariant restored is that buffers handed to a decoder (and ultimately to the page) begin fully zeroed, so uninitialized heap bytes can no longer survive into a presented frame.
zero_initialize_memory_ and makes the alpha-plane path respect it through the shared AllocateMemory helper.Attack Path
- Craft media that under-fills buffers
An attacker serves VP8/VP9 or AV1 content (via WebCodecs or a
<video>/media element) whose plane geometry leaves padding or stride bytes that the decoder does not overwrite. - Force an uninitialized allocation
Decoding triggers
FrameBufferPool::GetFrameBuffer(orAllocateAlphaPlaneForFrameBuffer) to allocate a new, non-zeroed buffer viabase::UncheckedMalloc, which still contains prior heap contents. - Read back residual bytes The decoded frame, including its uninitialized padding, is exposed to the page; reading it back (e.g. via canvas pixel access) discloses leftover heap memory (inferred as to what specific allocations it reveals).
Impact Assessment
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
Audit Directions
- Allocator flag parity across siblingsWhen one decoder/pool already zero-initializes (here, the ffmpeg decoder) and a sibling does not, audit every construction site of the shared pool/allocator for a missing flag.
- Flag-ignoring allocation pathsWhere a type carries a “zero on allocate” flag, check that every allocation method (main buffer, alpha/aux planes, resize paths) actually consults it rather than hard-coding
malloc. - Buffer reuse and partial writesAudit recycled/over-sized buffers whose consumers write only a geometry-dependent subset of bytes, since stride and padding gaps are classic uninitialized-leak sources that reach the page.
Patch
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,
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