High watch CVSS 3.1 chrome Uninitialized Memory 📄 Reporter bug report 🔧 Commit mapped

Overview

High
Severity
3.1
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUninitialized Use in Skia
DescriptionUninitialized Use in Skia
ComponentSkia
Sub-componentGanesh
Bug ClassUninitialized Memory
Action tierwatch — information disclosure — security-relevant, below the RCE-class bar
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.
Tracker536165038
Fix commit30d6603d43a5 (skia) +130/-3
CWECWE-457 (Use of uninitialized)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:L/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-07-18 (per vendor advisory)
Fix landed2026-07-22 (public git)
Disclosed2026-08-06
Patch gap15 days — fix public in source before the advisory

Background

Ganesh
Skia’s GPU backend that batches drawing into render tasks and executes them during a flush/submit cycle.
MSAA resolve
the step where a multisample color attachment is downsampled (“resolved”) into a single-sample texture that clients can read; resolve_and_mipmap performs this and regenerates mipmaps.
`didFlush`
the boolean returned by GrDrawingManager::flush that reports whether the recorded render tasks actually executed.
Uninitialized MSAA attachment
a freshly created multisample color buffer that no render pass has written yet, so its GPU memory holds indeterminate bytes.

Root Cause Analysis

The surface-flush entry point in GrDrawingManager.cpp called this->flush(proxies, access, info, newState) and then unconditionally looped over proxies calling resolve_and_mipmap(gpu, proxy), regardless of whether the flush succeeded. When the flush failed (or aborted early), the recorded draw never executed and the MSAA color attachment was never written, yet resolve_and_mipmap still resolved that never-written attachment into the client-visible single-sample texture, copying uninitialized GPU memory into readable pixels. The invariant violated is that a resolve must only follow a flush that genuinely executed the render tasks that populated the multisample buffer.

The fix guards the resolve loop with if (didFlush), so resolve_and_mipmap runs only when this->flush reports the tasks actually ran. A companion change in the early-return “nothing to flush” path switches return false to return true, so a legitimately empty flush is correctly reported as a success (its info.fSubmittedProc is already invoked with true) rather than being misclassified as a failure.

Key insight
The single mistake is treating “flush attempted” as “flush executed”: resolve_and_mipmap was run unconditionally, so a failed flush still resolved an uninitialized MSAA attachment into a client-readable texture. Gating the resolve on the didFlush return value ties the resolve step to actual render-task execution and prevents leaking uninitialized memory.

Attack Path

  1. Create a fresh MSAA render target Wrap a backend texture as a multisample surface so a new persistent MSAA color attachment is allocated whose contents are uninitialized GPU memory.
  2. Record a draw and force flush failure Record a full-surface draw (marking the target dirty) and cause this->flush to fail so the render tasks never execute, mirroring the test’s FailingPreFlushCallback.
  3. Trigger the unconditional resolve Because the pre-fix code always looped resolve_and_mipmap(gpu, proxy) after flush, the never-written MSAA attachment is resolved into the single-sample backing texture.
  4. Read back uninitialized contents Read the backing texture to obtain the resolved uninitialized bytes as client-visible pixel data, as the test’s read_backing_pixel demonstrates.
  5. Escalate from the disclosed bytes Use the leaked uninitialized GPU memory as an information-disclosure primitive (inferred); the diff shows only the uninitialized-read, not any specific downstream sink or memory corruption.

Impact Assessment

An attacker able to drive Skia’s Ganesh backend into a failed flush on a persistent MSAA target gains an uninitialized-memory read: previously uninitialized GPU buffer contents are resolved into a readable texture and exposed as pixel data, running in whatever process hosts Skia’s GPU rendering. Preconditions are a backend that requires explicit MSAA resolve (the test skips when msaaResolvesAutomatically() or preferDiscardableMSAAAttachment() is true) and the ability to make a flush fail. The vendor advisory rates this high severity and does not report in-the-wild exploitation; NVD scores it CVSS 3.1. Any escalation beyond information disclosure is (inferred).

Changed Functions

FunctionChangeNotes
if
src/gpu/ganesh/GrDrawingManager.cpp
modified
for
src/gpu/ganesh/GrDrawingManager.cpp
modified
if
tests/GrSurfaceResolveTest.cpp
modified

Files Changed

  • src/gpu/ganesh/GrDrawingManager.cpp
  • tests/GrSurfaceResolveTest.cpp

