Critical watch ~ plausible CVSS 8.3 chrome UAF 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
8.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Skia
DescriptionUse after free in Skia
ComponentSkia
Bug ClassUAF
Action tierwatch — corruption primitive but gated (uncommon) — not default-reachable — upheld 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 AIPlausible
Reviewer notes: Diff only nulls handles after destroy; the second destroy/use of the stale handle is not shown, so double-destroy is inferred. The third site (line ~357) is not shown to be an error path. Limited to Ganesh Vulkan backend.
A second, independent AI reviewer (not a human) re-derived this analysis from the fixing commit and the vendor advisory, looking for errors.
Tracker513972075
Fix commit3471ebf5af0c (skia) +3/-0
CWECWE-416 (Use-after-free)
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-05-17 (per vendor advisory)
Fix landed2026-05-19 (public git)
Disclosed2026-05-27
Patch gap8 days — fix public in source before the advisory

Background

`GrVkPipelineStateBuilder`
the Ganesh (Vulkan backend) helper that compiles SkSL to SPIR-V, creates VkShaderModule objects, and assembles them into a Vulkan graphics pipeline.
`VkShaderModule` / `VK_NULL_HANDLE`
a Vulkan handle to a compiled shader module; VK_NULL_HANDLE is the canonical “no object” sentinel that Vulkan calls treat as inert.
`DestroyShaderModule`
the Vulkan API (invoked via GR_VK_CALL) that frees a VkShaderModule; after it returns the handle value still sits in the array but now refers to a freed object.
`outShaderModules` / `shaderModules`
local arrays holding the shader-module handles created during pipeline-state construction, cleaned up on error/return paths.

Root Cause Analysis

Verify before building a PoC

This analysis is consistent with the fixing commit but not fully provable from it: part of the vulnerable path lives in code the fix commit does not touch. Confirm the trigger and the sink against the source before relying on it for exploit development.
What the commit does not establish: Diff only nulls handles after destroy; the second destroy/use of the stale handle is not shown, so double-destroy is inferred. The third site (line ~357) is not shown to be an error path. Limited to Ganesh Vulkan backend.

On the error/cleanup paths in GrVkPipelineStateBuilder, the code iterates the handle arrays and, for each non-null entry, calls DestroyShaderModule(...) on outShaderModules[i] / shaderModules[i], but left the now-dangling handle value in the array afterward. Because the if (outShaderModules[i]) / if (shaderModules[i]) guards test the raw handle value (not VK_NULL_HANDLE-vs-live), any subsequent cleanup pass — or a destructor/second traversal over the same array — would see the same non-null value, treat it as live, and call DestroyShaderModule again on an already-freed module, i.e. a double-free / use-after-free of the Vulkan object.

The fix sets outShaderModules[i] = VK_NULL_HANDLE; (and shaderModules[i] = VK_NULL_HANDLE;) immediately after each destroy at all three cleanup sites, so the guard sees a null handle and skips the freed module, breaking the double-destroy. This preserves the invariant that a handle slot is non-null only while it refers to a live module.

Key insight
The single mistake was destroying a VkShaderModule without clearing its array slot, so the non-null-handle guard could route a second destroy at the same freed handle; nulling the slot to VK_NULL_HANDLE after destruction makes the guard skip it.

Attack Path

  1. Trigger pipeline-state construction Content that exercises the Vulkan (Ganesh) backend drives GrVkPipelineStateBuilder to create one or more VkShaderModule handles into outShaderModules / shaderModules.
  2. Force an error/cleanup path A failure during pipeline creation reaches one of the cleanup loops, which calls DestroyShaderModule on each non-null handle but leaves the freed handle value in the array.
  3. Re-enter cleanup over the same array A subsequent traversal of the same array (inferred: a later cleanup pass or destructor over the still-populated handles) re-tests the stale non-null value, treats it as live, and calls DestroyShaderModule on the already-freed module — a double-free/use-after-free of the Vulkan object.
  4. Object reuse or corruption Destroying the freed handle a second time corrupts Vulkan driver bookkeeping for that object; any escalation beyond that is (inferred) and not demonstrated by the patch.

