CVE-2026-79147
Overview
Background
- `SkPictureShader`
- A shader that fills a draw by tiling the rasterized contents of an
SkPicture, which Ganesh backs by rendering the picture into an internal offscreen image. - Protected content
- GPU memory allocated in a hardware-protected buffer (via a “protected” flag) whose pixels cannot be read back by the CPU or copied into unprotected surfaces, used to keep DRM/secure media out of reach.
- `GrProtected`
- Skia’s enum expressing whether a surface or proxy is protected (
GrProtected::kYes) or not, queried throughisProtected(). - `GrSurfaceProxy`
- The deferred handle to a Ganesh render target/surface, whose
isProtected()reports the protectedness of the backing allocation.
Root Cause Analysis
When a SkPictureShader was realized on the GPU in GrFragmentProcessors.cpp, the internal offscreen image used to hold the picture’s rasterized tile was always created with the local constant kUnprotected = false, unconditionally requesting an unprotected SkSurfaces::RenderTarget via info.makeImage. This violated the invariant that content derived from protected inputs must itself remain protected: if the SkPicture drew a protected SkImage, its pixels were copied into a non-protected surface, defeating the hardware protection boundary and allowing the secure content to be read or leaked.
The fix computes isProtected from args.fSurfaceDrawContext->asSurfaceProxy()->isProtected() == GrProtected::kYes and passes that to makeImage, so the internal image inherits the protectedness of the destination surface. As the accompanying test documents, drawing a protected picture into an unprotected destination now correctly fails the final draw while the protected intermediate image still generates, keeping protected pixels from ever landing in unprotected memory.
SkPictureShader’s internal render target as unprotected (kUnprotected = false) instead of propagating the surface’s actual protection state; the fix derives isProtected from the destination proxy so protected picture content is realized into protected memory rather than being silently downgraded.Attack Path
- Obtain protected content
An attacker (or DRM-bearing web content) gets a protected
SkImagerendered on a context thatsupportsProtectedContent, such that its pixels live in hardware-protected GPU memory. - Wrap it in a picture shader
Record the protected
SkImageinto anSkPictureand install it as a shader viamakeShader, then use that paint in a draw so Ganesh must realize the picture through theSkPictureShaderpath inGrFragmentProcessors.cpp. - Force realization into unprotected memory
The picture is rasterized into an internal image created with
kUnprotected = false, so protected pixels are copied into an unprotectedSkSurfaces::RenderTargetregardless of the destination’s protection state. - Read back the leaked pixels
Because the intermediate now resides in unprotected memory, the content becomes observable through normal readback/copy paths that the protection boundary was meant to block, disclosing the protected image
(inferred).
Impact Assessment
SkPicture through the SkPictureShader path. The precondition is a context that supports protected content and attacker-controllable drawing of a protected image into an unprotected destination. The advisory classifies this as a LogicError causing an information leak in Skia and states no exploited-in-the-wild status; any escalation beyond disclosure of the protected pixels is (inferred).Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/gpu/ganesh/GrFragmentProcessors.cpp |
modified | |
fortests/ProtectedTest.cpp |
modified |
Files Changed
tests/ProtectedTest.cppsrc/gpu/ganesh/GrFragmentProcessors.cpp
Audit Directions
- Hardcoded protection flagsSearch for constants like
kUnprotectedor literalfalse/kNopassed to surface/image creation (SkSurfaces::RenderTarget,makeImage) where the value should instead be propagated from the source or destination viaisProtected(). - Intermediate/offscreen allocationsReview any code that rasterizes or copies content into internal render targets (shaders, image filters, mask/blur passes, saveLayer) to confirm the intermediate inherits the protectedness of its inputs and destination.
- Protectedness propagation across boundariesAudit conversions between
SkPicture,SkImage, andGrSurfaceProxyto ensureGrProtectedstate flows through every derivation rather than defaulting to unprotected.
Patch
From e30d3d14a4c33a4d594fa1a71ab3d4f74b7aeebe Mon Sep 17 00:00:00 2001 From: Robert Phillips <[email protected]> Date: Tue, 30 Jun 2026 17:35:01 +0000 Subject: [PATCH] Propagate protectedness to SkPictureShader draws This change seems harmless and is more correct. Basically, the protected-ness of the backing surface is propagated to the internal image created for a PictureShader. This is relevant if the SkPicture contains protected content. Bug: b/513969378 Change-Id: I9314aca6481850d098bab0515601cca51fb7a00a Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1281217 Reviewed-by: Michael Ludwig <[email protected]> Commit-Queue: Robert Phillips <[email protected]> --- diff --git a/src/gpu/ganesh/GrFragmentProcessors.cpp b/src/gpu/ganesh/GrFragmentProcessors.cpp index 5de0327..bfb26ea 100644 --- a/src/gpu/ganesh/GrFragmentProcessors.cpp +++ b/src/gpu/ganesh/GrFragmentProcessors.cpp @@ -755,7 +755,9 @@ } else { const int msaaSampleCount = 0; const bool createWithMips = false; - const bool kUnprotected = false; + const bool isProtected = + args.fSurfaceDrawContext->asSurfaceProxy()->isProtected() == + GrProtected::kYes; auto image = info.makeImage(SkSurfaces::RenderTarget(ctx, skgpu::Budgeted::kYes, info.imageInfo, @@ -763,7 +765,7 @@ kTopLeft_GrSurfaceOrigin, &info.props, createWithMips, - kUnprotected), + isProtected), shader->picture().get()); if (!image) { return nullptr; diff --git a/tests/ProtectedTest.cpp b/tests/ProtectedTest.cpp index 9f779c6..e3499fb 100644 --- a/tests/ProtectedTest.cpp +++ b/tests/ProtectedTest.cpp @@ -13,6 +13,8 @@ #include "include/core/SkCanvas.h" #include "include/core/SkColorSpace.h" #include "include/core/SkPaint.h" +#include "include/core/SkPicture.h" +#include "include/core/SkPictureRecorder.h" #include "include/core/SkSurface.h" #include "include/gpu/ganesh/GrBackendSurface.h" #include "include/gpu/ganesh/GrDirectContext.h" @@ -230,4 +232,60 @@ REPORTER_ASSERT(reporter, !cbContext.fResult); } +DEF_GANESH_TEST_FOR_ALL_CONTEXTS(Protected_pictureShader, reporter, + ctxInfo, CtsEnforcement::kNever) { + auto dContext = ctxInfo.directContext(); + + if (!dContext->supportsProtectedContent()) { + // Protected content not supported + return; + } + + sk_sp<SkImage> protectedImage = ProtectedUtils::CreateProtectedSkImage(dContext, + { kSize, kSize }, + SkColors::kBlue, + /* isProtected= */ true); + dContext->flushAndSubmit(GrSyncCpu::kYes); + + REPORTER_ASSERT(reporter, protectedImage); + REPORTER_ASSERT(reporter, protectedImage->isProtected()); + + sk_sp<SkPicture> protectedPicture; + { + SkPictureRecorder recorder; + recorder.beginRecording(100, 100)->drawImage(protectedImage.get(), 0, 0); + protectedPicture = recorder.finishRecordingAsPicture(); + REPORTER_ASSERT(reporter, protectedPicture); + } + + SkPaint protectedPaint; + protectedPaint.setShader(protectedPicture->makeShader(SkTileMode::kRepeat, + SkTileMode::kRepeat, + SkFilterMode::kNearest)); + + SkImageInfo ii = SkImageInfo::Make({ kSize, kSize }, + kRGBA_8888_SkColorType, + kPremul_SkAlphaType); + + for (bool isProtected : { true, false }) { + sk_sp<SkSurface> surface = SkSurfaces::RenderTarget(dContext, + skgpu::Budgeted::kNo, + ii, + /* sampleCount= */ 1, + kBottomLeft_GrSurfaceOrigin, + /* surfaceProps= */ nullptr, + /* shouldCreateWithMips= */ false, + isProtected); + + REPORTER_ASSERT(reporter, surface); + + // For the un-protected surface, drawing the SkPicture containing the protected + // image should cause the final draw of the generated image to the surface to fail + // while the generation of the internal (protected) image should succeed. + surface->getCanvas()->drawPaint(protectedPaint); + } + + dContext->flushAndSubmit(GrSyncCpu::kYes); +} + #endif // defined(SK_GANESH)