diff --git a/gpu/BUILD.gn b/gpu/BUILD.gn index 225e91875e477..00107ee435883 100644 --- a/gpu/BUILD.gn +++ b/gpu/BUILD.gn @@ -613,7 +613,10 @@ test("gpu_unittests") { } if (skia_use_dawn) { - sources += [ "command_buffer/service/graphite_shared_context_unittest.cc" ] + sources += [ + "command_buffer/service/graphite_buffer_manager_uaf_unittest.cc", + "command_buffer/service/graphite_shared_context_unittest.cc", + ] } configs += [ "//build/config:precompiled_headers" ] diff --git a/gpu/command_buffer/service/graphite_buffer_manager_uaf_unittest.cc b/gpu/command_buffer/service/graphite_buffer_manager_uaf_unittest.cc new file mode 100644 index 0000000000000..a62d30ea553db --- /dev/null +++ b/gpu/command_buffer/service/graphite_buffer_manager_uaf_unittest.cc @@ -0,0 +1,217 @@ +// 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. +// +// Regression test demonstrating that an outstanding BufferSubAllocator can +// re-stash a stale transfer-buffer pointer (raw const Buffer*) and stale +// mapped pointer back into DrawBufferManager after onFailedBuffer(), and +// that the assert-only failure branch of transferToRecording() lets it +// survive into the next snap. On Dawn backends without +// wgpu::FeatureName::BufferMapExtendedUsages (NVIDIA-Vulkan, non-UMA D3D12, +// Dawn-OpenGLES) this leads to a GPU-process wild write through the stale +// mapped pointer and a heap-use-after-free of the freed transfer Buffer when +// CopyBufferToBufferTask is recorded for it. + +#include +#include +#include + +#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/gpu/graphite/Context.h" +#include "third_party/skia/include/gpu/graphite/ContextOptions.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/dawn/DawnBackendContext.h" +#include "src/gpu/BufferWriter.h" +#include "src/gpu/graphite/BufferManager.h" +#include "src/gpu/graphite/Caps.h" +#include "src/gpu/graphite/ContextPriv.h" +#include "src/gpu/graphite/RecorderPriv.h" +#include "src/gpu/graphite/ResourceProvider.h" + +namespace gpu { +namespace { + +// Under ASAN, dawn::AllocNoThrow() returns null for sizes >= 0x70000000, +// so creating a kXferCpuToGpu buffer of this size with mappedAtCreation=true +// will return nullptr from device.CreateBuffer(), giving us a deterministic +// failure inside DrawBufferManager::getBuffer() that calls onFailedBuffer(). +// On a real device this corresponds to GPU-memory exhaustion driven from a +// renderer (large WebGL/WebGPU allocations). +constexpr size_t kHugeVertexBytes = 0x70000000u; // 1.75 GiB + +constexpr size_t kUniformSize = 64; + +TEST(GraphiteBufferManagerStaleTransferTest, StaleTransferBufferAfterFailedSnap) { + // ----------------------------------------------------------------------- + // 1) Set up a Dawn-SwiftShader-Vulkan device WITHOUT requesting + // BufferMapExtendedUsages so that DawnCaps::fDrawBufferCanBeMapped == false + // and DrawBufferManager takes the transfer-buffer path. This is exactly + // the production configuration on NVIDIA-Vulkan, non-UMA D3D12 and + // Dawn-OpenGLES (the feature is requested only as optional in Chrome and + // is not supported on those backends). + // ----------------------------------------------------------------------- + dawnProcSetProcs(&dawn::native::GetProcs()); + + wgpu::InstanceDescriptor instance_desc = {}; + static constexpr auto kTimedWaitAny = wgpu::InstanceFeatureName::TimedWaitAny; + instance_desc.requiredFeatureCount = 1; + instance_desc.requiredFeatures = &kTimedWaitAny; + dawn::native::Instance dawn_instance(&instance_desc); + + wgpu::RequestAdapterOptions options = {}; + options.backendType = wgpu::BackendType::Vulkan; + options.forceFallbackAdapter = true; // SwiftShader + options.featureLevel = wgpu::FeatureLevel::Core; + + std::vector adapters = + dawn_instance.EnumerateAdapters(&options); + ASSERT_FALSE(adapters.empty()); + + // Intentionally do NOT request BufferMapExtendedUsages. + wgpu::DeviceDescriptor device_desc = {}; + // Suppress noisy uncaptured-error logging from the intentional oversize + // buffer creation; the error itself is expected. + device_desc.SetUncapturedErrorCallback( + [](const wgpu::Device&, wgpu::ErrorType, wgpu::StringView) {}); + wgpu::Device device = + wgpu::Adapter(adapters[0].Get()).CreateDevice(&device_desc); + ASSERT_TRUE(device); + ASSERT_FALSE(device.HasFeature(wgpu::FeatureName::BufferMapExtendedUsages)); + + wgpu::Instance instance(dawn_instance.Get()); + + skgpu::graphite::DawnBackendContext backend_context = {}; + backend_context.fInstance = instance; + backend_context.fDevice = device; + backend_context.fQueue = device.GetQueue(); + + skgpu::graphite::ContextOptions ctx_opts; + std::unique_ptr context = + skgpu::graphite::ContextFactory::MakeDawn(backend_context, ctx_opts); + ASSERT_TRUE(context); + + std::unique_ptr recorder = context->makeRecorder(); + ASSERT_TRUE(recorder); + + const skgpu::graphite::Caps* caps = recorder->priv().caps(); + ASSERT_FALSE(caps->drawBufferCanBeMapped()) + << "Test requires the transfer-buffer path; got direct-map."; + + skgpu::graphite::DrawBufferManager* mgr = + recorder->priv().drawBufferManager(); + skgpu::graphite::ResourceProvider* rp = recorder->priv().resourceProvider(); + + // ----------------------------------------------------------------------- + // 2) Mimic DrawList::snapDrawPass: a stack-local UniformTracker holds a + // BufferSubAllocator obtained from getMappedUniformBuffer(). A subsequent + // vertex-buffer allocation fails, calling onFailedBuffer(). The + // UniformTracker's allocator destructs *after* onFailedBuffer() and + // BufferSubAllocator::reset() re-stashes it (with its stale fTransferBuffer + // raw Buffer* and fMappedPtr) into state.fAvailableBuffer. + // ----------------------------------------------------------------------- + uint8_t payload[kUniformSize] = {}; + for (auto& b : payload) { + b = 0xAB; + } + { + auto [w1, b1, uniformAlloc] = + mgr->getMappedUniformBuffer(kUniformSize, kUniformSize); + ASSERT_TRUE(static_cast(w1)); + ASSERT_TRUE(static_cast(uniformAlloc)); + w1.write(payload, kUniformSize); + + // renderStep.writeVertices(...) -> getMappedVertexBuffer() with a size + // the backend cannot satisfy (under ASAN, dawn::AllocNoThrow refuses the + // mappedAtCreation staging allocation for the transfer buffer). + auto [w2, b2, vertexAlloc] = + mgr->getMappedVertexBuffer(kHugeVertexBytes, /*stride=*/1); + ASSERT_FALSE(static_cast(w2)); + ASSERT_TRUE(mgr->hasMappingFailed()); + // vertexAlloc destructs (no-op, empty), then uniformAlloc destructs: + // BufferSubAllocator::reset() takes the unguarded `else` branch and + // re-stashes itself into state.fAvailableBuffer. + } + + // ----------------------------------------------------------------------- + // 3) Recorder::snap(): + // - DrawBufferManager::transferToRecording() takes the fMappingFailed + // branch which (in release) only does `fMappingFailed=false; return + // false;` -- the SkASSERTs that would catch the re-stashed + // fAvailableBuffer are stripped. + // - UploadBufferManager::transferToRecording() unmaps the transfer + // buffer T and moves its sk_sp<> into the recording. + // - valid==false -> recording = nullptr -> T's last sk_sp dropped. + // - forceProcessReturnedResources() returns T to the cache. + // ----------------------------------------------------------------------- + std::unique_ptr failed = recorder->snap(); + ASSERT_FALSE(failed); + ASSERT_FALSE(mgr->hasMappingFailed()); + + // ----------------------------------------------------------------------- + // 4) Make T purgeable and free it. T (a MapWrite kXferCpuToGpu buffer) is + // re-mapped asynchronously on return-to-cache; drain callbacks and force + // a purge so that the underlying DawnBuffer is actually `delete`d. In + // production this corresponds to ResourceCache pressure or T's slot being + // recycled for another origin's transfer buffer. + // ----------------------------------------------------------------------- + rp->setResourceCacheLimit(0); + for (int i = 0; i < 8; ++i) { + instance.ProcessEvents(); + rp->forceProcessReturnedResources(); + } + rp->freeGpuResources(); + + // ----------------------------------------------------------------------- + // 5) Next snap: writeUniforms -> getMappedUniformBuffer -> getBuffer() + // sees state.fAvailableBuffer has room and returns + // std::move(state.fAvailableBuffer) -- the stale allocator carrying + // fMappedPtr (T's pre-unmap VA) and fTransferBuffer.fBuffer = T (freed). + // writer.write() then performs a wild write through the stale fMappedPtr + // in the GPU process. + // ----------------------------------------------------------------------- + fprintf(stderr, + "[POC] requesting uniform buffer; if the bug is present, the stale " + "allocator with dangling fTransferBuffer/fMappedPtr is reused\n"); + { + auto [w3, b3, staleAlloc] = + mgr->getMappedUniformBuffer(kUniformSize, kUniformSize); + ASSERT_TRUE(static_cast(staleAlloc)); + if (w3) { + // Wild write through stale fMappedPtr (T's invalidated mapping). + fprintf(stderr, "[POC] writing through stale fMappedPtr\n"); + w3.write(payload, kUniformSize); + } + // staleAlloc destructs and re-stashes (else-branch) back into + // state.fAvailableBuffer with the stale fTransferBuffer still set. + } + + // ----------------------------------------------------------------------- + // 6) snap() again: transferToRecording() (success path) calls + // state.fAvailableBuffer.reset(), which pushes {sk_sp, {T_raw,...}} to + // fUsedBuffers, then constructs CopyBufferToBufferTask::Make(T_raw, ...). + // On insertRecording(), DawnCommandBuffer::onCopyBufferToBuffer does + // `static_cast(T_raw)->dawnBuffer()` -- a heap-UAF + // read of the freed transfer Buffer. + // ----------------------------------------------------------------------- + std::unique_ptr recording = recorder->snap(); + if (recording) { + fprintf(stderr, + "[POC] inserting recording with CopyBufferToBufferTask carrying " + "freed Buffer*\n"); + skgpu::graphite::InsertRecordingInfo info = {}; + info.fRecording = recording.get(); + context->insertRecording(info); + context->submit(skgpu::graphite::SyncToCpu::kYes); + } + + // If we got here, the bug either didn't reproduce on this configuration or + // ASAN did not catch the wild write / UAF. The asserts above would have + // failed if the precondition path wasn't reached. + SUCCEED(); +} + +} // namespace +} // namespace gpu