CVE-2026-19140
Overview
Background
- `SkiaOutputSurfaceImplOnGpu`
- the GPU-process object that drives Skia-based compositing output, including creating Vulkan release fences that signal when the compositor is done with a resource.
- `VkSemaphore` / `GrBackendSemaphore`
- a Vulkan synchronization primitive;
GrBackendSemaphores::GetVkSemaphore()extracts the rawVkSemaphorehandle from Skia’sGrBackendSemaphorewrapper. - `CreateReleaseFenceForVulkan`
- a helper that turns a semaphore into an exportable
gfx::GpuFenceHandleviaGetSemaphoreHandle(), previously also owning the semaphore’s lifetime on the failure path. - `VulkanFenceHelper`
- the canonical mechanism for deferring
VkSemaphoredestruction until the GPU has finished using it, avoiding destruction of an in-use handle. - `pending_release_fence_cbs_`
- a deque of
(GrBackendSemaphore, callback)items processed to emit release fences back to callers.
Root Cause Analysis
In the loop that drains pending_release_fence_cbs_, the old code called CreateReleaseFenceForVulkan(item.first), and inside that helper, when GetSemaphoreHandle() failed, it called vkDestroySemaphore(device, ...) directly to destroy the raw VkSemaphore and returned an invalid handle. However, the same item.first semaphore was (on the success path) also pushed into the semaphores vector via GrBackendSemaphores::GetVkSemaphore(item.first), meaning the handle was tracked/used elsewhere for destruction or signaling; destroying it immediately and inconsistently created a lifetime mismatch where a VkSemaphore could be destroyed while still referenced, a use-after-free of the Vulkan handle.
The fix restructures the loop to first extract the VkSemaphore, and only if it is non-VK_NULL_HANDLE push it into semaphores and then attempt CreateReleaseFenceForVulkan(semaphore); the helper is simplified to take a raw VkSemaphore, no longer performs the null check or the direct vkDestroySemaphore on failure, and simply returns whatever handle GetSemaphoreHandle() produced. Because the helper no longer destroys the semaphore, all destruction is now funneled through the normal path (VulkanFenceHelper, per the commit message), eliminating the divergent success/fail lifetime handling. The invariant restored is that a VkSemaphore is destroyed exactly once, through the deferred fence helper, never eagerly inside the fence-creation helper.
VkSemaphore eagerly with vkDestroySemaphore inside CreateReleaseFenceForVulkan’s failure branch while the same handle was also being tracked/used by the caller, causing double/early destruction; the fix removes that eager destruction so the semaphore’s lifetime is always managed by the standard VulkanFenceHelper path.Attack Path
- Trigger Vulkan release-fence path
A renderer drives compositing so that resources need Vulkan release fences, populating
pending_release_fence_cbs_with semaphores in the GPU process. - Force fence-export failure
Conditions cause
GetSemaphoreHandle()to fail so the old failure branch runsvkDestroySemaphoreon the semaphore. (inferred that the failure branch is reachable under attacker-influenceable conditions.) - Inconsistent lifetime
The same
VkSemaphoreremains referenced by the success-path bookkeeping / normal destruction path, so the handle is destroyed while still tracked, producing a use-after-free of the Vulkan semaphore object. - Post-fix outcome
After the patch the helper never destroys the semaphore; destruction flows solely through
VulkanFenceHelper, so the handle is freed exactly once.
Impact Assessment
VkSemaphore to be freed while still referenced, a use-after-free within the GPU process. The advisory classifies this as use-after-free in GPU and states no exploited-in-the-wild status; escalation to controlled corruption or code execution from this UAF is (inferred) and not demonstrated by the diff.Changed Functions
| Function | Change | Notes |
|---|---|---|
ifcomponents/viz/service/display_embedder/skia_output_surface_impl_on_gpu.cc |
modified |
Files Changed
components/viz/service/display_embedder/skia_output_surface_impl_on_gpu.cccomponents/viz/service/display_embedder/skia_output_surface_impl_on_gpu.h
Audit Directions
- Divergent success/failure lifetimeLook for helpers that destroy a resource on their error path while callers also track/destroy the same handle; unify ownership so a resource is freed in exactly one place.
- Eager vs. deferred GPU-object destructionAudit Vulkan/GL object teardown for direct
vkDestroy*calls that bypass deferred helpers likeVulkanFenceHelper, which can destroy handles still in use by the GPU. - Raw handle sharingReview paths where a raw
VkSemaphore(or similar) is extracted into more than one container/consumer, ensuring only one owner is responsible for destruction.
Patch
From 7247631764e0dc6dd1ec83912b0e05081b25bddc Mon Sep 17 00:00:00 2001 From: Vasiliy Telezhnikov <[email protected]> Date: Fri, 17 Jul 2026 06:15:18 -0700 Subject: [PATCH] Fix release fence vkSemaphore destruction There is no reason to handle fail case and success case differently, we should always use VulkanFenceHelper. Bug: 513044017 Change-Id: I00c0f359494fa81d129396a0ed9fa9070d831e59 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/8102907 Reviewed-by: Saifuddin Hitawala <[email protected]> Commit-Queue: Vasiliy Telezhnikov <[email protected]> Cr-Commit-Position: refs/heads/main@{#1663840} --- diff --git a/components/viz/service/display_embedder/skia_output_surface_impl_on_gpu.cc b/components/viz/service/display_embedder/skia_output_surface_impl_on_gpu.cc index 14cb0d6d8..d8485e6 100644 --- a/components/viz/service/display_embedder/skia_output_surface_impl_on_gpu.cc +++ b/components/viz/service/display_embedder/skia_output_surface_impl_on_gpu.cc @@ -2335,13 +2335,20 @@ semaphores.reserve(pending_release_fence_cbs_.size()); while (!pending_release_fence_cbs_.empty()) { + gfx::GpuFenceHandle release_fence; + auto& item = pending_release_fence_cbs_.front(); - auto release_fence = CreateReleaseFenceForVulkan(item.first); - if (release_fence.is_null()) { - LOG(ERROR) << "Unable to create a release fence for Vulkan."; - } else { - semaphores.emplace_back(GrBackendSemaphores::GetVkSemaphore(item.first)); + VkSemaphore semaphore = GrBackendSemaphores::GetVkSemaphore(item.first); + + if (semaphore != VK_NULL_HANDLE) { + semaphores.emplace_back(semaphore); + + release_fence = CreateReleaseFenceForVulkan(semaphore); + if (release_fence.is_null()) { + LOG(ERROR) << "Unable to create a release fence for Vulkan."; + } } + std::move(item.second).Run(std::move(release_fence)); pending_release_fence_cbs_.pop_front(); } @@ -2607,25 +2614,14 @@ #if BUILDFLAG(ENABLE_VULKAN) gfx::GpuFenceHandle SkiaOutputSurfaceImplOnGpu::CreateReleaseFenceForVulkan( - const GrBackendSemaphore& semaphore) { + VkSemaphore semaphore) { DCHECK(is_using_vulkan()); - if (GrBackendSemaphores::GetVkSemaphore(semaphore) == VK_NULL_HANDLE) { - return {}; - } - auto* implementation = vulkan_context_provider_->GetVulkanImplementation(); VkDevice device = vulkan_context_provider_->GetDeviceQueue()->GetVulkanDevice(); - auto handle = implementation->GetSemaphoreHandle( - device, GrBackendSemaphores::GetVkSemaphore(semaphore)); - if (!handle.is_valid()) { - vkDestroySemaphore(device, GrBackendSemaphores::GetVkSemaphore(semaphore), - /*pAllocator=*/nullptr); - LOG(ERROR) << "Failed to create a release fence for Vulkan."; - return {}; - } + auto handle = implementation->GetSemaphoreHandle(device, semaphore); return std::move(handle).ToGpuFenceHandle(); } diff --git a/components/viz/service/display_embedder/skia_output_surface_impl_on_gpu.h b/components/viz/service/display_embedder/skia_output_surface_impl_on_gpu.h index efaad72e9..5f5430b2 100644 --- a/components/viz/service/display_embedder/skia_output_surface_impl_on_gpu.h +++ b/components/viz/service/display_embedder/skia_output_surface_impl_on_gpu.h @@ -462,8 +462,7 @@ // by CreateAndStoreExternalSemaphoreVulkan(). May destroy VkSemaphore that // the |semaphore| stores if creation of a release fence fails. In this case, // invalid fence handle is returned. - gfx::GpuFenceHandle CreateReleaseFenceForVulkan( - const GrBackendSemaphore& semaphore); + gfx::GpuFenceHandle CreateReleaseFenceForVulkan(VkSemaphore semaphore); // Returns true if succeess. bool CreateAndStoreExternalSemaphoreVulkan( std::vector<GrBackendSemaphore>& end_semaphores);
Original Bug Report
Use-After-Free in GPU Process via Premature vkDestroySemaphore in Viz
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 Chrome Security team. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential Use-After-Free (UAF) vulnerability exists in the Viz display compositor when using Vulkan. If exporting a Vulkan semaphore fails after it has been submitted to the GPU, the code immediately destroys the semaphore while it is still in flight. This violation of the Vulkan specification can lead to driver-level memory corruption in the GPU process.
Affected files:
components/viz/service/display_embedder/skia_output_surface_impl_on_gpu.cccomponents/viz/service/display_embedder/skia_output_device.ccgpu/vulkan/vulkan_util_posix.cc
Estimated timestamp from git blame: Unknown (Google3 checkout)
Technical Description
A potential Use-After-Free vulnerability has been identified in SkiaOutputSurfaceImplOnGpu::CreateReleaseFenceForVulkan within the Viz display compositor. The issue stems from a violation of Vulkan validation rule VUID-vkDestroySemaphore-semaphore-05149, which states that all submitted batches referring to a semaphore must have completed execution before the semaphore is destroyed.
In the Viz Ganesh-on-Vulkan path, when a frame contains a resource requiring a release fence (kReleaseFence), a VkSemaphore is created, associated with the current Skia flush, and submitted to the GPU queue. Immediately after submission, Viz attempts to export this semaphore as a file descriptor (FD) to create a gfx::GpuFenceHandle.
If the export operation (vkGetSemaphoreFdKHR) fails—for example, due to file descriptor exhaustion in the GPU process—the code incorrectly performs an immediate destruction of the semaphore:
// components/viz/service/display_embedder/skia_output_surface_impl_on_gpu.cc
gfx::GpuFenceHandle SkiaOutputSurfaceImplOnGpu::CreateReleaseFenceForVulkan(
const GrBackendSemaphore& semaphore) {
// ...
auto handle = implementation->GetSemaphoreHandle(
device, GrBackendSemaphores::GetVkSemaphore(semaphore));
if (!handle.is_valid()) {
// POTENTIAL VULNERABILITY: Immediate destruction of an in-flight semaphore.
vkDestroySemaphore(device, GrBackendSemaphores::GetVkSemaphore(semaphore),
/*pAllocator=*/nullptr);
LOG(ERROR) << "Failed to create a release fence for Vulkan.";
return {};
}
return std::move(handle).ToGpuFenceHandle();
}
Because the semaphore was submitted to the GPU queue during the Submit() call preceding PostSubmit() (which calls CreateReleaseFenceForVulkan), the semaphore is still active on the GPU. Immediate destruction leads to a Use-After-Free when the GPU later attempts to signal the destroyed object.
Potential Attack Vector
A compromised renderer could potentially trigger this vulnerability through the following steps:
- Resource Exhaustion: The renderer exhausts file descriptors in the GPU process by repeatedly requesting the creation of native-buffer-backed SharedImages (e.g., via
CreateSharedImagewithSCANOUTusage). - Malicious Frame Submission: The renderer submits a
CompositorFramecontaining aTransferableResourcewithsynchronization_typeset tokReleaseFence. - Triggering the Error Path: When Viz processes this frame, it creates a Vulkan semaphore and submits it to the GPU. The subsequent attempt to export this semaphore to an FD fails due to the previously induced FD exhaustion.
- UAF Execution: The error path in
CreateReleaseFenceForVulkanexecutes, destroying the semaphore. The GPU then encounters the UAF when attempting to signal the now-freed semaphore memory.
Impact
Successful exploitation could lead to arbitrary memory corruption within the GPU process. On platforms like Android, the GPU process often operates with reduced sandboxing compared to the renderer (relying on OS-level isolation but lacking additional Chromium-specific sandbox layers by default), potentially allowing for privilege escalation from a compromised renderer to the system level.
Suggested Fix
Instead of immediately calling vkDestroySemaphore on the failure path in CreateReleaseFenceForVulkan, the semaphore should be added to the cleanup queue to ensure its destruction is deferred until the associated GPU work has completed. This can be achieved by ensuring the semaphore is always included in the semaphores vector in PostSubmit, which is subsequently passed to VulkanFenceHelper::EnqueueSemaphoresCleanupForSubmittedWork.
Evaluated with Chrome root at commit: b3153093eb3c78c3e88ccf562bcbc20437a04b0e
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.