Impact Assessment

An attacker who can steer GPU/rasterization work into the Vulkan backend and induce a shader-module cleanup path gains a double-free / use-after-free of a VkShaderModule in the process running Skia’s Ganesh Vulkan code (typically the GPU process). The advisory classifies this as a use-after-free in Skia; escalation to a controllable primitive or code execution is (inferred). The advisory does not state exploitation in the wild.

Changed Functions

FunctionChangeNotes
if
src/gpu/ganesh/vk/GrVkPipelineStateBuilder.cpp
modified

Files Changed

  • src/gpu/ganesh/vk/GrVkPipelineStateBuilder.cpp

Audit Directions

  • Destroy-without-null handle cleanup
    Search Vulkan (and other handle-based) teardown loops for Destroy*/vkDestroy* calls whose handle slot is not reset to VK_NULL_HANDLE/nullptr afterward, especially when guarded by a truthiness test on the handle.
  • Multiple cleanup passes over one array
    Audit builders/objects where the same handle array can be cleaned up on both an error path and a destructor (or nested scopes), which lets a stale non-null handle be freed twice.
  • Guard tests raw value, not liveness
    Flag if (handle) guards that assume non-null implies live; ensure freeing an entry also invalidates the value the guard reads.
From 3471ebf5af0cc82c45df7e2c60ef40a4d5de27f5 Mon Sep 17 00:00:00 2001
From: Greg Daniel <[email protected]>
Date: Mon, 18 May 2026 14:53:08 +0000
Subject: [PATCH] Null out VkShaderModule handles when destroying them.

Bug: b/513972075
Change-Id: Ieeaf92c87343aa328479302d9cffeb36b5b0c8e4
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1238159
Auto-Submit: Greg Daniel <[email protected]>
Reviewed-by: Robert Phillips <[email protected]>
---

diff --git a/src/gpu/ganesh/vk/GrVkPipelineStateBuilder.cpp b/src/gpu/ganesh/vk/GrVkPipelineStateBuilder.cpp
index 5012b5e..9f4bc32 100644
--- a/src/gpu/ganesh/vk/GrVkPipelineStateBuilder.cpp
+++ b/src/gpu/ganesh/vk/GrVkPipelineStateBuilder.cpp
@@ -151,6 +151,7 @@
             if (outShaderModules[i]) {
                 GR_VK_CALL(fGpu->vkInterface(),
                            DestroyShaderModule(fGpu->device(), outShaderModules[i], nullptr));
+                outShaderModules[i] = VK_NULL_HANDLE;
             }
         }
         return 0;
@@ -282,6 +283,7 @@
                 if (shaderModules[i]) {
                     GR_VK_CALL(fGpu->vkInterface(), DestroyShaderModule(fGpu->device(),
                                                                         shaderModules[i], nullptr));
+                    shaderModules[i] = VK_NULL_HANDLE;
                 }
             }
             return nullptr;
@@ -357,6 +359,7 @@
         if (shaderModules[i]) {
             GR_VK_CALL(fGpu->vkInterface(), DestroyShaderModule(fGpu->device(), shaderModules[i],
                                                                 nullptr));
+            shaderModules[i] = VK_NULL_HANDLE;
         }
     }
 
Loading diff…

Original Bug Report

reported by [email protected]

Potential double destruction of VkShaderModule in Skia Vulkan backend

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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.

Overview: A double-free vulnerability exists in the Skia Ganesh Vulkan backend when creating pipeline states. Stale VkShaderModule handles are not nulled after destruction during a failed cache-load attempt, which can lead to a second destruction if a subsequent from-source retry also fails.

Affected files:

  • third_party/skia/src/gpu/ganesh/vk/GrVkPipelineStateBuilder.cpp
  • third_party/skia/src/gpu/ganesh/vk/GrVkUtil.cpp

