Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in GPU
DescriptionOut of bounds read in GPU
ComponentGPU
Bug ClassOOB
Tracker497531791
Fix commit37d6e75fe5a7 (chromium/src) +212/-207
CISA KEVNot listed
CreditedGoogle
Disclosed2026-05-12

Changed Functions

FunctionChangeNotes
switch
components/viz/common/resources/shared_image_format_utils.cc
modified
if
gpu/command_buffer/common/BUILD.gn
modified
source_set
gpu/command_buffer/common/BUILD.gn
modified

Files Changed

  • components/viz/common/resources/shared_image_format_utils.cc
  • components/viz/common/resources/shared_image_format_utils.h
  • gpu/command_buffer/client/internal/mappable_buffer_io_surface.cc
  • gpu/command_buffer/common/BUILD.gn
  • gpu/command_buffer/common/iosurface_validation.cc
From 37d6e75fe5a7a5a0352c9d9e09d769fcd6a98d06 Mon Sep 17 00:00:00 2001
From: Bo Liu <[email protected]>
Date: Wed, 03 Jun 2026 14:33:13 -0700
Subject: [PATCH] gpu: Validate IOSurface in MappableBufferIOSurface

MappableBufferIOSurface should validate its IOSurface since it can be
sent from untrusted process to more trusted process; and it can be used
similar to shared memory without involving the "service" side of command
buffer that has the necessary validations.

This change duplicates the validation in
IOSurfaceImageBackingFactory::CreateSharedImageGMBs to
MappableBufferIOSurface::CreateFromHandleImpl, and change the latter to
return null instead of CHECK-ing on failure.

Changes:
* Move SharedImageFormatToIOSurfacePixelFormat to newly created
  gpu/command_buffer/common/iosurface_validation.h because it is needed
  for validation.
* Move validation code from IOSurfaceImageBackingFactory to
  iosurface_validation.h
* Relax the validation to consider RGBA and BGRA the same to avoid
  complexity
* Replace call new validation from MappableBufferIOSurface.

This also effectively reverts the previous bandaid fix that wasn't
really complete: https://chromium-review.googlesource.com/7710673

Bug: 497531791
Change-Id: I85a63f0747a68b0e241f3d2d1e5fb97357cf73e4
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7882552
Reviewed-by: Vasiliy Telezhnikov <[email protected]>
Reviewed-by: Dominic Farolino <[email protected]>
Auto-Submit: Bo Liu <[email protected]>
Commit-Queue: Bo Liu <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1641218}
---

diff --git a/components/viz/common/resources/shared_image_format_utils.cc b/components/viz/common/resources/shared_image_format_utils.cc
index 9891b5d5..9966920 100644
--- a/components/viz/common/resources/shared_image_format_utils.cc
+++ b/components/viz/common/resources/shared_image_format_utils.cc
@@ -377,55 +377,4 @@
   return kMappableSharedImageFormats;
 }
 
-#if BUILDFLAG(IS_MAC)
-
-bool MatchesSharedImageFormatWithIOSurfacePixelFormat(
-    const SharedImageFormat& format,
-    OSType io_surface_format,
-    bool override_rgba_to_brga) {
-  switch (io_surface_format) {
-    case kCVPixelFormatType_OneComponent8:
-      return format == SinglePlaneFormat::kR_8;
-    case kCVPixelFormatType_TwoComponent8:
-      return format == SinglePlaneFormat::kRG_88;
-    case kCVPixelFormatType_OneComponent16:
-      return format == SinglePlaneFormat::kR_16;
-    case kCVPixelFormatType_TwoComponent16:
-      return format == SinglePlaneFormat::kRG_1616;
-    case kCVPixelFormatType_ARGB2101010LEPacked:
-      return format == SinglePlaneFormat::kBGRA_1010102;
-    case kCVPixelFormatType_32BGRA:
-      return format == SinglePlaneFormat::kBGRA_8888 ||
-             format == SinglePlaneFormat::kBGRX_8888 ||
-             (override_rgba_to_brga &&
-              (format == SinglePlaneFormat::kRGBA_8888 ||
-               format == SinglePlaneFormat::kRGBX_8888));
-    case kCVPixelFormatType_32RGBA:
-      return format == SinglePlaneFormat::kRGBA_8888 ||
-             format == SinglePlaneFormat::kRGBX_8888;
-    case kCVPixelFormatType_64RGBAHalf:
-      return format == SinglePlaneFormat::kRGBA_F16;
-
-    case kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange:
-      return format == MultiPlaneFormat::kNV12;
-    case kCVPixelFormatType_422YpCbCr8BiPlanarVideoRange:
-      return format == MultiPlaneFormat::kNV16;
-    case kCVPixelFormatType_444YpCbCr8BiPlanarVideoRange:
-      return format == MultiPlaneFormat::kNV24;
-    case kCVPixelFormatType_420YpCbCr8VideoRange_8A_TriPlanar:
-      return format == MultiPlaneFormat::kNV12A;
-    case kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange:
-      return format == MultiPlaneFormat::kP010;
-    case kCVPixelFormatType_422YpCbCr10BiPlanarVideoRange:
-      return format == MultiPlaneFormat::kP210;
-    case kCVPixelFormatType_444YpCbCr10BiPlanarVideoRange:
-      return format == MultiPlaneFormat::kP410;
-    case kCVPixelFormatType_420YpCbCr8Planar:
-      return format == MultiPlaneFormat::kI420;
-  }
-  return false;
-}
-
-#endif  // BUILDFLAG(IS_MAC)
-
 }  // namespace viz
