diff --git a/content/browser/BUILD.gn b/content/browser/BUILD.gn index f3989b36aeeed..eddef9afb763e 100644 --- a/content/browser/BUILD.gn +++ b/content/browser/BUILD.gn @@ -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", diff --git a/content/browser/interest_group/DEPS b/content/browser/interest_group/DEPS index db49c22b37244..0b6cd71b38693 100644 --- a/content/browser/interest_group/DEPS +++ b/content/browser/interest_group/DEPS @@ -9,4 +9,7 @@ specific_include_rules = { ".*_unittest\.cc": [ "+content/services/auction_worklet", ], + ".*_fuzzer.cc": [ + "+third_party/libprotobuf-mutator/src/src/libfuzzer", + ], } diff --git a/content/browser/interest_group/ad_auction_service_mojolpm_fuzzer.cc b/content/browser/interest_group/ad_auction_service_mojolpm_fuzzer.cc new file mode 100644 index 0000000000000..f90542b48a94d --- /dev/null +++ b/content/browser/interest_group/ad_auction_service_mojolpm_fuzzer.cc @@ -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(); +} diff --git a/content/browser/interest_group/ad_auction_service_mojolpm_fuzzer.proto b/content/browser/interest_group/ad_auction_service_mojolpm_fuzzer.proto new file mode 100644 index 0000000000000..ce4078b7bd824 --- /dev/null +++ b/content/browser/interest_group/ad_auction_service_mojolpm_fuzzer.proto @@ -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]; +} diff --git a/content/test/data/fuzzer_corpus/ad_auction_service_mojolpm_fuzzer/basic_join.textproto b/content/test/data/fuzzer_corpus/ad_auction_service_mojolpm_fuzzer/basic_join.textproto new file mode 100644 index 0000000000000..396b84bc370ec --- /dev/null +++ b/content/test/data/fuzzer_corpus/ad_auction_service_mojolpm_fuzzer/basic_join.textproto @@ -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 diff --git a/content/test/fuzzer/BUILD.gn b/content/test/fuzzer/BUILD.gn index 5fcfd380bb1f3..fa2b1e5654ecc 100644 --- a/content/test/fuzzer/BUILD.gn +++ b/content/test/fuzzer/BUILD.gn @@ -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 diff --git a/third_party/blink/public/common/interest_group/interest_group.h b/third_party/blink/public/common/interest_group/interest_group.h index f4e5ec623fec1..a82be7ad3de4c 100644 --- a/third_party/blink/public/common/interest_group/interest_group.h +++ b/third_party/blink/public/common/interest_group/interest_group.h @@ -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.