CVE-2026-19161
Overview
Background
- Ganesh
- Skia’s GPU backend that batches drawing into render tasks and executes them during a
flush/submitcycle. - MSAA resolve
- the step where a multisample color attachment is downsampled (“resolved”) into a single-sample texture that clients can read;
resolve_and_mipmapperforms this and regenerates mipmaps. - `didFlush`
- the boolean returned by
GrDrawingManager::flushthat 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.
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
- 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.
- Record a draw and force flush failure
Record a full-surface draw (marking the target dirty) and cause
this->flushto fail so the render tasks never execute, mirroring the test’sFailingPreFlushCallback. - Trigger the unconditional resolve
Because the pre-fix code always looped
resolve_and_mipmap(gpu, proxy)afterflush, the never-written MSAA attachment is resolved into the single-sample backing texture. - 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_pixeldemonstrates. - 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
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
| Function | Change | Notes |
|---|---|---|
ifsrc/gpu/ganesh/GrDrawingManager.cpp |
modified | |
forsrc/gpu/ganesh/GrDrawingManager.cpp |
modified | |
iftests/GrSurfaceResolveTest.cpp |
modified |
Files Changed
src/gpu/ganesh/GrDrawingManager.cpptests/GrSurfaceResolveTest.cpp
Audit Directions
- Unchecked flush/submit return valuesAudit 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 executionLook 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 pathsReview 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.
Patch
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); +}
Original Bug Report
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.cppthird_party/skia/src/gpu/ganesh/GrRenderTask.cppthird_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.
- From a compromised renderer, create a SharedImage and issue
BeginRasterCHROMIUMwithmsaa_mode = gpu::raster::kMSAAandmsaa_sample_count >= 2on a platform where automatic MSAA resolve is disabled (e.g., Vulkan or plain GL). - Record draw operations (such as path drawing via
AtlasPathRenderer) to ensure theOpsTaskis marked as dirty. - Exhaust GPU resources or simulate memory pressure to induce a failure in
AtlasPathRenderer::preFlush()orGrResourceAllocator::assign()on the target flush. - Invoke
EndRasterCHROMIUM. When the GPU process handles the flush,flush()fails and removes the tasks, butresolve_and_mipmap()executes anyway. - 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.