CVE-2026-10977
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/gpu/ganesh/SurfaceContext.cpp |
modified |
Files Changed
src/gpu/ganesh/SurfaceContext.cpp
Patch
From e6c23e38d3afc3d06edfbddcabca26eceafe34b8 Mon Sep 17 00:00:00 2001 From: Michael Ludwig <[email protected]> Date: Tue, 26 May 2026 13:26:32 -0400 Subject: [PATCH] [ganesh] Use submitted proc to confirm async reads were issued Ganesh's finish proc doesn't have a status, but if flushSurface() failed for unrelated reasons (due to queued operations before the asyncRead was requested), the finish proc would be executed without actually submitting the copy command to the GPU. This tracks whether or not it was successfully submitted and makes that a requirement before handing the result to the original callback. The code is updated to more closely match Graphite's structure, including unmapping any buffers on failure. Bug: 513340227 Change-Id: If2998749a754d114b70e26e312a9a4cb08ea20e2 Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1242617 Commit-Queue: Michael Ludwig <[email protected]> Reviewed-by: Greg Daniel <[email protected]> --- diff --git a/src/gpu/ganesh/SurfaceContext.cpp b/src/gpu/ganesh/SurfaceContext.cpp index c6fa47f..1afa768 100644 --- a/src/gpu/ganesh/SurfaceContext.cpp +++ b/src/gpu/ganesh/SurfaceContext.cpp @@ -670,6 +670,114 @@ callbackContext); } +// Shared between RGBA and YUVA readbacks. +// +// It is used as a finish proc and a submitted proc to confirm that the commands to write to the +// buffer were submitted to the GPU. The finish proc then completes the process by notifying the +// async result callback. This class manages logic to trigger the client callback regardless of the +// order Ganesh runs the finish or submit procs and handles failure modes. +struct SurfaceContext::AsyncReadPixelContext { + ReadPixelsCallback* fClientCallback; + ReadPixelsContext fClientContext; + SkISize fSize; + GrClientMappedBufferManager* fMappedBufferManager; + + PixelTransferResult fPrimaryTransfer; // RGBA or Y + PixelTransferResult fUTransfer = {}; + PixelTransferResult fVTransfer = {}; + PixelTransferResult fATransfer = {}; + + AsyncReadPixelContext(ReadPixelsCallback* clientCallback, + ReadPixelsContext clientContext, + SkISize size, + GrClientMappedBufferManager* manager, + PixelTransferResult&& rgbaTransfer) + : fClientCallback(clientCallback) + , fClientContext(clientContext) + , fSize(size) + , fMappedBufferManager(manager) + , fPrimaryTransfer(std::move(rgbaTransfer)) + , fUTransfer{} + , fVTransfer{} + , fATransfer{} {} + + AsyncReadPixelContext(ReadPixelsCallback* clientCallback, + ReadPixelsContext clientContext, + SkISize size, + GrClientMappedBufferManager* manager, + PixelTransferResult&& yTransfer, + PixelTransferResult&& uTransfer, + PixelTransferResult&& vTransfer, + PixelTransferResult&& aTransfer) + : fClientCallback(clientCallback) + , fClientContext(clientContext) + , fSize(size) + , fMappedBufferManager(manager) + , fPrimaryTransfer(std::move(yTransfer)) + , fUTransfer(std::move(uTransfer)) + , fVTransfer(std::move(vTransfer)) + , fATransfer(std::move(aTransfer)) {} + + void setSubmitted(bool success) { + SkASSERT(fSubmitted == kUnsubmitted); + fSubmitted = success ? kSuccess : kFailure; + this->runClientCallbackMaybe(); + } + + void setFinished() { + fFinished = true; + this->runClientCallbackMaybe(); + } + + // This will destroy itself once it runs + void runClientCallbackMaybe() { + using AsyncReadResult = skgpu::TAsyncReadResult<GrGpuBuffer, + GrDirectContext::DirectContextID, + PixelTransferResult>; + + if (!fFinished || fSubmitted == kUnsubmitted) { + return; // wait for both finish and submit procs to trigger + } + + // Once both procs have fired, then proceed with the client callback. + std::unique_ptr<AsyncReadResult> result; + if (fSubmitted == kSuccess) { + result = std::make_unique<AsyncReadResult>(fMappedBufferManager->ownerID()); + } // else the submit failed so we need to invoke the client callback with null + + + using Plane = std::pair<const PixelTransferResult*, SkISize>; + SkISize uvSize = {fSize.width() / 2, fSize.height() / 2}; + for (auto [transfer, size] : {Plane{&fPrimaryTransfer, fSize}, + Plane{&fUTransfer, uvSize}, + Plane{&fVTransfer, uvSize}, + Plane{&fATransfer, fSize}}) { + if (!transfer->fTransferBuffer) { + // We reach this for RGBA transfers (just the first plane), or for YUV w/o an alpha. + break; + } + if (result && !result->addTransferResult(*transfer, + size, + transfer->fRowBytes, + fMappedBufferManager)) { + result.reset(); + } + if (!result && transfer->fTransferBuffer->isMapped()) { + transfer->fTransferBuffer->unmap(); + } + } + (*fClientCallback)(fClientContext, std::move(result)); + + delete this; + } + +private: + enum AsyncSubmitStatus { kUnsubmitted, kSuccess, kFailure }; + + AsyncSubmitStatus fSubmitted = kUnsubmitted; + bool fFinished = false; +}; + void SurfaceContext::asyncReadPixels(GrDirectContext* dContext, const SkIRect& rect, SkColorType colorType, @@ -707,37 +815,23 @@ return; } - struct FinishContext { - ReadPixelsCallback* fClientCallback; - ReadPixelsContext fClientContext; - SkISize fSize; - GrClientMappedBufferManager* fMappedBufferManager; - PixelTransferResult fTransferResult; - }; // Assumption is that the caller would like to flush. We could take a parameter or require an // explicit flush from the caller. We'd have to have a way to defer attaching the finish // callback to GrGpu until after the next flush that flushes our op list, though. - auto* finishContext = new FinishContext{callback, - callbackContext, - rect.size(), - mappedBufferManager, - std::move(transferResult)}; - auto finishCallback = [](GrGpuFinishedContext c) { - const auto* context = reinterpret_cast<const FinishContext*>(c); - auto manager = context->fMappedBufferManager; - auto result = std::make_unique<AsyncReadResult>(manager->ownerID()); - if (!result->addTransferResult(context->fTransferResult, - context->fSize, - context->fTransferResult.fRowBytes, - manager)) { - result.reset(); - } - (*context->fClientCallback)(context->fClientContext, std::move(result)); - delete context; - }; + auto* asyncContext = new AsyncReadPixelContext{callback, + callbackContext, + rect.size(), + mappedBufferManager, + std::move(transferResult)}; + GrFlushInfo flushInfo; - flushInfo.fFinishedContext = finishContext; - flushInfo.fFinishedProc = finishCallback; + flushInfo.fSubmittedContext = flushInfo.fFinishedContext = asyncContext; + flushInfo.fSubmittedProc = [](GrGpuSubmittedContext c, bool success) { + reinterpret_cast<AsyncReadPixelContext*>(c)->setSubmitted(success); + }; + flushInfo.fFinishedProc = [](GrGpuSubmittedContext c) { + reinterpret_cast<AsyncReadPixelContext*>(c)->setFinished(); + }; dContext->priv().flushSurface( this->asSurfaceProxy(), SkSurfaces::BackendSurfaceAccess::kNoAccess, flushInfo); @@ -977,71 +1071,27 @@ return; } - struct FinishContext { - ReadPixelsCallback* fClientCallback; - ReadPixelsContext fClientContext; - GrClientMappedBufferManager* fMappedBufferManager;
Original Bug Report
Potential cross-origin GPU memory leak in Ganesh asyncReadPixels via task failure
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 logic error in Skia’s Ganesh backend allows uninitialized GPU transfer buffers to be delivered to a renderer if internal task instantiation fails. Under memory pressure, a compromised renderer can exploit this to leak stale pixel data from other origins stored in recycled GPU memory.
Affected files:
third_party/skia/src/gpu/ganesh/SurfaceContext.cppthird_party/skia/src/gpu/ganesh/GrDrawingManager.cppthird_party/skia/src/gpu/ganesh/GrTransferFromRenderTask.cppgpu/command_buffer/service/raster_decoder.cc
Estimated timestamp from git blame: 2019-08-20
Description
An information disclosure vulnerability has been identified in the Ganesh GPU backend of Skia within Chromium. Asynchronous pixel readback operations (e.g., asyncReadPixels and asyncRescaleAndReadPixelsYUV420) can deliver uninitialized GPU-to-CPU transfer buffers to a renderer process if the internal transfer task fails to execute.
Root Cause Analysis
The vulnerability stems from a lack of error propagation during the task execution phase in Ganesh:
- Uninitialized Allocation: In
SurfaceContext::transferPixels, GPU-to-CPU transfer buffers are allocated withGrResourceProvider::ZeroInit::kNo. These buffers are often retrieved from a shared scratch resource cache or recycled memory blocks and are not zero-initialized. - Silent Task Failure: If the
GrResourceAllocatorfails to instantiate surface proxies (e.g., due to exhaustion ofDEVICE_LOCALmemory), theGrDrawingManager::flushfunction skips the execution of all render tasks in the DAG (GrDrawingManager.cpp:208). - Unconditional Completion Callback: Despite the execution failure, the completion callback (
fFinishedProc) is still installed and fired viagpu->executeFlushInfo. - Information Leak: The Ganesh callback API (
GrGpuFinishedProc) does not provide a success/failure status to the client. Consequently, the callback maps and delivers the transfer buffer even though it was never written to by the GPU, leaving it containing stale data from previous operations.
Potential Exploitation Scenario
An attacker with control over a compromised renderer could potentially trigger this leak by:
- Inducing high GPU memory pressure through large WebGL/WebGPU allocations to increase the probability of proxy instantiation failure.
- Issuing a
DoReadbackYUVImagePixelsINTERNALIPC command to trigger an asynchronous readback. - Forcing a flush through the command buffer. If the internal YUV proxies fail to instantiate, the
GrTransferFromRenderTaskwill be skipped. - The completion callback will execute, delivering a buffer populated with stale GPU memory. Since the
GrResourceCacheis shared across renderer processes in the GPU main thread, this memory may contain sensitive pixel data from other origins.
Suggested Fix
The Ganesh backend should be updated to propagate execution success or failure to the completion callbacks, similar to how the Graphite backend handles CallbackResult. Alternatively, transfer buffers used for asynchronous readbacks should be zero-initialized if there is any risk that the subsequent write task might be skipped.
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.