Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in GPU
DescriptionInsufficient validation of untrusted input in GPU
ComponentGPU
Bug ClassLogic Error
Tracker500225310
Fix commitbb7ece2d0b14 (chromium/src) +76/-0
CISA KEVNot listed
CreditedGoogle
Disclosed2026-07-29

Changed Functions

FunctionChangeNotes
if
components/viz/service/display_embedder/image_context_impl.cc
modified
for
components/viz/service/display_embedder/image_context_impl.cc
modified

Files Changed

  • components/viz/service/display_embedder/image_context_impl.cc
  • gpu/command_buffer/service/skia_utils.cc
  • gpu/command_buffer/service/skia_utils.h
From bb7ece2d0b14c526c3ec944102273e1e52fb539b Mon Sep 17 00:00:00 2001
From: Sunny Sachanandani <sunnyps@chromium.org>
Date: Tue, 16 Jun 2026 08:46:50 -0700
Subject: [PATCH] [viz] [gpu] Validate YCbCr info in Ganesh Vulkan path on Android

A compromised renderer can supply unvalidated Vulkan YCbCr conversion
metadata (VulkanYCbCrInfo) via a TransferableResource. If the
Ganesh-Vulkan backend fails to verify this metadata against the actual
backing texture, Skia can bind mismatched Vulkan samplers and image
views, leading to driver-level memory corruption and potential sandbox
escape.

This CL introduces a new helper `gpu::IsYCbCrInfoCompatible` in
`skia_utils` to validate that the promised YCbCr info matches the
actual Vulkan image info. We then call this validation helper in the
Ganesh-Vulkan path of `ImageContextImpl::BeginAccessIfNecessaryInternal`
on Android.

If a mismatch is detected, we generate a non-fatal crash report via
`DumpWithoutCrashing` and cleanly reject access, failing the draw.

Bug: 500225310
Change-Id: I3b5fce3547090c7fb292e32a5b454fda6a6a6964
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7876653
Reviewed-by: Vasiliy Telezhnikov <vasilyt@chromium.org>
Auto-Submit: Sunny Sachanandani <sunnyps@chromium.org>
Commit-Queue: Vasiliy Telezhnikov <vasilyt@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1647601}
---

diff --git a/components/viz/service/display_embedder/image_context_impl.cc b/components/viz/service/display_embedder/image_context_impl.cc
index 21c009b1..ab708a7 100644
--- a/components/viz/service/display_embedder/image_context_impl.cc
+++ b/components/viz/service/display_embedder/image_context_impl.cc
@@ -8,6 +8,9 @@
 
 #include "base/check.h"
 #include "base/check_op.h"
+#include "base/debug/crash_logging.h"
+#include "base/debug/dump_without_crashing.h"
+#include "base/feature_list.h"
 #include "base/metrics/histogram_functions.h"
 #include "base/trace_event/trace_event.h"
 #include "components/viz/common/resources/shared_image_format_utils.h"
@@ -31,8 +34,20 @@
 #include "third_party/skia/include/gpu/graphite/dawn/DawnTypes.h"
 #include "third_party/skia/include/private/chromium/GrPromiseImageTexture.h"
 