Audit Directions

  • Unchecked flush/submit return values
    Audit every site that performs post-flush work (resolves, mipmap regen, readbacks, copies) without checking the flush’s success return, since such work may operate on state the flush never produced.
  • Resolve/regeneration tied to attempt not execution
    Look for MSAA resolve, mipmap, or copy steps that run unconditionally after a batched GPU operation rather than being gated on the operation actually executing.
  • Success semantics of empty/early-return paths
    Review early-return branches in flush-like functions to confirm they report success or failure consistently with the callbacks they have already invoked, since a misclassified boolean can both hide failures and mislead callers.
From 30d6603d43a591eadedfff08c467fe76c7110779 Mon Sep 17 00:00:00 2001
From: Robert Phillips <[email protected]>
Date: Wed, 22 Jul 2026 09:29:24 -0400
Subject: [PATCH] [ganesh] Skip resolve/mipmap step on flush failure

This AI generated patch seems reasonable and harmless enough.

I do think that it is only a small part of a larger problem around Ganesh's handling of flush failures.

In practice, Chrome will have to have handled the flush failure via the callback system in order to respond to the failure. That handling should discard the texture the bug is worried about. This CL adds a bit of defense in depth (and seems harmless).

Bug: b/536165038
Change-Id: I55adb244deb8656a2895771491456a3770c8a2fa
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1299577
Reviewed-by: Michael Ludwig <[email protected]>
Commit-Queue: Robert Phillips <[email protected]>
---
diff --git a/src/gpu/ganesh/GrDrawingManager.cpp b/src/gpu/ganesh/GrDrawingManager.cpp
index 4fa514b..0b986eb 100644
--- a/src/gpu/ganesh/GrDrawingManager.cpp
+++ b/src/gpu/ganesh/GrDrawingManager.cpp
@@ -126,7 +126,9 @@
             if (info.fSubmittedProc) {
                 info.fSubmittedProc(info.fSubmittedContext, true);
             }
-            return false;
+            // Nothing to flush is a success (fSubmittedProc is already called with `true`
+            // above).
+            return true;
         }
     }
 
@@ -542,8 +544,11 @@
     // portion of the DAG required by 'proxies' in order to restore some of the
     // semantics of this method.
     bool didFlush = this->flush(proxies, access, info, newState);
-    for (GrSurfaceProxy* proxy : proxies) {
-        resolve_and_mipmap(gpu, proxy);
+    if (didFlush) {
+        // Only resolve/regen mips if the flush actually executed the render tasks.
+        for (GrSurfaceProxy* proxy : proxies) {
+            resolve_and_mipmap(gpu, proxy);
+        }
     }
 
     SkDEBUGCODE(this->validate());
diff --git a/tests/GrSurfaceResolveTest.cpp b/tests/GrSurfaceResolveTest.cpp
index fbe52a1..74b5bf2 100644
--- a/tests/GrSurfaceResolveTest.cpp
+++ b/tests/GrSurfaceResolveTest.cpp
@@ -512,3 +512,125 @@
         }
     }
 }