Estimated timestamp from git blame: 2020-06-10

Summary

A potential double-free vulnerability has been identified in third_party/skia/src/gpu/ganesh/vk/GrVkPipelineStateBuilder.cpp. The vulnerability occurs when the system fails to nullify stack-allocated Vulkan shader module handles after they are destroyed during a failed shader loading attempt. If a subsequent retry also fails, the same handles are destroyed a second time, leading to driver-side heap corruption in the GPU process.

Root Cause Analysis

In GrVkPipelineStateBuilder::finalize, a stack-allocated array shaderModules is used to store VkShaderModule handles:

// third_party/skia/src/gpu/ganesh/vk/GrVkPipelineStateBuilder.cpp:187
VkShaderModule shaderModules[kGrShaderTypeCount] = { VK_NULL_HANDLE, VK_NULL_HANDLE };

The function first attempts to load shaders from the persistent cache via loadShadersFromCache. If this function partially succeeds (e.g., installs one stage) but then fails on a subsequent stage, it enters a cleanup loop to destroy any successfully created modules:

// third_party/skia/src/gpu/ganesh/vk/GrVkPipelineStateBuilder.cpp:149
if (!success) {
    for (int i = 0; i < kGrShaderTypeCount; ++i) {
        if (outShaderModules[i]) {
            GR_VK_CALL(fGpu->vkInterface(),
                       DestroyShaderModule(fGpu->device(), outShaderModules[i], nullptr));
        }
    }
    return 0;
}

Crucially, the handles in outShaderModules (which point to the caller’s shaderModules array) are destroyed but not reset to VK_NULL_HANDLE.

When loadShadersFromCache returns 0, finalize proceeds to a “retry from sources” path. If this retry path also fails—for instance, if createVkShaderModule returns false due to an SkSL compilation error or memory pressure before it can overwrite the stale handle—finalize executes its own final cleanup loop:

// third_party/skia/src/gpu/ganesh/vk/GrVkPipelineStateBuilder.cpp:280
if (!success) {
    for (int i = 0; i < kGrShaderTypeCount; ++i) {
        if (shaderModules[i]) {
            GR_VK_CALL(fGpu->vkInterface(), DestroyShaderModule(fGpu->device(),
                                                                shaderModules[i], nullptr));
        }
    }
    return nullptr;
}

Because the stale handles from the first attempt were never nulled, the second cleanup loop calls vkDestroyShaderModule on a handle that has already been freed. In many Vulkan implementations, vkCreateShaderModule does not modify the output handle on failure, ensuring the stale handle persists and is processed again.

Potential Attack Vector

An attacker in a compromised renderer process could potentially trigger this vulnerability via Out-of-Process Rasterization (OOP-R). By providing specially crafted draw calls or paint commands that result in complex SkSL, the attacker could attempt to induce a failure in the Vulkan shader compilation or module creation path (e.g., via forced memory pressure or specific compilation errors). If the first attempt (cache load) partially installs shaders and fails, and the second attempt (from source) also fails, the double-free will be triggered.

Impact

This issue leads to a driver-heap double-free in the GPU process. Since Vulkan driver handles are involved, this is not protected by MiraclePtr. On platforms like Android, the GPU process is typically unsandboxed and shares the browser’s UID, making this a high-severity path for privilege escalation or remote code execution.

Suggested Fix

The cleanup loops in both loadShadersFromCache and finalize should be modified to nullify the handles immediately after destruction:

if (shaderModules[i]) {
    GR_VK_CALL(fGpu->vkInterface(), DestroyShaderModule(fGpu->device(), shaderModules[i], nullptr));
    shaderModules[i] = VK_NULL_HANDLE;
}

Evaluated with Chrome root at commit: 1a8d40fc44df2088d5945c0bf53584038aa1614a


Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:

  • If you are familiar with the severity guidelines, you may adjust the severity.
  • If this is a false positive, and there’s no work to be done, please close as WAI.
  • If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.

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