chromium/content/browser/interest_group/ad_auction_service_mojolpm_fuzzer.cc

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

// A MojoLPM fuzzer targeting the public API surfaces of the Protected Audiences
// API.
//
// See documentation in ad_auction_service_mojolpm_fuzzer_docs.md.

#include <stdint.h>

#include <memory>
#include <optional>
#include <utility>
#include <vector>

#include "base/functional/bind.h"
#include "base/functional/callback.h"
#include "base/functional/callback_helpers.h"
#include "base/memory/raw_ptr.h"
#include "base/memory/scoped_refptr.h"
#include "base/no_destructor.h"
#include "base/run_loop.h"
#include "base/synchronization/lock.h"
#include "base/task/sequenced_task_runner.h"
#include "base/test/scoped_feature_list.h"
#include "base/thread_annotations.h"
#include "content/browser/interest_group/ad_auction_service_impl.h"
#include "content/browser/interest_group/ad_auction_service_mojolpm_fuzzer.pb.h"
#include "content/browser/interest_group/ad_auction_service_mojolpm_fuzzer_stringifiers.h"
#include "content/browser/interest_group/ad_auction_service_mojolpm_fuzzer_stringifiers.pb.h"
#include "content/browser/interest_group/auction_process_manager.h"
#include "content/browser/interest_group/interest_group_features.h"
#include "content/browser/interest_group/interest_group_manager_impl.h"
#include "content/browser/renderer_host/render_frame_host_impl.h"
#include "content/common/features.h"
#include "content/public/browser/browser_context.h"
#include "content/public/browser/content_browser_client.h"
#include "content/public/browser/privacy_sandbox_invoking_api.h"
#include "content/public/browser/render_process_host.h"
#include "content/public/test/test_renderer_host.h"
#include "content/public/test/url_loader_interceptor.h"
#include "content/services/auction_worklet/auction_worklet_service_impl.h"
#include "content/test/fuzzer/mojolpm_fuzzer_support.h"
#include "content/test/test_content_browser_client.h"
#include "content/test/test_render_frame_host.h"
#include "content/test/test_web_contents.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "mojo/public/cpp/bindings/remote.h"
#include "services/data_decoder/public/cpp/test_support/in_process_data_decoder.h"
#include "third_party/blink/public/common/features.h"
#include "third_party/blink/public/mojom/interest_group/ad_auction_service.mojom-mojolpm.h"
#include "third_party/blink/public/mojom/interest_group/ad_auction_service.mojom.h"
#include "third_party/blink/public/mojom/interest_group/interest_group_types.mojom.h"
#include "third_party/libprotobuf-mutator/src/src/libfuzzer/libfuzzer_macro.h"
#include "url/gurl.h"
#include "url/origin.h"

namespace content::ad_auction_service_mojolpm_fuzzer {

class SiteInstance;

class AllowInterestGroupContentBrowserClient
    : public content::TestContentBrowserClient {
 public:
  explicit AllowInterestGroupContentBrowserClient() = default;
  ~AllowInterestGroupContentBrowserClient() override = default;

  AllowInterestGroupContentBrowserClient(
      const AllowInterestGroupContentBrowserClient&) = delete;
  AllowInterestGroupContentBrowserClient& operator=(
      const AllowInterestGroupContentBrowserClient&) = delete;

  // ContentBrowserClient overrides:
  bool IsInterestGroupAPIAllowed(content::RenderFrameHost* render_frame_host,
                                 InterestGroupApiOperation operation,
                                 const url::Origin& top_frame_origin,
                                 const url::Origin& api_origin) override {
    return true;
  }

  bool IsPrivacySandboxReportingDestinationAttested(
      content::BrowserContext* browser_context,
      const url::Origin& destination_origin,
      content::PrivacySandboxInvokingAPI invoking_api,
      bool post_impression_reporting) override {
    return true;
  }

  bool IsCookieDeprecationLabelAllowed(
      content::BrowserContext* browser_context) override {
    return true;
  }
};

constexpr char kFledgeUpdateHeaders[] =
    "HTTP/1.1 200 OK\n"
    "Content-type: Application/JSON\n"
    "Ad-Auction-Allowed: true\n";

constexpr char kInterestGroupUpdate[] = R"({
"ads": [{"renderURL": "https://example.com/new_render"
        }]
})";

constexpr char kFledgeScriptHeaders[] =
    "HTTP/1.1 200 OK\n"
    "Content-type: Application/Javascript\n"
    "Ad-Auction-Allowed: true\n";

constexpr char kBasicScript[] = R"(
function generateBid(interestGroup, auctionSignals, perBuyerSignals,
    trustedBiddingSignals, browserSignals, directFromSellerSignals) {
  const ad = interestGroup.ads[0];
  return {'ad': ad, 'bid': 1, 'render': ad.renderURL,
                'allowComponentAuction': true};
}

