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.