Overview

Low
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactInsufficient validation of untrusted input in Compositing
DescriptionInsufficient validation of untrusted input in Compositing
ComponentCompositing
Bug ClassLogic Error
Tracker496419374
Fix commitac53849daed5 (chromium/src) +53/-8
CISA KEVNot listed
CreditedGoogle
Disclosed2026-06-02

Changed Functions

FunctionChangeNotes
TEST
cc/paint/paint_op_buffer_unittest.cc
modified
switch
cc/paint/paint_op_reader.cc
modified

Files Changed

  • cc/paint/paint_op_buffer_unittest.cc
  • cc/paint/paint_op_reader.cc
From ac53849daed58ea11d51b9f003c674b00d7c2989 Mon Sep 17 00:00:00 2001
From: Florin Malita <[email protected]>
Date: Mon, 20 Apr 2026 07:31:03 -0700
Subject: [PATCH] Reland "[Paint] Harden SkSL shader deserialization"

This reverts commit fd4073c0adcaa2ce8a8db03602ea3a3a7e6ffbb2.

Reason for revert: relanding with fixes

MSAN fix: avoid reading serialized data in unit test, since
serialization uses uninitialized padding.

Original change's description:
> Revert "[Paint] Harden SkSL shader deserialization"
>
> This reverts commit 54b8b34b6536c7b174934f62913a3fc9e6f0b5e7.
>
> Reason for revert: MSAN failed:
> https://ci.chromium.org/ui/p/chromium/builders/ci/Linux%20ChromiumOS%20MSan%20Tests/59233/overview
>
> Original change's description:
> > [Paint] Harden SkSL shader deserialization
> >
> > Currently we attempt to deserialize/cache sksl effects regardless of
> > shader type. This can defeat an early is_privileged validation [1] which
> > is only performed for kSkSLCommand shaders.
> >
> > Since sksl deserialization only makes sense for kSkSLCommand shaders,
> > enforce this with an explicit check.
> >
> > Tangentially, simplify the kEmpty conditional to avoid an else block.
> >
> > [1]
> > https://source.chromium.org/chromium/chromium/src/+/main:cc/paint/paint_op_reader.cc;drc=a13485180764c82ed49e677e329f8ebdbdb48f05;l=699
> >
> > Bug: chromium:496419374
> > Change-Id: I3b9720eca7541b41e506c7d0092375e2f9905ed8
> > Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7763103
> > Commit-Queue: Florin Malita <[email protected]>
> > Reviewed-by: Sunny Sachanandani <[email protected]>
> > Cr-Commit-Position: refs/heads/main@{#1616300}
>
> Bug: chromium:496419374
> Change-Id: I8d4592ce9c8f0297431a7edc7441a525218dc983
> Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7771650
> Commit-Queue: Yoichi Osato <[email protected]>
> Bot-Commit: [email protected] <[email protected]>
> Reviewed-by: Yoichi Osato <[email protected]>
> Owners-Override: Yoichi Osato <[email protected]>
> Reviewed-by: Hiroki Nakagawa <[email protected]>
> Auto-Submit: Yoichi Osato <[email protected]>
> Cr-Commit-Position: refs/heads/main@{#1616383}

Bug: chromium:496419374
Change-Id: Ibb1f13e59f209be26b2983c460d142d87a189b16
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/7771169
Reviewed-by: Sunny Sachanandani <[email protected]>
Commit-Queue: Florin Malita <[email protected]>
Cr-Commit-Position: refs/heads/main@{#1617473}
---

diff --git a/cc/paint/paint_op_buffer_unittest.cc b/cc/paint/paint_op_buffer_unittest.cc
index 9ef23f27..7e3de675 100644
--- a/cc/paint/paint_op_buffer_unittest.cc
+++ b/cc/paint/paint_op_buffer_unittest.cc
@@ -135,6 +135,10 @@
                        {0.0f, 0.5f, 0.9f, 0.1f}};
     shader->positions_ = {0.f, 0.4f, 1.f};
   }
+
+  static void ResetShaderType(PaintShader* shader, PaintShader::Type type) {
+    shader->shader_type_ = type;
+  }
 };
 
 TEST(PaintOpBufferTest, Empty) {
@@ -4750,4 +4754,37 @@
             buffer2.content_color_usage());
 }
 
