CVE-2026-9893
Overview
Background
- `GrVkPipelineStateBuilder`
- the Ganesh (Vulkan backend) helper that compiles SkSL to SPIR-V, creates
VkShaderModuleobjects, and assembles them into a Vulkan graphics pipeline. - `VkShaderModule` / `VK_NULL_HANDLE`
- a Vulkan handle to a compiled shader module;
VK_NULL_HANDLEis the canonical “no object” sentinel that Vulkan calls treat as inert. - `DestroyShaderModule`
- the Vulkan API (invoked via
GR_VK_CALL) that frees aVkShaderModule; 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
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.
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
- Trigger pipeline-state construction
Content that exercises the Vulkan (Ganesh) backend drives
GrVkPipelineStateBuilderto create one or moreVkShaderModulehandles intooutShaderModules/shaderModules. - Force an error/cleanup path
A failure during pipeline creation reaches one of the cleanup loops, which calls
DestroyShaderModuleon each non-null handle but leaves the freed handle value in the array. - 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
DestroyShaderModuleon the already-freed module — a double-free/use-after-free of the Vulkan object. - 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
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
| Function | Change | Notes |
|---|---|---|
ifsrc/gpu/ganesh/vk/GrVkPipelineStateBuilder.cpp |
modified |
Files Changed
src/gpu/ganesh/vk/GrVkPipelineStateBuilder.cpp
Audit Directions
- Destroy-without-null handle cleanupSearch Vulkan (and other handle-based) teardown loops for
Destroy*/vkDestroy*calls whose handle slot is not reset toVK_NULL_HANDLE/nullptrafterward, especially when guarded by a truthiness test on the handle. - Multiple cleanup passes over one arrayAudit 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 livenessFlag
if (handle)guards that assume non-null implies live; ensure freeing an entry also invalidates the value the guard reads.
Patch
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; } }
Original Bug Report
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.cppthird_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.