diff --git a/gpu/BUILD.gn b/gpu/BUILD.gn index 29b076c8350f0..70cb93c0f6c66 100644 --- a/gpu/BUILD.gn +++ b/gpu/BUILD.gn @@ -605,7 +605,10 @@ test("gpu_unittests") { } if (skia_use_dawn) { - sources += [ "command_buffer/service/graphite_shared_context_unittest.cc" ] + sources += [ + "command_buffer/service/dawn_graphite_static_sampler_uaf_test.cc", + "command_buffer/service/graphite_shared_context_unittest.cc", + ] } configs += [ "//build/config:precompiled_headers" ] diff --git a/gpu/command_buffer/service/dawn_graphite_static_sampler_uaf_test.cc b/gpu/command_buffer/service/dawn_graphite_static_sampler_uaf_test.cc new file mode 100644 index 0000000000000..b873fb1aa508b --- /dev/null +++ b/gpu/command_buffer/service/dawn_graphite_static_sampler_uaf_test.cc @@ -0,0 +1,212 @@ +// Copyright 2025 The Chromium Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// +// Unit-test reproduction of a heap-use-after-free in Skia's Graphite Dawn +// backend (third_party/skia/src/gpu/graphite/dawn/DawnGraphicsPipeline.cpp). +// +// DawnGraphicsPipeline::Make() declares a default-constructed +// skia_private::TArray and, in its +// per-sampler loop, stores an interior pointer (`&emplace_back()`) into +// entries[i].nextInChain. When the TArray exceeds its initial capacity (8 +// elements after the first emplace_back), it reallocates and frees its old +// buffer, leaving every previously-stored entries[*].nextInChain dangling. +// Those dangling pointers are then dereferenced inside Dawn's +// CreateBindGroupLayout (Unpack walks the nextInChain list). +// +// In production this path is reachable only on Android (Graphite-on-Dawn-on- +// Vulkan with AHardwareBuffer-backed YCbCr textures). This test forces the +// same code path on Linux by using Dawn's Null backend (which enables all +// features, including StaticSamplers and YCbCrVulkanSamplers) and composing +// >=9 YCbCr-backed image shaders into a single Graphite pipeline. + +#include + +#include "base/logging.h" +#include "gpu/command_buffer/service/graphite_shared_context.h" +#include "gpu/command_buffer/service/skia_utils.h" +#include "testing/gtest/include/gtest/gtest.h" +#include "third_party/dawn/include/dawn/dawn_proc.h" +#include "third_party/dawn/include/dawn/native/DawnNative.h" // nogncheck +#include "third_party/skia/include/core/SkBlendMode.h" +#include "third_party/skia/include/core/SkCanvas.h" +#include "third_party/skia/include/core/SkColorSpace.h" +#include "third_party/skia/include/core/SkImage.h" +#include "third_party/skia/include/core/SkPaint.h" +#include "third_party/skia/include/core/SkShader.h" +#include "third_party/skia/include/core/SkTileMode.h" +#include "third_party/skia/include/gpu/graphite/BackendTexture.h" +#include "third_party/skia/include/gpu/graphite/Context.h" +#include "third_party/skia/include/gpu/graphite/Image.h" +#include "third_party/skia/include/gpu/graphite/Recorder.h" +#include "third_party/skia/include/gpu/graphite/Recording.h" +#include "third_party/skia/include/gpu/graphite/Surface.h" +#include "third_party/skia/include/gpu/graphite/dawn/DawnBackendContext.h" +#include "third_party/skia/include/gpu/graphite/dawn/DawnGraphiteTypes.h" + +namespace gpu { +namespace { + +// Number of immutable (YCbCr) samplers to bind in a single pipeline. The +// first emplace_back() on the TArray rounds capacity up to 8; the 9th forces +// a realloc. Use a generous margin in case allocator slack differs. +constexpr int kNumYcbcrImages = 12; + +TEST(DawnGraphiteStaticSamplerUAFTest, ManyImmutableSamplersInOnePipeline) { + // ---- Set up a Dawn Null device with the YCbCr / StaticSamplers features. + dawnProcSetProcs(&dawn::native::GetProcs()); + + // StaticSamplers / YCbCrVulkanSamplers are experimental Dawn features and + // are only exposed when the allow_unsafe_apis toggle is set. + static constexpr const char* kEnabledToggles[] = {"allow_unsafe_apis"}; + wgpu::DawnTogglesDescriptor toggles = {}; + toggles.enabledToggleCount = std::size(kEnabledToggles); + toggles.enabledToggles = kEnabledToggles; + + wgpu::InstanceDescriptor instance_desc = {}; + static constexpr auto kTimedWaitAny = + wgpu::InstanceFeatureName::TimedWaitAny; + instance_desc.requiredFeatureCount = 1; + instance_desc.requiredFeatures = &kTimedWaitAny; + instance_desc.nextInChain = &toggles; + dawn::native::Instance dawn_instance(&instance_desc); + + wgpu::RequestAdapterOptions options = {}; + options.backendType = wgpu::BackendType::Null; + options.featureLevel = wgpu::FeatureLevel::Core; + options.nextInChain = &toggles; + std::vector adapters = + dawn_instance.EnumerateAdapters(&options); + ASSERT_FALSE(adapters.empty()) << "No Dawn Null adapter found"; + + wgpu::Adapter adapter(adapters[0].Get()); + ASSERT_TRUE(adapter.HasFeature(wgpu::FeatureName::StaticSamplers)); + ASSERT_TRUE(adapter.HasFeature(wgpu::FeatureName::YCbCrVulkanSamplers)); + + static constexpr wgpu::FeatureName kFeatures[] = { + wgpu::FeatureName::StaticSamplers, + wgpu::FeatureName::YCbCrVulkanSamplers, + }; + wgpu::DeviceDescriptor device_desc = {}; + device_desc.requiredFeatureCount = std::size(kFeatures); + device_desc.requiredFeatures = kFeatures; + device_desc.nextInChain = &toggles; + device_desc.SetUncapturedErrorCallback( + [](const wgpu::Device&, wgpu::ErrorType, wgpu::StringView msg) { + LOG(ERROR) << "Dawn device error: " + << std::string_view(msg.data, msg.length); + }); + wgpu::Device device = adapter.CreateDevice(&device_desc); + ASSERT_TRUE(device); + + // ---- Create the Graphite-on-Dawn context. + skgpu::graphite::DawnBackendContext backend_context = {}; + backend_context.fInstance = wgpu::Instance(dawn_instance.Get()); + backend_context.fDevice = device; + backend_context.fQueue = device.GetQueue(); + + skgpu::graphite::ContextOptions context_options; + std::unique_ptr context = + skgpu::graphite::ContextFactory::MakeDawn(backend_context, + context_options); + ASSERT_TRUE(context) << "Failed to create Graphite-on-Dawn(Null) context"; + + std::unique_ptr recorder = context->makeRecorder(); + ASSERT_TRUE(recorder); + + // ---- Build a YCbCr Dawn texture-info that Graphite will treat as needing + // an immutable sampler (DawnCaps::getImmutableSamplerInfo() returns + // non-default whenever DawnDescriptorIsValid() is true). + wgpu::YCbCrVkDescriptor ycbcr = {}; + // VK_FORMAT_G8_B8R8_2PLANE_420_UNORM (a "known" Vulkan YCbCr format so that + // usesExternalFormat()==false and the texture is treated as a normal 2D + // sampled texture in WGSL). + ycbcr.vkFormat = 1000156003u; + ycbcr.vkYCbCrModel = 1; // YCBCR_IDENTITY + ycbcr.vkYCbCrRange = 0; // ITU_FULL + ycbcr.vkXChromaOffset = 0; + ycbcr.vkYChromaOffset = 0; + ycbcr.vkChromaFilter = wgpu::FilterMode::Nearest; + + skgpu::graphite::DawnTextureInfo tex_info; + tex_info.fSampleCount = skgpu::graphite::SampleCount::k1; + tex_info.fMipmapped = skgpu::Mipmapped::kNo; + tex_info.fFormat = wgpu::TextureFormat::OpaqueYCbCrAndroid; + tex_info.fViewFormat = wgpu::TextureFormat::OpaqueYCbCrAndroid; + tex_info.fUsage = wgpu::TextureUsage::TextureBinding; + tex_info.fAspect = wgpu::TextureAspect::All; + tex_info.fYcbcrVkDescriptor = ycbcr; + + // ---- Create N OpaqueYCbCrAndroid textures (and views) on the Null device, + // wrap each one as a Graphite BackendTexture/SkImage, and turn it into an + // image shader. + const SkISize kSize = SkISize::Make(8, 8); + wgpu::TextureDescriptor wgpu_tex_desc = {}; + wgpu_tex_desc.usage = wgpu::TextureUsage::TextureBinding; + wgpu_tex_desc.size = {static_cast(kSize.width()), + static_cast(kSize.height()), 1}; + wgpu_tex_desc.format = wgpu::TextureFormat::OpaqueYCbCrAndroid; + + std::vector textures; + std::vector views; + std::vector> images; + sk_sp composed; + + for (int i = 0; i < kNumYcbcrImages; ++i) { + wgpu::Texture tex = device.CreateTexture(&wgpu_tex_desc); + ASSERT_TRUE(tex) << "CreateTexture(OpaqueYCbCrAndroid) failed (i=" << i + << ")"; + + wgpu::TextureViewDescriptor view_desc = {}; + view_desc.dimension = wgpu::TextureViewDimension::e2D; + view_desc.nextInChain = &ycbcr; + wgpu::TextureView view = tex.CreateView(&view_desc); + ASSERT_TRUE(view) << "CreateView(YCbCr) failed (i=" << i << ")"; + + skgpu::graphite::BackendTexture be_tex = + skgpu::graphite::BackendTextures::MakeDawn(kSize, tex_info, view.Get()); + ASSERT_TRUE(be_tex.isValid()) << "BackendTexture invalid (i=" << i << ")"; + + sk_sp image = SkImages::WrapTexture( + recorder.get(), be_tex, kPremul_SkAlphaType, SkColorSpace::MakeSRGB()); + ASSERT_TRUE(image) << "WrapTexture failed (i=" << i << ")"; + + sk_sp shader = image->makeShader( + SkTileMode::kClamp, SkTileMode::kClamp, SkSamplingOptions()); + ASSERT_TRUE(shader); + + composed = composed ? SkShaders::Blend(SkBlendMode::kPlus, + std::move(composed), shader) + : shader; + textures.push_back(std::move(tex)); + views.push_back(std::move(view)); + images.push_back(std::move(image)); + } + + // ---- Draw with all N image shaders so that they end up in a single + // Graphite fragment pipeline. + SkImageInfo target_ii = + SkImageInfo::Make(32, 32, kRGBA_8888_SkColorType, kPremul_SkAlphaType); + sk_sp surface = + SkSurfaces::RenderTarget(recorder.get(), target_ii); + ASSERT_TRUE(surface); + + SkPaint paint; + paint.setShader(std::move(composed)); + surface->getCanvas()->drawRect(SkRect::MakeWH(32, 32), paint); + + // Pipeline creation (DawnGraphicsPipeline::Make) is triggered here. With + // >=9 immutable samplers, staticSamplerLayouts reallocates mid-loop, + // leaving every earlier entries[*].nextInChain dangling. Dawn's + // CreateBindGroupLayout then dereferences the freed allocation. + std::unique_ptr recording = recorder->snap(); + ASSERT_TRUE(recording); + + skgpu::graphite::InsertRecordingInfo info = {}; + info.fRecording = recording.get(); + context->insertRecording(info); + context->submit(skgpu::graphite::SyncToCpu::kYes); +} + +} // namespace +} // namespace gpu