High watch CVSS 5.3 chrome OOB 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
5.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactOut of bounds read in GPU
DescriptionOut of bounds read in GPU
ComponentGPU
Sub-componentGPU
Bug ClassOOB
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
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.
Tracker497531791
Fix commit37d6e75fe5a7 (chromium/src) +212/-207
CWECWE-125 (Out-of-bounds read)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-03-29 (per vendor advisory)
Fix landed2026-06-03 (public git)
Disclosed2026-05-12
Patch gapNot computed: several commits cite this bug and it is not yet established which one is the fix.

Background

`IOSurface`
A macOS shared-memory image buffer; on Chrome it can be passed from a less-trusted process to a more-trusted one, so its pixel format and dimensions are attacker-influenceable.
`MappableBufferIOSurface`
A GPU command-buffer client object that wraps an IOSurface and lets it be used much like shared memory without going through the command buffer’s “service”-side validation.
`SharedImageFormat` / `MatchesSharedImageFormatWithIOSurfacePixelFormat`
The declared pixel layout of a shared image, and the (now-removed) helper that only checked whether an IOSurface’s pixel format matched that SharedImageFormat.
`ValidateIOSurface`
The new consolidated validator in gpu/command_buffer/common/iosurface_validation.h/.cc that checks the IOSurface against the expected format and size, returning false (with an error string) instead of crashing.

Root Cause Analysis

MappableBufferIOSurface::CreateFromHandleImpl accepted an IOSurface handle originating from an untrusted process and used it like shared memory, but only ever CHECK-ed that its pixel format matched the SharedImageFormat — the inline TODO(crbug.com/497531791) comment shows the format/size validation was known-incomplete, and the dimensions were not validated against the expected size. Because this path bypasses the command buffer’s service-side checks, an IOSurface whose real dimensions were smaller than the claimed size could be mapped and read beyond its backing storage, an out-of-bounds read.

The fix moves SharedImageFormatToIOSurfacePixelFormat and the validation logic (previously only in IOSurfaceImageBackingFactory::CreateSharedImageGMBs) into a shared iosurface_validation.{h,cc}, and replaces the format-only CHECK with a call to ValidateIOSurface(handle.io_surface(), format, size, &validation_error) that also checks size and, on failure, logs and returns nullptr instead of CHECK-crashing. This re-establishes the invariant that an untrusted IOSurface is fully validated (format and dimensions) before being mapped, and the commit notes it supersedes an earlier incomplete bandaid fix (crrev 7710673).

Key insight
The core mistake was mapping an untrusted IOSurface after only a format CHECK, never validating its dimensions against the expected size; the fix routes it through ValidateIOSurface, which validates both format and size and returns null on mismatch.

Attack Path

  1. Supply a mismatched IOSurface A less-trusted process sends an IOSurface handle whose real dimensions/format do not match the SharedImageFormat and size that will be claimed for it.
  2. Reach the client wrapper Creation flows into MappableBufferIOSurface::CreateFromHandleImpl, which (pre-patch) only CHECK-ed the pixel format via MatchesSharedImageFormatWithIOSurfacePixelFormat.
  3. Map and use as shared memory The surface is used like shared memory without the service-side validation, so the undersized/mismatched buffer is accessed per the larger claimed geometry.
  4. Out-of-bounds read Reads extend past the IOSurface’s actual backing storage, the out-of-bounds read the advisory reports; any resulting information disclosure beyond the OOB read is (inferred) and not shown in the diff.

Impact Assessment

An attacker in a less-trusted process could cause an out-of-bounds read in the more-trusted GPU process by handing it an IOSurface whose dimensions/format are inconsistent with the claimed SharedImageFormat/size. The vendor advisory rates this high severity and describes an out-of-bounds read in GPU (macOS, given the IS_MAC/Apple-gated code); escalation beyond the read (e.g., disclosure of adjacent memory or a sandbox-relevant leak) is (inferred). The precondition is the ability to send a crafted IOSurface handle to the GPU process.

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

Audit Directions

  • Untrusted handles bypassing service validation
    Audit client-side paths (MappableBuffer*, GMB/IOSurface wrappers) that consume handles from a less-trusted process and use them like shared memory, confirming each validates format and dimensions before mapping.
  • Format-only vs. size validation
    Review callers that only check pixel format against a SharedImageFormat and assume the geometry is trustworthy; the missing size check is the OOB-enabling gap here.
  • `CHECK` on attacker-influenced input
    Flag CHECK/DCHECK used as “validation” on data from a lower-privilege process; these should be graceful failures (return null) and, more importantly, must cover every dimension that governs later memory access.
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