High watch CVSS 8.3 chrome Input Validation 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in GPU
DescriptionInsufficient validation of untrusted input in GPU
ComponentGPU
Sub-componentViz
Bug ClassInput Validation
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable
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.
Tracker500225310
Fix commitbb7ece2d0b14 (chromium/src) +76/-0
CWECWE-20 (Improper input validation)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:C/C:H/I:H/A:H
CISA KEVNot listed
CreditedGoogle
Reported2026-04-07 (per vendor advisory)
Fix landed2026-06-16 (public git)
Disclosed2026-07-29
Patch gap43 days — fix public in source before the advisory

Background

`VulkanYCbCrInfo` / `skgpu::VulkanYcbcrConversionInfo`
metadata describing how a YCbCr (YUV) image is sampled and converted in Vulkan, used to build the sampler and image view for a texture.
`TransferableResource`
the renderer-supplied descriptor for a shared image resource passed to viz; on Android it can carry YCbCr conversion metadata.
`GrPromiseImageTexture` / `GrVkImageInfo`
Skia’s promise texture and the concrete Vulkan image info (including fYcbcrConversionInfo, fFormat, fImageTiling) of the actual backing texture.
`ImageContextImpl::BeginAccessIfNecessaryInternal`
the viz display code path that begins read access to a shared image before Skia draws with it.

Root Cause Analysis

On Android with the Ganesh-Vulkan backend, ImageContextImpl::BeginAccessIfNecessaryInternal (enclosing function named in the commit message; the @@ context shows only the surrounding plane loop) built promise image textures from renderer-supplied resources without confirming that the promised YCbCr conversion metadata matched the actual backing Vulkan image. A compromised renderer could therefore supply unvalidated VulkanYCbCrInfo via a TransferableResource, and Skia would bind samplers and image views computed from the attacker’s metadata rather than from the real image.

The patch adds a check (gated by the kValidateGaneshVulkanYcbcrInfo feature) that, when the backend is GrBackendApi::kVulkan, extracts the real GrVkImageInfo from the promise texture, recomputes the expected skgpu::VulkanYcbcrConversionInfo via gpu::CreateVulkanYcbcrConversionInfo, and compares it to the image’s fYcbcrConversionInfo. On mismatch it records crash keys, calls base::debug::DumpWithoutCrashing, resets representation_scoped_read_access_, and returns false before any promise texture is pushed, so mismatched samplers can no longer be bound. This adds validation of untrusted input where previously none existed on this path.

Key insight
The core mistake was trusting renderer-supplied Vulkan YCbCr conversion metadata without verifying it against the actual backing image; the fix recomputes the expected conversion info from the real GrVkImageInfo and rejects the access (returning false) on any mismatch before Skia binds samplers.

Attack Path

  1. Supply forged metadata A compromised renderer sends a TransferableResource carrying VulkanYCbCrInfo that does not match the real Vulkan backing image (wrong format, tiling, model, range, or chroma parameters).
  2. Begin access on Android/Vulkan viz calls ImageContextImpl::BeginAccessIfNecessaryInternal, and pre-patch it proceeds to build promise image textures using the attacker’s YCbCr info.
  3. Bind mismatched samplers Skia constructs Vulkan samplers/image views from the promised (mismatched) conversion info and binds them against the actual image, per the advisory.
  4. Driver memory corruption (inferred) Sampling an image with a conversion descriptor inconsistent with its true format can drive the Vulkan driver into memory corruption; the advisory notes “potential sandbox escape,” which is (inferred) beyond what the patch shows.

Impact Assessment

A compromised renderer supplying mismatched YCbCr conversion metadata could cause the Ganesh-Vulkan backend in the GPU process on Android to bind inconsistent Vulkan samplers/image views, which the advisory states can lead to driver-level memory corruption and potential sandbox escape ((inferred)). The advisory classifies this as insufficient validation of untrusted input in GPU; no exploited-in-the-wild status is stated. Preconditions are Android, the Vulkan/Ganesh backend, and a renderer able to craft TransferableResource YCbCr metadata; the fix is feature-gated and default-enabled.

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

Audit Directions

  • Renderer-supplied GPU metadata
    Audit other paths that consume TransferableResource fields (formats, color spaces, YCbCr info) and feed them to Skia/driver object creation without cross-checking against the real backing resource.
  • Promise-texture backends
    Review Graphite/Dawn and non-Android Vulkan paths in image_context_impl.cc for equivalent unchecked promised-vs-actual image info, since this fix is scoped to Android Ganesh-Vulkan.
  • Feature-gated validations
    Track validations added behind default-enabled features (e.g. kValidateGaneshVulkanYcbcrInfo) to ensure the unvalidated path is not reachable when the flag is disabled.
From bb7ece2d0b14c526c3ec944102273e1e52fb539b Mon Sep 17 00:00:00 2001
From: Sunny Sachanandani <[email protected]>
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 <[email protected]>
Auto-Submit: Sunny Sachanandani <[email protected]>
Commit-Queue: Vasiliy Telezhnikov <[email protected]>
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 [email protected]

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
Uploaded PoC files