CVE-2026-9117
Overview
Background
- `NativePixmapPlane`
- a
gfxstruct describing one plane of a native pixmap buffer, holdingstride,offset,size, and a file descriptor. - `NativePixmapHandle` Mojo traits
- the serialization glue in
native_handle_types_mojom_traits.hthat marshals aNativePixmapPlaneacross process boundaries. - `base::saturated_cast` / `base::checked_cast`
- numeric conversions that respectively clamp a value to the target type’s range or CHECK-fail if it does not fit.
Root Cause Analysis
The pre-fix NativePixmapPlane constructor took int stride and int offset (ui/gfx/native_pixmap_handle.cc:35-36 in the diff; declared in ui/gfx/native_pixmap_handle.h), while the struct’s internal members are wider (per the commit message, uint32_t/uint64_t). Passing a plane offset through a signed 32-bit parameter truncates or sign-extends offsets of 2 GB or more. The silent corruption path is the Mojo trait: pre-fix native_handle_types_mojom_traits.h serialized the offset as return base::saturated_cast<int32_t>(plane.offset);, clamping any offset >= 0x80000000 to the int32_t maximum during IPC so the deserialized plane carries a wrong offset.
The fix widens the constructor to uint32_t stride, uint64_t offset (diff in native_pixmap_handle.cc/.h) and changes the trait to static uint64_t offset(...) { return plane.offset; }, giving full 64-bit fidelity end to end. The call sites in media/gpu and chromeos/ash are updated to feed the new signature with base::checked_cast<uint32_t>(plane.stride) and base::strict_cast<uint64_t>/base::checked_cast<uint64_t>(plane.offset) (diffs in mock_native_pixmap_dmabuf.cc:62-63, native_pixmap_frame_resource.cc:128-129, platform_video_frame_utils.cc:525-526, test_gbm_buffer_manager.cc, test_shared_image_interface.cc), replacing the prior base::checked_cast<int> conversions.
The restored invariant: a plane’s offset survives construction and serialization without truncation or clamping.
int offset and a saturated_cast<int32_t> Mojo trait), so offsets at or above 2 GB were sign-extended or clamped; the fix carries them as uint64_t throughout.Attack Path
- Supply a large-offset plane
A component able to populate a
NativePixmapHandlesets a planeoffsetof 2 GB or more. (inferred: Chrome-side control of the offset over this path is not shown in the material.) - Serialize across Mojo
The handle is marshaled; the pre-fix trait clamps the offset to
int32_trange viasaturated_cast<int32_t>, so the receiver observes a different offset than intended. - Corrupt buffer access Downstream pixmap use relies on the mismatched offset, leading to memory corruption. (inferred: the advisory asserts memory corruption; the corrupting access is not in the shown source.)
Impact Assessment
NativePixmapPlane was constructed or serialized over Mojo, which the advisory states can cause memory corruption. The advisory rates this high severity and records it as Type Confusion with no in-the-wild exploitation; the process reached and any escalation are (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
formedia/gpu/chromeos/native_pixmap_frame_resource.cc |
modified | |
formedia/gpu/chromeos/platform_video_frame_utils.cc |
modified | |
TEST_Fui/gfx/mojom/mojom_traits_unittest.cc |
modified |
Files Changed
chromeos/ash/experiences/arc/video_accelerator/protected_buffer_manager.ccgpu/command_buffer/client/test_shared_image_interface.ccmedia/gpu/chromeos/mock_native_pixmap_dmabuf.ccmedia/gpu/chromeos/native_pixmap_frame_resource.ccmedia/gpu/chromeos/platform_video_frame_utils.ccmedia/gpu/test/test_gbm_buffer_manager.ccui/gfx/mojom/mojom_traits_unittest.ccui/gfx/mojom/native_handle_types_mojom_traits.hui/gfx/native_pixmap_handle.cc
Audit Directions
- Signed/narrow conversions on sizesFlag
int/int32_tparameters or casts carrying offsets, sizes, or strides whose backing members areuint32_t/uint64_t. - Saturating Mojo traitsAudit Mojo
StructTraitsaccessors thatsaturated_cast/checked_casta wider field into a narrower wire type, which silently corrupts large values. - Constructor vs member type parityVerify that constructor parameter widths match the struct member widths they initialize, especially for cross-process buffer descriptors.
Patch
From 8c46973c3a1b6acd295f191436857a667ff2819b Mon Sep 17 00:00:00 2001 From: Sergio Solano <[email protected]> Date: Wed, 15 Apr 2026 14:29:34 -0700 Subject: [PATCH] [ui/gfx] Fix NativePixmapPlane sign-extension and clamping issues This is a quick fix for a potential security issue where the NativePixmapPlane constructor and Mojo traits used signed 32-bit integers. This led to sign-extension or clamping of offsets >= 2GB, potentially causing memory corruption (b:497542537). - Updates the constructor to use uint32_t and uint64_t to match internal member types. - Updates relevant call sites in media/gpu and chromeos/ash to use the new constructor signature. - Modernizes Mojo traits to remove redundant int casts and clamping (base::saturated_cast), allowing full 64-bit fidelity. - Strengthens unit tests with a 2GB (0x80000000) offset check. - Hardens test utility arithmetic using base::CheckMul. For further reference: (Internal only) go/code-terracotta-review-explainer Project: Project-Fortify Bug: b:497542537 Test: gfx_unittests --gtest_filter="StructTraitsTest.NativePixmapHandle" Change-Id: I89545fa76f0638fc919194272e293131c6f4a200 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7718888 Reviewed-by: Tom Sepez <[email protected]> Reviewed-by: Michael Spang <[email protected]> Reviewed-by: Vasiliy Telezhnikov <[email protected]> Commit-Queue: Sergio Solano <[email protected]> Reviewed-by: Stephen Nusko <[email protected]> Reviewed-by: Andres Calderon Jaramillo <[email protected]> Cr-Commit-Position: refs/heads/main@{#1615423} --- diff --git a/chromeos/ash/experiences/arc/video_accelerator/protected_buffer_manager.cc b/chromeos/ash/experiences/arc/video_accelerator/protected_buffer_manager.cc index 5f9f451..0161ead2 100644 --- a/chromeos/ash/experiences/arc/video_accelerator/protected_buffer_manager.cc +++ b/chromeos/ash/experiences/arc/video_accelerator/protected_buffer_manager.cc @@ -505,7 +505,7 @@ // also on failure. gfx::NativePixmapHandle pixmap_handle; pixmap_handle.planes.emplace_back( - gfx::NativePixmapPlane(0, 0, 0u, std::move(dummy_fd))); + gfx::NativePixmapPlane(0u, 0u, 0u, std::move(dummy_fd))); ui::OzonePlatform* platform = ui::OzonePlatform::GetInstance(); ui::SurfaceFactoryOzone* factory = platform->GetSurfaceFactoryOzone(); scoped_refptr<gfx::NativePixmap> pixmap = diff --git a/gpu/command_buffer/client/test_shared_image_interface.cc b/gpu/command_buffer/client/test_shared_image_interface.cc index 224b06d..91f8bd56 100644 --- a/gpu/command_buffer/client/test_shared_image_interface.cc +++ b/gpu/command_buffer/client/test_shared_image_interface.cc @@ -16,6 +16,8 @@ #include "base/check.h" #include "base/notreached.h" +#include "base/numerics/checked_math.h" +#include "base/numerics/safe_conversions.h" #include "build/build_config.h" #include "components/viz/common/resources/shared_image_format_utils.h" #include "gpu/command_buffer/client/client_shared_image.h" @@ -440,7 +442,8 @@ viz::SharedMemoryRowSizeForSharedImageFormat(format, i, size.width()) .value(); native_pixmap_handle.planes.emplace_back( - stride, 0, height_in_pixels * stride, + base::checked_cast<uint32_t>(stride), 0, + base::CheckMul(height_in_pixels, stride).ValueOrDie<uint64_t>(), base::ScopedFD(open("/dev/zero", O_RDWR))); } diff --git a/media/gpu/chromeos/mock_native_pixmap_dmabuf.cc b/media/gpu/chromeos/mock_native_pixmap_dmabuf.cc index 12fe2ab8..2fc8bed 100644 --- a/media/gpu/chromeos/mock_native_pixmap_dmabuf.cc +++ b/media/gpu/chromeos/mock_native_pixmap_dmabuf.cc @@ -59,8 +59,8 @@ LOG(ERROR) << "Failed to open a file"; return nullptr; } - handle.planes.emplace_back(base::checked_cast<int>(plane.stride), - base::checked_cast<int>(plane.offset), + handle.planes.emplace_back(base::checked_cast<uint32_t>(plane.stride), + base::strict_cast<uint64_t>(plane.offset), base::strict_cast<uint64_t>(plane.size), base::ScopedFD(file.TakePlatformFile())); } diff --git a/media/gpu/chromeos/native_pixmap_frame_resource.cc b/media/gpu/chromeos/native_pixmap_frame_resource.cc index b37d341..3554099 100644 --- a/media/gpu/chromeos/native_pixmap_frame_resource.cc +++ b/media/gpu/chromeos/native_pixmap_frame_resource.cc @@ -125,8 +125,8 @@ handle.planes.reserve(num_planes); for (size_t i = 0; i < num_planes; ++i) { const auto& plane = layout.planes()[i]; - handle.planes.emplace_back(base::checked_cast<int>(plane.stride), - base::checked_cast<int>(plane.offset), + handle.planes.emplace_back(base::checked_cast<uint32_t>(plane.stride), + base::strict_cast<uint64_t>(plane.offset), base::strict_cast<uint64_t>(plane.size), std::move(dmabuf_fds[i])); } diff --git a/media/gpu/chromeos/platform_video_frame_utils.cc b/media/gpu/chromeos/platform_video_frame_utils.cc index d337cd20..bf0e518 100644 --- a/media/gpu/chromeos/platform_video_frame_utils.cc +++ b/media/gpu/chromeos/platform_video_frame_utils.cc @@ -522,8 +522,8 @@ for (size_t i = 0; i < num_planes; ++i) { const auto& plane = video_frame->layout().planes()[i]; native_pixmap_handle.planes.emplace_back( - base::checked_cast<int>(plane.stride), - base::checked_cast<int>(plane.offset), + base::checked_cast<uint32_t>(plane.stride), + base::strict_cast<uint64_t>(plane.offset), base::strict_cast<uint64_t>(plane.size), std::move(duped_fds[i])); } handle = gfx::GpuMemoryBufferHandle(std::move(native_pixmap_handle)); diff --git a/media/gpu/test/test_gbm_buffer_manager.cc b/media/gpu/test/test_gbm_buffer_manager.cc index ee71fed..5388fe94 100644 --- a/media/gpu/test/test_gbm_buffer_manager.cc +++ b/media/gpu/test/test_gbm_buffer_manager.cc @@ -104,8 +104,9 @@ for (size_t i = 0; i < static_cast<size_t>(gbm_bo_get_plane_count(buffer_object)); ++i) { native_pixmap_handle.planes.push_back(gfx::NativePixmapPlane( - base::checked_cast<int>(gbm_bo_get_stride_for_plane(buffer_object, i)), - base::checked_cast<int>(gbm_bo_get_offset(buffer_object, i)), + base::checked_cast<uint32_t>( + gbm_bo_get_stride_for_plane(buffer_object, i)), + base::checked_cast<uint64_t>(gbm_bo_get_offset(buffer_object, i)), base::strict_cast<uint64_t>(gbm_bo_get_plane_size(buffer_object, i)), base::ScopedFD(gbm_bo_get_plane_fd(buffer_object, i)))); } diff --git a/ui/gfx/mojom/mojom_traits_unittest.cc b/ui/gfx/mojom/mojom_traits_unittest.cc index 1aa11985..cc1c61d 100644 --- a/ui/gfx/mojom/mojom_traits_unittest.cc +++ b/ui/gfx/mojom/mojom_traits_unittest.cc @@ -20,9 +20,11 @@ #include "ui/gfx/mojom/buffer_types_mojom_traits.h" #include "ui/gfx/mojom/hdr_metadata.mojom.h" #include "ui/gfx/mojom/hdr_metadata_mojom_traits.h" +#include "ui/gfx/mojom/native_handle_types_mojom_traits.h" #include "ui/gfx/mojom/presentation_feedback.mojom.h" #include "ui/gfx/mojom/presentation_feedback_mojom_traits.h" #include "ui/gfx/mojom/traits_test_service.mojom.h" +#include "ui/gfx/native_pixmap_handle.h" #include "ui/gfx/native_ui_types.h" #include "ui/gfx/selection_bound.h" @@ -269,6 +271,28 @@ EXPECT_EQ(input, output); } +#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS) +TEST_F(StructTraitsTest, NativePixmapHandle) { + // Test with a large offset that would trigger sign-extension if treated as + // int. + gfx::NativePixmapHandle native_pixmap_handle; + const uint32_t kStride = 1024; + const uint64_t kOffset = 0x80000000; + const uint64_t kSize = 4096; + native_pixmap_handle.planes.emplace_back(kStride, kOffset, kSize, + CreateValidLookingBufferHandle()); + + gfx::NativePixmapHandle output; + ASSERT_TRUE( + mojo::test::SerializeAndDeserialize<gfx::mojom::NativePixmapHandle>( + native_pixmap_handle, output)); + ASSERT_FALSE(output.planes.empty()); + EXPECT_EQ(kStride, output.planes[0].stride); + EXPECT_EQ(kOffset, output.planes[0].offset); + EXPECT_EQ(kSize, output.planes[0].size); +} +#endif + TEST_F(StructTraitsTest, GpuMemoryBufferHandle) { const uint32_t kOffset = 126; const uint32_t kStride = 256; diff --git a/ui/gfx/mojom/native_handle_types_mojom_traits.h b/ui/gfx/mojom/native_handle_types_mojom_traits.h index 5bc99124..6101c5b 100644 --- a/ui/gfx/mojom/native_handle_types_mojom_traits.h +++ b/ui/gfx/mojom/native_handle_types_mojom_traits.h @@ -57,8 +57,8 @@ static uint32_t stride(const gfx::NativePixmapPlane& plane) { return plane.stride; } - static int32_t offset(const gfx::NativePixmapPlane& plane) { - return base::saturated_cast<int32_t>(plane.offset); + static uint64_t offset(const gfx::NativePixmapPlane& plane) { + return plane.offset; } static uint64_t size(const gfx::NativePixmapPlane& plane) { return plane.size; diff --git a/ui/gfx/native_pixmap_handle.cc b/ui/gfx/native_pixmap_handle.cc index d4f1df1..46f9a25 100644 --- a/ui/gfx/native_pixmap_handle.cc +++ b/ui/gfx/native_pixmap_handle.cc @@ -33,8 +33,8 @@ NativePixmapPlane::NativePixmapPlane() : stride(0), offset(0), size(0) {}
Regression Test / PoC
diff --git a/media/gpu/test/test_gbm_buffer_manager.cc b/media/gpu/test/test_gbm_buffer_manager.cc
index ee71fed..5388fe94 100644
--- a/media/gpu/test/test_gbm_buffer_manager.cc
+++ b/media/gpu/test/test_gbm_buffer_manager.cc
@@ -104,8 +104,9 @@
for (size_t i = 0;
i < static_cast<size_t>(gbm_bo_get_plane_count(buffer_object)); ++i) {
native_pixmap_handle.planes.push_back(gfx::NativePixmapPlane(
- base::checked_cast<int>(gbm_bo_get_stride_for_plane(buffer_object, i)),
- base::checked_cast<int>(gbm_bo_get_offset(buffer_object, i)),
+ base::checked_cast<uint32_t>(
+ gbm_bo_get_stride_for_plane(buffer_object, i)),
+ base::checked_cast<uint64_t>(gbm_bo_get_offset(buffer_object, i)),
base::strict_cast<uint64_t>(gbm_bo_get_plane_size(buffer_object, i)),
base::ScopedFD(gbm_bo_get_plane_fd(buffer_object, i))));
}
diff --git a/ui/gfx/mojom/mojom_traits_unittest.cc b/ui/gfx/mojom/mojom_traits_unittest.cc
index 1aa11985..cc1c61d 100644
--- a/ui/gfx/mojom/mojom_traits_unittest.cc
+++ b/ui/gfx/mojom/mojom_traits_unittest.cc
@@ -20,9 +20,11 @@
#include "ui/gfx/mojom/buffer_types_mojom_traits.h"
#include "ui/gfx/mojom/hdr_metadata.mojom.h"
#include "ui/gfx/mojom/hdr_metadata_mojom_traits.h"
+#include "ui/gfx/mojom/native_handle_types_mojom_traits.h"
#include "ui/gfx/mojom/presentation_feedback.mojom.h"
#include "ui/gfx/mojom/presentation_feedback_mojom_traits.h"
#include "ui/gfx/mojom/traits_test_service.mojom.h"
+#include "ui/gfx/native_pixmap_handle.h"
#include "ui/gfx/native_ui_types.h"
#include "ui/gfx/selection_bound.h"
@@ -269,6 +271,28 @@
EXPECT_EQ(input, output);
}
+#if BUILDFLAG(IS_LINUX) || BUILDFLAG(IS_CHROMEOS)
+TEST_F(StructTraitsTest, NativePixmapHandle) {
+ // Test with a large offset that would trigger sign-extension if treated as
+ // int.
+ gfx::NativePixmapHandle native_pixmap_handle;
+ const uint32_t kStride = 1024;
+ const uint64_t kOffset = 0x80000000;
+ const uint64_t kSize = 4096;
+ native_pixmap_handle.planes.emplace_back(kStride, kOffset, kSize,
+ CreateValidLookingBufferHandle());
+
+ gfx::NativePixmapHandle output;
+ ASSERT_TRUE(
+ mojo::test::SerializeAndDeserialize<gfx::mojom::NativePixmapHandle>(
+ native_pixmap_handle, output));
+ ASSERT_FALSE(output.planes.empty());
+ EXPECT_EQ(kStride, output.planes[0].stride);
+ EXPECT_EQ(kOffset, output.planes[0].offset);
+ EXPECT_EQ(kSize, output.planes[0].size);
+}
+#endif
+
TEST_F(StructTraitsTest, GpuMemoryBufferHandle) {
const uint32_t kOffset = 126;
const uint32_t kStride = 256;