// Copyright 2024 The PDFium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
//
// Demonstrates that CFWL_Caret / CFWL_ScrollBar own a CFX_Timer whose
// deregistration is tied to the C++ destructor (sweep), not PreFinalize().
// Because PDFium's cppgc heap is configured for kIncrementalAndConcurrent
// sweeping, a window exists between PreFinalize and the destructor in which
// the embedder can fire the timer, calling OnTimerFired() on a dead widget
// whose `outer_` Member<> may already point at swept (poisoned) memory.

#include <memory>

#include "core/fxcrt/cfx_timer.h"
#include "fxjs/gc/heap.h"
#include "testing/fxgc_unittest.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "testing/v8_test_environment.h"
#include "v8/include/cppgc/allocation.h"
#include "v8/include/cppgc/explicit-management.h"
#include "v8/include/cppgc/garbage-collected.h"
#include "v8/include/cppgc/persistent.h"
#include "v8/include/cppgc/prefinalizer.h"
#include "v8/include/libplatform/libplatform.h"
#include "xfa/fwl/cfwl_app.h"
#include "xfa/fwl/cfwl_caret.h"
#include "xfa/fwl/cfwl_widget.h"
#include "xfa/fwl/cfwl_widgetmgr.h"
#include "xfa/fwl/ifwl_themeprovider.h"

namespace pdfium {
namespace {

// ---------------------------------------------------------------------------
// Fake embedder timer handler — captures the registered callback so the test
// can fire it at a chosen moment, exactly like Chrome's base::RepeatingTimer
// would on the PDF-process main thread.
// ---------------------------------------------------------------------------
class FakeTimerHandler final : public CFX_Timer::HandlerIface {
 public:
  int32_t SetTimer(int32_t uElapse, TimerCallback fn) override {
    int32_t id = ++next_id_;
    last_id_ = id;
    fn_ = fn;
    set_calls_++;
    return id;
  }
  void KillTimer(int32_t id) override { kill_calls_++; }

  void Fire() {
    if (fn_) {
      fn_(last_id_);
    }
  }

  int set_calls_ = 0;
  int kill_calls_ = 0;
  int32_t next_id_ = 0;
  int32_t last_id_ = 0;
  TimerCallback fn_ = nullptr;
};

// ---------------------------------------------------------------------------
// Minimal fake CFWL_App adapter so we can construct a real CFWL_App + widgets
// without pulling in the entire CXFA_FFApp stack.
// ---------------------------------------------------------------------------
class FakeAppAdapter final
    : public cppgc::GarbageCollected<FakeAppAdapter>,
      public CFWL_App::AdapterIface,
      public CFWL_WidgetMgr::AdapterIface {
 public:
  CONSTRUCT_VIA_MAKE_GARBAGE_COLLECTED;
  ~FakeAppAdapter() override = default;

  void Trace(cppgc::Visitor* visitor) const override {}

  // CFWL_App::AdapterIface
  CFWL_WidgetMgr::AdapterIface* GetWidgetMgrAdapter() override { return this; }
  CFX_Timer::HandlerIface* GetTimerHandler() override { return handler_; }
  IFWL_ThemeProvider* GetThemeProvider() override { return nullptr; }
  cppgc::Heap* GetHeap() override { return heap_; }

  // CFWL_WidgetMgr::AdapterIface
  void RepaintWidget(CFWL_Widget* pWidget) override { repaint_calls_++; }
  bool GetPopupPos(CFWL_Widget* pWidget,
                   float fMinHeight,
                   float fMaxHeight,
                   const CFX_RectF& rtAnchor,
                   CFX_RectF* pPopupRect) override {
    return false;
  }

  cppgc::Heap* heap_ = nullptr;
  FakeTimerHandler* handler_ = nullptr;
  int repaint_calls_ = 0;

 private:
  FakeAppAdapter() = default;
};

// Sentinel that lets us observe whether PreFinalize has run during a GC cycle.
class PreFinalizeSentinel final
    : public cppgc::GarbageCollected<PreFinalizeSentinel> {
  CPPGC_USING_PRE_FINALIZER(PreFinalizeSentinel, PreFinalize);

 public:
  PreFinalizeSentinel() = default;
  ~PreFinalizeSentinel() { dtor_count_++; }
  void PreFinalize() { prefinalize_count_++; }
  void Trace(cppgc::Visitor*) const {}
  static int prefinalize_count_;
  static int dtor_count_;
};
int PreFinalizeSentinel::prefinalize_count_ = 0;
int PreFinalizeSentinel::dtor_count_ = 0;

// Large garbage object used to push HeapGrowing past limit_for_atomic_gc_,
// which schedules a GC with sweeping_type == kIncrementalAndConcurrent.
class Bloater final : public cppgc::GarbageCollected<Bloater> {
 public:
  void Trace(cppgc::Visitor*) const {}
  uint8_t bloat_[65536];
};

}  // namespace

class CFWLCaretTimerUAFTest : public FXGCUnitTest {
 public:
  void SetUp() override {
    FXGCUnitTest::SetUp();
    PreFinalizeSentinel::prefinalize_count_ = 0;
    PreFinalizeSentinel::dtor_count_ = 0;
    CFX_Timer::InitializeGlobals();
  }
  void TearDown() override {
    CFX_Timer::DestroyGlobals();
    FXGCUnitTest::TearDown();
  }

