Overview

Low
Severity
5.3
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInformation leak in Skia
DescriptionInformation leak in Skia
ComponentSkia
Bug ClassInfo Disclosure
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.
Tracker513969378
Fix commite30d3d14a4c3 (skia) +62/-2
CWECWE-200 (Information exposure)
CVSS vectorCVSS:3.1/AV:N/AC:H/PR:N/UI:R/S:U/C:H/I:N/A:N
CISA KEVNot listed
CreditedGoogle
Reported2026-05-17 (per vendor advisory)
Fix landed2026-06-30 (public git)
Disclosed2026-08-25
Patch gap56 days — fix public in source before the advisory

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 through isProtected().
`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.

Key insight
The single mistake was hardcoding the 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

  1. Obtain protected content An attacker (or DRM-bearing web content) gets a protected SkImage rendered on a context that supportsProtectedContent, such that its pixels live in hardware-protected GPU memory.
  2. Wrap it in a picture shader Record the protected SkImage into an SkPicture and install it as a shader via makeShader, then use that paint in a draw so Ganesh must realize the picture through the SkPictureShader path in GrFragmentProcessors.cpp.
  3. Force realization into unprotected memory The picture is rasterized into an internal image created with kUnprotected = false, so protected pixels are copied into an unprotected SkSurfaces::RenderTarget regardless of the destination’s protection state.
  4. 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

An attacker gains an information leak of protected (e.g. DRM/secure-media) pixel content that hardware protection is supposed to keep unreadable, achieved within the GPU/rendering context that draws a protected 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

FunctionChangeNotes
if
src/gpu/ganesh/GrFragmentProcessors.cpp
modified
for
tests/ProtectedTest.cpp
modified

Files Changed

  • tests/ProtectedTest.cpp
  • src/gpu/ganesh/GrFragmentProcessors.cpp

Audit Directions

  • Hardcoded protection flags
    Search for constants like kUnprotected or literal false/kNo passed to surface/image creation (SkSurfaces::RenderTarget, makeImage) where the value should instead be propagated from the source or destination via isProtected().
  • Intermediate/offscreen allocations
    Review 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 boundaries
    Audit conversions between SkPicture, SkImage, and GrSurfaceProxy to ensure GrProtected state flows through every derivation rather than defaulting to unprotected.
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)
Loading diff…

Original Bug Report

The reporter's bug is still restricted on the tracker. Chrome de-restricts security bugs ~30–90 days after the fix ships; a later run will backfill it here.