CVE-2026-7920
Overview
Changed Functions
| Function | Change | Notes |
|---|---|---|
ifsrc/core/SkRuntimeEffect.cpp |
modified |
Files Changed
include/effects/SkRuntimeEffect.hsrc/core/SkRuntimeEffect.cpp
Patch
From d3934f5b07ccbdb8e36e733c12d00f4f53811097 Mon Sep 17 00:00:00 2001 From: Kaylee Lubick <[email protected]> Date: Mon, 27 Apr 2026 18:23:31 +0000 Subject: [PATCH] Make local optimized copy of program when creating SkRP version As per the linked bug, there could be a problem if the gpu backend tried to use a compiled runtime effect at the same time as the cpu backend made its first call to getRPProgram(). This could result in the fBaselineProgram being mutated out from under the gpu backend's version. This makes an optimized copy of the existing program to then convert to SkRasterPipeline. By setting optimize to true and a non-zero inlining limit, compiler.convertProgram will call the inliner and remove unused functions (as well as other things like unused local/global variables). To prevent further mutation of fBaseProgram, I made it be pointer to const (I'd initially been puzzled how a const getRPProgram could be mutating it, but only the pointer was const). Performance change looks negligible (parsing is pretty quick): ``` $ out/Release/nanobench_baseline --match sksl_skrp Timer overhead: 23.5ns curr/maxrss loops min median mean max stddev samples config bench 92/90 MB 3 5.35Β΅s 5.38Β΅s 5.38Β΅s 5.45Β΅s 0% ββ ββββββββ nonrendering sksl_skrp_tiny 92/90 MB 3 14.7Β΅s 14.9Β΅s 15.1Β΅s 17.5Β΅s 6% ββββββββββ nonrendering sksl_skrp_small 92/90 MB 1 125Β΅s 132Β΅s 135Β΅s 164Β΅s 9% ββ ββββββββ nonrendering sksl_skrp_medium 92/90 MB 1 321Β΅s 338Β΅s 339Β΅s 364Β΅s 5% βββββββββ β nonrendering sksl_skrp_large $ out/Release/nanobench_with_change --match sksl_skrp Timer overhead: 23.5ns curr/maxrss loops min median mean max stddev samples config bench 92/90 MB 3 5.62Β΅s 5.64Β΅s 5.64Β΅s 5.69Β΅s 0% βββββββββ β nonrendering sksl_skrp_tiny 92/90 MB 4 14.8Β΅s 14.9Β΅s 15.3Β΅s 17.1Β΅s 5% βββββ βββββ nonrendering sksl_skrp_small 92/90 MB 2 125Β΅s 130Β΅s 131Β΅s 154Β΅s 7% ββββββββββ nonrendering sksl_skrp_medium 92/90 MB 1 322Β΅s 344Β΅s 342Β΅s 369Β΅s 5% βββ βββββββ nonrendering sksl_skrp_large ``` Bug: b/498989348 Change-Id: Idf32b817f5d3de823bb95f588e69e4e8a58ead80 Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1219876 Reviewed-by: Michael Ludwig <[email protected]> Commit-Queue: Kaylee Lubick <[email protected]> --- diff --git a/include/effects/SkRuntimeEffect.h b/include/effects/SkRuntimeEffect.h index e36f92f9..a53e721 100644 --- a/include/effects/SkRuntimeEffect.h +++ b/include/effects/SkRuntimeEffect.h @@ -324,8 +324,8 @@ uint32_t fStableKey = 0; SkString fName; - std::unique_ptr<SkSL::Program> fBaseProgram; - std::unique_ptr<SkSL::RP::Program> fRPProgram; + std::unique_ptr<const SkSL::Program> fBaseProgram; + std::unique_ptr<const SkSL::RP::Program> fRPProgram; mutable SkOnce fCompileRPProgramOnce; const SkSL::FunctionDefinition& fMain; std::vector<Uniform> fUniforms; diff --git a/src/core/SkRuntimeEffect.cpp b/src/core/SkRuntimeEffect.cpp index ee4bfcb..be07904 100644 --- a/src/core/SkRuntimeEffect.cpp +++ b/src/core/SkRuntimeEffect.cpp @@ -222,30 +222,48 @@ fCompileRPProgramOnce([&] { // We generally do not run the inliner when an SkRuntimeEffect program is initially created, // because the final compile to native shader code will do this. However, in SkRP, there's - // no additional compilation occurring, so we need to manually inline here if we want the - // performance boost of inlining. - if (!(fFlags & kDisableOptimization_Flag)) { - SkSL::Compiler compiler; - fBaseProgram->fConfig->fSettings.fInlineThreshold = SkSL::kDefaultInlineThreshold; - compiler.runInliner(*fBaseProgram); + // no additional compilation occurring, so we need to optimize/inline here if we want the + // performance boost of inlining. Since fBaseProgram is a shared (const) object, we can't + // mutate it in-place (e.g. calling compiler.runInliner). If optimization is neccesary, + // we re-compile the program from source with inlining and optimization enabled to get a + // freshly optimized copy (it's pretty cheap to re-compile and there's no easy way to copy + // an SkSL::Program). + const SkSL::Program* programToUse = fBaseProgram.get(); + const SkSL::FunctionDefinition* mainToUse = &fMain; - // After inlining, the program is likely to have dead functions left behind. - while (SkSL::Transform::EliminateDeadFunctions(*fBaseProgram)) { - // Removing dead functions may cause more functions to become unreferenced. + std::unique_ptr<SkSL::Program> optimizedCopy; + bool shouldOptimize = !(fFlags & kDisableOptimization_Flag); + SkSL::ProgramSettings settings = fBaseProgram->fConfig->fSettings; + bool needsOptimization = !settings.fOptimize || + settings.fInlineThreshold < SkSL::kDefaultInlineThreshold; + if (shouldOptimize && needsOptimization) { + SkSL::Compiler compiler; + settings.fOptimize = true; + settings.fInlineThreshold = SkSL::kDefaultInlineThreshold; + optimizedCopy = compiler.convertProgram( + fBaseProgram->fConfig->fKind, *fBaseProgram->fSource, settings); + SkASSERT(optimizedCopy); + if (optimizedCopy) { + const auto* mainDecl = optimizedCopy->getFunction("main"); + SkASSERT(mainDecl); + if (mainDecl) { + programToUse = optimizedCopy.get(); + mainToUse = mainDecl->definition(); + } } } SkSL::DebugTracePriv tempDebugTrace; if (debugTrace) { const_cast<SkRuntimeEffect*>(this)->fRPProgram = MakeRasterPipelineProgram( - *fBaseProgram, fMain, debugTrace, /*writeTraceOps=*/true); + *programToUse, *mainToUse, debugTrace, /*writeTraceOps=*/true); } else if (kRPEnableLiveTrace) { debugTrace = &tempDebugTrace; const_cast<SkRuntimeEffect*>(this)->fRPProgram = MakeRasterPipelineProgram( - *fBaseProgram, fMain, debugTrace, /*writeTraceOps=*/false); + *programToUse, *mainToUse, debugTrace, /*writeTraceOps=*/false); } else { const_cast<SkRuntimeEffect*>(this)->fRPProgram = MakeRasterPipelineProgram( - *fBaseProgram, fMain, /*debugTrace=*/nullptr, /*writeTraceOps=*/false); + *programToUse, *mainToUse, /*debugTrace=*/nullptr, /*writeTraceOps=*/false); } if (kRPEnableLiveTrace) { @@ -450,8 +468,9 @@ } SkSL::ProgramSettings SkRuntimeEffect::MakeSettings(const Options& options) { + constexpr int kDisableSKSLInlining = 0; SkSL::ProgramSettings settings; - settings.fInlineThreshold = 0; + settings.fInlineThreshold = kDisableSKSLInlining; settings.fForceNoInline = options.forceUnoptimized; settings.fOptimize = !options.forceUnoptimized; settings.fMaxVersionAllowed = options.maxVersionAllowed;
Original Bug Report
UAF in SkRuntimeEffect due to data race on fBaseProgram IR
Project Fortify, an experimental security project, has identified the following potential security issue. If you’re a feature owner CC-ed on this bug, please do your best to review these reports without the security team.
Overview: A potential data race exists in Skia’s SkRuntimeEffect where getRPProgram() mutates the global fBaseProgram AST via inlining while other threads concurrently read it. If triggered by a compromised renderer executing operations concurrently on the gpu_main and viz_compositor threads, this race can lead to a Use-After-Free of SkSL IR nodes. Exploitation could lead to Remote Code Execution in the GPU process.
Affected files:
third_party/skia/src/core/SkRuntimeEffect.cppthird_party/skia/src/sksl/SkSLInliner.cppthird_party/skia/src/sksl/codegen/SkSLPipelineStageCodeGenerator.cppthird_party/skia/src/sksl/transform/SkSLEliminateDeadFunctions.cppthird_party/skia/src/gpu/graphite/ShaderCodeDictionary.cppthird_party/skia/src/gpu/ganesh/effects/GrSkSLFP.cpp
Estimated timestamp from git blame: 2024-07-29
Summary
There is a potential data race and Use-After-Free (UAF) vulnerability in Skia’s SkRuntimeEffect affecting the Chrome GPU process. The issue stems from unsynchronized access to the fBaseProgram Abstract Syntax Tree (AST). While SkRuntimeEffect::getRPProgram() lazily optimizes this AST in-place using an SkOnce block, other threads can concurrently read the AST during GPU shader generation. Because SkRuntimeEffect disables memory pooling, SkSL AST node replacements directly trigger standard heap deallocations, allowing a concurrent reader to dereference a dangling pointer and invoke virtual methods on freed memory.
Note: The exploitation steps below are suggested/potential steps derived from codebase analysis; our tooling agent does not yet have the ability to run code to provide a working proof-of-concept.
Technical Details
- Shared Singletons: Built-in runtime effects like
SkLumaColorFilterare implemented via a leaked, globally sharedSkRuntimeEffectsingleton (SkKnownRuntimeEffects::GetKnownRuntimeEffect). This singleton holds the parsed SkSL AST infBaseProgram. - In-place Mutation & Deallocation: When evaluated for a constant input,
GrSkSLFP::constantOutputForConstantInputcallsfEffect->getRPProgram()to run the effect on the CPU. InsidegetRPProgram(), anSkOnceblock lazily optimizes the AST by invokingSkSL::Compiler::runInliner. The inliner replaces nodes likeFunctionCallwith their inlined equivalents. Crucially,SkRuntimeEffectexplicitly disables SkSL memory pools (fUseMemoryPool = false), meaning the destroyed nodes are immediately freed to the system heap via::operator delete. Skia structures do not use MiraclePtr (BRP), so these allocations are unprotected. - Unsynchronized Concurrent Reads: While
SkOnceensures the compilation only happens once, it provides zero protection against concurrent reads. Other components, such as Graphite’sGenerateRuntimeShaderPreamble(which runsSkSL::PipelineStage::ConvertProgram), deeply traversefBaseProgramto generate GPU shaders without acquiring any read locks. - Exploitation Primitive:
SkSL::IRNodeand its subclasses are highly polymorphic. A concurrent reader thread traversing the AST can hold a dangling pointer to a node just deleted by the inliner on another thread. When the reader invokes a virtual method on this node (e.g.,expr.description()orexpr.type().matches()), it dereferences the vtable, providing a strong primitive for arbitrary code execution if the heap is groomed.
Potential Exploitation Steps
An attacker with a compromised renderer process could attempt to exploit this via the following sequence:
- Heap Grooming: The attacker sends a sequence of IPC messages (e.g., allocating specific WebGL textures or SharedImages) to groom the GPU process heap.
- Task B (Read Trigger): The attacker embeds a
DrawRectOpusingSkLumaColorFilter(reachable via CSSmask-mode: luminanceor canvas) and a variable shader input into aCompositorFrame. This frame is submitted to the Viz service, targeting theviz_compositorthread. If Graphite is enabled, this forcesGenerateRuntimeShaderPreambleto traverse theSkLumaColorFilterAST. - Task A (Mutation Trigger): Concurrently, the attacker dispatches a
DrawRectOpwithSkLumaColorFilterand a constant solid color through theRasterDecoder(OOP-R), targeting thegpu_mainthread. Ganesh’s optimization pass (GrColorFragmentProcessorAnalysis) evaluates the constant output, invokinggetRPProgram()and triggering the inliner mutation. - Race Condition: If timed correctly, the
gpu_mainthread deletes theFunctionCallnode while theviz_compositorthread holds a reference to it. - Hijack: The attacker’s groomed data reclaims the freed memory block and supplies a fake vtable pointer. The
viz_compositorthread resumes execution, calls a virtual method on the freed node, and jumps to the attacker’s ROP chain, achieving RCE in the GPU process.
Suggested Fix
The root cause is that fBaseProgram is treated as a conceptually const object but is physically mutated during lazy optimization. Potential fixes include:
- Deep Copy: Have
getRPProgramcreate a deep clone offBaseProgrambefore running the inliner and dead-code elimination, rather than mutating the shared AST in-place. - AOT Optimization: Run the inliner and dead-code elimination ahead-of-time when the
SkRuntimeEffectis initially created, removing the need to mutate it lazily ingetRPProgram. - Read/Write Lock: Protect
fBaseProgramaccesses with abase::subtle::AtomicWordor anstd::shared_mutexallowing multiple concurrent readers but taking an exclusive lock during the one-time lazy compilation.
Evaluated with Chrome root at commit: ff3d2b74fa39431785bd60e51463b08fcc71ee33
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. And please feel free to reach out to me directly if you have concerns or feedback on the project.