+#if BUILDFLAG(IS_ANDROID) && BUILDFLAG(ENABLE_VULKAN)
+#include "components/viz/common/gpu/vulkan_context_provider.h"
+#include "third_party/skia/include/gpu/ganesh/vk/GrVkBackendSurface.h"
+#include "third_party/skia/include/gpu/ganesh/vk/GrVkTypes.h"
+#endif
+
 namespace {
 
+#if BUILDFLAG(IS_ANDROID) && BUILDFLAG(ENABLE_VULKAN)
+BASE_FEATURE(kValidateGaneshVulkanYcbcrInfo,
+             "ValidateGaneshVulkanYcbcrInfo",
+             base::FEATURE_ENABLED_BY_DEFAULT);
+#endif
+
 // These values are persisted to logs. Entries should not be renumbered and
 // numeric values should never be reused.
 // LINT.IfChange(CreateFallbackImageResult)
@@ -463,6 +478,47 @@
     }
   } else {
     CHECK(context_state->gr_context());
+#if BUILDFLAG(IS_ANDROID) && BUILDFLAG(ENABLE_VULKAN)
+    if (base::FeatureList::IsEnabled(kValidateGaneshVulkanYcbcrInfo) &&
+        context_state->gr_context()->backend() == GrBackendApi::kVulkan) {
+      auto* promise_texture =
+          representation_scoped_read_access_->promise_image_texture(0);
+      if (promise_texture) {
+        GrVkImageInfo image_info;
+        if (GrBackendTextures::GetVkImageInfo(promise_texture->backendTexture(),
+                                              &image_info)) {
+          VkPhysicalDevice physical_device =
+              context_state->vk_context_provider()
+                  ->GetDeviceQueue()
+                  ->GetVulkanPhysicalDevice();
+          skgpu::VulkanYcbcrConversionInfo expected_ycbcr_info =
+              gpu::CreateVulkanYcbcrConversionInfo(
+                  physical_device, image_info.fImageTiling, image_info.fFormat,
+                  format(), color_space(), ycbcr_info());
+
+          const auto& retrieved_ycbcr_info = image_info.fYcbcrConversionInfo;
+
+          if (retrieved_ycbcr_info != expected_ycbcr_info) {
+            SCOPED_CRASH_KEY_STRING32("viz", "image_format",
+                                      format().ToString());
+            SCOPED_CRASH_KEY_STRING64("viz", "color_space",
+                                      color_space().ToString());
+            SCOPED_CRASH_KEY_STRING32("viz", "image_size", size().ToString());
+            SCOPED_CRASH_KEY_STRING256(
+                "viz", "expected_ycbcr_info",
+                gpu::VulkanYcbcrConversionInfoToString(expected_ycbcr_info));
+            SCOPED_CRASH_KEY_STRING256(
+                "viz", "retrieved_ycbcr_info",
+                gpu::VulkanYcbcrConversionInfoToString(retrieved_ycbcr_info));
+
+            base::debug::DumpWithoutCrashing();
+            representation_scoped_read_access_.reset();
+            return false;
+          }
+        }
+      }
+    }
+#endif
     for (int plane_index = 0; plane_index < num_planes; plane_index++) {
       promise_image_textures_.push_back(
           representation_scoped_read_access_->promise_image_texture(
diff --git a/gpu/command_buffer/service/skia_utils.cc b/gpu/command_buffer/service/skia_utils.cc
index 4016c69..b1be616 100644
--- a/gpu/command_buffer/service/skia_utils.cc
+++ b/gpu/command_buffer/service/skia_utils.cc
@@ -535,6 +535,23 @@
   }
 }
 
+std::string VulkanYcbcrConversionInfoToString(
+    const skgpu::VulkanYcbcrConversionInfo& info) {
+  if (!info.isValid()) {
+    return "invalid";
+  }
+  return base::StringPrintf(
+      "fmt:%u,ext:0x%" PRIx64
+      ",model:%u,range:%u,x:%u,y:%u,filter:%u,"
+      "force:%u,must_match:%u,linear:%u",
+      static_cast<uint32_t>(info.format()), info.externalFormat(),
+      static_cast<uint32_t>(info.model()), static_cast<uint32_t>(info.range()),
+      static_cast<uint32_t>(info.xChromaOffset()),
+      static_cast<uint32_t>(info.yChromaOffset()),
+      static_cast<uint32_t>(info.chromaFilter()),
+      static_cast<uint32_t>(info.forceExplicitReconstruction()),
+      info.samplerFilterMustMatchChromaFilter(), info.supportsLinearFilter());
+}
 #endif  // BUILDFLAG(ENABLE_VULKAN)
 
 bool ShouldVulkanSyncCpuForSkiaSubmit(
diff --git a/gpu/command_buffer/service/skia_utils.h b/gpu/command_buffer/service/skia_utils.h
index a6dc57a..391e1b4 100644
--- a/gpu/command_buffer/service/skia_utils.h
+++ b/gpu/command_buffer/service/skia_utils.h
@@ -142,6 +142,9 @@
     const viz::SharedImageFormat& si_format,
     const gfx::ColorSpace& color_space,
     const std::optional<VulkanYCbCrInfo>& ycbcr_info);
+
+GPU_GLES2_EXPORT std::string VulkanYcbcrConversionInfoToString(
+    const skgpu::VulkanYcbcrConversionInfo& info);
 #endif  // BUILDFLAG(ENABLE_VULKAN)
 
 // Helper that returns true when Vulkan memory usage is high enough
Loading diff…

Original Bug Report

reported by vm...@google.com

Potential GPU Sandbox Escape via Unvalidated VulkanYCbCrInfo in Ganesh

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 without the security team.

Overview: A compromised renderer can supply unvalidated Vulkan YCbCr conversion metadata to the GPU process via a TransferableResource. The Ganesh-Vulkan backend fails to verify this metadata against the actual backing texture, causing Skia to bind mismatched Vulkan samplers and image views. This violates Vulkan specifications and can lead to driver-level memory corruption and a GPU sandbox escape.

Affected files:

  • components/viz/service/display_embedder/image_context_impl.cc
  • gpu/command_buffer/service/skia_utils.cc
  • gpu/ipc/common/vulkan_ycbcr_info_mojom_traits.h
  • services/viz/public/mojom/compositing/transferable_resource.mojom

Estimated timestamp from git blame: 2023-05-02

Summary

There is a potential sandbox escape vulnerability in the Ganesh-Vulkan rendering path within the GPU process. When a renderer provides a TransferableResource, it can optionally include gpu.mojom.VulkanYCbCrInfo. This metadata is deserialized via Mojo without validation. While the newer Skia Graphite path correctly validates that the provided YCbCr info matches the actual backing texture, the Ganesh path skips this validation entirely.

This discrepancy allows a compromised renderer to trick Skia into creating a Vulkan rendering pipeline with an attacker-controlled VkSamplerYcbcrConversion object (e.g., using an arbitrary externalFormat), but fulfilling it with a standard texture that lacks this conversion. Binding these mismatched objects violates Vulkan specification VUID-VkWriteDescriptorSet-descriptorType-01948, leading to undefined behavior in the GPU driver and potential memory corruption in the highly privileged GPU process.

Technical Details

  1. Unvalidated Deserialization: A renderer can populate the ycbcr_info field of a TransferableResource. The Mojo traits in gpu/ipc/common/vulkan_ycbcr_info_mojom_traits.h deserialize this data directly into a gpu::VulkanYCbCrInfo struct without bounds checking or validation.
  2. Missing Ganesh Validation: In components/viz/service/display_embedder/image_context_impl.cc, the function ImageContextImpl::BeginAccessIfNecessaryInternal retrieves the actual fulfillment texture.
    • For the Graphite path, it performs a strict compatibility check (DawnYCbCrVkDescriptorsAreCompatible) to ensure the promised YCbCr info matches the actual texture.
    • For the Ganesh path (the else block at lines 469-476), this validation is missing. It simply accepts the texture.
  3. Promise Image Creation: Viz uses the attacker’s ycbcr_info via SkiaOutputSurfaceImpl::GetGrBackendFormatForTexture to create a GrBackendFormat. This format is passed to SkImages::PromiseTextureFrom to create a Skia promise image. Skia uses this promised format to build the Vulkan pipeline (GrVkPipelineState), creating an immutable VkSampler configured with the attacker’s VkSamplerYcbcrConversion object.
  4. Skia Release Build Blind Spot: When Skia fulfills the promise image during drawing, the provided GrBackendTexture may have a standard format (e.g., RGB with no externalFormat). Skia’s GrSurfaceProxy::validateSurface attempts to check if the fulfillment format matches the promised format, but this check is wrapped in SkDEBUGCODE and is compiled out of release builds.
  5. Vulkan Spec Violation: Skia creates a VkImageView from the fulfillment texture, which lacks the VkSamplerYcbcrConversion (because the actual texture doesn’t have one). During drawing (GrVkPipelineState::setAndBindTextures), Skia binds the immutable sampler (with conversion) and the VkImageView (without conversion) into the same descriptor set. This violates Vulkan VUID-VkWriteDescriptorSet-descriptorType-01948.

Suggested Attacker Steps

(Note: These are potential steps based on code analysis; our tooling cannot yet execute a working proof of concept.)

  1. Compromise a renderer process.
  2. Create a standard shared image (e.g., RGBA8) in the GPU process.
  3. Construct a viz::CompositorFrame containing a viz::TransferableResource that references the standard shared image.
  4. Populate the ycbcr_info field of the TransferableResource with crafted values, specifically targeting the 64-bit external_format field with an arbitrary or out-of-bounds value.
  5. Submit the frame via viz::mojom::CompositorFrameSink::SubmitCompositorFrame to a device utilizing Ganesh over Vulkan (e.g., many Android devices).
  6. When the GPU process attempts to composite the frame, the Vulkan driver will be forced to consume the mismatched descriptors, triggering driver-level undefined behavior (e.g., out-of-bounds read/write) that the attacker can potentially shape into a full GPU process compromise.

Suggested Fix

Implement a validation check for the Ganesh branch in ImageContextImpl::BeginAccessIfNecessaryInternal (located in components/viz/service/display_embedder/image_context_impl.cc).

Before populating promise_image_textures_, the code should compare the ycbcr_info() promised by the renderer against the actual skgpu::VulkanYcbcrConversionInfo or GrBackendFormat of the backend textures retrieved from representation_scoped_read_access_. If they do not match, the access should be rejected (similar to how the Graphite branch returns false), preventing Skia from consuming mismatched Vulkan resources.

Evaluated with Chrome root at commit: f200f57a19490707ff8bc7aa5de3cbc443a3afad


Results 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