diff --git a/skia/BUILD.gn b/skia/BUILD.gn index baa11b3c0e6ea..3e898acbc52dd 100644 --- a/skia/BUILD.gn +++ b/skia/BUILD.gn @@ -1173,6 +1173,7 @@ test("skia_unittests") { "ext/convolver_unittest.cc", "ext/geometry_unittest.cc", "ext/image_operations_unittest.cc", + "ext/opstask_stencil_clear_unittest.cc", "ext/platform_canvas_unittest.cc", "ext/recursive_gaussian_convolution_unittest.cc", "ext/rgba_to_yuva_unittest.cc", diff --git a/skia/ext/opstask_stencil_clear_unittest.cc b/skia/ext/opstask_stencil_clear_unittest.cc new file mode 100644 index 0000000000000..54b73a8072de7 --- /dev/null +++ b/skia/ext/opstask_stencil_clear_unittest.cc @@ -0,0 +1,188 @@ +// POC unit test for: Ganesh OpsTask marks shared stencil GrAttachment +// 'initial-cleared' before render-pass creation; backend failure leaves +// uninitialized VRAM flagged-clean -> kLoad on next use. +// +// This test does not exercise real VRAM (it uses GrMockGpu). It demonstrates +// the state-machine ordering defect deterministically: +// +// 1. OpsTask::onExecute() calls stencil->markHasPerformedInitialClear() +// BEFORE create_render_pass(). +// 2. Every backend's onGetOpsRenderPass() has a documented return-nullptr +// path (GL DMSAA OOM, Vk framebuffer/secondary-CB failure, ...). +// 3. When that fires, onExecute() returns early at L644 with the one-way +// flag latched but NO stencil clear issued. +// 4. The stencil is keyed/shared (GrResourceProvider::attachStencilAttachment), +// so the next OpsTask attaching a same-key stencil sees +// hasPerformedInitialClear()==true and falls through to GrLoadOp::kLoad +// of never-initialized GPU memory as its clip mask. +// +// The test injects the (2) failure into GrMockGpu and asserts (3) and (4). + +// Skia private headers use static-locals with non-trivial dtors; harmless in a +// test TU. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wexit-time-destructors" + +#include + +#include "testing/gtest/include/gtest/gtest.h" + +#include "third_party/skia/include/core/SkCanvas.h" +#include "third_party/skia/include/core/SkPaint.h" +#include "third_party/skia/include/core/SkPath.h" +#include "third_party/skia/include/core/SkPathBuilder.h" +#include "third_party/skia/include/core/SkSurface.h" +#include "third_party/skia/include/gpu/ganesh/GrDirectContext.h" +#include "third_party/skia/include/gpu/ganesh/SkSurfaceGanesh.h" +#include "third_party/skia/include/gpu/ganesh/mock/GrMockTypes.h" +#include "third_party/skia/include/private/gpu/ganesh/GrTypesPriv.h" +#include "third_party/skia/src/gpu/ganesh/GrAttachment.h" +#include "third_party/skia/src/gpu/ganesh/GrDirectContextPriv.h" +#include "third_party/skia/src/gpu/ganesh/mock/GrMockGpu.h" + +namespace { + +const char* LoadOpName(GrLoadOp op) { + switch (op) { + case GrLoadOp::kLoad: + return "kLoad"; + case GrLoadOp::kClear: + return "kClear"; + case GrLoadOp::kDiscard: + return "kDiscard"; + } + return "?"; +} + +// Draw under a complex clip path so that Ganesh's clip stack is forced to use +// stencil clipping (SurfaceDrawContext::setNeedsStencil -> +// fInitialStencilContent = kUserBitsCleared). +void DrawWithStencilClip(SkSurface* surf) { + SkCanvas* c = surf->getCanvas(); + c->save(); + SkPath clip = SkPathBuilder() + .addCircle(50, 50, 50) + .addCircle(50, 50, 10, SkPathDirection::kCCW) + .detach(); + c->clipPath(clip, /*doAntiAlias=*/false); + SkPaint p; + p.setColor(SK_ColorBLUE); + c->drawRect(SkRect::MakeWH(100, 100), p); + c->restore(); +} + +} // namespace + +TEST(OpsTaskStencilClear, FlagLatchedDespiteRenderPassFailure) { + GrMockOptions mock_opts; + // Stencil clipping requires MSAA renderability for the format used by the + // surface (matches the DMSAA / kUserBitsCleared web-reachable path). + mock_opts.fConfigOptions[(int)GrColorType::kRGBA_8888].fRenderability = + GrMockOptions::ConfigOptions::Renderability::kMSAA; + sk_sp dc = GrDirectContext::MakeMock(&mock_opts); + ASSERT_TRUE(dc); + auto* gpu = static_cast(dc->priv().getGpu()); + ASSERT_TRUE(gpu); + + // Ensure no recycled stencil from a previous test. + dc->freeGpuResources(); + + SkImageInfo ii = SkImageInfo::Make(100, 100, kRGBA_8888_SkColorType, + kPremul_SkAlphaType); + + // Two same-dimension surfaces -> same stencil unique key + // (GrAttachment::ComputeSharedAttachmentUniqueKey). + sk_sp surf_a = + SkSurfaces::RenderTarget(dc.get(), skgpu::Budgeted::kYes, ii, 1, nullptr); + sk_sp surf_b = + SkSurfaces::RenderTarget(dc.get(), skgpu::Budgeted::kYes, ii, 1, nullptr); + ASSERT_TRUE(surf_a); + ASSERT_TRUE(surf_b); + + // Instantiate proxies without stencil so a fresh stencil GrAttachment is + // allocated on first use. + surf_a->getCanvas()->clear(SK_ColorWHITE); + surf_b->getCanvas()->clear(SK_ColorWHITE); + dc->flushAndSubmit(surf_a.get(), GrSyncCpu::kNo); + dc->flushAndSubmit(surf_b.get(), GrSyncCpu::kNo); + gpu->fPocStencilLog.clear(); + + // ---- Phase 1: stencil-using OpsTask whose render-pass creation FAILS. ---- + DrawWithStencilClip(surf_a.get()); + gpu->fPocFailNextOpsRenderPassCount = 1; + dc->flushAndSubmit(surf_a.get(), GrSyncCpu::kNo); + + // Find the failed render pass and the stencil it attached. + GrAttachment* stencil = nullptr; + bool saw_forced_fail = false; + GrLoadOp failed_op = GrLoadOp::kDiscard; + for (const auto& rec : gpu->fPocStencilLog) { + std::cerr << "[phase1] onGetOpsRenderPass: stencilLoadOp=" + << LoadOpName(rec.fLoadOp) + << " stencil=" << static_cast(rec.fStencil) + << " forcedFail=" << (rec.fForcedFail ? 1 : 0) << "\n"; + if (rec.fForcedFail) { + saw_forced_fail = true; + failed_op = rec.fLoadOp; + stencil = rec.fStencil; + } + } + ASSERT_TRUE(saw_forced_fail) + << "Fault injection did not intercept the stencil-using OpsTask. " + "(Stencil clipping was not selected -- adjust clip complexity.)"; + ASSERT_NE(nullptr, stencil) + << "Stencil attachment was not bound to the failed OpsTask."; + + // OpsTask requested kClear for this brand-new stencil... + EXPECT_EQ(GrLoadOp::kClear, failed_op); + // ...but onGetOpsRenderPass() returned nullptr, so no GPU clear was ever + // issued. CWE-908: the one-way 'hasPerformedInitialClear' flag was latched + // BEFORE the render pass was created, and is NOT rolled back on failure. + EXPECT_TRUE(stencil->hasPerformedInitialClear()) + << "If this fires, the bug has been fixed: the flag is no longer " + "latched before render-pass creation."; + std::cerr << "[BUG] After failed render-pass creation: stencil(" + << static_cast(stencil) + << ")->hasPerformedInitialClear() == " + << (stencil->hasPerformedInitialClear() ? 1 : 0) + << " (stencil VRAM was NEVER cleared)\n"; + + gpu->fPocStencilLog.clear(); + + // ---- Phase 2: a second OpsTask attaches the SAME shared stencil. ---- + DrawWithStencilClip(surf_b.get()); + dc->flushAndSubmit(surf_b.get(), GrSyncCpu::kNo); + + GrAttachment* stencil2 = nullptr; + GrLoadOp second_op = GrLoadOp::kDiscard; + for (const auto& rec : gpu->fPocStencilLog) { + std::cerr << "[phase2] onGetOpsRenderPass: stencilLoadOp=" + << LoadOpName(rec.fLoadOp) + << " stencil=" << static_cast(rec.fStencil) + << " forcedFail=" << (rec.fForcedFail ? 1 : 0) << "\n"; + if (rec.fStencil) { + stencil2 = rec.fStencil; + second_op = rec.fLoadOp; + } + } + ASSERT_NE(nullptr, stencil2); + // The stencil GrAttachment is keyed by {format, dimensions, usage, + // sampleCnt} and shared across render targets, so surf_b receives the same + // object that surf_a falsely marked as cleared. + EXPECT_EQ(stencil, stencil2) + << "Same-dimension RTs should share the keyed stencil GrAttachment."; + + // Because hasPerformedInitialClear()==true, OpsTask falls through to + // GrLoadOp::kLoad: the never-initialized stencil VRAM is used as the clip + // mask for surf_b. On a real backend this is recoverable cross-origin GPU + // memory disclosure (~1 bit/pixel via Canvas2D getImageData). + EXPECT_EQ(GrLoadOp::kLoad, second_op) + << "If this fires, the bug has been fixed: the second OpsTask correctly " + "re-clears the stencil."; + std::cerr << "[BUG] Second OpsTask on same-keyed stencil(" + << static_cast(stencil2) + << ") issued stencilLoadOp=" << LoadOpName(second_op) + << " of NEVER-CLEARED VRAM (expected kClear)\n"; +} + +#pragma clang diagnostic pop diff --git a/third_party/skia/src/gpu/ganesh/mock/GrMockGpu.cpp b/third_party/skia/src/gpu/ganesh/mock/GrMockGpu.cpp index 90733758e9..69ee5c6851 100644 --- a/third_party/skia/src/gpu/ganesh/mock/GrMockGpu.cpp +++ b/third_party/skia/src/gpu/ganesh/mock/GrMockGpu.cpp @@ -67,13 +67,25 @@ std::unique_ptr GrMockGpu::Make(const GrMockOptions* mockOptions, GrOpsRenderPass* GrMockGpu::onGetOpsRenderPass(GrRenderTarget* rt, bool /*useMSAASurface*/, - GrAttachment*, + GrAttachment* stencil, GrSurfaceOrigin origin, const SkIRect& bounds, const GrOpsRenderPass::LoadAndStoreInfo& colorInfo, - const GrOpsRenderPass::StencilLoadAndStoreInfo&, + const GrOpsRenderPass::StencilLoadAndStoreInfo& stencilInfo, const TArray& sampledProxies, GrXferBarrierFlags renderPassXferBarriers) { + bool forcedFail = false; + if (fPocFailNextOpsRenderPassCount > 0) { + --fPocFailNextOpsRenderPassCount; + forcedFail = true; + } + fPocStencilLog.push_back({stencilInfo.fLoadOp, stencil, forcedFail}); + if (forcedFail) { + // Simulate the documented backend failure path (e.g. + // GrGLRenderTarget::ensureDynamicMSAAAttachment() OOM, or + // GrVkOpsRenderPass::init() vkCreateFramebuffer failure). + return nullptr; + } return new GrMockOpsRenderPass(this, rt, origin, colorInfo); } diff --git a/third_party/skia/src/gpu/ganesh/mock/GrMockGpu.h b/third_party/skia/src/gpu/ganesh/mock/GrMockGpu.h index 1bac35056f..f54c144df7 100644 --- a/third_party/skia/src/gpu/ganesh/mock/GrMockGpu.h +++ b/third_party/skia/src/gpu/ganesh/mock/GrMockGpu.h @@ -82,6 +82,19 @@ public: void checkFinishedCallbacks() override {} void finishOutstandingGpuWork() override {} + // ---- POC test hooks (not part of upstream Skia) ---- + // When >0, the next onGetOpsRenderPass() calls return nullptr (simulating + // the documented backend failure paths in GL/Vk/Metal/D3D). + int fPocFailNextOpsRenderPassCount = 0; + // Records every stencil load-op + attachment passed to onGetOpsRenderPass, + // including calls that were forced to fail. + struct PocStencilRecord { + GrLoadOp fLoadOp; + GrAttachment* fStencil; + bool fForcedFail; + }; + skia_private::TArray fPocStencilLog; + private: GrMockGpu(GrDirectContext*, const GrMockOptions&, const GrContextOptions&);