function reportWin(auctionSignals, perBuyerSignals, sellerSignals,
    browserSignals, directFromSellerSignals) {
}

function scoreAd(adMetadata, bid, auctionConfig, trustedScoringSignals,
                 browserSignals) {
  return {desirability: bid,
          allowComponentAuction: true};
}

function reportResult(auctionConfig, browserSignals) {
    return {};
}
)";

// For handling network requests made by the Protected Audience API -- also
// prevents those requests from being made to real servers.
class NetworkResponder {
 public:
  void SetScript(content::fuzzing::ad_auction_service::proto::Script script) {
    base::AutoLock auto_lock(lock_);
    script_ = Stringify(script);
  }

 private:
  bool RequestHandler(content::URLLoaderInterceptor::RequestParams* params) {
    base::AutoLock auto_lock(lock_);
    if (params->url_request.url.path().find(".js") != std::string::npos) {
      content::URLLoaderInterceptor::WriteResponse(
          kFledgeScriptHeaders, script_ ? *script_ : kBasicScript,
          params->client.get());
    } else {
      content::URLLoaderInterceptor::WriteResponse(
          kFledgeUpdateHeaders, kInterestGroupUpdate, params->client.get());
    }
    return true;
  }

  mutable base::Lock lock_;

  // Handles network requests for interest group updates.
  content::URLLoaderInterceptor network_interceptor_{
      base::BindRepeating(&NetworkResponder::RequestHandler,
                          base::Unretained(this))};

  std::optional<std::string> script_ GUARDED_BY(lock_);
};

// AuctionProcessManager that allows running auctions in-proc.
class SameProcessAuctionProcessManager : public content::AuctionProcessManager {
 public:
  SameProcessAuctionProcessManager() = default;
  SameProcessAuctionProcessManager(const SameProcessAuctionProcessManager&) =
      delete;
  SameProcessAuctionProcessManager& operator=(
      const SameProcessAuctionProcessManager&) = delete;
  ~SameProcessAuctionProcessManager() override = default;

 private:
  content::RenderProcessHost* LaunchProcess(
      mojo::PendingReceiver<auction_worklet::mojom::AuctionWorkletService>
          auction_worklet_service_receiver,
      const ProcessHandle* handle,
      const std::string& display_name) override {
    // Create one AuctionWorkletServiceImpl per Mojo pipe, just like in
    // production code. Don't bother to delete the service on pipe close,
    // though; just keep it in a vector instead.
    auction_worklet_services_.push_back(
        auction_worklet::AuctionWorkletServiceImpl::CreateForService(
            std::move(auction_worklet_service_receiver)));
    return nullptr;
  }

  scoped_refptr<content::SiteInstance> MaybeComputeSiteInstance(
      content::SiteInstance* frame_site_instance,
      const url::Origin& worklet_origin) override {
    return nullptr;
  }

  bool TryUseSharedProcess(ProcessHandle* process_handle) override {
    return false;
  }

  std::vector<std::unique_ptr<auction_worklet::AuctionWorkletServiceImpl>>
      auction_worklet_services_;
};

const char* const kCmdline[] = {"ad_auction_service_mojolpm_fuzzer", nullptr};

content::mojolpm::FuzzerEnvironment& GetEnvironment() {
  static base::NoDestructor<content::mojolpm::FuzzerEnvironment> environment(
      1, kCmdline);
  return *environment;
}

scoped_refptr<base::SequencedTaskRunner> GetFuzzerTaskRunner() {
  return GetEnvironment().fuzzer_task_runner();
}

