High chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized Use in Skia
DescriptionUninitialized Use in Skia
ComponentSkia
Bug ClassUninitialized Memory
Tracker513340227
Fix commite6c23e38d3af (skia) +140/-88
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
if
src/gpu/ganesh/SurfaceContext.cpp
modified

Files Changed

  • src/gpu/ganesh/SurfaceContext.cpp
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;
Loading diff…

Original Bug Report

reported by [email protected]

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.cpp
  • third_party/skia/src/gpu/ganesh/GrDrawingManager.cpp
  • third_party/skia/src/gpu/ganesh/GrTransferFromRenderTask.cpp
  • gpu/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:

  1. Uninitialized Allocation: In SurfaceContext::transferPixels, GPU-to-CPU transfer buffers are allocated with GrResourceProvider::ZeroInit::kNo. These buffers are often retrieved from a shared scratch resource cache or recycled memory blocks and are not zero-initialized.
  2. Silent Task Failure: If the GrResourceAllocator fails to instantiate surface proxies (e.g., due to exhaustion of DEVICE_LOCAL memory), the GrDrawingManager::flush function skips the execution of all render tasks in the DAG (GrDrawingManager.cpp:208).
  3. Unconditional Completion Callback: Despite the execution failure, the completion callback (fFinishedProc) is still installed and fired via gpu->executeFlushInfo.
  4. 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:

  1. Inducing high GPU memory pressure through large WebGL/WebGPU allocations to increase the probability of proxy instantiation failure.
  2. Issuing a DoReadbackYUVImagePixelsINTERNAL IPC command to trigger an asynchronous readback.
  3. Forcing a flush through the command buffer. If the internal YUV proxies fail to instantiate, the GrTransferFromRenderTask will be skipped.
  4. The completion callback will execute, delivering a buffer populated with stale GPU memory. Since the GrResourceCache is 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.

View on issue tracker