  // Run exactly one foreground task (e.g. the GCTask posted by GCInvoker).
  bool PumpOne() {
    return v8::platform::PumpMessageLoop(
        V8TestEnvironment::GetInstance()->platform(),
        V8TestEnvironment::GetInstance()->isolate());
  }

  FakeTimerHandler timer_handler_;
};

// -----------------------------------------------------------------------------
// Part 1 — root cause: the embedder timer is *not* deregistered by
// CFWL_Widget::PreFinalize(). After an incremental GC has marked the caret
// dead and run its prefinalizer, the CFX_Timer entry in g_pwl_timer_map is
// still live and can be fired by the embedder.
// -----------------------------------------------------------------------------
TEST_F(CFWLCaretTimerUAFTest, TimerSurvivesPreFinalize) {
  // Drain any startup tasks so the first task we observe is the GCTask.
  while (PumpOne()) {
  }

  cppgc::Persistent<FakeAppAdapter> adapter =
      cppgc::MakeGarbageCollected<FakeAppAdapter>(heap()->GetAllocationHandle());
  adapter->heap_ = heap();
  adapter->handler_ = &timer_handler_;

  cppgc::Persistent<CFWL_App> app = cppgc::MakeGarbageCollected<CFWL_App>(
      heap()->GetAllocationHandle(), adapter.Get());

  // Parent (the eventual UAF target) and child caret with outer_ == parent.
  cppgc::Persistent<CFWL_Caret> parent =
      cppgc::MakeGarbageCollected<CFWL_Caret>(heap()->GetAllocationHandle(),
                                              app.Get(),
                                              CFWL_Widget::Properties(),
                                              nullptr);
  cppgc::Persistent<CFWL_Caret> child =
      cppgc::MakeGarbageCollected<CFWL_Caret>(heap()->GetAllocationHandle(),
                                              app.Get(),
                                              CFWL_Widget::Properties(),
                                              parent.Get());

  child->ShowCaret();  // Arms the 600ms blink timer via FakeTimerHandler.
  ASSERT_EQ(1, timer_handler_.set_calls_);
  ASSERT_EQ(0, timer_handler_.kill_calls_);

  // Sentinel that will tell us when PreFinalize has executed for this cycle.
  cppgc::MakeGarbageCollected<PreFinalizeSentinel>(
      heap()->GetAllocationHandle());

  // Drop *all* roots so the entire CFWL graph becomes unreachable, mirroring
  // the state when CPDFXFA_Context releases its Persistent<CXFA_FFApp>.
  parent.Clear();
  child.Clear();
  app.Clear();
  adapter.Clear();

  // Drive HeapGrowing past limit_for_atomic_gc_ so GCInvoker posts a GCTask
  // (kNoConservativeStackScan forces the posted-task path).
  for (int i = 0; i < 200 && PreFinalizeSentinel::prefinalize_count_ == 0;
       ++i) {
    for (int j = 0; j < 32; ++j) {
      cppgc::MakeGarbageCollected<Bloater>(heap()->GetAllocationHandle());
    }
    // Run exactly one task. The first one to flip the sentinel is the GCTask:
    // it performs atomic marking, runs PreFinalizers, starts the
    // kIncrementalAndConcurrent sweeper, then returns *without* running any
    // destructors.
    PumpOne();
  }
  ASSERT_EQ(1, PreFinalizeSentinel::prefinalize_count_)
      << "GCTask never ran; could not enter incremental-sweep window";

  // ----- This is the bug -----
  // PreFinalize has run on every dead CFWL_Widget, but ~CFWL_Caret() — and
  // therefore ~CFX_Timer() / KillTimer() — has *not* run yet. The embedder's
  // base::RepeatingTimer is still armed and pointing at the dead caret.
  EXPECT_EQ(0, PreFinalizeSentinel::dtor_count_)
      << "destructor ran inside GCTask — sweep was not incremental";
  EXPECT_EQ(0, timer_handler_.kill_calls_)
      << "would only be safe if PreFinalize had reset the timer";

  // Fire the embedder timer in the [PreFinalize, sweep) window. This calls
  // CFX_Timer::TimerProc -> caret->OnTimerFired() on a *dead* (unmarked,
  // pre-finalized) cppgc object, walking its outer_ chain — exactly the
  // UAF described in the report.
  timer_handler_.Fire();

  // Finish sweeping so TearDown is clean.
  ForceGCAndPump();
  EXPECT_EQ(1, timer_handler_.kill_calls_);  // ~CFX_Timer finally ran.
}

// -----------------------------------------------------------------------------
// Part 2 — UAF sink, deterministic ASAN repro.
//
// In production this state is reached when the foreground IncrementalSweepTask
// finalizes the page containing the caret's `outer_` (running its destructor
// and ASAN-poisoning / zeroing its bytes) before the page containing the
// caret itself. Here we reproduce that exact post-sweep state with the
// public cppgc primitive that performs `Finalize() + SetMemoryInaccessible()`
// on a single object — the same operations the sweeper applies — so that
// firing the still-registered timer reads through the poisoned `outer_`.
//
// Under ASAN this test produces:
//   AddressSanitizer: use-after-poison
//     #0 CFWL_Widget::GetOuter() ...        <-- read of swept outer_->outer_
//     #1 CFWL_WidgetMgr::RepaintWidget(...)
//     #2 CFWL_Widget::RepaintRect(...)
//     #3 CFWL_Caret::OnTimerFired()
//     #4 CFX_Timer::TimerProc(...)
// -----------------------------------------------------------------------------
TEST_F(CFWLCaretTimerUAFTest, TimerFiresOnSweptOuter) {
  cppgc::Persistent<FakeAppAdapter> adapter =
      cppgc::MakeGarbageCollected<FakeAppAdapter>(heap()->GetAllocationHandle());
  adapter->heap_ = heap();
  adapter->handler_ = &timer_handler_;

  cppgc::Persistent<CFWL_App> app = cppgc::MakeGarbageCollected<CFWL_App>(
      heap()->GetAllocationHandle(), adapter.Get());

  cppgc::Persistent<CFWL_Caret> parent =
      cppgc::MakeGarbageCollected<CFWL_Caret>(heap()->GetAllocationHandle(),
                                              app.Get(),
                                              CFWL_Widget::Properties(),
                                              nullptr);
  cppgc::Persistent<CFWL_Caret> child =
      cppgc::MakeGarbageCollected<CFWL_Caret>(heap()->GetAllocationHandle(),
                                              app.Get(),
                                              CFWL_Widget::Properties(),
                                              parent.Get());

  child->ShowCaret();  // Arms timer; g_pwl_timer_map[id] -> child.
  ASSERT_EQ(1, timer_handler_.set_calls_);

  // Drop the parent from the widget-manager map (RemoveWidget recursively
  // erases the child too) so nothing but `child->outer_` references it —
  // the same reachability shape as the post-PreFinalize incremental-sweep
  // window.
  app->GetWidgetMgr()->RemoveWidget(parent.Get());

  // Finalize + poison the parent slot, exactly as InlinedFinalizationBuilder
  // does for a swept page (header->Finalize(); SetMemoryInaccessible(...)).
  CFWL_Caret* raw_parent = parent.Get();
  parent.Clear();
  cppgc::subtle::FreeUnreferencedObject(heap()->GetHeapHandle(), *raw_parent);

  // The child's CFX_Timer is still registered (its destructor has not run).
  ASSERT_EQ(0, timer_handler_.kill_calls_);

  // Embedder timer tick. Under ASAN this is a use-after-poison reading
  // parent->outer_ from CFWL_WidgetMgr::RepaintWidget(); under release it
  // is a vptr/Member read from zeroed-then-reused cppgc memory.
  timer_handler_.Fire();

  // Cleanup so TearDown's atomic GC doesn't trip dangling UnownedPtr checks.
  child->HideCaret();
}

}  // namespace pdfium