+
+// Directly read back 'tex' and return the first pixel color.
+static bool read_backing_pixel(GrDirectContext* dContext,
+                               const GrBackendTexture& tex,
+                               const SkImageInfo& info,
+                               SkColor* outPixel) {
+    sk_sp<SkSurface> reader = SkSurfaces::WrapBackendTexture(dContext,
+                                                             tex,
+                                                             kTopLeft_GrSurfaceOrigin,
+                                                             /*sampleCnt=*/1,
+                                                             kRGBA_8888_SkColorType,
+                                                             nullptr,
+                                                             nullptr);
+    if (!reader) {
+        return false;
+    }
+    SkBitmap bm;
+    bm.allocPixels(info);
+    if (!reader->readPixels(bm, 0, 0)) {
+        return false;
+    }
+    *outPixel = bm.getColor(0, 0);
+    return true;
+}
+
+// This test wraps a backend texture as an MSAA render target twice. The first wrap establishes a
+// known baseline color in the single-sample texture via a *successful* flush. The second wrap
+// creates a *fresh, never-written* MSAA color attachment (on GL a new renderbuffer, on Vulkan a
+// scratch/new GrVkImage), records a draw, forces the flush to fail via a failing preFlush
+// callback, and then reads back the single-sample texture. The read-back must not show the
+// uninitialized MSAA attachment contents; if it does, resolve_and_mipmap() ran despite the failed
+// flush and copied uninitialized GPU memory into the client-visible texture.
+DEF_GANESH_TEST_FOR_RENDERING_CONTEXTS(SurfaceResolveAfterFailedFlush,
+                                       reporter,
+                                       ctxInfo,
+                                       CtsEnforcement::kNever) {
+    auto dContext = ctxInfo.directContext();
+    const GrCaps* caps = dContext->priv().caps();
+
+    // Only meaningful on backends that require an explicit resolve of a persistent MSAA attachment.
+    if (caps->msaaResolvesAutomatically() || caps->preferDiscardableMSAAAttachment()) {
+        return;
+    }
+
+    SkImageInfo info = SkImageInfo::Make(8, 8, kRGBA_8888_SkColorType, kPremul_SkAlphaType);
+
+    auto managedTex = ManagedBackendTexture::MakeFromInfo(
+            dContext, info, skgpu::Mipmapped::kNo, GrRenderable::kYes);
+    if (!managedTex) {
+        return;
+    }
+    const GrBackendTexture& tex = managedTex->texture();
+
+    constexpr SkColor kBaseline = SK_ColorBLUE;   // known content of the single-sample texture
+    constexpr SkColor kIntended = SK_ColorGREEN;  // what the failed flush *would* have drawn
+
+    // 1. Wrap once and successfully draw kBaseline so the single-sample texture holds known bytes.
+    {
+        sk_sp<SkSurface> surface = SkSurfaces::WrapBackendTexture(dContext,
+                                                                  tex,
+                                                                  kTopLeft_GrSurfaceOrigin,
+                                                                  /*sampleCnt=*/4,
+                                                                  kRGBA_8888_SkColorType,
+                                                                  nullptr,
+                                                                  nullptr);
+        if (!surface) {
+            return;  // MSAA=4 unsupported on this config.
+        }
+        surface->getCanvas()->clear(kBaseline);
+        dContext->flush(surface.get());
+        dContext->submit(GrSyncCpu::kYes);
+    }
+    // Ensure the second wrap below allocates a *fresh* MSAA attachment rather than recycling the
+    // one from the wrap above (whose contents would coincidentally match kBaseline).
+    dContext->purgeUnlockedResources(GrPurgeResourceOptions::kAllResources);
+
+    SkColor pixel = 0;
+    REPORTER_ASSERT(reporter, read_backing_pixel(dContext, tex, info, &pixel));
+    REPORTER_ASSERT(reporter, pixel == kBaseline,
+                    "baseline flush failed to resolve, got 0x%x", pixel);
+
+    // 2. Re-wrap: this creates a *new* persistent MSAA color attachment that has never been
+    //    written by any render pass in this test.
+    sk_sp<SkSurface> surface = SkSurfaces::WrapBackendTexture(dContext,
+                                                              tex,
+                                                              kTopLeft_GrSurfaceOrigin,
+                                                              /*sampleCnt=*/4,
+                                                              kRGBA_8888_SkColorType,
+                                                              nullptr,
+                                                              nullptr);
+    if (!surface) {
+        return;
+    }
+
+    // 3. Record a full-surface draw so OpsTask::onMakeClosed() returns kTargetDirty and
+    //    markMSAADirty() is called during closeAllTasks(). Force the flush to fail.
+    FailingPreFlushCallback failCB(1);
+    dContext->priv().addOnFlushCallbackObject(&failCB);
+
+    surface->getCanvas()->clear(kIntended);
+    GrSemaphoresSubmitted result = dContext->flush(surface.get(), GrFlushInfo{});
+    dContext->submit(GrSyncCpu::kYes);
+
+    GrDrawingManager* drawingManager = dContext->priv().drawingManager();
+    drawingManager->testingOnly_removeOnFlushCallbackObject(&failCB);
+
+    REPORTER_ASSERT(reporter, result == GrSemaphoresSubmitted::kNo,
+                    "expected flush to report semaphores not submitted");
+
+    // 4. Read back the single-sample backing texture.
+    REPORTER_ASSERT(reporter, read_backing_pixel(dContext, tex, info, &pixel));
+
+    // The recorded draw never executed, so kIntended should not appear.
+    REPORTER_ASSERT(reporter, pixel != kIntended,
+                    "flush failed but draw, somehow, occurred (got 0x%x)", pixel);
+
+    // Since the flush failed the MSAA resolve step should not have occurred and the contents
+    // of the backend texture should have remained at 'kBaseline'.
+    REPORTER_ASSERT(reporter, pixel == kBaseline,
+                    "Invalid resolve after a failed flush: expected 0x%x, actual 0x%x",
+                    kBaseline, pixel);
+}
Loading diff…

Original Bug Report

