Critical chrome Integer Overflow 📄 Reporter bug report 🔧 Commit mapped

Overview

Critical
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInteger overflow in Skia
DescriptionInteger overflow in Skia
ComponentSkia
Bug ClassInteger Overflow
Tracker502636904
Fix commit3150bddf3edd (skia) +83/-21
CISA KEVNot listed
Creditedq@calif.io
Disclosed2026-05-12

Changed Functions

FunctionChangeNotes
switch
tests/SkGlyphTest.cpp
modified
SkString
tests/SkGlyphTest.cpp
modified
for
tests/SkGlyphTest.cpp
modified
DEF_TEST
tests/SkGlyphTest.cpp
modified

Files Changed

  • src/effects/imagefilters/SkRuntimeImageFilter.cpp
  • tests/SkGlyphTest.cpp
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);
+            }
         }
     }
 }
Loading diff…

Original Bug Report

reported by qq...@calif.io

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?
View on issue tracker