CVE-2026-17653
Overview
Background
- `skgpu::graphite`
- Skia’s next-generation GPU backend that records draw commands and translates them into native GPU API calls, here targeting Dawn.
- Dawn
- Chrome’s WebGPU implementation;
wgpu::CreateBindGroupLayoutconsumes descriptor structs whose optional extensions are supplied through thenextInChainpointer field. - `wgpu::StaticSamplerBindingLayout`
- an immutable/static-sampler extension struct chained off a
wgpu::BindGroupLayoutEntryvianextInChain, so its storage must outlive theCreateBindGroupLayoutcall. - `SkTBlockList`
- a Skia container that appends elements into fixed blocks, guaranteeing each element a stable address across growth, unlike
skia_private::TArray/std::vectorwhich reallocate.
Root Cause Analysis
In DawnGraphicsPipeline::Make, Graphite collected wgpu::StaticSamplerBindingLayout structs for immutable samplers into a growable skia_private::TArray and stored a pointer to each appended element into the corresponding wgpu::BindGroupLayoutEntry::nextInChain. Because a TArray reallocates its backing storage when it grows past capacity, appending a second (or later) static sampler moved the previously appended elements, invalidating the earlier nextInChain pointers so they dangled into freed storage. When Dawn later dereferenced those chained pointers during CreateBindGroupLayout, it performed a use-after-free read in the GPU process. The condition only manifested when more than one static sampler was present and the append forced a reallocation, which is why the subject notes it “likely never happened” in practice with the usual 0-1 immutable samplers.
The fix replaces the container with SkTBlockList<wgpu::StaticSamplerBindingLayout, 1>, whose elements retain stable addresses across growth, keeping every chained pointer valid until Dawn reads it.
TArray) reallocates on growth, but its elements’ addresses were treated as stable because they had been handed out as raw nextInChain pointers that must survive until a later API consumption point.Attack Path
- Web content drives a WebGPU/Graphite workload whose pipeline requires two or more immutable/static samplers in a single bind group layout.
-
Graphite appends each
wgpu::StaticSamplerBindingLayoutto the growableTArrayand writes the element’s current address into aBindGroupLayoutEntry::nextInChain. -
A subsequent append exceeds the
TArraycapacity, triggering reallocation that relocates and frees the earlier elements, leaving priornextInChainpointers dangling. -
Graphite calls
wgpu::CreateBindGroupLayout, and Dawn walks thenextInChainchain, reading from the freed storage — a use-after-free in the GPU process. - With controlled reallocation timing and heap grooming, the freed slot could be reoccupied to influence what Dawn reads, escalating the read toward exploitable behavior.
Impact Assessment
- Use-after-free read in the GPU process reachable from WebGPU-exercising web content, potentially leaking or corrupting adjacent heap data consumed by Dawn.
- Practical reachability is low because it requires multiple static samplers plus a capacity-forcing reallocation, but the sandboxed GPU process is a valuable target and controlled heap reuse could raise severity.
Files Changed
src/gpu/graphite/dawn/DawnGraphicsPipeline.cpp
Audit Directions
- Address-stability of chained pointersaudit all
nextInChainand other raw-pointer-into-container patterns where an element’s address is stored and consumed later; ensure the backing container does not reallocate (preferSkTBlockList, node-based, or reserved-capacity storage). - Deferred-consumption lifetimes across API boundariesflag any struct whose pointer is passed into Dawn/WebGPU descriptors and read at a later
Create*call, verifying the pointed-to storage outlives that call and is not moved by intervening appends. - Container migration regressionswhen refactoring
std::vector/TArrayusage, check whether any caller relied on element-address stability before assuming a growable container is a safe substitute.
Patch
From 6a4be3addd0a6e271e68a6a1bb873877e24813e9 Mon Sep 17 00:00:00 2001 From: Michael Ludwig <[email protected]> Date: Fri, 12 Jun 2026 11:24:39 -0400 Subject: [PATCH] [graphite] Use stable collection for static bindings Since the layouts are passed by pointer in the `nextInChain` field, their addresses need to stay valid until the BindGroupLayout is created. With vector, if it ever grew, that would not remain the case. Since there are usually only 0 to 1 immutable samplers, this likely never happened (and also why it uses a built-in storage for 1). Also removes the include for vector and uses TArray (we had been mixing both throughout the file). Bug: 520514458 Fixed: 520514458 Change-Id: I44c9566c68ab0e6c6d659ea77a423ddc50d53c76 Reviewed-on: https://skia-review.googlesource.com/c/skia/+/1264157 Reviewed-by: Thomas Smith <[email protected]> Auto-Submit: Michael Ludwig <[email protected]> Commit-Queue: Michael Ludwig <[email protected]> --- diff --git a/src/gpu/graphite/dawn/DawnGraphicsPipeline.cpp b/src/gpu/graphite/dawn/DawnGraphicsPipeline.cpp index d5f6f61..2d771ef 100644 --- a/src/gpu/graphite/dawn/DawnGraphicsPipeline.cpp +++ b/src/gpu/graphite/dawn/DawnGraphicsPipeline.cpp @@ -10,7 +10,8 @@ #include "include/gpu/graphite/TextureInfo.h" #include "include/gpu/graphite/dawn/DawnGraphiteTypes.h" #include "include/private/SkLog.h" -#include "include/private/SkTemplates.h" +#include "include/private/SkTArray.h" +#include "src/base/SkTBlockList.h" #include "src/core/SkTraceEvent.h" #include "src/gpu/SkSLToBackend.h" #include "src/gpu/Swizzle.h" @@ -34,7 +35,8 @@ #include "src/sksl/ir/SkSLProgram.h" #include <atomic> -#include <vector> + +using namespace skia_private; namespace skgpu::graphite { @@ -152,7 +154,7 @@ size_t create_vertex_attributes(SkSpan<const Attribute> attrs, int shaderLocationOffset, - std::vector<wgpu::VertexAttribute>* out) { + TArray<wgpu::VertexAttribute>* out) { SkASSERT(out && out->empty()); out->resize(attrs.size()); size_t vertexAttributeOffset = 0; @@ -499,12 +501,16 @@ !(samplerDescArrPtr && samplerDescArrPtr->at(0).isImmutable())) { groupLayouts[1] = sharedContext->getSingleTextureSamplerBindGroupLayout(); } else { - std::vector<wgpu::BindGroupLayoutEntry> entries(numTexturesAndSamplers); + TArray<wgpu::BindGroupLayoutEntry> entries; + entries.reset(numTexturesAndSamplers); + #if !defined(__EMSCRIPTEN__) // Static sampler layouts are passed into Dawn by address and therefore must stay // alive until the BindGroupLayoutDescriptor is created. So, store them outside of - // the loop that iterates over each BindGroupLayoutEntry. - skia_private::TArray<wgpu::StaticSamplerBindingLayout> staticSamplerLayouts; + // the loop that iterates over each BindGroupLayoutEntry. Use a TBlockList so that + // any append StaticSamplerBindingLayouts have a stable address in case we have to + // grow the collection. + SkTBlockList<wgpu::StaticSamplerBindingLayout, 1> staticSamplerLayouts; // Note that the number of samplers is equivalent to numTexturesAndSamplers / 2. So, // a sampler's index within any container that only pertains to sampler information @@ -598,7 +604,7 @@ // Vertex state std::array<wgpu::VertexBufferLayout, kNumVertexBuffers> vertexBufferLayouts; // Static data buffer layout - std::vector<wgpu::VertexAttribute> staticDataAttributes; + TArray<wgpu::VertexAttribute> staticDataAttributes; { auto arrayStride = create_vertex_attributes(step->staticAttributes(), 0, @@ -622,7 +628,7 @@ } // Append data buffer layout - std::vector<wgpu::VertexAttribute> appendDataAttributes; + TArray<wgpu::VertexAttribute> appendDataAttributes; { // Note: the shaderLocationOffset in this function call needs to be the staticAttributeSize auto arrayStride = create_vertex_attributes(step->appendAttributes(),
Original Bug Report
Potential Heap Use-After-Free in DawnGraphicsPipeline due to staticSamplerLayouts reallocation
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. Please see https://chromium.googlesource.com/chromium/src/+/main/docs/security/ai-generated-security-bugs-faq.md for more information.
Overview: A potential heap use-after-free vulnerability exists in Skia’s Graphite Dawn backend (DawnGraphicsPipeline.cpp) due to the reallocation of a TArray holding static sampler layouts while pointers to its elements are still held in a descriptor chain. When the layout is subsequently processed during bind group layout creation, the deallocated memory is traversed, potentially leading to a GPU process crash or code execution.
Affected files:
third_party/skia/src/gpu/graphite/dawn/DawnGraphicsPipeline.cpp
Estimated timestamp from git blame: 2024-10-03
Description
A potential heap use-after-free (UAF) vulnerability has been identified in DawnGraphicsPipeline::Make() within Skia’s Graphite Dawn backend (third_party/skia/src/gpu/graphite/dawn/DawnGraphicsPipeline.cpp).
The issue stems from the lifecycle management of skia_private::TArray<wgpu::StaticSamplerBindingLayout> staticSamplerLayouts during the creation of a bind group layout.
Detailed Analysis
In DawnGraphicsPipeline::Make(), a local vector is declared to hold static sampler layouts:
skia_private::TArray<wgpu::StaticSamplerBindingLayout> staticSamplerLayouts;
As the loop iterates over textures and samplers, if a sampler is immutable, a layout is added to the array and its address is saved into the stack-allocated entries array:
wgpu::StaticSamplerBindingLayout& immutableSamplerBinding =
staticSamplerLayouts.emplace_back();
...
entries[i].nextInChain = &immutableSamplerBinding;
Because staticSamplerLayouts is default-constructed with a capacity of 0 and is not pre-reserved, appending elements can trigger a vector reallocation. When TArray::emplace_back() exhausts its capacity, it reallocates the underlying buffer via growAndConstructAtEnd() -> installDataAndUpdateCapacity(), move-constructs elements to the new buffer, and frees the old buffer via sk_free().
If a reallocation occurs, any pointers already stored in entries[*].nextInChain pointing to the previous array storage become dangling.
Later, Dawn processes these entries inside device.CreateBindGroupLayout(&groupLayoutDesc). In release builds, DeviceBase::CreateBindGroupLayout calls ConvertAndExpandBGLEntries(), which invokes the generated Unpack<BindGroupLayoutEntry>() function. This function traverses the linked list starting at the dangling nextInChain pointer.
If an attacker is able to reclaim and populate the freed heap memory before the unpack traversal, they could control the structure’s sType and sampler pointer. When StaticSamplerBindingInfo::From() is called, it constructs a Ref<SamplerBase>{layout.sampler}, which increments and decrements the reference count at an attacker-controlled offset and potentially triggers a virtual method call on destruction, leading to arbitrary code execution within the GPU process.
Triggering Preconditions
On 64-bit systems, the first allocation of staticSamplerLayouts is rounded up to a minimum capacity of 8 elements (256 bytes, fitting PartitionAlloc’s 256-byte bucket). Thus, a pipeline containing 9 or more immutable (Vulkan YCbCr) samplers is required to trigger a reallocation. Under normal operation, immutable samplers are typically produced only for textures backed by a YCbCr descriptor (such as Android AHardwareBuffer SharedImages).
Potential Steps to Reproduce (Theoretical)
Note: These are potential steps based on source code analysis; our tools do not currently have the capability to run code or verify this dynamically.
- From a compromised renderer, serialize OOP-R paint commands that reference 9 or more distinct, unique
AHardwareBuffer-backed YCbCr images in a single paint shader/filter tree. - Ensure the resulting pipeline description is unique to prevent hitting the pipeline cache.
- Once the raster command is executed, the GPU process calls
DawnGraphicsPipeline::Make(). On the 9th immutable sampler,staticSamplerLayoutsreallocates, leaving dangling pointers inentries[0..7].nextInChain. - Concurrently, spray the PartitionAlloc 256-byte bucket to reclaim the freed block and overwrite its contents with forged layout descriptions.
- When
device.CreateBindGroupLayout()parses the entries, it traverses the forged chained layouts, performing refcount increments and virtual calls.
Suggested Fix
To resolve this issue, reserve sufficient capacity in staticSamplerLayouts prior to entering the loop. Since the maximum possible number of static samplers is bounded by numTexturesAndSamplers / 2, pre-allocating or reserving this capacity prevents any subsequent reallocation:
skia_private::TArray<wgpu::StaticSamplerBindingLayout> staticSamplerLayouts;
staticSamplerLayouts.reserve(numTexturesAndSamplers / 2);
Evaluated with Chrome root at commit: e9507a33bb4148ee071aaaf8a7e9ad68770359bf
Results so far have been promising, but there can be wrong deductions. Feel free to adjust as follows:
- If you are familiar with the severity guidelines, you may adjust the severity.
- If this is a false positive, and there’s no work to be done, please close as WAI.
- If there is work to do here but not a vulnerability, please change the issue type to Task/Bug/FR.
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.