0

1/N Add fuzzer for Protected Audience browser mojo API

In this CL, add the fuzzer logic and seed corpus needed to fuzz the
JoinInterestGroup() Mojo API. All code involved in joining interest
groups, including writing to the database, should be covered by fuzzer
logic and basic_join.textproto seed corpus in this CL.

While the fuzzer as-is will try to randomly invoke all methods in
AdAuctionService, the infrastructure to allow deeper coverage into other
the other AdAuctionService methods (such as, responding to auction
network requests with valid scripts, etc.) will come in follow-up CLs.

MojoLPM docs:
https://chromium.googlesource.com/chromium/src/+/main/mojo/docs/mojolpm.md

Bug: 1517411
Change-Id: I2831d7c55e250ddbaffca1f0a3935abd4fa396d0
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/5230380
Reviewed-by: Matt Menke <mmenke@chromium.org>
Reviewed-by: Chris Bookholt <bookholt@chromium.org>
Reviewed-by: Jonathan Metzman <metzman@chromium.org>
Reviewed-by: Paul Semel <paulsemel@chromium.org>
Commit-Queue: Caleb Raitto <caraitto@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1257537}
This commit is contained in:
Caleb Raitto
2024-02-07 20:25:01 +00:00
committed by Chromium LUCI CQ
parent ad0292f204
commit c729f7c7df
7 changed files with 440 additions and 0 deletions
content
browser
test
data
fuzzer_corpus
ad_auction_service_mojolpm_fuzzer
fuzzer
third_party/blink/public/common/interest_group