// Per-testcase state needed to run the interface being tested.
//
// The lifetime of this is scoped to a single testcase, and it is created and
// destroyed from the fuzzer sequence (checked with `this->sequence_checker_`).
//
// Test cases may create one or more service instances, send Mojo messages to
// remotes for those service instances, and run IO and UI thread tasks (the
// fuzzer itself runs on its own thread, distinct from the UI and IO threads).
//
// For each input Testcase proto, SetUp() is run first. (This is why expensive
// "stateless" initialization happens just once, in GetEnvironment(), before
// SetUp() is run). Then, "new service" actions from the Testcase proto instruct
// the fuzzer to create new service implementation instances; they are owned by
// the RFH through DocumentService, and the RFH is owned by `test_adapter_`.
// Actions use an ID to determine which service instance to use, allowing
// control over which remote to use when running remote actions.  When all the
// actions in the current Testcase proto have been executed, TearDown() is
// called, and then this AdAuctionServiceTestcase is destroyed.  After that, the
// process repeats with the next Testcase proto input.
class AdAuctionServiceTestcase
    : public ::mojolpm::Testcase<
          content::fuzzing::ad_auction_service::proto::Testcase,
          content::fuzzing::ad_auction_service::proto::Action> {
 public:
  using ProtoTestcase = content::fuzzing::ad_auction_service::proto::Testcase;
  using ProtoAction = content::fuzzing::ad_auction_service::proto::Action;
  explicit AdAuctionServiceTestcase(
      const content::fuzzing::ad_auction_service::proto::Testcase& testcase);
  ~AdAuctionServiceTestcase();

  void SetUp(base::OnceClosure done_closure) override;
  void TearDown(base::OnceClosure done_closure) override;

  void RunAction(const ProtoAction& action,
                 base::OnceClosure done_closure) override;

 private:
  void SetUpOnUIThread();
  void TearDownOnUIThread();

  // Create and bind a new AdAuctionServiceImpl instance, and register the
  // remote with MojoLPM.
  //
  // Runs on fuzzer thread, calling CreateAdAuctionServiceImplOnUIThread() on
  // the UI thread to create the implementation.
  void AddAdAuctionService(uint32_t id, base::OnceClosure done_closure);
  void CreateAdAuctionServiceImplOnUIThread(
      mojo::PendingReceiver<blink::mojom::AdAuctionService>&& receiver);

  // This is run every time we run the RunUntilIdle action -- this ensures that,
  // for instance, completion callbacks posted from the AdAuctionService
  // implementation's database thread are run on the UI thread.
  void RunUntilIdleOnUIThread();

  // All the below fields must be accessed on the UI thread.
  base::test::ScopedFeatureList feature_list_;
  base::test::ScopedFeatureList fenced_frame_feature_list_;

  AllowInterestGroupContentBrowserClient content_browser_client_;
  raw_ptr<content::ContentBrowserClient> old_content_browser_client_ = nullptr;
  content::mojolpm::RenderViewHostTestHarnessAdapter test_adapter_;
  raw_ptr<content::TestRenderFrameHost> render_frame_host_ = nullptr;

  // Must be destroyed before test_adapter_::TearDown().
  std::optional<NetworkResponder> network_responder_;

  data_decoder::test::InProcessDataDecoder in_process_data_decoder_;
};

AdAuctionServiceTestcase::AdAuctionServiceTestcase(
    const ProtoTestcase& testcase)
    : Testcase<ProtoTestcase, ProtoAction>(testcase) {
  feature_list_.InitWithFeatures(
      /*enabled_features=*/
      {blink::features::kInterestGroupStorage,
       blink::features::kAdInterestGroupAPI, blink::features::kFledge},
      /*disabled_features=*/{});
  fenced_frame_feature_list_.InitAndEnableFeatureWithParameters(
      blink::features::kFencedFrames, {{"implementation_type", "mparch"}});
  test_adapter_.SetUp();
  network_responder_.emplace();
}

AdAuctionServiceTestcase::~AdAuctionServiceTestcase() {
  network_responder_.reset();
  test_adapter_.TearDown();
}

void AdAuctionServiceTestcase::RunAction(const ProtoAction& action,
                                         base::OnceClosure run_closure) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(this->sequence_checker_);
  const auto ThreadId_UI =
      content::fuzzing::ad_auction_service::proto::RunThreadAction_ThreadId_UI;
  const auto ThreadId_IO =
      content::fuzzing::ad_auction_service::proto::RunThreadAction_ThreadId_IO;
  switch (action.action_case()) {
    case ProtoAction::kRunThread:
      // These actions ensure that any tasks currently queued on the named
      // thread have chance to run before the fuzzer continues.
      //
      // We don't provide any particular guarantees here; this does not mean
      // that the named thread is idle, nor does it prevent any other threads
      // from running (or the consequences of any resulting callbacks, for
      // example).
      if (action.run_thread().id() == ThreadId_UI) {
        content::GetUIThreadTaskRunner({})->PostTaskAndReply(
            FROM_HERE, base::DoNothing(), std::move(run_closure));
      } else if (action.run_thread().id() == ThreadId_IO) {
        content::GetIOThreadTaskRunner({})->PostTaskAndReply(
            FROM_HERE, base::DoNothing(), std::move(run_closure));
      }
      return;
    case ProtoAction::kNewAdAuctionService:
      // Create and bind a new AdAuctionServiceImpl instance, and register the
      // remote with MojoLPM.
      AddAdAuctionService(action.new_ad_auction_service().id(),
                          std::move(run_closure));
      return;
    case ProtoAction::kRunUntilIdle:
      // On the UI thread, call RunUntilIdle() -- this is often needed to wait
      // for other threads, like the AdAuctionService API implementation's
      // database thread -- that thread posts results to the UI thread, the UI
      // thread needs to be able to run the completion callbacks that receive
      // the database results.
      content::GetUIThreadTaskRunner({})->PostTaskAndReply(
          FROM_HERE,
          base::BindOnce(&AdAuctionServiceTestcase::RunUntilIdleOnUIThread,
                         base::Unretained(this)),
          std::move(run_closure));
      return;
    case ProtoAction::kNetResponseAction:
      network_responder_->SetScript(action.net_response_action().script());
      break;
    case ProtoAction::kAdAuctionServiceRemoteAction:
      // Invoke one of the service methods on AdAuctionService, with parameters
      // specified in the ad_auction_service_remote_action() proto, on the
      // remote given by the id in the proto.
      ::mojolpm::HandleRemoteAction(action.ad_auction_service_remote_action());
      break;
    case ProtoAction::ACTION_NOT_SET:
      break;
  }
  GetFuzzerTaskRunner()->PostTask(FROM_HERE, std::move(run_closure));
}

