High watch ✔︎ verified CVSS 7.5 chrome Type Confusion 🔧 Commit mapped

Overview

High
Severity
7.5
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactType Confusion in GFX
DescriptionType Confusion in GFX
ComponentGFX
Sub-componentUi/gfx
Bug ClassType Confusion
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable — factors set by independent review by AI
Computed by PatchGap's deterministic exploitability ruler from the fixing commit. patch-now = RCE-class & attacker-reachable in a default config, or exploited in the wild; watch = real security signal below that bar; noise = no security relevance.
Independent review by AIConfirmed
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker497542537
Fix commit8c46973c3a1b (chromium/src) +44/-16
CWECWE-843 (Type confusion)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-01 (per vendor advisory)
Fix landed2026-04-15 (public git)
Disclosed2026-05-19
Patch gap34 days — fix public in source before the advisory

Background

`NativePixmapPlane`
a gfx struct describing one plane of a native pixmap buffer, holding stride, offset, size, and a file descriptor.
`NativePixmapHandle` Mojo traits
the serialization glue in native_handle_types_mojom_traits.h that marshals a NativePixmapPlane across 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.

Key insight
Plane offsets flowed through signed 32-bit integers (constructor 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

  1. Supply a large-offset plane A component able to populate a NativePixmapHandle sets a plane offset of 2 GB or more. (inferred: Chrome-side control of the offset over this path is not shown in the material.)
  2. Serialize across Mojo The handle is marshaled; the pre-fix trait clamps the offset to int32_t range via saturated_cast<int32_t>, so the receiver observes a different offset than intended.
  3. 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

A plane offset at or above 2 GB was silently truncated/clamped when a 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

FunctionChangeNotes
for
media/gpu/chromeos/native_pixmap_frame_resource.cc
modified
for
media/gpu/chromeos/platform_video_frame_utils.cc
modified
TEST_F
ui/gfx/mojom/mojom_traits_unittest.cc
modified

Files Changed

  • chromeos/ash/experiences/arc/video_accelerator/protected_buffer_manager.cc
  • gpu/command_buffer/client/test_shared_image_interface.cc
  • media/gpu/chromeos/mock_native_pixmap_dmabuf.cc
  • media/gpu/chromeos/native_pixmap_frame_resource.cc
  • media/gpu/chromeos/platform_video_frame_utils.cc
  • media/gpu/test/test_gbm_buffer_manager.cc
  • ui/gfx/mojom/mojom_traits_unittest.cc
  • ui/gfx/mojom/native_handle_types_mojom_traits.h
  • ui/gfx/native_pixmap_handle.cc

Audit Directions

  • Signed/narrow conversions on sizes
    Flag int/int32_t parameters or casts carrying offsets, sizes, or strides whose backing members are uint32_t/uint64_t.
  • Saturating Mojo traits
    Audit Mojo StructTraits accessors that saturated_cast/checked_cast a wider field into a narrower wire type, which silently corrupts large values.
  • Constructor vs member type parity
    Verify that constructor parameter widths match the struct member widths they initialize, especially for cross-process buffer descriptors.
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) {}
Loading diff…

Regression Test / PoC

shipped with the fix
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;
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.