CVE-2025-3619
Overview
Background
- MediaFoundationVideoEncodeAccelerator
- The Windows Media Foundation-backed hardware video encoder in Chrome’s GPU process, which the commit “spanifies” (converts raw pointer + size pairs into
base::span). - `base::span`
- A bounds-carrying view over a contiguous buffer (pointer + length) that keeps size and data together so subspan/access operations can be range-checked instead of relying on a separately-passed
size_t. - `CopyD3D11TexToMem`
- A helper in
gpu/ipc/common/dxgi_helpers.ccthat maps a D3D11 NV12 texture and copies it into a destination shared-memory buffer vialibyuv::NV12Copy. - NV12
- A planar YUV format with a full-size Y plane followed by an interleaved UV plane at half height; the copy destination must hold
Height * Width * 3 / 2bytes.
Root Cause Analysis
Before the patch, CopyD3D11TexToMem received the destination buffer as a raw uint8_t* dst_buffer and a separately-passed size_t buffer_size, so the length and the pointer were decoupled. The function computed the UV plane destination with raw pointer arithmetic (dst_buffer + texture_desc.Height * dest_stride) passed to libyuv::NV12Copy, trusting that the caller-supplied buffer_size accurately described the memory behind dst_buffer. In the MediaFoundationVideoEncodeAccelerator path, the underlying IMFMediaBuffer was locked via MediaBufferScopedPointer whose bounds tracking mixed max_length and current_length, and VideoFrame::WrapExternalYuvData accepted raw y_data/uv_data pointers wrapped into spans only with UNSAFE_TODO, so plane sizes were derived from strides rather than the real allocation. When texture dimensions (and thus copy_size) exceeded the true capacity of the destination, the NV12Copy writes to the Y and UV plane pointers ran past the buffer, producing an out-of-bounds write.
The fix threads a single base::span<uint8_t> dst_buffer end to end, replaces manual UV offsetting with dst_buffer.subspan(...), and adds a span-based WrapExternalYuvData overload plus a corrected MediaBufferScopedPointer that stores a base::raw_span sized by max_length, so bounds travel with the buffer.
base::span/raw_span closes the gap where NV12 plane arithmetic could write out of bounds.Attack Path
- Reach the encoder
A renderer drives hardware video encoding (e.g. WebCodecs/WebRTC) so frames flow to
MediaFoundationVideoEncodeAcceleratorin the GPU process. - Control texture geometry
The attacker influences the input frame/texture dimensions such that
copy_size = Height * Width * 3 / 2is computed for a copy into a destination buffer whose real capacity is smaller than assumed. - Defeat the stale size check
Because
buffer_sizeis passed independently ofdst_buffer, the guardbuffer_size < copy_sizereflects a claimed size rather than the true allocation, allowing the copy to proceed. - Trigger the OOB write
libyuv::NV12Copywrites the Y plane and the UV plane atdst_buffer + Height * dest_stride, overrunning the destination buffer in the GPU process. - Corrupt memory The out-of-bounds write clobbers adjacent GPU-process memory, enabling potential memory corruption and exploitation.
Impact Assessment
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifgpu/ipc/common/dxgi_helpers.cc |
modified | |
ifmedia/base/video_frame.cc |
modified | |
current_length_media/base/win/mf_helpers.cc |
modified |
Files Changed
gpu/ipc/common/dxgi_helpers.ccgpu/ipc/common/dxgi_helpers.hmedia/base/video_frame.ccmedia/base/video_frame.hmedia/base/win/mf_helpers.cc
Audit Directions
- Decoupled pointer + size across API boundariesFlag media/GPU functions that pass a
uint8_t*buffer and itssize_tlength as separate parameters; the checked length can drift from the real allocation, so preferbase::spanend to end. - NV12/planar plane pointer arithmeticReview manual UV/plane offset math like
ptr + Height * stridebeforelibyuvcopies; verify the offset and the copy length stay within the destination’s true capacity, ideally viasubspan. - `UNSAFE_TODO`/`UNSAFE_BUFFERS` span constructionAudit remaining
base::span(ptr, size)built from strides ormax_length/current_length(e.g. lockedIMFMediaBufferregions) to confirm the size reflects the actual backing memory, not a caller-claimed value.
Patch
From b094accf6189985f07e7bfe576c6a11001099896 Mon Sep 17 00:00:00 2001
From: Eugene Zemtsov <eugene@chromium.org>
Date: Thu, 10 Apr 2025 19:59:52 -0700
Subject: [PATCH] media: Spanify MediaFoundationVideoEncodeAccelerator
Bug: 409619251, 40285824, 338570700
Change-Id: Icbe9b7deb2d485f11327d1e233b5629480b40aad
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6448912
Commit-Queue: Eugene Zemtsov <eugene@chromium.org>
Reviewed-by: Dale Curtis <dalecurtis@chromium.org>
Reviewed-by: Mustafa Emre Acer <meacer@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1445651}
---
diff --git a/gpu/ipc/common/dxgi_helpers.cc b/gpu/ipc/common/dxgi_helpers.cc
index c152b2c3..ac2c0a4 100644
--- a/gpu/ipc/common/dxgi_helpers.cc
+++ b/gpu/ipc/common/dxgi_helpers.cc
@@ -81,9 +81,6 @@
Microsoft::WRL::ComPtr<ID3D11Texture2D>* staging_texture) {
DCHECK(d3d11_device);
- uint8_t* dest_buffer = shared_memory.data();
- size_t dst_buffer_size = shared_memory.size_bytes();
-
Microsoft::WRL::ComPtr<ID3D11Device1> device1;
HRESULT hr = d3d11_device->QueryInterface(IID_PPV_ARGS(&device1));
if (FAILED(hr)) {
@@ -100,19 +97,18 @@
return false;
}
- return CopyD3D11TexToMem(texture.Get(), dest_buffer, dst_buffer_size,
- d3d11_device, staging_texture);
+ return CopyD3D11TexToMem(texture.Get(), shared_memory, d3d11_device,
+ staging_texture);
}
bool CopyD3D11TexToMem(
ID3D11Texture2D* src_texture,
- uint8_t* dst_buffer,
- size_t buffer_size,
+ base::span<uint8_t> dst_buffer,
ID3D11Device* d3d11_device,
Microsoft::WRL::ComPtr<ID3D11Texture2D>* staging_texture) {
DCHECK(d3d11_device);
DCHECK(staging_texture);
- DCHECK(dst_buffer);
+ DCHECK(!dst_buffer.empty());
DCHECK(src_texture);
D3D11_TEXTURE2D_DESC texture_desc = {};
@@ -124,7 +120,7 @@
return false;
}
size_t copy_size = texture_desc.Height * texture_desc.Width * 3 / 2;
- if (buffer_size < copy_size) {
+ if (dst_buffer.size() < copy_size) {
DLOG(ERROR) << "Invalid buffer size for copy.";
return false;
}
@@ -199,12 +195,12 @@
const uint32_t source_stride = mapped_resource.RowPitch;
const uint32_t dest_stride = texture_desc.Width;
- return libyuv::NV12Copy(source_buffer, source_stride,
- source_buffer + texture_desc.Height * source_stride,
- source_stride, dst_buffer, dest_stride,
- dst_buffer + texture_desc.Height * dest_stride,
- dest_stride, texture_desc.Width,
- texture_desc.Height) == 0;
+ return libyuv::NV12Copy(
+ source_buffer, source_stride,
+ source_buffer + texture_desc.Height * source_stride, source_stride,
+ dst_buffer.data(), dest_stride,
+ dst_buffer.subspan(texture_desc.Height * dest_stride).data(),
+ dest_stride, texture_desc.Width, texture_desc.Height) == 0;
}
GPU_EXPORT bool CopyShMemToDXGIBuffer(base::span<uint8_t> shared_memory,
diff --git a/gpu/ipc/common/dxgi_helpers.h b/gpu/ipc/common/dxgi_helpers.h
index 1d580019..0e815f27 100644
--- a/gpu/ipc/common/dxgi_helpers.h
+++ b/gpu/ipc/common/dxgi_helpers.h
@@ -70,8 +70,7 @@
// input texture size or format. Returns true if succeeded.
GPU_EXPORT bool CopyD3D11TexToMem(
ID3D11Texture2D* input_texture,
- uint8_t* dst_buffer,
- size_t buffer_size,
+ base::span<uint8_t> dst_buffer,
ID3D11Device* d3d11_device,
Microsoft::WRL::ComPtr<ID3D11Texture2D>* staging_texture);
diff --git a/media/base/video_frame.cc b/media/base/video_frame.cc
index c50826a..31b7b3de 100644
--- a/media/base/video_frame.cc
+++ b/media/base/video_frame.cc
@@ -785,6 +785,8 @@
return frame;
}
+// TODO(crbug.com/338570700): This method needs to be remove in favour
+// of its span version.
// static
scoped_refptr<VideoFrame> VideoFrame::WrapExternalYuvData(
VideoPixelFormat format,
@@ -796,6 +798,31 @@
const uint8_t* y_data,
const uint8_t* uv_data,
base::TimeDelta timestamp) {
+ auto layout = VideoFrameLayout::CreateWithStrides(format, coded_size,
+ {y_stride, uv_stride});
+ if (!layout) {
+ DLOG(ERROR) << "Invalid layout.";
+ return nullptr;
+ }
+
+ return WrapExternalYuvData(
+ format, coded_size, visible_rect, natural_size, y_stride, uv_stride,
+ UNSAFE_TODO(base::span(y_data, layout->planes()[Plane::kY].size)),
+ UNSAFE_TODO(base::span(uv_data, layout->planes()[Plane::kUV].size)),
+ timestamp);
+}
+
+// static
+scoped_refptr<VideoFrame> VideoFrame::WrapExternalYuvData(
+ VideoPixelFormat format,
+ const gfx::Size& coded_size,
+ const gfx::Rect& visible_rect,
+ const gfx::Size& natural_size,
+ size_t y_stride,
+ size_t uv_stride,
+ base::span<const uint8_t> y_data,
+ base::span<const uint8_t> uv_data,
+ base::TimeDelta timestamp) {
const StorageType storage = STORAGE_UNOWNED_MEMORY;
if (!IsValidConfig(format, storage, coded_size, visible_rect, natural_size)) {
DLOG(ERROR) << __func__ << " Invalid config."
@@ -819,11 +846,9 @@
auto frame = base::MakeRefCounted<VideoFrame>(base::PassKey<VideoFrame>(),
*layout, storage, visible_rect,
natural_size, timestamp);
- std::array<const uint8_t*, 2> data = {y_data, uv_data};
+ std::array<base::span<const uint8_t>, 2> data = {y_data, uv_data};
for (size_t plane = 0; plane < NumPlanes(format); ++plane) {
- // TODO(crbug.com/338570700): y_data, uv_data should be spans
- frame->data_[plane] =
- UNSAFE_TODO(base::span(data[plane], layout->planes()[plane].size));
+ frame->data_[plane] = data[plane];
}
return frame;
}
diff --git a/media/base/video_frame.h b/media/base/video_frame.h
index c44af122..4a7ca53 100644
--- a/media/base/video_frame.h
+++ b/media/base/video_frame.h
@@ -397,6 +397,17 @@
const uint8_t* uv_data,
base::TimeDelta timestamp);
+ static scoped_refptr<VideoFrame> WrapExternalYuvData(
+ VideoPixelFormat format,
+ const gfx::Size& coded_size,
+ const gfx::Rect& visible_rect,
+ const gfx::Size& natural_size,
+ size_t y_stride,
+ size_t uv_stride,
+ base::span<const uint8_t> y_data,
+ base::span<const uint8_t> uv_data,
+ base::TimeDelta timestamp);
+
// Wraps |gpu_memory_buffer|. This will transfer ownership of
// |gpu_memory_buffer| to the returned VideoFrame.
// For use in contexts where the GPUMemoryBuffer has no SharedImage
diff --git a/media/base/win/mf_helpers.cc b/media/base/win/mf_helpers.cc
index 11724a3..d1812736 100644
--- a/media/base/win/mf_helpers.cc
+++ b/media/base/win/mf_helpers.cc
@@ -271,16 +271,20 @@
}
MediaBufferScopedPointer::MediaBufferScopedPointer(IMFMediaBuffer* media_buffer)
- : media_buffer_(media_buffer),
- buffer_(nullptr),
- max_length_(0),
- current_length_(0) {
- HRESULT hr = media_buffer_->Lock(&buffer_.AsEphemeralRawAddr(), &max_length_,
- ¤t_length_);
+ : media_buffer_(media_buffer) {
+ uint8_t* buffer;
+ DWORD max_length;
+
+ HRESULT hr = media_buffer_->Lock(&buffer, &max_length, nullptr);
CHECK(SUCCEEDED(hr));
+
+ // SAFETY: `IMFMediaBuffer::Lock` docs states that `max_length` is the maximum
+ // amount of data that can be written to the buffer.
+ data_ = UNSAFE_BUFFERS(base::raw_span<uint8_t>(buffer, max_length));
}
Regression Test / PoC
diff --git a/media/gpu/windows/mf_video_processor_accelerator_unittest.cc b/media/gpu/windows/mf_video_processor_accelerator_unittest.cc
index d62cf58f..f7c7ccc 100644
--- a/media/gpu/windows/mf_video_processor_accelerator_unittest.cc
+++ b/media/gpu/windows/mf_video_processor_accelerator_unittest.cc
@@ -171,8 +171,8 @@
template <typename F>
void ValidateResult(IMFMediaBuffer* buffer, UINT size, F validation_func) {
MediaBufferScopedPointer scoped_buffer(buffer);
- ASSERT_EQ(scoped_buffer.current_length(), size);
- validation_func(scoped_buffer.get());
+ ASSERT_EQ(scoped_buffer.as_span().size(), size);
+ validation_func(scoped_buffer.as_span().data());
}
scoped_refptr<DXGIDeviceManager> dxgi_device_man_;
Original Bug Report
Buffer Overflow (GPU process) in Chrome Windows Media Foundation Encode Accelerator
Report description
Buffer Overflow (GPU process) in Chrome Windows Media Foundation Encode Accelerator
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://www.google.com/chrome/
The problem
Please describe the technical details of the vulnerability
Summary
There is a Buffer Overflow 0-day in Google Chrome on Windows. The Windows Media Foundation Video Encode Accelerator (which is used by default for H.264 encoding) estimates the buffer size required for encoded frames, and never explicitly checks for each encoded frame whether it actually fits in the provided buffer. This allows an attacker who can lure a victim using Windows to his HTML/JS page to trigger a buffer overflow in the highly privileged GPU process by calling the VideoEncoder in javascript using specific settings and high-entropy input data that will result in large encoded frame sizes. The buffer overflow is happening in the shared memory region in the virtual address space that used for inter-process communication.
Vulnerable Code
The vulnerable calls are memcpy here:
https://source.chromium.org/chromium/chromium/src/+/main:media/gpu/windows/media_foundation_video_encode_accelerator_win.cc;drc=34737a18832c71452ad1fdb4ca9970439daefcf4;l=2446
https://source.chromium.org/chromium/chromium/src/+/main:media/gpu/windows/media_foundation_video_encode_accelerator_win.cc;drc=34737a18832c71452ad1fdb4ca9970439daefcf4;l=797
The buffer size estimation happens here: https://source.chromium.org/chromium/chromium/src/+/main:media/video/video_encode_accelerator.cc;drc=1e05ab5d3b3951f622ec8f64f326310aa47fc3c5;l=274
While this estimate is sufficient for most cases, an attacker can specifically craft video data that will result in larger frames. The assumption that the encoded frame can never be larger than the input data plus headers is wrong, and the second limit based on the bitrate can be bypassed by using a large constant bitrate. However, in my opinion, the root issue is not the insufficient buffer size estimate, but the fact that an estimate is used at all without a rigid security check for each frame.
Proof of Concept
In our PoC, we need to make the encoder generate at least one oversized frame. I have appended two scripts: video13_configurable.html allows the user to set parameters for video encoding and will trigger on the click of a button. It will automatically and randomly generate high-entropy data to attempt the generation of an oversized frame. It also computes the buffer size using the methods from the source code (“Raw” and “Expected”, and “Final” is the maximum of those two that is used as the actual buffer size). video15_autoload.html will run without user interaction with the right settings (those that triggered the vulnerability on my machine) and a fixed seed for all random calls.
The exact parameters may depend on the browser version used as well as the Windows version, GPU and GPU driver version (everything that could affect how the Windows Media Foundation H.264 encoder is behaving). I executed my PoC on a GPU VPS (Basic GPU VPS - P600) from the hosting provider Database Mart LLC: https://www.vps-mart.com/gpu-server
The GPU used is NVIDIA Quadro P600. The driver version (according to nvidia-smi) is 556.39.
The Windows specifications are as follows:
Edition Windows 10 Pro
Version 22H2
Installed on 3/6/2025
OS build 19045.5608
Experience Windows Feature Experience Pack 1000.19061.1000.0
The Chrome version is the latest on the Stable Channel, at the time of writing:
135.0.7049.85 (Official Build) (64-bit) (cohort: 135.0.7049.84 Rollout)
Important note: I was unable to trigger the vulnerability on the exact same version of the Chromium ASAN build - not because it seems fixed (according to the source code, it is not), but because - according to the data of my exploit script - I failed to find parameters that make the encoder exceed the estimated frame size for this version. Apparently, Chromium and Chrome use different code somewhere in the H.264 encoding process, and Chromium generates smaller frames. I was able to generate larger frames in an older ASAN build of Chromium (111.0.5554.0). It should be noted that no ASAN stack trace was generated by this build when the vulnerability was triggered - which is not surprising as the buffer overflow occurs in the shared memory region, where ASAN is not designed to detect buffer overflows, to my knowledge.
Chrome should be started with the following command:
"C:\Program Files\Google\Chrome\Application\chrome.exe" --enable-logging=stderr --v=1
Use the following settings in the HTML page:
Width: 3400
Height: 100
Bitrate: 800
Framerate: 62
Bitrate Mode: Constant (CBR)
AVC Profile: High Profile (level 5.0)
The pattern settings can be left at default.
Once the vulnerability is triggered you will see a black-white flash covering the whole Chrome window and something like the following output in the command line:
[6824:7236:0409/152447.039:ERROR:gpu_process_host.cc(954)] GPU process exited unexpectedly: exit_code=-1073741819 [6824:7236:0409/152447.039:WARNING:gpu_process_host.cc(1395)] The GPU process has crashed 1 time(s)
[6824:7236:0409/152447.480:WARNING:gpu_process_host.cc(976)] Reinitialized the GPU process after a crash. The reported initialization time was 349 ms
I have appended a video that shows that the crash happens indeed exactly when we are expecting it according to our buffer size calculations, and otherwise the encoding functions normally.
Potential for ACE Exploitation
This buffer overflow may be exploitable for ACE, which could be very dangerous as it happens in a highly privileged process (GPU process). Furthermore, the buffer overflow happens inside the shared memory region for inter-process communication. This could allow an attacker to tamper with memory sections that are relevant for communicating with the main process, even if it is not possible to gain ACE in the GPU process.
Writing arbitrary data is only constrained by what the H.264 algorithms can produce. I do not expect this to be the major challenge here, but it certainly an additional barrier for an attacker that the submitted data goes through a mathematical transformation first. Furthermore, guard areas, ASLR and other protections might make exploitation hard or impossible. I will investigate this further in the coming weeks and follow up here if successful to (hopefully) receive an increased bounty. I decided to submit this initial report right now in order to allow you immediate mitigation, given the severity of this bug (and to reduce the risk of someone else having submitted this issue first).
Suggested Patch
Check whether the encoder output size is smaller or equal than bitstream_buffer_size_ before calling memcpy.
Impact analysis – Please briefly explain who can exploit the vulnerability, and what they gain when doing so
This vulnerability allows an attacker to trigger a buffer overflow in the highly privileged Chrome GPU process on Windows, which might in the worst case result in Arbitrary Code Execution in the security context of the GPU process. The only user interaction required is visiting a HTML/JS website.
The cause
What version of Chrome have you found the security issue in?
135.0.7049.85 [stable]
Is the security issue related to a crash?
Yes, it is related to a crash.
Choose the type of vulnerability
Memory Corruption
How would you like to be publicly acknowledged for your report?
Elias Hohl
- https://bughunters.google.com/about/rules/5745167867576320/chrome-vulnerability-reward-program-rules
- https://source.chromium.org/chromium/chromium/src/+/main:media/gpu/windows/media_foundation_video_encode_accelerator_win.cc;drc=34737a18832c71452ad1fdb4ca9970439daefcf4;l=2446
- https://source.chromium.org/chromium/chromium/src/+/main:media/gpu/windows/media_foundation_video_encode_accelerator_win.cc;drc=34737a18832c71452ad1fdb4ca9970439daefcf4;l=797
- https://source.chromium.org/chromium/chromium/src/+/main:media/video/video_encode_accelerator.cc;drc=1e05ab5d3b3951f622ec8f64f326310aa47fc3c5;l=274
- https://www.google.com/chrome/
- https://www.vps-mart.com/gpu-server