void AdAuctionServiceTestcase::SetUp(base::OnceClosure done_closure) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(this->sequence_checker_);

  content::GetUIThreadTaskRunner({})->PostTaskAndReply(
      FROM_HERE,
      base::BindOnce(&AdAuctionServiceTestcase::SetUpOnUIThread,
                     base::Unretained(this)),
      std::move(done_closure));
}

void AdAuctionServiceTestcase::SetUpOnUIThread() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  test_adapter_.NavigateAndCommit(GURL("https://owner.test:443"));
  render_frame_host_ =
      static_cast<content::TestWebContents*>(test_adapter_.web_contents())
          ->GetPrimaryMainFrame();
  render_frame_host_->InitializeRenderFrameIfNeeded();
  old_content_browser_client_ =
      SetBrowserClientForTesting(&content_browser_client_);

  auto* manager_ = static_cast<content::InterestGroupManagerImpl*>(
      test_adapter_.browser_context()
          ->GetDefaultStoragePartition()
          ->GetInterestGroupManager());
  // Process creation crashes in the Chrome zygote init in unit tests, so run
  // the auction "processes" in-process instead.
  manager_->set_auction_process_manager_for_testing(
      std::make_unique<SameProcessAuctionProcessManager>());
}

void AdAuctionServiceTestcase::TearDown(base::OnceClosure done_closure) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(this->sequence_checker_);
  content::GetUIThreadTaskRunner({})->PostTaskAndReply(
      FROM_HERE,
      base::BindOnce(&AdAuctionServiceTestcase::TearDownOnUIThread,
                     base::Unretained(this)),
      std::move(done_closure));
}

void AdAuctionServiceTestcase::TearDownOnUIThread() {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  SetBrowserClientForTesting(old_content_browser_client_);
}

void AdAuctionServiceTestcase::AddAdAuctionService(
    uint32_t id,
    base::OnceClosure run_closure) {
  DCHECK_CALLED_ON_VALID_SEQUENCE(this->sequence_checker_);
  mojo::Remote<blink::mojom::AdAuctionService> remote;
  auto receiver = remote.BindNewPipeAndPassReceiver();
  ::mojolpm::GetContext()->AddInstance(id, std::move(remote));
  content::GetUIThreadTaskRunner({})->PostTaskAndReply(
      FROM_HERE,
      base::BindOnce(
          &AdAuctionServiceTestcase::CreateAdAuctionServiceImplOnUIThread,
          base::Unretained(this), std::move(receiver)),
      std::move(run_closure));
}

void AdAuctionServiceTestcase::CreateAdAuctionServiceImplOnUIThread(
    mojo::PendingReceiver<blink::mojom::AdAuctionService>&& receiver) {
  DCHECK_CURRENTLY_ON(content::BrowserThread::UI);
  content::AdAuctionServiceImpl::CreateMojoService(render_frame_host_,
                                                   std::move(receiver));
}

void AdAuctionServiceTestcase::RunUntilIdleOnUIThread() {
  test_adapter_.task_environment()->RunUntilIdle();
}

DEFINE_BINARY_PROTO_FUZZER(
    const content::fuzzing::ad_auction_service::proto::Testcase&
        proto_testcase) {
  if (!proto_testcase.actions_size() || !proto_testcase.sequences_size() ||
      !proto_testcase.sequence_indexes_size()) {
    return;
  }

  GetEnvironment();

  AdAuctionServiceTestcase testcase(proto_testcase);

  base::RunLoop main_run_loop;
  GetFuzzerTaskRunner()->PostTask(
      FROM_HERE,
      base::BindOnce(&::mojolpm::RunTestcase<AdAuctionServiceTestcase>,
                     base::Unretained(&testcase), GetFuzzerTaskRunner(),
                     main_run_loop.QuitClosure()));
  main_run_loop.Run();
}

}  // namespace content::ad_auction_service_mojolpm_fuzzer