diff --git a/components/viz/common/resources/shared_image_format_utils.h b/components/viz/common/resources/shared_image_format_utils.h
index ba264df..83902bb 100644
--- a/components/viz/common/resources/shared_image_format_utils.h
+++ b/components/viz/common/resources/shared_image_format_utils.h
@@ -6,14 +6,8 @@
 #define COMPONENTS_VIZ_COMMON_RESOURCES_SHARED_IMAGE_FORMAT_UTILS_H_
 
 #include "base/component_export.h"
-#include "build/build_config.h"
 #include "components/viz/common/resources/shared_image_format.h"
 
-#if BUILDFLAG(IS_MAC)
-// Includes `MacTypes.h` for an io surface pixel format, i.e. `OSType`.
-#include <MacTypes.h>
-#endif  // BUILDFLAG(IS_MAC)
-
 enum SkColorType : int;
 
 namespace viz {
@@ -136,16 +130,6 @@
 COMPONENT_EXPORT(VIZ_SHARED_IMAGE_FORMAT)
 base::span<const SharedImageFormat> GetMappableSharedImageFormatForTesting();
 
-#if BUILDFLAG(IS_MAC)
-// Returns whether the given shared image format matches io surface pixel
-// format or not.
-COMPONENT_EXPORT(VIZ_SHARED_IMAGE_FORMAT)
-bool MatchesSharedImageFormatWithIOSurfacePixelFormat(
-    const SharedImageFormat& format,
-    OSType io_surface_format,
-    bool override_rgba_to_brga);
-#endif  // BUILDFLAG(IS_MAC)
-
 }  // namespace viz
 
 #endif  // COMPONENTS_VIZ_COMMON_RESOURCES_SHARED_IMAGE_FORMAT_UTILS_H_
diff --git a/gpu/command_buffer/client/internal/mappable_buffer_io_surface.cc b/gpu/command_buffer/client/internal/mappable_buffer_io_surface.cc
index de2db7f..77e6b415 100644
--- a/gpu/command_buffer/client/internal/mappable_buffer_io_surface.cc
+++ b/gpu/command_buffer/client/internal/mappable_buffer_io_surface.cc
@@ -13,6 +13,7 @@
 #include "base/notimplemented.h"
 #include "base/numerics/safe_conversions.h"
 #include "components/viz/common/resources/shared_image_format_utils.h"
+#include "gpu/command_buffer/common/iosurface_validation.h"
 #include "ui/gfx/mac/io_surface.h"
 
 namespace gpu {
@@ -132,14 +133,12 @@
     return nullptr;
   }
 
-  // TODO(crbug.com/497531791): Check whether IOSurface pixel format matches
-  // the given SharedImageFormat: `format` or not.
-  // Since there is no information about whether `gr_context_type_ ==
-  // GrContextType::kGL` or not, we will treat `override_rgba_to_brga` as always
-  // true. c.f. IOSurfaceImageBackingFactory::CreateSharedImageGMBs().
-  CHECK(viz::MatchesSharedImageFormatWithIOSurfacePixelFormat(
-      format, IOSurfaceGetPixelFormat(handle.io_surface().get()),
-      true /*override_rgba_to_brga*/));
+  std::string validation_error;
+  if (!ValidateIOSurface(handle.io_surface(), format, size,
+                         &validation_error)) {
+    LOG(ERROR) << validation_error;
+    return nullptr;
+  }
 
   int64_t io_surface_width = IOSurfaceGetWidth(handle.io_surface().get());
   int64_t io_surface_height = IOSurfaceGetHeight(handle.io_surface().get());
diff --git a/gpu/command_buffer/common/BUILD.gn b/gpu/command_buffer/common/BUILD.gn
index 2e98429..fad6c570 100644
--- a/gpu/command_buffer/common/BUILD.gn
+++ b/gpu/command_buffer/common/BUILD.gn
@@ -108,6 +108,18 @@
   ]
 
   deps = [ "//base" ]