+TEST(PaintOpBufferTest, SkSLShaderPrivilegeEnforcement) {
+  // Create an sksl shader masquerading as a different type.
+  static constexpr char kSkSLCommand[] =
+      "half4 main(float2 coord) { return half4(0.5); }";
+  auto shader =
+      PaintShader::MakeSkSLCommand(kSkSLCommand, {}, {}, {}, {}, nullptr);
+  PaintOpSerializationTestUtils::ResetShaderType(shader.get(),
+                                                 PaintShader::Type::kColor);
+
+  PaintFlags flags;
+  flags.setShader(std::move(shader));
+
+  PaintOpBuffer buffer;
+  buffer.push<DrawRectOp>(SkRect::MakeXYWH(1, 2, 3, 4), flags);
+
+  auto memory = AllocateSerializedBuffer();
+  TestOptionsProvider options_provider;
+  SimpleBufferSerializer serializer(memory.data(), kDefaultSerializedBufferSize,
+                                    options_provider.serialize_options());
+  serializer.Serialize(buffer);
+  ASSERT_TRUE(serializer.valid());
+
+  PaintOp::DeserializeOptions d_options(options_provider.deserialize_options());
+  // Simulation of unprivileged renderer process.
+  d_options.is_privileged = false;
+
+  auto deserialized_buffer = PaintOpBuffer::MakeFromMemory(
+      memory.first(serializer.written()), d_options);
+
+  // SkSL deserialization should always fail for unprivileged processes.
+  EXPECT_FALSE(deserialized_buffer);
+}
+
 }  // namespace cc
diff --git a/cc/paint/paint_op_reader.cc b/cc/paint/paint_op_reader.cc
index e511117..3d16c6c9 100644
--- a/cc/paint/paint_op_reader.cc
+++ b/cc/paint/paint_op_reader.cc
@@ -792,16 +792,26 @@
     return;
   }
 