@@ -41,6 +41,7 @@ source_set("browser") {
"//content/app:*",
"//content/public/browser:browser_sources",
"//content/test/fuzzer:ad_auction_headers_util_fuzzer",
"//content/test/fuzzer:ad_auction_service_mojolpm_fuzzer",
"//content/test/fuzzer:browser_accessibility_fuzzer",
"//content/test/fuzzer:clipboard_host_mojolpm_fuzzer",
"//content/test/fuzzer:first_party_set_parser_fuzzer_support",

@@ -9,4 +9,7 @@ specific_include_rules = {
".*_unittest\.cc": [
"+content/services/auction_worklet",
],
".*_fuzzer.cc": [
"+third_party/libprotobuf-mutator/src/src/libfuzzer",
],
}

@@ -0,0 +1,311 @@
// 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.
#include <stdint.h>
#include <optional>
#include <utility>
#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/task/sequenced_task_runner.h"
#include "base/test/scoped_feature_list.h"
#include "components/aggregation_service/features.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/interest_group_features.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/test/test_renderer_host.h"
#include "content/public/test/url_loader_interceptor.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 "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"
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;
}
};
// For handling network requests made by the Protected Audience API -- also
// prevents those requests from being made to real servers.
class NetworkResponder {
private:
bool RequestHandler(content::URLLoaderInterceptor::RequestParams* params) {
return true;
}
// Handles network requests.
content::URLLoaderInterceptor network_interceptor_{
base::BindRepeating(&NetworkResponder::RequestHandler,
base::Unretained(this))};
};
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);
// 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_;
};
AdAuctionServiceTestcase::AdAuctionServiceTestcase(
const ProtoTestcase& testcase)
: Testcase<ProtoTestcase, ProtoAction>(testcase) {
feature_list_.InitWithFeatures(
/*enabled_features=*/
{blink::features::kInterestGroupStorage,
blink::features::kAdInterestGroupAPI, blink::features::kFledge,
blink::features::kFledgeClearOriginJoinedAdInterestGroups,
blink::features::kFledgeNegativeTargeting,
blink::features::kPrivateAggregationApiMultipleCloudProviders,
aggregation_service::kAggregationServiceMultipleCloudProviders,
features::kEnableUpdatingUserBiddingSignals,
features::kEnableUpdatingExecutionModeToFrozenContext},
/*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::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_);
}
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));
}
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();
}

@@ -0,0 +1,51 @@
// 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.
// Message format for the MojoLPM fuzzer for the AdAuctionService interface.
syntax = "proto2";
package content.fuzzing.ad_auction_service.proto;
import "third_party/blink/public/mojom/interest_group/ad_auction_service.mojom.mojolpm.proto";
// Bind a new AdAuctionService remote
message NewAdAuctionServiceAction {
required uint32 id = 1;
}
// Run the specific sequence for (an indeterminate) period. This is not
// intended to create a specific ordering, but to allow the fuzzer to delay a
// later task until previous tasks have completed.
message RunThreadAction {
enum ThreadId {
IO = 0;
UI = 1;
}
required ThreadId id = 1;
}
// Actions that can be performed by the fuzzer.
message Action {
oneof action {
NewAdAuctionServiceAction new_ad_auction_service = 1;
RunThreadAction run_thread = 2;
mojolpm.blink.mojom.AdAuctionService.RemoteAction
ad_auction_service_remote_action = 3;
}
}
// Sequence provides a level of indirection which allows Testcase to compactly
// express repeated sequences of actions.
message Sequence {
repeated uint32 action_indexes = 1 [packed = true];
}
// Testcase is the top-level message type interpreted by the fuzzer.
message Testcase {
repeated Action actions = 1;
repeated Sequence sequences = 2;
repeated uint32 sequence_indexes = 3 [packed = true];
}

@@ -0,0 +1,51 @@
actions {
new_ad_auction_service {
id: 1
}
}
actions {
ad_auction_service_remote_action {
id: 1
m_join_interest_group {
m_group {
new {
id: 1
m_name: "shoes"
m_owner: {
new {
id: 1
m_scheme: "https"
m_host: "owner.test"
m_port: 443
}
}
m_expiry {
new {
id: 1
# Set a large time that's far in the future (int64 max) -- the
# browser will clamp this to the max expiration.
m_internal_value: 0x7FFFFFFFFFFFFFFF
}
}
# The default values of required fields need to be explicitly set to
# avoid build warnings.
m_priority: 0.0
m_enable_bidding_signals_prioritization: false
m_all_sellers_capabilities {
}
m_execution_mode: InterestGroup_ExecutionMode_kCompatibilityMode
m_trusted_bidding_signals_slot_size_mode: InterestGroup_TrustedBiddingSignalsSlotSizeMode_kNone
m_auction_server_request_flags {
}
m_max_trusted_bidding_signals_url_length: 0
}
}
}
}
}
sequences {
action_indexes: 0
action_indexes: 1
}
sequence_indexes: 0

@@ -423,6 +423,26 @@ mojolpm_fuzzer_test("clipboard_host_mojolpm_fuzzer") {
proto_deps = [ "//third_party/blink/public/mojom:mojom_platform_mojolpm" ]
}
mojolpm_fuzzer_test("ad_auction_service_mojolpm_fuzzer") {
sources =
[ "../../browser/interest_group/ad_auction_service_mojolpm_fuzzer.cc" ]
proto_source =
"../../browser/interest_group/ad_auction_service_mojolpm_fuzzer.proto"
testcase_proto_kind = "content.fuzzing.ad_auction_service.proto.Testcase"
deps = [
":mojolpm_fuzzer_support",
"//content/browser:browser",
"//content/browser:for_content_tests",
"//content/public/browser:browser_sources",
]
proto_deps = [ "//third_party/blink/public/mojom:mojom_platform_mojolpm" ]
seed_corpus_sources = [ "//content/test/data/fuzzer_corpus/ad_auction_service_mojolpm_fuzzer/basic_join.textproto" ]
}
if (enable_mojom_fuzzer) {
static_library("controller_presentation_service_delegate_for_fuzzing") {
# Should only be used in the fuzzer this was made for

@@ -193,6 +193,9 @@ these:
* ParseUpdateJson in interest_group_update_manager.cc
* Update AdAuctionServiceImplTest.UpdateAllUpdatableFields
If the new field is a required Mojo field, set a value for it in all the
texprotos in the ad_auction_service_mojolpm_fuzzer/ directory.
See crrev.com/c/3517534 for an example (adding the priority field), and also
remember to update bidder_worklet.cc too.