+
+  if (is_apple) {
+    sources += [
+      "iosurface_validation.cc",
+      "iosurface_validation.h",
+    ]
+
+    frameworks = [
+      "CoreVideo.framework",
+      "IOSurface.framework",
+    ]
+  }
 }
 
 source_set("mailbox_sources") {
diff --git a/gpu/command_buffer/common/iosurface_validation.cc b/gpu/command_buffer/common/iosurface_validation.cc
new file mode 100644
index 0000000..eab41bf4
--- /dev/null
+++ b/gpu/command_buffer/common/iosurface_validation.cc
@@ -0,0 +1,153 @@
+// Copyright 2026 The Chromium Authors
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
Loading diff…

Original Bug Report

reported by [email protected]

Potential GPU process OOB read via unvalidated MappableBufferIOSurface planes

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: On macOS, MappableBufferIOSurface::CreateFromHandleImpl fails to validate IOSurface plane properties against the claimed SharedImageFormat. A compromised renderer can provide an IOSurface with a maliciously under-allocated plane, leading to a massive out-of-bounds memory read in the GPU process during video encoding fallback paths. This leaks GPU process memory back to the renderer within the video bitstream, potentially defeating ASLR.

Affected files:

  • gpu/command_buffer/client/internal/mappable_buffer_io_surface.cc
  • media/base/mac/video_frame_mac.mm
  • gpu/command_buffer/client/client_shared_image.cc
  • media/mojo/services/mojo_video_encode_accelerator_service.cc
  • media/base/video_frame.cc

Estimated timestamp from git blame: 2026-01-12

Disclaimer

Note: The following attack steps are potential and based on static analysis. Our tooling agent does not currently have the ability to run code to provide a working proof-of-concept.

Vulnerability Details

In gpu/command_buffer/client/internal/mappable_buffer_io_surface.cc, the CreateFromHandleImpl function takes an IOSurface from an IPC handle and wraps it for use in the GPU process. While the function verifies that the global IOSurface width and height are at least as large as the requested size, it completely fails to validate the pixel format, the number of planes, or the dimensions of individual planes against the claimed viz::SharedImageFormat.

Because the claimed SharedImageFormat is later trusted to calculate memory span sizes when the surface is mapped, an attacker can intentionally under-allocate a specific plane (e.g., the UV plane of an NV12 image) while maintaining large global dimensions.

Potential Attack Path

An attacker with a compromised macOS renderer process could exploit this via the following steps:

  1. Create Malicious IOSurface: The renderer natively creates an IOSurface with large global dimensions (e.g., 4096x4096), but specifies a custom kIOSurfacePlaneInfo array where Plane 1 (the UV plane) is drastically under-allocated (e.g., only 2 rows high instead of 2048).
  2. Send via IPC: The renderer wraps this in an ExportedSharedImage and sends it to the GPU process via media.mojom.VideoEncodeAccelerator::Encode(). It claims the format is NV12, the size is 4096x4096, and intentionally mismatches visible_rect from coded_size to force a cropping operation.
  3. Validation Bypass: The GPU process receives the handle. MappableBufferIOSurface::CreateFromHandleImpl checks IOSurfaceGetWidth/Height (which are 4096), accepts the handle, and ignores the maliciously small Plane 1.
  4. Fallback Mapping: In media/base/mac/video_frame_mac.mm, the mismatching visible_rect forces WrapVideoFrameInCVPixelBuffer to take a software mapping fallback path via ConvertToMemoryMappedFrame().
  5. OOB Span Calculation: ClientSharedImage::ScopedMapping::GetMemoryForPlane(1) calculates the expected memory span for Plane 1 based on the claimed NV12 format. For a 4096x4096 image, it expects 2048 rows. It computes span_length using the IOSurface stride multiplied by 2047 rows.
  6. Information Leak: The resulting base::span extends wildly out of bounds of the actual 2-row allocation, encompassing ~8MB of adjacent GPU process memory. This span is wrapped in a CVPixelBuffer and passed to VideoToolbox for hardware encoding.
  7. ASLR Defeat: VideoToolbox encodes the out-of-bounds GPU heap memory into the H.264/HEVC bitstream. The bitstream is sent back to the renderer via the BitstreamBufferReady callback, allowing the attacker to decode it, observe leaked pointers/data, and defeat GPU process ASLR.

Suggested Fix

Update MappableBufferIOSurface::CreateFromHandleImpl to strictly validate the properties of the imported IOSurface. Specifically:

  1. Ensure IOSurfaceGetPixelFormat(io_surface) matches the expected pixel format derived from SharedImageFormat (similar to the checks performed in IOSurfaceImageBackingFactory::CreateSharedImageGMBs).
  2. Verify that IOSurfaceGetPlaneCount(io_surface) matches format.NumberOfPlanes().
  3. Iterate over the planes and ensure that IOSurfaceGetWidthOfPlane and IOSurfaceGetHeightOfPlane meet or exceed the expected dimensions for each plane given the claimed SharedImageFormat and size.

Evaluated with Chrome root at commit: a9cbf6e8b275fe4147435aa905f3b7f5a656f5f0


Results from so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; 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.

View on issue tracker