-  uint32_t entry_state_int = 0u;
-  ReadSimple(&entry_state_int);
-  if (entry_state_int > static_cast<uint32_t>(PaintCacheEntryState::kLast)) {
+  uint32_t sksl_entry_state_int = 0u;
+  ReadSimple(&sksl_entry_state_int);
+  if (sksl_entry_state_int >
+      static_cast<uint32_t>(PaintCacheEntryState::kLast)) {
+    valid_ = false;
+    return;
+  }
+
+  const PaintCacheEntryState sksl_entry_state =
+      static_cast<PaintCacheEntryState>(sksl_entry_state_int);
+  if (sksl_entry_state != PaintCacheEntryState::kEmpty &&
+      shader_type != PaintShader::Type::kSkSLCommand) {
+    // Reject any sksl caching/deserialization ops for non-sksl shaders.
     valid_ = false;
     return;
   }
 
   auto* cache = options_.paint_cache;
   CHECK(cache);
-  switch (static_cast<PaintCacheEntryState>(entry_state_int)) {
+  switch (sksl_entry_state) {
     case PaintCacheEntryState::kCached: {
       sk_sp<SkRuntimeEffect> cached_effect_shader = nullptr;
       if (!cache->GetEffect(ref.sk_runtime_effect_id_, &cached_effect_shader)) {
@@ -830,10 +840,8 @@
       break;
     }
     case PaintCacheEntryState::kEmpty: {
-      if (ref.shader_type() != PaintShader::Type::kSkSLCommand &&
-          ref.sk_runtime_effect_id_ == 0u) {
-        // Deserializing a non-SkRuntimeEffect shader.
-      } else {
+      if (ref.shader_type() == PaintShader::Type::kSkSLCommand ||
+          ref.sk_runtime_effect_id_ != 0u) {
         // A compromised client could send garbage data. Invalidate it.
         valid_ = false;
         return;
Loading diff…

Regression Test / PoC

shipped with the fix
diff --git a/cc/paint/paint_op_buffer_unittest.cc b/cc/paint/paint_op_buffer_unittest.cc
index 9ef23f27..7e3de675 100644
--- a/cc/paint/paint_op_buffer_unittest.cc
+++ b/cc/paint/paint_op_buffer_unittest.cc
@@ -135,6 +135,10 @@
                        {0.0f, 0.5f, 0.9f, 0.1f}};
     shader->positions_ = {0.f, 0.4f, 1.f};
   }
+
+  static void ResetShaderType(PaintShader* shader, PaintShader::Type type) {
+    shader->shader_type_ = type;
+  }
 };
 
 TEST(PaintOpBufferTest, Empty) {
@@ -4750,4 +4754,37 @@
             buffer2.content_color_usage());
 }
 
+TEST(PaintOpBufferTest, SkSLShaderPrivilegeEnforcement) {
+  // Create an sksl shader masquerading as a different type.
+  static constexpr char kSkSLCommand[] =
+      "half4 main(float2 coord) { return half4(0.5); }";
+  auto shader =
+      PaintShader::MakeSkSLCommand(kSkSLCommand, {}, {}, {}, {}, nullptr);
+  PaintOpSerializationTestUtils::ResetShaderType(shader.get(),
+                                                 PaintShader::Type::kColor);
+
+  PaintFlags flags;
+  flags.setShader(std::move(shader));
+
+  PaintOpBuffer buffer;
+  buffer.push<DrawRectOp>(SkRect::MakeXYWH(1, 2, 3, 4), flags);
+
+  auto memory = AllocateSerializedBuffer();
+  TestOptionsProvider options_provider;
+  SimpleBufferSerializer serializer(memory.data(), kDefaultSerializedBufferSize,
+                                    options_provider.serialize_options());
+  serializer.Serialize(buffer);
+  ASSERT_TRUE(serializer.valid());
+
+  PaintOp::DeserializeOptions d_options(options_provider.deserialize_options());
+  // Simulation of unprivileged renderer process.
+  d_options.is_privileged = false;
+
+  auto deserialized_buffer = PaintOpBuffer::MakeFromMemory(
+      memory.first(serializer.written()), d_options);
+
+  // SkSL deserialization should always fail for unprivileged processes.
+  EXPECT_FALSE(deserialized_buffer);
+}
+
 }  // namespace cc
Loading diff…

Original Bug Report

reported by [email protected]

SkSL Compilation Privilege Bypass in PaintOpReader via Non-kSkSLCommand Shader Types

Project Fortify, an experimental security project, has identified the following potential security issue.

Overview: A compromised renderer can bypass a privilege check in PaintOpReader to trigger SkSL compilation in the GPU process. By providing a non-kSkSLCommand shader type with an inlined cache state, an attacker can expose the SkSL compiler’s attack surface.

Affected files:

  • cc/paint/paint_op_reader.cc
  • cc/paint/paint_shader.cc
  • cc/paint/paint_shader.h

Estimated timestamp from git blame: 2025-03-18

Description

cc/paint/paint_op_reader.cc implements a privilege check in PaintOpReader::Read(sk_sp<PaintShader>* shader) intended to restrict the use of SkSL-based shaders to privileged processes (e.g., the browser process). The check specifically targets PaintShader::Type::kSkSLCommand, which was added as part of attack surface reduction measures:

  if (!options_.is_privileged &&
      shader_type == PaintShader::Type::kSkSLCommand) {
    valid_ = false;
    return;
  }

However, the subsequent deserialization logic for PaintShader fails to enforce that SkSL source code is only processed for kSkSLCommand shaders. If an attacker provides a different shader_type (such as kColor) and sets the entry_state to PaintCacheEntryState::kInlined, the reader proceeds to invoke the SkSL compiler:

    case PaintCacheEntryState::kInlined: {
      Read(&ref.sksl_command_);
      if (!valid_) {
        return;
      }
      sk_sp<SkRuntimeEffect> effect =
          SkRuntimeEffect::MakeForShader(ref.sksl_command_).effect;
      // ...

An attacker can craft a PaintOp buffer where shader_type is set to PaintShader::Type::kColor (enum value 1) and entry_state_int is set to 2 (kInlined). This configuration bypasses the privilege check because kColor != kSkSLCommand, yet it still triggers SkRuntimeEffect::MakeForShader with attacker-controlled SkSL input.

Because PaintShader::IsValid() for kColor returns true unconditionally (in cc/paint/paint_shader.cc), the resulting shader is accepted by the reader. While the compiled effect may not be used for actual rendering in this case, the attacker successfully triggers the SkSL compiler’s frontend (lexer, parser, IR generator, and optimizer) in the GPU process.

This behavior violates the assumption documented in cc/paint/paint_op_writer.cc that “only browser UI is submitting the shader commands.”

Potential Attacker Steps (Note: Fortify LLM agent hasn’t executed code)

  1. Compromise a renderer process.
  2. Generate a RasterCHROMIUM paint-op buffer containing a PaintShader with:
    • has_shader = true
    • shader_type = 1 (kColor)
    • entry_state_int = 2 (kInlined)
    • sk_runtime_effect_id_ = any non-zero value.
    • Followed by an SkString containing arbitrary SkSL source code.
  3. Submit this buffer via the renderer’s raster command buffer to the GPU process.
  4. The GPU process’s RasterDecoderImpl::DoRasterCHROMIUM (in gpu/command_buffer/service/raster_decoder.cc) will set is_privileged = false for the renderer channel.
  5. PaintOpReader::Read will skip the privilege check because shader_type != kSkSLCommand.
  6. The code enters the kInlined branch and calls SkRuntimeEffect::MakeForShader on the provided SkSL.

Impact

This vulnerability exposes the complex SkSL compiler frontend in the GPU process to unprivileged renderer-controlled input. This significantly expands the attack surface of the GPU process, undermining the isolation intended for SkSL. An attacker can leverage this bypass in conjunction with any existing or undiscovered memory corruption bugs in the Skia SkSL compiler to gain code execution in the GPU process, which is a key step in a sandbox escape.

Suggested Fix

Ensure that the kInlined and kCached branches in PaintOpReader::Read for PaintShader also validate that the shader_type is compatible with SkSL effects (i.e., it must be kSkSLCommand), similar to the validation performed in the kEmpty branch (lines 824-833).

    case PaintCacheEntryState::kInlined: {
      if (ref.shader_type() != PaintShader::Type::kSkSLCommand) {
        valid_ = false;
        return;
      }
      Read(&ref.sksl_command_);
      // ...

Evaluated with Chrome root at commit: 0eb4855bda702feaaa8b899336664f97e3df88b8


Results so far have been promising, but there can be wrong deductions. If this proves to be a false positive, please close as WAI; data from false positives will be used to improve accuracy over time. Please feel free to reach out to me if you have concerns or feedback.

View on issue tracker