CVE-2026-16807
Overview
Files Changed
media/gpu/v4l2/v4l2_video_decoder_backend_stateless.ccmedia/gpu/v4l2/v4l2_video_decoder_backend_stateless.hmedia/gpu/vp8_decoder.cc
Patch
From ea9a69e41999d48fad7c4cf252b00b1301c3df6f Mon Sep 17 00:00:00 2001
From: Hirokazu Honda <hiroh@chromium.org>
Date: Mon, 15 Jun 2026 15:04:06 -0700
Subject: [PATCH] media/gpu/v4l2: Verify chroma and detect bit depth changes
This CL updates the V4L2 stateless video decoder to reject streams
with chroma subsampling other than 4:2:0, which are unsupported.
It also introduces bit depth tracking. Previously, changes in bit
depth that did not accompany a resolution change were ignored. Now,
a change in bit depth correctly triggers the resolution change
workflow so that buffers can be reallocated with the appropriate
format.
Bug: 518237034
Test: video.ChromeStackDecoder.*
Change-Id: I3c55372ec3a454b208215626b3962b14f71230d6
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7939189
Commit-Queue: Hirokazu Honda <hiroh@chromium.org>
Reviewed-by: Nathan Hebert <nhebert@chromium.org>
Auto-Submit: Hirokazu Honda <hiroh@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1647107}
---
diff --git a/media/gpu/v4l2/v4l2_video_decoder_backend_stateless.cc b/media/gpu/v4l2/v4l2_video_decoder_backend_stateless.cc
index 50d48c83..15f0b43e 100644
--- a/media/gpu/v4l2/v4l2_video_decoder_backend_stateless.cc
+++ b/media/gpu/v4l2/v4l2_video_decoder_backend_stateless.cc
@@ -24,6 +24,7 @@
#include "media/base/decoder_status.h"
#include "media/base/video_codecs.h"
#include "media/base/video_frame.h"
+#include "media/base/video_types.h"
#include "media/gpu/accelerated_video_decoder.h"
#include "media/gpu/chromeos/dmabuf_video_frame_pool.h"
#include "media/gpu/chromeos/video_frame_resource.h"
@@ -414,6 +415,11 @@
<< base::strict_cast<int>(decoder_->GetBitDepth());
return false;
}
+ if (decoder_->GetChromaSampling() != VideoChromaSampling::k420) {
+ VLOGF(2) << "Unsupported chroma sampling: "
+ << static_cast<int>(decoder_->GetChromaSampling());
+ return false;
+ }
if (profile_ != decoder_->GetProfile()) {
DVLOGF(3) << "Profile is changed: " << profile_ << " -> "
@@ -426,7 +432,8 @@
profile_ = decoder_->GetProfile();
}
- if (pic_size_ == decoder_->GetPicSize()) {
+ if (pic_size_ == decoder_->GetPicSize() &&
+ bit_depth_ == decoder_->GetBitDepth()) {
// There is no need to do anything in V4L2 API when only a profile is
// changed.
DVLOGF(3) << "Only profile is changed. No need to do anything.";
@@ -625,6 +632,8 @@
}
pic_size_ = decoder_->GetPicSize();
+ bit_depth_ = decoder_->GetBitDepth();
+
client_->CompleteFlush();
task_runner_->PostTask(
FROM_HERE, base::BindOnce(&V4L2StatelessVideoDecoderBackend::DoDecodeWork,
@@ -721,6 +730,7 @@
DVLOGF(3);
pic_size_ = gfx::Size();
+ bit_depth_ = kDefaultBitDepth;
CHECK(input_queue_->SupportsRequests());
diff --git a/media/gpu/v4l2/v4l2_video_decoder_backend_stateless.h b/media/gpu/v4l2/v4l2_video_decoder_backend_stateless.h
index 2932bdcc..afefa34f 100644
--- a/media/gpu/v4l2/v4l2_video_decoder_backend_stateless.h
+++ b/media/gpu/v4l2/v4l2_video_decoder_backend_stateless.h
@@ -112,6 +112,8 @@
kRanOutOfSurfaces,
};
+ static constexpr uint8_t kDefaultBitDepth = 8;
+
// Callback which is called when the output buffer is not used anymore.
static void ReuseOutputBufferThunk(
scoped_refptr<base::SequencedTaskRunner> task_runner,
@@ -150,6 +152,9 @@
// Video coded size we are decoding.
gfx::Size pic_size_;
+ // Video bit depth we are decoding.
+ uint8_t bit_depth_ = kDefaultBitDepth;
+
// Video decoder used to parse stream headers by software.
std::unique_ptr<AcceleratedVideoDecoder> decoder_;
diff --git a/media/gpu/vp8_decoder.cc b/media/gpu/vp8_decoder.cc
index 30074d9..f6ee52c 100644
--- a/media/gpu/vp8_decoder.cc
+++ b/media/gpu/vp8_decoder.cc
@@ -186,9 +186,7 @@
}
VideoChromaSampling VP8Decoder::GetChromaSampling() const {
- // VP8 decoder currently does not rely on chroma sampling format for
- // creating/reconfiguring decoder, so return an unknown format.
- return VideoChromaSampling::kUnknown;
+ return VideoChromaSampling::k420;
}
VideoColorSpace VP8Decoder::GetVideoColorSpace() const {
Original Bug Report
Potential OOB DMA write in V4L2 stateless decoder backend on mid-stream bit-depth change
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: The V4L2StatelessVideoDecoderBackend drops kConfigChange notifications when the coded picture size is unchanged, ignoring mid-stream bit-depth transitions (e.g., from 8-bit to 10-bit). This leaves the CAPTURE queue and allocated user-space DMA-BUFs configured for 8-bit NV12 layout while the per-request controls instruct the hardware decoder to output in 10-bit format. Depending on the V4L2 driver, this inconsistency can result in a potential out-of-bounds DMA write into adjacent GPU process memory.
Affected files:
media/gpu/v4l2/v4l2_video_decoder_backend_stateless.ccmedia/gpu/v4l2/v4l2_video_decoder_backend_stateless.h
Estimated timestamp from git blame: 2024-02-26
Root Cause
In V4L2StatelessVideoDecoderBackend::PumpDecodeTask(), the kConfigChange event is handled as follows:
// media/gpu/v4l2/v4l2_video_decoder_backend_stateless.cc:411-441
case AcceleratedVideoDecoder::kConfigChange:
if (decoder_->GetBitDepth() != 8u && decoder_->GetBitDepth() != 10u) {
...
return false;
}
if (profile_ != decoder_->GetProfile()) {
...
if (!IsSupportedProfile(decoder_->GetProfile())) return false;
profile_ = decoder_->GetProfile();
}
if (pic_size_ == decoder_->GetPicSize()) {
// There is no need to do anything in V4L2 API when only a profile is
// changed.
DVLOGF(3) << "Only profile is changed. No need to do anything.";
continue; // <-- Bug: bit_depth change dropped
}
...
output_request_queue_.push(OutputRequest::ChangeResolutionFence());
When a mid-stream bit-depth change occurs (e.g., 8-bit to 10-bit) but the coded resolution remains unchanged, pic_size_ == decoder_->GetPicSize() evaluates to true. Because the backend does not track or compare the stream’s bit_depth, it incorrectly treats this configuration change as a no-op, logs "Only profile is changed...", and continues decoding.
As a result, V4L2VideoDecoder::SetupOutputFormat() is never invoked to re-negotiate the CAPTURE queue format, and the allocated VideoFramePool is not recreated. The CAPTURE queue and user-space DMA-BUFs remain configured for 8-bit NV12 layout (~1.5 bytes/pixel). However, the stateless decoder delegates (such as V4L2VideoDecoderDelegateAV1::SubmitDecode()) correctly query the parser and send the new bit-depth (10) to the kernel driver in the per-request controls (e.g., v4l2_seq_params.bit_depth = 10 in media/gpu/v4l2/v4l2_video_decoder_delegate_av1.cc:120).
Depending on the specific platform driver and hardware decoder implementation, the hardware may program its output write-back engine using the per-request 10-bit format parameters. This causes the hardware to write 10-bit P010-layout frames (~2 bytes/pixel) into the smaller, stale 8-bit NV12 buffers, leading to a potential out-of-bounds DMA write into adjacent GPU-process-owned memory pages.
Potential Trigger Steps
Note: These are potential steps based on static code analysis; our tooling agent does not have the ability to run code or construct functional exploits.
- A webpage plays an AV1/HEVC/VP9 stream via MSE or a
<video>tag. - The first sequence header configures an 8-bit stream at 1920x1080. The backend successfully processes the initial
kConfigChangeand negotiates an 8-bit NV12 CAPTURE format. TheVideoFramePoolallocates DMA-BUFs of ~3.1 MB per frame. - The stream transitions to a second sequence header specifying a 10-bit depth but retaining the identical 1920x1080 resolution (such that the profile remains under the same AV1 Main Profile bracket).
- The software codec parser identifies the bit-depth change and yields
kConfigChange. - The backend catches
kConfigChange, skips the profile block, checkspic_size_ == decoder_->GetPicSize()(which is true), and drops the configuration update viacontinue. - The decoder requests a surface backed by a stale 3.1 MB DMA-BUF, but configures the per-request V4L2 control to specify a bit-depth of 10.
- When the decode request is submitted, the hardware writes the decoded frame in 10-bit format (~4.15 MB), writing approximately 1.05 MB past the allocated boundary of the 3.1 MB DMA-BUF and corrupting adjacent GPU memory.
Suggested Fix
Track the bit-depth explicitly in V4L2StatelessVideoDecoderBackend and include it in the resolution change trigger check.
- Declare
uint8_t bit_depth_ = 0;inside the private member variables ofV4L2StatelessVideoDecoderBackendinmedia/gpu/v4l2/v4l2_video_decoder_backend_stateless.h:
// Video coded size we are decoding.
gfx::Size pic_size_;
// Video bit depth we are decoding.
uint8_t bit_depth_ = 0;
- Modify
PumpDecodeTask()inmedia/gpu/v4l2/v4l2_video_decoder_backend_stateless.ccto verify both the picture size and bit-depth before skipping the resolution change:
if (pic_size_ == decoder_->GetPicSize() && bit_depth_ == decoder_->GetBitDepth()) {
// There is no need to do anything in V4L2 API when only a profile is
// changed.
DVLOGF(3) << "Only profile is changed. No need to do anything.";
continue;
}
bit_depth_ = decoder_->GetBitDepth();
Evaluated with Chrome root at commit: 208ca3371d87589335b108c431b95a36d768dc47
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.