CVE-2026-11678
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsource/convert_from.cc |
modified |
Files Changed
source/convert_from.ccsource/planar_functions.ccsource/rotate.ccsource/rotate_common.cc
Patch
From ebe6fef90344936e2cd623f727024482fca72a87 Mon Sep 17 00:00:00 2001
From: Wan-Teh Chang <wtc@google.com>
Date: Thu, 28 May 2026 13:56:44 -0700
Subject: [PATCH] Fix integer overflow in multiplications of stride
Audit all occurrences of "stride *" in the libyuv source tree. Ensure
that these multiplications are performed in the ptrdiff_t type.
For functions not declared in a public header (such as static
functions), prefer to declare the stride parameters (typically named
src_stride and dst_stride) and related stride local variables as
ptrdiff_t. If this is not possible, add ptrdiff_t casts to the stride
parameters in multiplications. If intptr_t or int64_t casts were used,
change them to ptrdiff_t casts.
Bug: chromium:516986556
Change-Id: I6cd8a8eb00cbb5380db828bf83e4d89ff95891f3
Reviewed-on: https://chromium-review.googlesource.com/c/libyuv/libyuv/+/7882967
Commit-Queue: Wan-Teh Chang <wtc@google.com>
Reviewed-by: Frank Barchard <fbarchard@google.com>
---
diff --git a/source/convert_from.cc b/source/convert_from.cc
index 5cf88fa..363edc2 100644
--- a/source/convert_from.cc
+++ b/source/convert_from.cc
@@ -10,6 +10,8 @@
#include "libyuv/convert_from.h"
+#include <stddef.h>
+
#include "libyuv/basic_types.h"
#include "libyuv/convert.h" // For I420Copy
#include "libyuv/cpu_id.h"
@@ -782,7 +784,7 @@
break;
case FOURCC_NV12: {
int dst_y_stride = dst_sample_stride ? dst_sample_stride : width;
- uint8_t* dst_uv = dst_sample + dst_y_stride * height;
+ uint8_t* dst_uv = dst_sample + (ptrdiff_t)dst_y_stride * height;
r = I420ToNV12(y, y_stride, u, u_stride, v, v_stride, dst_sample,
dst_sample_stride ? dst_sample_stride : width, dst_uv,
dst_sample_stride ? dst_sample_stride : width, width,
@@ -791,7 +793,7 @@
}
case FOURCC_NV21: {
int dst_y_stride = dst_sample_stride ? dst_sample_stride : width;
- uint8_t* dst_vu = dst_sample + dst_y_stride * height;
+ uint8_t* dst_vu = dst_sample + (ptrdiff_t)dst_y_stride * height;
r = I420ToNV21(y, y_stride, u, u_stride, v, v_stride, dst_sample,
dst_sample_stride ? dst_sample_stride : width, dst_vu,
dst_sample_stride ? dst_sample_stride : width, width,
@@ -807,11 +809,11 @@
uint8_t* dst_u;
uint8_t* dst_v;
if (format == FOURCC_YV12) {
- dst_v = dst_sample + dst_sample_stride * height;
- dst_u = dst_v + halfstride * halfheight;
+ dst_v = dst_sample + (ptrdiff_t)dst_sample_stride * height;
+ dst_u = dst_v + (ptrdiff_t)halfstride * halfheight;
} else {
- dst_u = dst_sample + dst_sample_stride * height;
- dst_v = dst_u + halfstride * halfheight;
+ dst_u = dst_sample + (ptrdiff_t)dst_sample_stride * height;
+ dst_v = dst_u + (ptrdiff_t)halfstride * halfheight;
}
r = I420Copy(y, y_stride, u, u_stride, v, v_stride, dst_sample,
dst_sample_stride, dst_u, halfstride, dst_v, halfstride,
@@ -825,11 +827,11 @@
uint8_t* dst_u;
uint8_t* dst_v;
if (format == FOURCC_YV16) {
- dst_v = dst_sample + dst_sample_stride * height;
- dst_u = dst_v + halfstride * height;
+ dst_v = dst_sample + (ptrdiff_t)dst_sample_stride * height;
+ dst_u = dst_v + (ptrdiff_t)halfstride * height;
} else {
- dst_u = dst_sample + dst_sample_stride * height;
- dst_v = dst_u + halfstride * height;
+ dst_u = dst_sample + (ptrdiff_t)dst_sample_stride * height;
+ dst_v = dst_u + (ptrdiff_t)halfstride * height;
}
r = I420ToI422(y, y_stride, u, u_stride, v, v_stride, dst_sample,
dst_sample_stride, dst_u, halfstride, dst_v, halfstride,
@@ -842,11 +844,11 @@
uint8_t* dst_u;
uint8_t* dst_v;
if (format == FOURCC_YV24) {
- dst_v = dst_sample + dst_sample_stride * height;
- dst_u = dst_v + dst_sample_stride * height;
+ dst_v = dst_sample + (ptrdiff_t)dst_sample_stride * height;
+ dst_u = dst_v + (ptrdiff_t)dst_sample_stride * height;
} else {
- dst_u = dst_sample + dst_sample_stride * height;
- dst_v = dst_u + dst_sample_stride * height;
+ dst_u = dst_sample + (ptrdiff_t)dst_sample_stride * height;
+ dst_v = dst_u + (ptrdiff_t)dst_sample_stride * height;
}
r = I420ToI444(y, y_stride, u, u_stride, v, v_stride, dst_sample,
dst_sample_stride, dst_u, dst_sample_stride, dst_v,
diff --git a/source/planar_functions.cc b/source/planar_functions.cc
index 149dde3..016ea24 100644
--- a/source/planar_functions.cc
+++ b/source/planar_functions.cc
@@ -3088,7 +3088,7 @@
}
// Subsample 2 rows of UV to half width and half height.
ScaleRowDown2(alpha, alpha_stride, halfalpha, halfwidth);
- alpha += alpha_stride * 2;
+ alpha += (ptrdiff_t)alpha_stride * 2;
BlendPlaneRow(src_u0, src_u1, halfalpha, dst_u, halfwidth);
BlendPlaneRow(src_v0, src_v1, halfalpha, dst_v, halfwidth);
src_u0 += src_stride_u0;
diff --git a/source/rotate.cc b/source/rotate.cc
index 5208062..d51b313 100644
--- a/source/rotate.cc
+++ b/source/rotate.cc
@@ -128,7 +128,7 @@
// Rotate by 90 is a transpose with the source read
// from bottom to top. So set the source pointer to the end
// of the buffer and flip the sign of the source stride.
- src += src_stride * (height - 1);
+ src += (ptrdiff_t)src_stride * (height - 1);
src_stride = -src_stride;
TransposePlane(src, src_stride, dst, dst_stride, width, height);
}
@@ -143,7 +143,7 @@
// Rotate by 270 is a transpose with the destination written
// from bottom to top. So set the destination pointer to the end
// of the buffer and flip the sign of the destination stride.
- dst += dst_stride * (width - 1);
+ dst += (ptrdiff_t)dst_stride * (width - 1);
dst_stride = -dst_stride;
TransposePlane(src, src_stride, dst, dst_stride, width, height);
}
@@ -160,8 +160,8 @@
assert(row);
if (!row)
return;
- const uint8_t* src_bot = src + src_stride * (height - 1);
- uint8_t* dst_bot = dst + dst_stride * (height - 1);
+ const uint8_t* src_bot = src + (ptrdiff_t)src_stride * (height - 1);
+ uint8_t* dst_bot = dst + (ptrdiff_t)dst_stride * (height - 1);
int half_height = (height + 1) >> 1;
int y;
void (*MirrorRow)(const uint8_t* src, uint8_t* dst, int width) = MirrorRow_C;
@@ -354,7 +354,7 @@
int dst_stride_b,
int width,
int height) {
- src += src_stride * (height - 1);
+ src += (ptrdiff_t)src_stride * (height - 1);
src_stride = -src_stride;
SplitTransposeUV(src, src_stride, dst_a, dst_stride_a, dst_b, dst_stride_b,
@@ -533,7 +533,7 @@
// Rotate by 90 is a transpose with the source read
// from bottom to top. So set the source pointer to the end
// of the buffer and flip the sign of the source stride.
- src += src_stride * (height - 1);
+ src += (ptrdiff_t)src_stride * (height - 1);
src_stride = -src_stride;
TransposePlane_16(src, src_stride, dst, dst_stride, width, height);
}
@@ -547,7 +547,7 @@
// Rotate by 270 is a transpose with the destination written
// from bottom to top. So set the destination pointer to the end
// of the buffer and flip the sign of the destination stride.
- dst += dst_stride * (width - 1);
+ dst += (ptrdiff_t)dst_stride * (width - 1);
dst_stride = -dst_stride;
TransposePlane_16(src, src_stride, dst, dst_stride, width, height);
}
@@ -558,8 +558,8 @@
int dst_stride,
int width,
int height) {
- const uint16_t* src_bot = src + src_stride * (height - 1);
- uint16_t* dst_bot = dst + dst_stride * (height - 1);
+ const uint16_t* src_bot = src + (ptrdiff_t)src_stride * (height - 1);
+ uint16_t* dst_bot = dst + (ptrdiff_t)dst_stride * (height - 1);
int half_height = (height + 1) >> 1;
int y;
diff --git a/source/rotate_common.cc b/source/rotate_common.cc
index e0341fe..ad42048 100644
--- a/source/rotate_common.cc
+++ b/source/rotate_common.cc
@@ -8,6 +8,8 @@
* be found in the AUTHORS file in the root of the source tree.
*/
+#include <stddef.h>
+
#include "libyuv/rotate_row.h"
#include "libyuv/row.h"
@@ -191,10 +193,10 @@
((uint32_t*)(dst3))[1] = p31;
Regression Test / PoC
diff --git a/unit_test/scale_plane_test.cc b/unit_test/scale_plane_test.cc
index b952a6f..7d38c4d 100644
--- a/unit_test/scale_plane_test.cc
+++ b/unit_test/scale_plane_test.cc
@@ -42,6 +42,108 @@
namespace libyuv {
+// POC: int row_stride = src_stride * 2 overflows to a small negative value
+// when src_stride is close to INT_MAX, causing src_ptr to walk backward
+// past the start of the source allocation on the second loop iteration.
+// With src_stride = 0x7FFFFFFE, row_stride = (int)0xFFFFFFFC = -4, so on
+// y=1 ScaleRowDown2Box reads 4 bytes before the heap allocation.
+TEST_F(LibYUVScaleTest, ScalePlaneDown2_RowStrideOverflow) {
+ constexpr int kSrcStride = 0x7FFFFFFE; // INT_MAX - 1
+ constexpr int kSrcW = 64;
+ constexpr int kSrcH = 4;
+ constexpr int kDstW = 32;
+ constexpr int kDstH = 2;
+ // src_size = (kSrcH - 1) * stride + width.
+ size_t src_size = kSrcH - 1;
+ if (src_size > SIZE_MAX / kSrcStride) {
+ GTEST_SKIP() << "could not represent allocation size in size_t";
+ }
+ src_size *= kSrcStride;
+ if (src_size > SIZE_MAX - kSrcW) {
+ GTEST_SKIP() << "could not represent allocation size in size_t";
+ }
+ src_size += kSrcW;
+
+#if defined(__aarch64__)
+ // Infer malloc can accept a large size for cpu with dot product (a76/a55)
+ int has_large_malloc = TestCpuFlag(kCpuHasNeonDotProd);
+#else
+ int has_large_malloc = 1;
+#endif
+ if (!has_large_malloc) {
+ GTEST_SKIP() << "large allocation may assert for " << src_size << " bytes";
+ }
+
+ uint8_t* src = new (std::nothrow) uint8_t[src_size];
+ if (!src) {
+ GTEST_SKIP() << "could not allocate " << src_size << " bytes";
+ }
+ uint8_t dst[kDstW * kDstH];
+ uint8_t* src_row = src;
+ for (int i = 0; i < kSrcH; i++) {
+ memset(src_row, 0x41, kSrcW);
+ src_row += kSrcStride;
+ }
+ // Force the C row kernel: the SIMD kernels are inline asm that ASAN does not
+ // instrument, so they silently read OOB without a report.
+ MaskCpuFlags(1);
+ // 2*dst == src on both axes -> ScalePlane dispatches to ScalePlaneDown2.
+ // int row_stride = kSrcStride * 2 wraps to -4; on y=1 src_ptr underflows.
+ ScalePlane(src, kSrcStride, kSrcW, kSrcH, dst, kDstW, kDstW, kDstH,
+ kFilterBox);
+ MaskCpuFlags(0);
+ delete[] src;
+}
+
+// POC: same defect in the 1/4 fast path. src_stride = 0x3FFFFFFF gives
+// int row_stride = src_stride * 4 = (int)0xFFFFFFFC = -4.
+TEST_F(LibYUVScaleTest, ScalePlaneDown4_RowStrideOverflow) {
+ constexpr int kSrcStride = 0x3FFFFFFF; // INT_MAX / 4 (rounded down)
+ constexpr int kSrcW = 64;
+ constexpr int kSrcH = 8;
+ constexpr int kDstW = 16;
+ constexpr int kDstH = 2;
+ // src_size = (kSrcH - 1) * stride + width.
+ size_t src_size = kSrcH - 1;
+ if (src_size > SIZE_MAX / kSrcStride) {
+ GTEST_SKIP() << "could not represent allocation size in size_t";
+ }
+ src_size *= kSrcStride;
+ if (src_size > SIZE_MAX - kSrcW) {
+ GTEST_SKIP() << "could not represent allocation size in size_t";
+ }
+ src_size += kSrcW;
+
+#if defined(__aarch64__)
+ // Infer malloc can accept a large size for cpu with dot product (a76/a55)
+ int has_large_malloc = TestCpuFlag(kCpuHasNeonDotProd);
+#else
+ int has_large_malloc = 1;
+#endif
+ if (!has_large_malloc) {
+ GTEST_SKIP() << "large allocation may assert for " << src_size << " bytes";
+ }
+
+ uint8_t* src = new (std::nothrow) uint8_t[src_size];
+ if (!src) {
+ GTEST_SKIP() << "could not allocate " << src_size << " bytes";
+ }
+ uint8_t dst[kDstW * kDstH];
+ uint8_t* src_row = src;
+ for (int i = 0; i < kSrcH; i++) {
+ memset(src_row, 0x41, kSrcW);
+ src_row += kSrcStride;
+ }
+ // Force the C row kernel: the SIMD kernels are inline asm that ASAN does not
+ // instrument, so they silently read OOB without a report.
+ MaskCpuFlags(1);
+ // 4*dst == src on both axes with kFilterBox -> ScalePlaneDown4.
+ ScalePlane(src, kSrcStride, kSrcW, kSrcH, dst, kDstW, kDstW, kDstH,
+ kFilterBox);
+ MaskCpuFlags(0);
+ delete[] src;
+}
+
#ifdef ENABLE_ROW_TESTS
#ifdef HAS_SCALEROWDOWN2_SSSE3
TEST_F(LibYUVScaleTest, TestScaleRowDown2Box_Odd_SSSE3) {
Original Bug Report
Potential out-of-bounds heap read in libyuv downscaling due to integer overflow
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: A potential integer overflow in multiple libyuv downscaling routines can result in negative row strides. When pointer arithmetic is applied on 64-bit systems, this can lead to a sign-extended backward out-of-bounds heap read. If triggered via video frame conversion in Chromium, this could potentially allow a compromised renderer to disclose memory from the GPU process.
Affected files:
third_party/libyuv/source/scale.ccthird_party/libyuv/source/scale_argb.ccthird_party/libyuv/source/scale_uv.cc
Estimated timestamp from git blame: 2013-05-30
Root Cause Analysis
In third_party/libyuv/source/scale.cc, scale_argb.cc, and scale_uv.cc, multiple optimized downscaling functions (such as ScalePlaneDown2, ScalePlaneDown4, ScaleARGBDown2, ScaleARGBDown4Box, ScaleUVDown2, and ScaleUVDown4Box) calculate the row stride using 32-bit signed integer arithmetic.
For example, in ScalePlaneDown2 (third_party/libyuv/source/scale.cc:54):
int row_stride = src_stride * 2;
Additionally, the entry point ScalePlane (third_party/libyuv/source/scale.cc:1943) accepts stride arguments as a 32-bit signed int src_stride. When Chromium passes a 64-bit size_t stride from a media::VideoFrame to libyuv, the value is implicitly narrowed and converted to a signed 32-bit integer.
If the stride is extremely large (for example, 2147483649 or 0x80000001 in hex), the signed representation is interpreted as negative (-2147483647). During pointer updates, adding this negative value to the 64-bit source pointer results in sign-extension to 64 bits:
src_ptr += src_stride; // or src_ptr += row_stride;
This causes the pointer to jump backward (e.g., ~2 GB prior to the source buffer) into unrelated heap memory, resulting in a backward out-of-bounds read when processing subsequent rows.
Potential Attack Scenario
An attacker could potentially trigger this vulnerability by performing the following steps:
- A compromised renderer allocates a shared memory region of sufficient size to pass layout validation checks.
- The renderer transmits a serialized
media.mojom.VideoFrameover Mojo to the GPU process, specifying an extremely large stride value (e.g.,2147483649). - The GPU process deserializes the frame and verifies the layout via
VideoFrameLayout::FitsInContiguousBufferOfSize. Because the shared memory region is large enough to hold the computed plane size, validation succeeds. - The GPU process invokes
VideoFrameConverter::ConvertAndScale(commonly used by hardware video encoder adapters or video processors), which routes the frame tolibyuv::ScalePlane. - The large stride is truncated to a negative signed integer inside libyuv, causing the downscaling loops to read memory out of bounds and write the leaked data into the destination frame.
- The encoded frame containing the leaked heap bytes is returned to the renderer, facilitating a cross-origin information disclosure.
Note: These steps are suggested/potential based on static code analysis, as our tooling environment does not currently have the capability to run code or execute a proof of concept.
Suggested Fix
Modify the affected downscaling routines in third_party/libyuv to compute row strides using ptrdiff_t or intptr_t instead of int, mirroring the safe implementation pattern already used in sibling functions such as ScaleARGBDownEven:
ptrdiff_t row_stride = (ptrdiff_t)((dy >> 16) * (intptr_t)src_stride);
Alternatively, validate that incoming stride values in ScalePlane and related wrappers do not exceed safe thresholds (such as INT_MAX / 4) before performing any scaling arithmetic.
Evaluated with Chrome root at commit: b1520ef4a76878853a31f0943b565e42060edec8
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.