CVE-2026-8510
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
switchtests/SkGlyphTest.cpp |
modified | |
SkStringtests/SkGlyphTest.cpp |
modified | |
fortests/SkGlyphTest.cpp |
modified | |
DEF_TESTtests/SkGlyphTest.cpp |
modified |
Files Changed
src/effects/imagefilters/SkRuntimeImageFilter.cpptests/SkGlyphTest.cpp
Patch
From 3150bddf3edd9ff7e9c3171a196cea2fb4d53a1f Mon Sep 17 00:00:00 2001
From: Michael Ludwig <michaelludwig@google.com>
Date: Tue, 21 Apr 2026 13:49:08 -0400
Subject: [PATCH] [sksl] Check allowSkSL for SkRuntimeImageFilter::CreateProc
Since SkRuntimeImageFilter doesn't create its runtime shaders until
actually evaluating the image filter, SkRuntimeShader's CreateProc
was not being reached; it must be responsible for validating allowSkSL.
Updates the unit test to confirm that all sources of runtime effects
in drawables are detected when allowSkSl is false.
Bug: b/502636904
Change-Id: I391ba2608010431429ac3e3c03e106f380304edb
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1214636
Reviewed-by: Kaylee Lubick <kjlubick@google.com>
Reviewed-by: Jorge Betancourt <jmbetancourt@google.com>
Commit-Queue: Michael Ludwig <michaelludwig@google.com>
---
diff --git a/src/effects/imagefilters/SkRuntimeImageFilter.cpp b/src/effects/imagefilters/SkRuntimeImageFilter.cpp
index 9675780..5665f83 100644
--- a/src/effects/imagefilters/SkRuntimeImageFilter.cpp
+++ b/src/effects/imagefilters/SkRuntimeImageFilter.cpp
@@ -156,7 +156,11 @@
return nullptr;
}
- // Read the SkSL string and convert it into a runtime effect
+ // Read the SkSL string and convert it into a runtime effect (if allowed)
+ if (!buffer.validate(buffer.allowSkSL())) {
+ return nullptr;
+ }
+
SkString sksl;
buffer.readString(&sksl);
auto effect = SkMakeCachedRuntimeEffect(SkRuntimeEffect::MakeForShader, std::move(sksl));
diff --git a/tests/SkGlyphTest.cpp b/tests/SkGlyphTest.cpp
index 444e761..015a998 100644
--- a/tests/SkGlyphTest.cpp
+++ b/tests/SkGlyphTest.cpp
@@ -17,6 +17,7 @@
#include "include/core/SkRefCnt.h"
#include "include/core/SkString.h"
#include "include/core/SkTypes.h"
+#include "include/effects/SkImageFilters.h"
#include "include/effects/SkRuntimeEffect.h"
#include "src/base/SkArenaAlloc.h"
#include "src/core/SkCanvasPriv.h"
@@ -373,20 +374,68 @@
REPORTER_ASSERT(reporter, !badReadBuffer.isValid());
}
-static sk_sp<SkDrawable> make_sksl_drawable() {
+namespace {
+
+// There is currently on SkMaskFilter that directly creates runtime effects
+enum class SkSLSource {
+ kShader, kColorFilter, kBlender, kImageFilter
+};
+
+} // anonymous namespace
+
+static sk_sp<SkDrawable> make_sksl_drawable(SkSLSource source) {
SkRect rect = SkRect::MakeWH(50, 50);
SkPictureRecorder recorder;
SkCanvas* canvas = recorder.beginRecording(rect);
- const sk_sp<SkRuntimeEffect> effect =
- SkRuntimeEffect::MakeForShader(
+ SkPaint paint;
+
+ switch (source) {
+ case SkSLSource::kShader: {
+ const sk_sp<SkRuntimeEffect> effect = SkRuntimeEffect::MakeForShader(
SkString("half4 main(float2 xy) { return half4(0, 1, 0, 1); }"))
.effect;
- SkASSERT(effect);
+ SkASSERT(effect);
- SkPaint paint;
- paint.setShader(effect->makeShader(/*uniforms=*/nullptr, /*children=*/{}));
+ paint.setShader(effect->makeShader(/*uniforms=*/nullptr, /*children=*/{}));
+ break;
+ }
+
+ case SkSLSource::kColorFilter: {
+ const sk_sp<SkRuntimeEffect> effect = SkRuntimeEffect::MakeForColorFilter(
+ SkString("half4 main(half4 color) { return 0.5 * color; }"))
+ .effect;
+ SkASSERT(effect);
+
+ paint.setColorFilter(effect->makeColorFilter(/*uniforms=*/nullptr, /*children=*/{}));
+ break;
+ }
+
+ case SkSLSource::kBlender: {
+ const sk_sp<SkRuntimeEffect> effect = SkRuntimeEffect::MakeForBlender(
+ SkString("half4 main(half4 src, half4 dst) { return src + dst; }"))
+ .effect;
+ SkASSERT(effect);
+
+ paint.setBlender(effect->makeBlender(/*uniforms=*/nullptr, /*children=*/{}));
+ break;
+ }
+
+ case SkSLSource::kImageFilter: {
+ const sk_sp<SkRuntimeEffect> effect =
+ SkRuntimeEffect::MakeForShader(
+ SkString("uniform shader child;"
+ "half4 main(float2 xy) { return 0.5 * child.eval(xy); }"))
+ .effect;
+ SkASSERT(effect);
+
+ SkRuntimeEffectBuilder builder{std::move(effect)};
+ paint.setImageFilter(SkImageFilters::RuntimeShader(builder, "child", nullptr));
+ break;
+ }
+ }
+
// See note in make_nested_sksl_drawable: We include enough ops that this drawable will be
// preserved as a sub-picture when we wrap it in a second layer.
for (int i = 0; i < kMaxPictureOpsToUnrollInsteadOfRef + 1; ++i) {
@@ -396,13 +445,13 @@
return recorder.finishRecordingAsDrawable();
}
-static sk_sp<SkDrawable> make_nested_sksl_drawable() {
+static sk_sp<SkDrawable> make_nested_sksl_drawable(SkSLSource source) {
SkRect rect = SkRect::MakeWH(50, 50);
SkPictureRecorder recorder;
SkCanvas* canvas = recorder.beginRecording(rect);
- auto sksl_drawable = make_sksl_drawable();
+ auto sksl_drawable = make_sksl_drawable(source);
sk_sp<SkPicture> sksl_picture = sksl_drawable->makePictureSnapshot();
// We need to ensure that the op count of our picture is larger than this threshold, so we
@@ -414,23 +463,32 @@
}
DEF_TEST(SkPictureBackedGlyphDrawable_SkSL, reporter) {
- for (const sk_sp<SkDrawable>& drawable : {make_sksl_drawable(), make_nested_sksl_drawable()}) {
- for (bool allowSkSL : {true, false}) {
- REPORTER_ASSERT(reporter, drawable);
+ for (SkSLSource source : {SkSLSource::kShader,
+ SkSLSource::kColorFilter,
+ SkSLSource::kBlender,
+ SkSLSource::kImageFilter}) {
+ for (const sk_sp<SkDrawable>& drawable : {make_sksl_drawable(source),
+ make_nested_sksl_drawable(source)}) {
+ for (bool allowSkSL : {true, false}) {
+ skiatest::ReporterContext ctx{reporter,
+ SkStringPrintf("source: %d, allow sksl: %d", (int) source, allowSkSL)};
- SkBinaryWriteBuffer writeBuffer({});
- SkPictureBackedGlyphDrawable::FlattenDrawable(writeBuffer, drawable.get());
+ REPORTER_ASSERT(reporter, drawable);
- sk_sp<SkData> data = writeBuffer.snapshotAsData();
+ SkBinaryWriteBuffer writeBuffer({});
+ SkPictureBackedGlyphDrawable::FlattenDrawable(writeBuffer, drawable.get());
- SkReadBuffer readBuffer{data->data(), data->size()};
- readBuffer.setAllowSkSL(allowSkSL);
+ sk_sp<SkData> data = writeBuffer.snapshotAsData();
- sk_sp<SkPictureBackedGlyphDrawable> dstDrawable =
- SkPictureBackedGlyphDrawable::MakeFromBuffer(readBuffer);
+ SkReadBuffer readBuffer{data->data(), data->size()};
+ readBuffer.setAllowSkSL(allowSkSL);
- REPORTER_ASSERT(reporter, readBuffer.isValid() == allowSkSL);
- REPORTER_ASSERT(reporter, !!dstDrawable == allowSkSL);
+ sk_sp<SkPictureBackedGlyphDrawable> dstDrawable =
+ SkPictureBackedGlyphDrawable::MakeFromBuffer(readBuffer);
+
+ REPORTER_ASSERT(reporter, readBuffer.isValid() == allowSkSL);
+ REPORTER_ASSERT(reporter, !!dstDrawable == allowSkSL);
+ }
}
}
}
Original Bug Report
Renderer-to-GPU sandbox escape via Skia SPIR-V injection
Summary
A compromised renderer sends a crafted glyph drawable through the
strike-cache IPC. The GPU process deserialises it into SkSL (finding 1:
missing allowSkSL check on SkRuntimeImageFilter), compiles it to
SPIR-V with a truncated instruction word-count (finding 2: 16-bit
overflow in SPIRVCodeGenerator::writeOpCode), and feeds the malformed module to
vkCreateGraphicsPipelines. The Vulkan driver (Mesa RADV) hits a
stack-buffer-overflow parsing the truncated OpTypeStruct. This is a
renderer-to-GPU sandbox escape on Vulkan-raster configurations
(Linux, Android, ChromeOS, and potentially Windows). ASAN-confirmed.
Repro
This technically affects other GPUs as well but we only tested on AMDGPU. This should also affect Chrome Stable and older versions.
Requirements: Linux, AMD GPU (RDNA or later), Wayland session, clang, meson, ninja, llvm. Tested on Arch with Mesa 26.0.4 source and Chromium 148.0.7778.24 (rev 0f0efd8976ddaa54b04d945a440a5a9da3d8ccb9).
# Create build dir
mkdir -p out/asan
cat > out/asan/args.gn <<'EOF'
is_asan = true
is_debug = false
is_component_build = true
symbol_level = 1
dcheck_always_on = false
EOF
gn gen out/asan
# Apply the PoC patch to Skia and build
cd third_party/skia
git apply /tmp/renderer.patch # Change this to correct path
cd ../..
autoninja -C out/asan chrome
# Clone Mesa (if needed)
git clone https://gitlab.freedesktop.org/mesa/mesa.git
cd mesa
git checkout mesa-26.0.4 # or any recent release
# Configure - AMD Vulkan driver only, no Gallium
CC=clang CXX=clang++ meson setup build-asan \
-Dvulkan-drivers=amd \
-Dgallium-drivers= \
-Dplatforms=wayland \
-Db_sanitize=address \
-Dbuildtype=debugoptimized \
-Db_ndebug=true \
-Db_lundef=false \
-Dllvm=enabled
# Build only RADV
ninja -C build-asan \
src/amd/vulkan/libvulkan_radeon.so \
src/amd/vulkan/radeon_devenv_icd.x86_64.json
Run
You may need to point VK_ICD_FILENAMES to the correct path
POC_GLYPH_SKSL=1 \
VK_ICD_FILENAMES=mesa/build-asan/src/amd/vulkan/radeon_devenv_icd.x86_64.json \
ASAN_OPTIONS="detect_leaks=0:detect_odr_violation=0" \
ASAN_SYMBOLIZER_PATH=/usr/bin/llvm-symbolizer \
RADV_DEBUG=nocache \
out/asan/chrome \
--no-sandbox --disable-gpu-sandbox \
--use-vulkan --enable-features=Vulkan \
--enable-gpu-rasterization --enable-oop-rasterization \
--allow-file-access-from-files --enable-logging=stderr \
--user-data-dir=/tmp/poc --ozone-platform=wayland \
--disable-in-process-stack-traces --disable-breakpad --disable-crash-reporter \
--disable-gpu-watchdog \
"file://$PWD/poc.html" 2>&1 | tee poc.log
See more root cause analysis in: crbug/501471710 (reported by me). I reopened this bug with more concise information and patch with clearer IsRenderer guard. If you fail to reproduce this, please let me know:
- At what step your command failed? Please give me the command and diagnostic information
- Are you on AMDGPU?