CVE-2026-17702
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/gpu/ganesh/GrTextureResolveRenderTask.cpp |
modified | |
FailingPreFlushCallbacktests/GrSurfaceResolveTest.cpp |
modified | |
iftests/GrSurfaceResolveTest.cpp |
modified | |
fortests/GrSurfaceResolveTest.cpp |
modified |
Files Changed
src/gpu/ganesh/GrDrawingManager.cppsrc/gpu/ganesh/GrTextureResolveRenderTask.cppsrc/gpu/ganesh/GrTextureResolveRenderTask.htests/GrSurfaceResolveTest.cpp
Patch
From 04dbd15284b8b10a72da7636f23db5a077641437 Mon Sep 17 00:00:00 2001
From: Greg Daniel <egdaniel@google.com>
Date: Fri, 17 Jul 2026 13:28:05 -0400
Subject: [PATCH] [Ganesh] If a resolve task fails to execute unwind the dirty tracking.
If a flush fails for some reason then we can get in an inconsistent
state with our dirty rect tracking for msaa resolves and mip maps.
This happens because we immediately update the proxies tracking of these
values when we recording a resolve task. But if that resolve task
never executes for some reason then we can end up in a bad state.
This changes makes it so that if a resolve task is ended without ever
executing, then it resets the proxies state to what it was before.
Technically if the draws before the resolve also never execute we will
now be marking a region dirty that isn't neccessarily dirty. This
could cause an extra resolve on future draws but is safe. However in
practice is flushes fail, clients will usually either tear everything
down (and thus it doesn't matter), or repeat the same draws again
(which would end up with the same resolve rect anyways). So this
possible extra resolve doesn't have a large real world impact.
Bug: b/517973093
Change-Id: Icc54310b87061471631f7a81adcfe661794a4e9c
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1297216
Reviewed-by: Thomas Smith <thomsmit@google.com>
Commit-Queue: Greg Daniel <egdaniel@google.com>
---
diff --git a/src/gpu/ganesh/GrDrawingManager.cpp b/src/gpu/ganesh/GrDrawingManager.cpp
index 55b58d9..4fa514b 100644
--- a/src/gpu/ganesh/GrDrawingManager.cpp
+++ b/src/gpu/ganesh/GrDrawingManager.cpp
@@ -510,8 +510,9 @@
if (auto* textureProxy = proxy->asTextureProxy()) {
if (textureProxy->mipmapsAreDirty()) {
SkASSERT(textureProxy->peekTexture());
- gpu->regenerateMipMapLevels(textureProxy->peekTexture());
- textureProxy->markMipmapsClean();
+ if (gpu->regenerateMipMapLevels(textureProxy->peekTexture())) {
+ textureProxy->markMipmapsClean();
+ }
}
}
}
diff --git a/src/gpu/ganesh/GrTextureResolveRenderTask.cpp b/src/gpu/ganesh/GrTextureResolveRenderTask.cpp
index 76595fa..0cb2505 100644
--- a/src/gpu/ganesh/GrTextureResolveRenderTask.cpp
+++ b/src/gpu/ganesh/GrTextureResolveRenderTask.cpp
@@ -94,24 +94,25 @@
// Resolve all msaa back-to-back, before regenerating mipmaps.
SkASSERT(fResolves.size() == this->numTargets());
for (int i = 0; i < fResolves.size(); ++i) {
- const Resolve& resolve = fResolves[i];
+ Resolve& resolve = fResolves[i];
if (GrSurfaceProxy::ResolveFlags::kMSAA & resolve.fFlags) {
GrSurfaceProxy* proxy = this->target(i);
// peekRenderTarget might be null if there was an instantiation error.
if (GrRenderTarget* renderTarget = proxy->peekRenderTarget()) {
flushState->gpu()->resolveRenderTarget(renderTarget, resolve.fMSAAResolveRect);
+ resolve.fFlags &= ~GrSurfaceProxy::ResolveFlags::kMSAA;
}
}
}
// Regenerate all mipmaps back-to-back.
for (int i = 0; i < fResolves.size(); ++i) {
- const Resolve& resolve = fResolves[i];
+ Resolve& resolve = fResolves[i];
if (GrSurfaceProxy::ResolveFlags::kMipMaps & resolve.fFlags) {
// peekTexture might be null if there was an instantiation error.
GrTexture* texture = this->target(i)->peekTexture();
- if (texture && texture->mipmapsAreDirty()) {
- flushState->gpu()->regenerateMipMapLevels(texture);
- SkASSERT(!texture->mipmapsAreDirty());
+ if (texture && (!texture->mipmapsAreDirty() ||
+ flushState->gpu()->regenerateMipMapLevels(texture))) {
+ resolve.fFlags &= ~GrSurfaceProxy::ResolveFlags::kMipMaps;
}
}
}
@@ -119,6 +120,29 @@
return true;
}
+void GrTextureResolveRenderTask::endFlush(GrDrawingManager* drawingMgr) {
+ // Any flags still set here correspond to resolves that were recorded by addProxy() but never
+ // executed (the flush was dropped before render-task execution, this task was skipped because
+ // its targets failed to instantiate, or a per-target operation failed). Re-mark those proxies
+ // dirty so a subsequent flush will re-record the resolve.
+ SkASSERT(fResolves.size() == this->numTargets());
+ for (int i = 0; i < fResolves.size(); ++i) {
+ const Resolve& resolve = fResolves[i];
+ GrSurfaceProxy* proxy = this->target(i);
+ if (GrSurfaceProxy::ResolveFlags::kMSAA & resolve.fFlags) {
+ if (GrRenderTargetProxy* rtProxy = proxy->asRenderTargetProxy()) {
+ rtProxy->markMSAADirty(resolve.fMSAAResolveRect);
+ }
+ }
+ if (GrSurfaceProxy::ResolveFlags::kMipMaps & resolve.fFlags) {
+ if (GrTextureProxy* texProxy = proxy->asTextureProxy()) {
+ texProxy->markMipmapsDirty();
+ }
+ }
+ }
+ this->GrRenderTask::endFlush(drawingMgr);
+}
+
#ifdef SK_DEBUG
void GrTextureResolveRenderTask::visitProxies_debugOnly(const GrVisitProxyFunc&) const {}
#endif
diff --git a/src/gpu/ganesh/GrTextureResolveRenderTask.h b/src/gpu/ganesh/GrTextureResolveRenderTask.h
index 5eee9ed..5cd6d02 100644
--- a/src/gpu/ganesh/GrTextureResolveRenderTask.h
+++ b/src/gpu/ganesh/GrTextureResolveRenderTask.h
@@ -44,6 +44,12 @@
bool onExecute(GrOpFlushState*) override;
+ // addProxy() optimistically marks the proxy resolved/clean at recording time. If the flush
+ // is dropped before this task executes (or a per-target operation fails) we must restore the
+ // proxy's dirty state so a later flush will re-record the resolve.
+ bool requiresExplicitCleanup() const override { return true; }
+ void endFlush(GrDrawingManager*) override;
+
#if defined(GPU_TEST_UTILS)
const char* name() const final { return "TextureResolve"; }
#endif
diff --git a/tests/GrSurfaceResolveTest.cpp b/tests/GrSurfaceResolveTest.cpp
index 330c941..fbe52a1 100644
--- a/tests/GrSurfaceResolveTest.cpp
+++ b/tests/GrSurfaceResolveTest.cpp
@@ -21,18 +21,23 @@
#include "include/core/SkTypes.h"
#include "include/gpu/GpuTypes.h"
#include "include/gpu/ganesh/GrBackendSurface.h"
+#include "include/gpu/ganesh/GrContextOptions.h"
#include "include/gpu/ganesh/GrDirectContext.h"
#include "include/gpu/ganesh/GrTypes.h"
#include "include/gpu/ganesh/SkSurfaceGanesh.h"
+#include "include/gpu/ganesh/mock/GrMockTypes.h"
#include "include/private/gpu/ganesh/GrTypesPriv.h"
#include "src/core/SkColorData.h"
#include "src/gpu/SkBackingFit.h"
#include "src/gpu/Swizzle.h"
#include "src/gpu/ganesh/GrCaps.h"
#include "src/gpu/ganesh/GrDirectContextPriv.h"
+#include "src/gpu/ganesh/GrDrawingManager.h"
#include "src/gpu/ganesh/GrFragmentProcessor.h"
+#include "src/gpu/ganesh/GrOnFlushResourceProvider.h"
#include "src/gpu/ganesh/GrPaint.h"
#include "src/gpu/ganesh/GrProxyProvider.h"
+#include "src/gpu/ganesh/GrRenderTargetProxy.h"
#include "src/gpu/ganesh/GrSamplerState.h"
#include "src/gpu/ganesh/GrSurfaceProxy.h"
#include "src/gpu/ganesh/GrSurfaceProxyView.h"
@@ -356,3 +361,154 @@
}
}
}
+
+namespace {
+// A preFlush callback that fails the first N flushes, simulating an allocation failure in an
+// onFlush callback (e.g. atlas instantiation under memory pressure).
+class FailingPreFlushCallback : public GrOnFlushCallbackObject {
+public:
+ explicit FailingPreFlushCallback(int failCount) : fRemainingFailures(failCount) {}
+ bool preFlush(GrOnFlushResourceProvider*) override {
+ if (fRemainingFailures > 0) {
+ --fRemainingFailures;
+ return false;
+ }
+ return true;
+ }
+ bool retainOnFreeGpuResources() override { return true; }
+
+private:
+ int fRemainingFailures;
+};
+} // namespace
+
+// GrTextureResolveRenderTask::addProxy() optimistically marks a proxy's mipmaps clean / MSAA
+// resolved at recording time. If the owning flush is dropped without executing render tasks
+// (e.g., a preFlush callback or the resource allocator fails) the proxy must be re-dirtied so a
+// later flush will record a new resolve.
+DEF_GANESH_TEST(SurfaceResolveProxyStateAfterFailedFlush,
+ reporter,
+ /* options */,
+ CtsEnforcement::kNever) {
+ using ResolveFlags = GrSurfaceProxy::ResolveFlags;
+ using Enable = GrContextOptions::Enable;
+ using MipmapMode = GrSamplerState::MipmapMode;
+
+ for (auto reduceOpsTaskSplitting : {Enable::kYes, Enable::kNo}) {
+ for (int sampleCount : {1, 4}) {
+ GrMockOptions mockOptions;
+ mockOptions.fMipmapSupport = true;
+ mockOptions.fConfigOptions[(int)GrColorType::kRGBA_8888].fRenderability =
+ GrMockOptions::ConfigOptions::Renderability::kMSAA;
+ GrContextOptions ctxOptions;
+ ctxOptions.fReduceOpsTaskSplitting = reduceOpsTaskSplitting;
Original Bug Report
Potential State Desynchronization in GrTextureResolveRenderTask Exposes Stale GPU Memory
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: A potential state desynchronization vulnerability exists in Skia’s Ganesh GPU backend where MSAA targets and mipmaps are eagerly marked resolved/clean during DAG recording. If a subsequent flush is aborted or skipped due to pre-flush or resource allocation failures, these state mutations are not rolled back. This can cause subsequent readbacks to skip necessary GPU-side resolves, potentially exposing recycled or stale GPU memory.
Affected files:
third_party/skia/src/gpu/ganesh/GrTextureResolveRenderTask.cppthird_party/skia/src/gpu/ganesh/SurfaceContext.cpp
Estimated timestamp from git blame: 2019-08-14
Vulnerability Mechanics
In Skia’s Ganesh GPU backend, GrTextureResolveRenderTask::addProxy() eagerly updates a surface proxy’s state during the Directed Acyclic Graph (DAG) recording phase, before any GPU commands are actually dispatched or executed:
// third_party/skia/src/gpu/ganesh/GrTextureResolveRenderTask.cpp
if (GrSurfaceProxy::ResolveFlags::kMSAA & newFlags) {
GrRenderTargetProxy* renderTargetProxy = proxy->asRenderTargetProxy();
...
resolve->fMSAAResolveRect = renderTargetProxy->msaaDirtyRect();
renderTargetProxy->markMSAAResolved(); // Cleared immediately during recording
}
if (GrSurfaceProxy::ResolveFlags::kMipMaps & newFlags) {
GrTextureProxy* textureProxy = proxy->asTextureProxy();
...
textureProxy->markMipmapsClean(); // Cleared immediately during recording
}
Similarly, inside SurfaceContext::internalWritePixels(), mipmaps are marked clean immediately after recording a write task:
// third_party/skia/src/gpu/ganesh/SurfaceContext.cpp
if (numLevels > 1) {
dstProxy->asTextureProxy()->markMipmapsClean();
}
During GrDrawingManager::flush(), several validations are performed. If a pre-flush callback fails (for instance, AtlasPathRenderer::preFlush() failing due to allocation limits under memory pressure), or if GrResourceAllocator::assign() fails due to a lazy proxy instantiation error, the drawing manager skips task execution entirely. It then clears the DAG using removeRenderTasks():
// third_party/skia/src/gpu/ganesh/GrDrawingManager.cpp
bool preFlushSuccessful = true;
for (GrOnFlushCallbackObject* onFlushCBObject : fOnFlushCBObjects) {
preFlushSuccessful &= onFlushCBObject->preFlush(&onFlushProvider);
}
...
if (preFlushSuccessful) {
...
cachePurgeNeeded = !resourceAllocator.failedInstantiation() &&
this->executeRenderTasks(&flushState); // Skipped on failure
}
this->removeRenderTasks(); // Clears DAG; proxies remain marked 'resolved' and 'clean'
Because removeRenderTasks() discards the scheduled tasks but does not roll back the state changes on the corresponding proxies, the proxies remain permanently marked as “resolved” and “clean” in memory, even though the actual GPU-side textures were never resolved or updated.
Potential Security Impact
If the proxy state is desynchronized from the actual GPU memory contents, subsequent operations that check the dirty flags will assume the buffers are up-to-date:
GrRenderTask::addDependency()andresolve_and_mipmap()will observe the proxy state as clean and skip inserting necessary resolve or regeneration tasks.- Subsequent readback or copy operations (e.g.,
asyncRescaleAndReadPixelsYUV420) will execute directly on the unresolved MSAA resolve attachment or uninitialized mip levels. - Since
GrResourceCacherecycles allocations across different origins under memory pressure, this allows a compromised renderer to potentially read back stale GPU memory belonging to other pages or contexts, resulting in a cross-origin read-only information disclosure.
Suggested Attack Vector / Trigger Steps (Theoretical)
An attacker could potentially attempt the following sequence to trigger the vulnerability:
- Allocate an MSAA-enabled surface or a mipmapped surface and perform drawing operations, marking the proxy dirty.
- Record a read or dependency task that schedules a resolve via
GrTextureResolveRenderTask::addProxy(), immediately clearing the proxy’s dirty flags in the recording state. - Artificially induce a resource allocation failure or pre-flush failure (e.g., by queueing extremely large or complex SVG path rendering commands to exhaust atlas allocation limits) in the same flush phase, forcing the drawing manager to skip execution of the DAG.
- Trigger a subsequent readback operation (such as
asyncRescaleAndReadPixelsYUV420) in a new flush. Since the proxy was already marked clean, the resolve is skipped, transferring the stale/unresolved GPU memory contents back to host memory.
Note: These steps are based on static code analysis and represent a potential path; actual behavior has not been validated with a functional proof-of-concept.
Suggested Remediation
To resolve this desynchronization, Skia should avoid mutating the proxy’s active state during the DAG recording phase. Alternatively, if eager mutation is required for dependency tracking, a rollback mechanism must be introduced.
One potential fix is to implement a rollback or state restoration mechanism inside GrDrawingManager::removeRenderTasks() (or during task destruction) to revert any pending proxy state changes back to their original dirty state if the task is discarded without being executed.
Evaluated with Chrome root at commit: fb72408a8493c46bc75fae1c70d03daec96b3040
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.