reported by [email protected]

Potential cross-origin GPU data leak due to unconditional resolve on flush failure in Ganesh

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: In Skia’s Ganesh backend, GrDrawingManager::flushSurfaces unconditionally triggers a resolve on provided surface proxies, regardless of whether the preceding flush() call succeeded. On flush failures (e.g., due to allocator or pre-flush failures), discarded tasks leave the target marked dirty, causing recycled or uninitialized cross-origin MSAA attachment data to be copied into the user-readable single-sample texture.

Affected files:

  • third_party/skia/src/gpu/ganesh/GrDrawingManager.cpp
  • third_party/skia/src/gpu/ganesh/GrRenderTask.cpp
  • third_party/skia/src/gpu/ganesh/ops/OpsTask.cpp

Estimated timestamp from git blame: 2019-05-09

Description

There is a potential cross-origin GPU-process information disclosure vulnerability in the Skia Ganesh rendering backend of Chromium.

Root Cause

In third_party/skia/src/gpu/ganesh/GrDrawingManager.cpp, flushSurfaces() invokes resolve_and_mipmap() on the user-supplied proxies regardless of whether this->flush() succeeded:

// third_party/skia/src/gpu/ganesh/GrDrawingManager.cpp:543-546
bool didFlush = this->flush(proxies, access, info, newState);
for (GrSurfaceProxy* proxy : proxies) {
    resolve_and_mipmap(gpu, proxy);          // Unconditional execution
}

Inside this->flush(), the drawing manager first closes all tasks in the DAG via this->closeAllTasks() (which calls GrRenderTask::makeClosed()). During this closing phase, if any drawing operations were recorded, OpsTask::onMakeClosed() returns kTargetDirty, which triggers rtProxy->markMSAADirty() on the target proxy’s render target [Cite: third_party/skia/src/gpu/ganesh/GrRenderTask.cpp:84-94].

If the flush subsequently fails—for example, due to a resource allocator instantiation failure under memory pressure or a pre-flush callback failure (such as AtlasPathRenderer::preFlush() failing to allocate atlas space)—this->executeRenderTasks() is skipped entirely, and the DAG of render tasks is discarded via this->removeRenderTasks() [Cite: third_party/skia/src/gpu/ganesh/GrDrawingManager.cpp:172-214].

Because the render tasks never ran, nothing was cleared or written to the persistent MSAA color attachment. However, because resolve_and_mipmap() runs unconditionally, it checks rtProxy->isMSAADirty() (which is true from the closed task) and executes a GPU-side resolve (vkCmdResolveImage or glBlitFramebuffer) [Cite: third_party/skia/src/gpu/ganesh/GrDrawingManager.cpp:499-504]. This copies the uninitialized or recycled cross-origin scratch MSAA buffer contents directly into the readable single-sample SharedImage texture.

Since Out-of-Process Rasterization (OOP-R) renderers share a single GrDirectContext (and hence a single GrResourceCache) in the GPU process, the recycled MSAA attachments may contain pixel data belonging to other origins.


Potential Trigger Steps

Note: These are potential steps because our tooling currently lacks the ability to execute code and verify the exploit chain with a running Proof of Concept.

  1. From a compromised renderer, create a SharedImage and issue BeginRasterCHROMIUM with msaa_mode = gpu::raster::kMSAA and msaa_sample_count >= 2 on a platform where automatic MSAA resolve is disabled (e.g., Vulkan or plain GL).
  2. Record draw operations (such as path drawing via AtlasPathRenderer) to ensure the OpsTask is marked as dirty.
  3. Exhaust GPU resources or simulate memory pressure to induce a failure in AtlasPathRenderer::preFlush() or GrResourceAllocator::assign() on the target flush.
  4. Invoke EndRasterCHROMIUM. When the GPU process handles the flush, flush() fails and removes the tasks, but resolve_and_mipmap() executes anyway.
  5. Read back the SharedImage texture to obtain the resolved, uninitialized, or recycled cross-origin GPU memory residue.

Suggested Fix

To prevent resolving dirty surfaces when the drawing operations have been aborted, resolve_and_mipmap should only execute if this->flush() succeeded. Gate the resolve loop on the success of didFlush:

    bool didFlush = this->flush(proxies, access, info, newState);
    if (didFlush) {
        for (GrSurfaceProxy* proxy : proxies) {
            resolve_and_mipmap(gpu, proxy);
        }
    }

Evaluated with Chrome root at commit: bf775e5d75cb9e1767e2cd02cc93efa0077d14a5


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