Change the runner interface to prepare the migration to the engine ABI.

On the high-level, this makes the runner interface very close to the engine interface, but without the part of input metdata and emitting coverage/findings/errors, which is handled by the runner.

Had to increase the address space limit for a bit due to the additional input object allocations.

PiperOrigin-RevId: 954788419
diff --git a/centipede/BUILD b/centipede/BUILD
index 0513725..3ad6fae 100644
--- a/centipede/BUILD
+++ b/centipede/BUILD
@@ -1162,6 +1162,7 @@
     linkstatic = True,  # Must be linked statically even when dynamic_mode=on.
     deps = [
         ":centipede_runner_no_main",
+        ":execution_metadata",
         ":mutation_data",
         "@abseil-cpp//absl/base:nullability",
         "@abseil-cpp//absl/types:span",
@@ -1980,7 +1981,7 @@
     timeout = "long",
     srcs = ["centipede_test.cc"],
     data = [
-        "@com_google_fuzztest//centipede",
+        ":centipede",
         "@com_google_fuzztest//centipede/testing:abort_fuzz_target",
         "@com_google_fuzztest//centipede/testing:async_failing_target",
         "@com_google_fuzztest//centipede/testing:expensive_startup_fuzz_target",
diff --git a/centipede/centipede_flags.inc b/centipede/centipede_flags.inc
index f6d6da2..87a8e60 100644
--- a/centipede/centipede_flags.inc
+++ b/centipede/centipede_flags.inc
@@ -122,7 +122,7 @@
     // https://bugs.chromium.org/p/chromium/issues/detail?id=853873#c2
     0
 #else
-    8192
+    10240
 #endif
     ,
     "If not zero, instructs the target to set setrlimit(RLIMIT_AS) to "
diff --git a/centipede/centipede_test.cc b/centipede/centipede_test.cc
index e242d2d..58539ff 100644
--- a/centipede/centipede_test.cc
+++ b/centipede/centipede_test.cc
@@ -963,14 +963,8 @@
   CentipedeDefaultCallbacks callbacks(env, stop_condition);
 
   std::vector<ByteArray> seeds;
-  EXPECT_EQ(callbacks.GetSeeds(10, seeds), 10);
-  EXPECT_THAT(seeds, testing::ContainerEq(std::vector<ByteArray>{
-                         {0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}}));
-  EXPECT_EQ(callbacks.GetSeeds(5, seeds), 10);
-  EXPECT_THAT(seeds, testing::ContainerEq(
-                         std::vector<ByteArray>{{0}, {1}, {2}, {3}, {4}}));
-  EXPECT_EQ(callbacks.GetSeeds(100, seeds), 10);
-  EXPECT_THAT(seeds, testing::ContainerEq(std::vector<ByteArray>{
+  callbacks.GetSeeds(10, seeds);
+  EXPECT_THAT(seeds, IsSupersetOf(std::vector<ByteArray>{
                          {0}, {1}, {2}, {3}, {4}, {5}, {6}, {7}, {8}, {9}}));
 }
 
@@ -1277,12 +1271,13 @@
       "centipede/testing/fuzz_target_with_custom_mutator");
   CentipedeDefaultCallbacks callbacks(env, stop_condition);
 
-  const std::vector<ByteArray> inputs = {{1}, {2}, {3}, {4}, {5}, {6}};
-  const std::vector<Mutant> mutants = callbacks.Mutate(
-      GetMutationInputRefsFromDataInputs(inputs), inputs.size());
+  const std::vector<ByteArray> inputs = {{99}};
+  const std::vector<Mutant> mutants =
+      callbacks.Mutate(GetMutationInputRefsFromDataInputs(inputs), 5);
 
-  // The custom mutator just returns the original inputs as mutants.
-  EXPECT_EQ(inputs, GetDataFromMutants(mutants));
+  // The custom mutator just duplicates the original inputs as mutants.
+  EXPECT_EQ(GetDataFromMutants(mutants),
+            (std::vector<ByteArray>{{99}, {99}, {99}, {99}, {99}}));
 }
 
 TEST_F(CentipedeWithTemporaryLocalDir, FailsOnMisbehavingCustomMutator) {
diff --git a/centipede/runner.cc b/centipede/runner.cc
index 82b2b49..e734e6d 100644
--- a/centipede/runner.cc
+++ b/centipede/runner.cc
@@ -124,12 +124,12 @@
 static void CheckWatchdogLimits() {
   const uint64_t curr_time = time(nullptr);
   struct Resource {
-    const char *what;
-    const char *units;
+    const char* what;
+    const char* units;
     uint64_t value;
     uint64_t limit;
     bool ignore_report;
-    const char *failure;
+    const char* failure;
   };
   const uint64_t input_start_time = state->input_start_time;
   if (input_start_time == 0) return;
@@ -152,7 +152,7 @@
           /*failure=*/kExecutionFailureRssLimitExceeded.data(),
       }},
   };
-  for (const auto &resource : resources) {
+  for (const auto& resource : resources) {
     if (resource.limit != 0 && resource.value > resource.limit) {
       if (!watchdog_failure_found.exchange(true)) {
         if (resource.ignore_report) {
@@ -188,7 +188,7 @@
 
 // Watchdog thread. Periodically checks if it's time to abort due to a
 // timeout/OOM.
-[[noreturn]] static void *WatchdogThread(void *unused) {
+[[noreturn]] static void* WatchdogThread(void* unused) {
   // Since the watchdog is internal and does not execute user code, disable
   // SanCov tracing and TLS traversal.
   tls.traced = false;
@@ -252,7 +252,7 @@
 // Byte array mutation fallback for a custom mutator, as defined here:
 // https://github.com/google/fuzzing/blob/master/docs/structure-aware-fuzzing.md
 extern "C" __attribute__((weak)) size_t
-CentipedeLLVMFuzzerMutateCallback(uint8_t *data, size_t size, size_t max_size) {
+CentipedeLLVMFuzzerMutateCallback(uint8_t* data, size_t size, size_t max_size) {
   // TODO(kcc): [as-needed] fix the interface mismatch.
   // LLVMFuzzerMutate is an array-based interface (for compatibility reasons)
   // while ByteArray has a vector-based interface.
@@ -275,7 +275,7 @@
   return array.size();
 }
 
-extern "C" size_t LLVMFuzzerMutate(uint8_t *data, size_t size,
+extern "C" size_t LLVMFuzzerMutate(uint8_t* data, size_t size,
                                    size_t max_size) {
   return CentipedeLLVMFuzzerMutateCallback(data, size, max_size);
 }
@@ -283,7 +283,7 @@
 // An arbitrary large size for input data.
 static const size_t kMaxDataSize = 1 << 20;
 
-static void WriteFeaturesToFile(FILE *file, const feature_t *features,
+static void WriteFeaturesToFile(FILE* file, const feature_t* features,
                                 size_t size) {
   if (!size) return;
   auto bytes_written = fwrite(features, 1, sizeof(features[0]) * size, file);
@@ -307,19 +307,30 @@
   PrepareSancov(full_clear);
 }
 
-void RunnerCallbacks::GetSeeds(std::function<void(ByteSpan)> seed_callback) {
-  seed_callback({0});
-}
+void RunnerCallbacks::GetPresetSeedInputs(
+    const std::function<void(void*)>& seed_callback) {}
+
+void* RunnerCallbacks::GetRandomSeedInput() { return DeserializeInput({0}); }
 
 std::string RunnerCallbacks::GetSerializedTargetConfig() { return ""; }
 
-bool RunnerCallbacks::Mutate(
-    absl::Span<const MutationInputRef> /*inputs*/, size_t /*num_mutants*/,
-    std::function<void(MutantRef)> /*new_mutant_callback*/) {
+void* RunnerCallbacks::Mutate(void* origin,
+                              const ExecutionMetadata& origin_metadata) {
+  RunnerCheck(HasCustomMutator(),
+              "RunnerCallbacks::Mutate called despite HasCustomMutator() "
+              "returning false. This is a runner bug!");
   RunnerCheck(!HasCustomMutator(),
               "Class deriving from RunnerCallbacks must implement Mutate() if "
               "HasCustomMutator() returns true.");
-  return true;
+  // Should be unreachable here, but still return something safe.
+  return DeserializeInput({0});
+}
+
+void* RunnerCallbacks::CrossOver(void* origin,
+                                 const ExecutionMetadata& origin_metadata,
+                                 void* other,
+                                 const ExecutionMetadata& other_metadata) {
+  return Mutate(origin, origin_metadata);
 }
 
 class LegacyRunnerCallbacks : public RunnerCallbacks {
@@ -331,10 +342,11 @@
         custom_mutator_cb_(custom_mutator_cb),
         custom_crossover_cb_(custom_crossover_cb) {}
 
-  bool Execute(ByteSpan input) override {
+  bool Execute(void* input) override {
     PrintErrorAndExitIf(test_one_input_cb_ == nullptr,
                         "missing test_on_input_cb");
-    const int retval = test_one_input_cb_(input.data(), input.size());
+    const auto* input_ba = reinterpret_cast<const ByteArray*>(input);
+    const int retval = test_one_input_cb_(input_ba->data(), input_ba->size());
     PrintErrorAndExitIf(
         retval != -1 && retval != 0,
         "test_on_input_cb returns invalid value other than -1 and 0");
@@ -345,8 +357,15 @@
     return custom_mutator_cb_ != nullptr;
   }
 
-  bool Mutate(absl::Span<const MutationInputRef> inputs, size_t num_mutants,
-              std::function<void(MutantRef)> new_mutant_callback) override;
+  void SerializeInput(void* input,
+                      const std::function<void(ByteSpan)>& bytes_sink) override;
+  void* DeserializeInput(ByteSpan input_bytes) override;
+  void FreeInput(void* input) override;
+
+  void* Mutate(void* origin, const ExecutionMetadata& origin_metadata) override;
+  void* CrossOver(void* origin, const ExecutionMetadata& origin_metadata,
+                  void* other,
+                  const ExecutionMetadata& other_metadata) override;
 
  private:
   FuzzerTestOneInputCallback test_one_input_cb_;
@@ -362,8 +381,7 @@
       test_one_input_cb, custom_mutator_cb, custom_crossover_cb);
 }
 
-static void RunOneInput(const uint8_t *data, size_t size,
-                        RunnerCallbacks &callbacks) {
+static void RunOneInput(void* input, RunnerCallbacks& callbacks) {
   state->stats = {};
   size_t last_time_usec = 0;
   auto UsecSinceLast = [&last_time_usec]() {
@@ -376,7 +394,7 @@
   PrepareCoverage(/*full_clear=*/false);
   state->stats.prep_time_usec = UsecSinceLast();
   state->ResetTimers();
-  int target_return_value = callbacks.Execute({data, size}) ? 0 : -1;
+  int target_return_value = callbacks.Execute(input) ? 0 : -1;
   state->stats.exec_time_usec = UsecSinceLast();
   CheckWatchdogLimits();
   if (fuzztest::internal::state->input_start_time.exchange(0) != 0) {
@@ -390,18 +408,20 @@
 // Runs one input provided in file `input_path`.
 // Produces coverage data in file `input_path`-features.
 __attribute__((noinline))  // so that we see it in profile.
-static void ReadOneInputExecuteItAndDumpCoverage(const char *input_path,
-                                                 RunnerCallbacks &callbacks) {
+static void ReadOneInputExecuteItAndDumpCoverage(const char* input_path,
+                                                 RunnerCallbacks& callbacks) {
   // Read the input.
   auto data = ReadBytesFromFilePath<uint8_t>(input_path);
 
-  RunOneInput(data.data(), data.size(), callbacks);
+  void* input = callbacks.DeserializeInput(data);
+  RunOneInput(input, callbacks);
+  callbacks.FreeInput(input);
 
   // Dump features to a file.
   char features_file_path[PATH_MAX];
   snprintf(features_file_path, sizeof(features_file_path), "%s-features",
            input_path);
-  FILE *features_file = fopen(features_file_path, "w");
+  FILE* features_file = fopen(features_file_path, "w");
   PrintErrorAndExitIf(features_file == nullptr, "can't open coverage file");
 
   const SanCovRuntimeRawFeatureParts sancov_features =
@@ -413,13 +433,13 @@
 
 // Starts sending the outputs (coverage, etc.) to `outputs_blobseq`.
 // Returns true on success.
-static bool StartSendingOutputsToEngine(BlobSequence &outputs_blobseq) {
+static bool StartSendingOutputsToEngine(BlobSequence& outputs_blobseq) {
   return BatchResult::WriteInputBegin(outputs_blobseq);
 }
 
 // Copy all the sancov features to `data` with given `capacity` in bytes.
 // Returns the byte size of sancov features.
-static size_t CopyFeatures(uint8_t *data, size_t capacity) {
+static size_t CopyFeatures(uint8_t* data, size_t capacity) {
   const SanCovRuntimeRawFeatureParts sancov_features =
       SanCovRuntimeGetFeatures();
   const size_t features_len_in_bytes =
@@ -431,7 +451,7 @@
 
 // Finishes sending the outputs (coverage, etc.) to `outputs_blobseq`.
 // Returns true on success.
-static bool FinishSendingOutputsToEngine(BlobSequence &outputs_blobseq) {
+static bool FinishSendingOutputsToEngine(BlobSequence& outputs_blobseq) {
   {
     LockGuard lock(state->execution_result_override_mu);
     bool has_overridden_execution_result = false;
@@ -476,30 +496,32 @@
 // Handles an ExecutionRequest, see RequestExecution(). Reads inputs from
 // `inputs_blobseq`, runs them, saves coverage features to `outputs_blobseq`.
 // Returns EXIT_SUCCESS on success and EXIT_FAILURE otherwise.
-static int ExecuteInputsFromShmem(BlobSequence &inputs_blobseq,
-                                  BlobSequence &outputs_blobseq,
-                                  RunnerCallbacks &callbacks) {
+static int ExecuteInputsFromShmem(BlobSequence& inputs_blobseq,
+                                  BlobSequence& outputs_blobseq,
+                                  RunnerCallbacks& callbacks) {
   size_t num_inputs = 0;
   if (!IsExecutionRequest(inputs_blobseq.Read())) return EXIT_FAILURE;
   if (!IsNumInputs(inputs_blobseq.Read(), num_inputs)) return EXIT_FAILURE;
 
-  CentipedeBeginExecutionBatch();
-
+  std::vector<void*> inputs;
+  inputs.reserve(num_inputs);
   for (size_t i = 0; i < num_inputs; i++) {
     auto blob = inputs_blobseq.Read();
     // TODO(kcc): distinguish bad input from end of stream.
-    if (!blob.IsValid()) return EXIT_SUCCESS;  // no more blobs to read.
+    if (!blob.IsValid()) break;  // no more blobs to read.
     if (!IsDataInput(blob)) return EXIT_FAILURE;
 
     // TODO(kcc): [impl] handle sizes larger than kMaxDataSize.
     size_t size = std::min(kMaxDataSize, blob.size);
-    // Copy from blob to data so that to not pass the shared memory further.
-    std::vector<uint8_t> data(blob.data, blob.data + size);
+    inputs.push_back(callbacks.DeserializeInput({blob.data, size}));
+  }
 
-    // Starting execution of one more input.
+  CentipedeBeginExecutionBatch();
+
+  for (void* input : inputs) {
     if (!StartSendingOutputsToEngine(outputs_blobseq)) break;
 
-    RunOneInput(data.data(), data.size(), callbacks);
+    RunOneInput(input, callbacks);
 
     if (state->has_failure_description.load()) break;
 
@@ -508,13 +530,30 @@
 
   CentipedeEndExecutionBatch();
 
+  for (void* input : inputs) {
+    callbacks.FreeInput(input);
+  }
+
   return state->has_failure_description.load() ? EXIT_FAILURE : EXIT_SUCCESS;
 }
 
 // Dumps seed inputs to `output_dir`. Also see `GetSeedsViaExternalBinary()`.
-static void DumpSeedsToDir(RunnerCallbacks &callbacks, const char *output_dir) {
+static void DumpSeedsToDir(RunnerCallbacks& callbacks, const char* output_dir) {
   size_t seed_index = 0;
-  callbacks.GetSeeds([&](ByteSpan seed) {
+  // Declare it on the outer scope to save allocations.
+  ByteArray serialized;
+  auto dump_seed_callback = [&](void* seed) {
+    serialized.clear();
+    callbacks.SerializeInput(seed, [&](ByteSpan bytes) {
+      // Cannot use `vector::insert` due to potential conflict of this file
+      // without sanitizers when linking other files with sanitizers that uses
+      // the same symbol. Other symbols used here seem safe. This a dirty hack
+      // that is expected to go away soon.
+      const size_t cur = serialized.size();
+      serialized.resize(cur + bytes.size());
+      std::memcpy(serialized.data() + cur, bytes.data(), bytes.size());
+    });
+    callbacks.FreeInput(seed);
     // Cap seed index within 9 digits. If this was triggered, the dumping would
     // take forever..
     if (seed_index >= 1000000000) return;
@@ -523,22 +562,26 @@
         snprintf(seed_path_buf, PATH_MAX, "%s/%09lu", output_dir, seed_index);
     PrintErrorAndExitIf(num_path_chars >= PATH_MAX,
                         "seed path reaches PATH_MAX");
-    FILE *output_file = fopen(seed_path_buf, "w");
+    FILE* output_file = fopen(seed_path_buf, "w");
     const size_t num_bytes_written =
-        fwrite(seed.data(), 1, seed.size(), output_file);
-    PrintErrorAndExitIf(num_bytes_written != seed.size(),
+        fwrite(serialized.data(), 1, serialized.size(), output_file);
+    PrintErrorAndExitIf(num_bytes_written != serialized.size(),
                         "wrong number of bytes written for cf table");
     fclose(output_file);
     ++seed_index;
-  });
+  };
+  callbacks.GetPresetSeedInputs(dump_seed_callback);
+  while (seed_index < 32) {
+    dump_seed_callback(callbacks.GetRandomSeedInput());
+  }
 }
 
 // Dumps serialized target config to `output_file_path`. Also see
 // `GetSerializedTargetConfigViaExternalBinary()`.
-static void DumpSerializedTargetConfigToFile(RunnerCallbacks &callbacks,
-                                             const char *output_file_path) {
+static void DumpSerializedTargetConfigToFile(RunnerCallbacks& callbacks,
+                                             const char* output_file_path) {
   const std::string config = callbacks.GetSerializedTargetConfig();
-  FILE *output_file = fopen(output_file_path, "w");
+  FILE* output_file = fopen(output_file_path, "w");
   const size_t num_bytes_written =
       fwrite(config.data(), 1, config.size(), output_file);
   PrintErrorAndExitIf(
@@ -551,6 +594,57 @@
 // TODO(kcc): [as-needed] optionally pass an external seed.
 static unsigned GetRandomSeed() { return time(nullptr); }
 
+void MutateInputs(RunnerCallbacks& callbacks,
+                  absl::Span<const MutationInputRef> inputs, size_t num_mutants,
+                  std::function<void(MutantRef)> new_mutant_callback) {
+  static unsigned int seed = GetRandomSeed();
+  std::vector<void*> input_objects;
+  input_objects.resize(inputs.size(), nullptr);
+  for (size_t i = 0; i < inputs.size(); ++i) {
+    input_objects[i] = callbacks.DeserializeInput(inputs[i].data);
+  }
+  ByteArray mutant_data;
+  ExecutionMetadata empty_metadata;
+  for (size_t i = 0; i < num_mutants; ++i) {
+    const size_t origin_index = rand_r(&seed) % inputs.size();
+    void* mutant = nullptr;
+    if (rand_r(&seed) % 100 < state->run_time_flags.crossover_level) {
+      // Perform crossover `crossover_level`% of the time.
+      const size_t other_index = rand_r(&seed) % inputs.size();
+      mutant = callbacks.CrossOver(input_objects[origin_index],
+                                   inputs[origin_index].metadata != nullptr
+                                       ? *inputs[origin_index].metadata
+                                       : empty_metadata,
+                                   input_objects[other_index],
+                                   inputs[other_index].metadata != nullptr
+                                       ? *inputs[other_index].metadata
+                                       : empty_metadata);
+    } else {
+      mutant = callbacks.Mutate(input_objects[origin_index],
+                                inputs[origin_index].metadata != nullptr
+                                    ? *inputs[origin_index].metadata
+                                    : empty_metadata);
+    }
+    mutant_data.clear();
+    callbacks.SerializeInput(mutant, [&mutant_data](ByteSpan bytes) {
+      // Cannot use `vector::insert` due to potential conflict of this file
+      // without sanitizers when linking other files with sanitizers that uses
+      // the same symbol. Other symbols used here seem safe.
+      // This a dirty hack that is expected to go away soon.
+      const size_t cur = mutant_data.size();
+      mutant_data.resize(cur + bytes.size());
+      std::memcpy(mutant_data.data() + cur, bytes.data(), bytes.size());
+    });
+    new_mutant_callback(
+        MutantRef{{(unsigned char*)mutant_data.data(), mutant_data.size()},
+                  origin_index});
+    callbacks.FreeInput(mutant);
+  }
+  for (void* input_object : input_objects) {
+    callbacks.FreeInput(input_object);
+  }
+}
+
 // Handles a Mutation Request, see RequestMutation().
 // Mutates inputs read from `inputs_blobseq`,
 // writes the mutants to `outputs_blobseq`
@@ -560,9 +654,9 @@
 // returns EXIT_FAILURE.
 //
 // TODO(kcc): [impl] make use of custom_crossover_cb, if available.
-static int MutateInputsFromShmem(BlobSequence &inputs_blobseq,
-                                 BlobSequence &outputs_blobseq,
-                                 RunnerCallbacks &callbacks) {
+static int MutateInputsFromShmem(BlobSequence& inputs_blobseq,
+                                 BlobSequence& outputs_blobseq,
+                                 RunnerCallbacks& callbacks) {
   // Read max_num_mutants.
   size_t num_mutants = 0;
   size_t num_inputs = 0;
@@ -610,60 +704,90 @@
   }
   if (!callbacks.HasCustomMutator()) return EXIT_SUCCESS;
 
-  if (!callbacks.Mutate(input_refs, num_mutants, [&](MutantRef mutant) {
-        MutationResult::WriteMutant(mutant, outputs_blobseq);
-      })) {
-    return EXIT_FAILURE;
+  {
+    bool succ = true;
+    MutateInputs(callbacks, input_refs, num_mutants, [&](MutantRef mutant) {
+      succ = succ && MutationResult::WriteMutant(mutant, outputs_blobseq);
+    });
+    if (!succ) return EXIT_FAILURE;
   }
+
   return EXIT_SUCCESS;
 }
 
-bool LegacyRunnerCallbacks::Mutate(
-    absl::Span<const MutationInputRef> inputs, size_t num_mutants,
-    std::function<void(MutantRef)> new_mutant_callback) {
-  if (custom_mutator_cb_ == nullptr) return false;
-  unsigned int seed = GetRandomSeed();
-  const size_t num_inputs = inputs.size();
-  const size_t max_mutant_size = state->run_time_flags.max_len;
-  constexpr size_t kAverageMutationAttempts = 2;
-  // Reused across iterations to save memory allocations.
-  Mutant mutant;
-  for (size_t attempt = 0, num_outputs = 0;
-       attempt < num_mutants * kAverageMutationAttempts &&
-       num_outputs < num_mutants;
-       ++attempt) {
-    mutant.origin = rand_r(&seed) % num_inputs;
-    const auto& input_data = inputs[mutant.origin].data;
+void LegacyRunnerCallbacks::SerializeInput(
+    void* input, const std::function<void(ByteSpan)>& bytes_sink) {
+  const auto* input_object = reinterpret_cast<const ByteArray*>(input);
+  bytes_sink(*input_object);
+}
 
-    size_t size = std::min(input_data.size(), max_mutant_size);
-    mutant.data.resize(max_mutant_size);
-    std::copy(input_data.cbegin(), input_data.cbegin() + size,
-              mutant.data.begin());
-    size_t new_size = 0;
-    if ((custom_crossover_cb_ != nullptr) &&
-        rand_r(&seed) % 100 < state->run_time_flags.crossover_level) {
-      // Perform crossover `crossover_level`% of the time.
-      const auto &other_data = inputs[rand_r(&seed) % num_inputs].data;
-      new_size = custom_crossover_cb_(input_data.data(), input_data.size(),
-                                      other_data.data(), other_data.size(),
-                                      mutant.data.data(), max_mutant_size,
-                                      rand_r(&seed));
-    } else {
-      new_size = custom_mutator_cb_(mutant.data.data(), size, max_mutant_size,
-                                    rand_r(&seed));
+void* LegacyRunnerCallbacks::DeserializeInput(ByteSpan input_bytes) {
+  return reinterpret_cast<void*>(
+      new ByteArray{input_bytes.begin(), input_bytes.end()});
+}
+
+void LegacyRunnerCallbacks::FreeInput(void* input) {
+  delete reinterpret_cast<const ByteArray*>(input);
+}
+
+void* LegacyRunnerCallbacks::Mutate(void* origin,
+                                    const ExecutionMetadata& origin_metadata) {
+  const auto* origin_ba = reinterpret_cast<const ByteArray*>(origin);
+  state->byte_array_mutator->SetMetadata(origin_metadata);
+  const size_t max_mutant_size = state->run_time_flags.max_len;
+  const size_t size = std::min(max_mutant_size, origin_ba->size());
+  auto* mutant = new ByteArray();
+  mutant->resize(max_mutant_size);
+  std::copy(origin_ba->begin(), origin_ba->begin() + size, mutant->begin());
+  static unsigned int seed = GetRandomSeed();
+  size_t new_size =
+      custom_mutator_cb_(mutant->data(), size, max_mutant_size, rand_r(&seed));
+  if (new_size == 0) {
+    new_size = 1;
+    mutant->assign({0});
+  } else {
+    if (new_size >= max_mutant_size) {
+      new_size = max_mutant_size;
     }
-    if (new_size == 0) continue;
-    if (new_size > max_mutant_size) new_size = max_mutant_size;
-    mutant.data.resize(new_size);
-    new_mutant_callback(MutantRef{mutant});
-    ++num_outputs;
+    mutant->resize(new_size);
   }
-  return true;
+  return reinterpret_cast<void*>(mutant);
+}
+
+void* LegacyRunnerCallbacks::CrossOver(
+    void* origin, const ExecutionMetadata& origin_metadata, void* other,
+    const ExecutionMetadata& other_metadata) {
+  if (custom_crossover_cb_ == nullptr) {
+    return Mutate(origin, origin_metadata);
+  }
+  const auto* origin_ba = reinterpret_cast<const ByteArray*>(origin);
+  const auto* other_ba = reinterpret_cast<const ByteArray*>(other);
+  static unsigned int seed = GetRandomSeed();
+  state->byte_array_mutator->SetMetadata((rand_r(&seed) & 1) ? origin_metadata
+                                                             : other_metadata);
+  const size_t max_mutant_size = state->run_time_flags.max_len;
+  const size_t size = std::min(max_mutant_size, origin_ba->size());
+  auto* mutant = new ByteArray();
+  mutant->resize(max_mutant_size);
+  std::copy(origin_ba->begin(), origin_ba->begin() + size, mutant->begin());
+  size_t new_size = custom_crossover_cb_(
+      origin_ba->data(), origin_ba->size(), other_ba->data(), other_ba->size(),
+      mutant->data(), max_mutant_size, rand_r(&seed));
+  if (new_size == 0) {
+    new_size = 1;
+    mutant->assign({0});
+  } else {
+    if (new_size >= max_mutant_size) {
+      new_size = max_mutant_size;
+    }
+    mutant->resize(new_size);
+  }
+  return reinterpret_cast<void*>(mutant);
 }
 
 // Returns the current process VmSize, in bytes.
 static size_t GetVmSizeInBytes() {
-  FILE *f = fopen("/proc/self/statm", "r");  // man proc
+  FILE* f = fopen("/proc/self/statm", "r");  // man proc
   if (!f) return 0;
   size_t vm_size = 0;
   // NOTE: Ignore any (unlikely) failures to suppress a compiler warning.
@@ -874,7 +998,7 @@
 //  Default: Execute ReadOneInputExecuteItAndDumpCoverage() for all inputs.//
 //
 //  Note: argc/argv are used for only ReadOneInputExecuteItAndDumpCoverage().
-int RunnerMain(int argc, char **argv, RunnerCallbacks &callbacks) {
+int RunnerMain(int argc, char** argv, RunnerCallbacks& callbacks) {
   state->centipede_runner_main_executed = true;
 
   fprintf(stderr, "Centipede fuzz target runner; argv[0]: %s flags: %s\n",
@@ -915,7 +1039,7 @@
 }  // namespace fuzztest::internal
 
 extern "C" int LLVMFuzzerRunDriver(
-    int *absl_nonnull argc, char ***absl_nonnull argv,
+    int* absl_nonnull argc, char*** absl_nonnull argv,
     FuzzerTestOneInputCallback test_one_input_cb) {
   if (LLVMFuzzerInitialize) LLVMFuzzerInitialize(argc, argv);
   return RunnerMain(*argc, *argv,
@@ -945,9 +1069,9 @@
       timeout_per_input;
 }
 
-extern "C" __attribute__((weak)) const char *absl_nullable
+extern "C" __attribute__((weak)) const char* absl_nullable
 CentipedeGetRunnerFlags() {
-  if (const char *runner_flags_env = getenv("CENTIPEDE_RUNNER_FLAGS"))
+  if (const char* runner_flags_env = getenv("CENTIPEDE_RUNNER_FLAGS"))
     return strdup(runner_flags_env);
   return nullptr;
 }
@@ -999,7 +1123,7 @@
   return old_traced;
 }
 
-extern "C" size_t CentipedeGetExecutionResult(uint8_t *data, size_t capacity) {
+extern "C" size_t CentipedeGetExecutionResult(uint8_t* data, size_t capacity) {
   fuzztest::internal::BlobSequence outputs_blobseq(data, capacity);
   if (!fuzztest::internal::StartSendingOutputsToEngine(outputs_blobseq))
     return 0;
@@ -1008,11 +1132,11 @@
   return outputs_blobseq.offset();
 }
 
-extern "C" size_t CentipedeGetCoverageData(uint8_t *data, size_t capacity) {
+extern "C" size_t CentipedeGetCoverageData(uint8_t* data, size_t capacity) {
   return fuzztest::internal::CopyFeatures(data, capacity);
 }
 
-extern "C" void CentipedeSetExecutionResult(const uint8_t *data, size_t size) {
+extern "C" void CentipedeSetExecutionResult(const uint8_t* data, size_t size) {
   using fuzztest::internal::state;
   fuzztest::internal::LockGuard lock(state->execution_result_override_mu);
   if (!state->execution_result_override)
@@ -1020,14 +1144,14 @@
   state->execution_result_override->ClearAndResize(1);
   if (data == nullptr) return;
   // Removing const here should be fine as we don't write to `blobseq`.
-  fuzztest::internal::BlobSequence blobseq(const_cast<uint8_t *>(data), size);
+  fuzztest::internal::BlobSequence blobseq(const_cast<uint8_t*>(data), size);
   state->execution_result_override->Read(blobseq);
   fuzztest::internal::RunnerCheck(
       state->execution_result_override->num_outputs_read() == 1,
       "Failed to set execution result from CentipedeSetExecutionResult");
 }
 
-extern "C" void CentipedeSetFailureDescription(const char *description) {
+extern "C" void CentipedeSetFailureDescription(const char* description) {
   using fuzztest::internal::state;
   if (state->failure_description_path == nullptr) return;
   if (state->has_failure_description.exchange(true)) return;
diff --git a/centipede/runner_interface.h b/centipede/runner_interface.h
index c622daf..9b560bd 100644
--- a/centipede/runner_interface.h
+++ b/centipede/runner_interface.h
@@ -12,7 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 //
-// WARNING: this interface is not yet stable and may change at any point.
+// WARNING: This interface is not part of the engine; it's not yet stable and
+// may change at any point.
 
 #ifndef THIRD_PARTY_CENTIPEDE_RUNNER_INTERFACE_H_
 #define THIRD_PARTY_CENTIPEDE_RUNNER_INTERFACE_H_
@@ -26,38 +27,45 @@
 
 #include "absl/base/nullability.h"
 #include "absl/types/span.h"
+#include "./centipede/execution_metadata.h"
 #include "./centipede/mutation_data.h"
 #include "./common/defs.h"
 
-// Typedefs for the libFuzzer API, https://llvm.org/docs/LibFuzzer.html
-using FuzzerTestOneInputCallback = int (*)(const uint8_t *data, size_t size);
-using FuzzerInitializeCallback = int (*)(int *argc, char ***argv);
-using FuzzerCustomMutatorCallback = size_t (*)(uint8_t *data, size_t size,
+// Legacy support for the libFuzzer API, https://llvm.org/docs/LibFuzzer.html
+//
+// WARNING: The legacy support will soon be deprecated.
+// Consider using the FuzzTest proper or fuzztest/llvm_fuzzer_{wrapper,main}.cc.
+
+using FuzzerTestOneInputCallback = int (*)(const uint8_t* data, size_t size);
+using FuzzerInitializeCallback = int (*)(int* argc, char*** argv);
+using FuzzerCustomMutatorCallback = size_t (*)(uint8_t* data, size_t size,
                                                size_t max_size,
                                                unsigned int seed);
 using FuzzerCustomCrossOverCallback = size_t (*)(
-    const uint8_t *data1, size_t size1, const uint8_t *data2, size_t size2,
-    uint8_t *out, size_t max_out_size, unsigned int seed);
+    const uint8_t* data1, size_t size1, const uint8_t* data2, size_t size2,
+    uint8_t* out, size_t max_out_size, unsigned int seed);
 
 // This is the header-less interface of libFuzzer, see
 // https://llvm.org/docs/LibFuzzer.html.
 extern "C" {
-int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size);
-__attribute__((weak)) int LLVMFuzzerInitialize(int *absl_nonnull argc,
-                                               char ***absl_nonnull argv);
-__attribute__((weak)) size_t LLVMFuzzerCustomMutator(uint8_t *data, size_t size,
+int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size);
+__attribute__((weak)) int LLVMFuzzerInitialize(int* absl_nonnull argc,
+                                               char*** absl_nonnull argv);
+__attribute__((weak)) size_t LLVMFuzzerCustomMutator(uint8_t* data, size_t size,
                                                      size_t max_size,
                                                      unsigned int seed);
 __attribute__((weak)) size_t LLVMFuzzerCustomCrossOver(
-    const uint8_t *data1, size_t size1, const uint8_t *data2, size_t size2,
-    uint8_t *out, size_t max_out_size, unsigned int seed);
+    const uint8_t* data1, size_t size1, const uint8_t* data2, size_t size2,
+    uint8_t* out, size_t max_out_size, unsigned int seed);
 }  // extern "C"
 
 // https://llvm.org/docs/LibFuzzer.html#using-libfuzzer-as-a-library
 extern "C" int LLVMFuzzerRunDriver(
-    int *absl_nonnull argc, char ***absl_nonnull argv,
+    int* absl_nonnull argc, char*** absl_nonnull argv,
     FuzzerTestOneInputCallback test_one_input_cb);
 
+// LibFuzzer API declarations end here.
+
 // Reconfigures the RSS limit to `rss_limit_mb` - 0 indicates no limit.
 extern "C" void CentipedeSetRssLimit(size_t rss_limit_mb);
 
@@ -73,10 +81,10 @@
 //
 // It should return either a nullptr or a constant string that is valid
 // throughout the entire process life-time.
-extern "C" const char *absl_nullable CentipedeGetRunnerFlags();
+extern "C" const char* absl_nullable CentipedeGetRunnerFlags();
 
 // An overridable function to override `LLVMFuzzerMutate` behavior.
-extern "C" size_t CentipedeLLVMFuzzerMutateCallback(uint8_t *data, size_t size,
+extern "C" size_t CentipedeLLVMFuzzerMutateCallback(uint8_t* data, size_t size,
                                                     size_t max_size);
 
 // Prepares to run a batch of test executions that ends with calling
@@ -109,26 +117,26 @@
 // processing an input. This function saves the data to the provided buffer and
 // returns the size of the saved data. It may be called after
 // CentipedeFinalizeProcessing().
-extern "C" size_t CentipedeGetExecutionResult(uint8_t *data, size_t capacity);
+extern "C" size_t CentipedeGetExecutionResult(uint8_t* data, size_t capacity);
 
 // Retrieves the coverage data collected during the processing of an input.
 // This function saves the raw coverage data to the provided buffer and returns
 // the size of the saved data. It may be called after
 // CentipedeFinalizeProcessing().
-extern "C" size_t CentipedeGetCoverageData(uint8_t *data, size_t capacity);
+extern "C" size_t CentipedeGetCoverageData(uint8_t* data, size_t capacity);
 
 // Set the current execution result to the opaque memory `data` with `size`.
 // Such data is retrieved using `CentipedeGetExecutionResult`, possibly from
 // another process. When `data` is `nullptr`, will set the execution result to
 // "empty" with no features or metadata.
-extern "C" void CentipedeSetExecutionResult(const uint8_t *data, size_t size);
+extern "C" void CentipedeSetExecutionResult(const uint8_t* data, size_t size);
 
 // Set the failure description for the runner to propagate further. Only the
 // description from the first call will be used.
 //
 // If used during executing batch inputs, the rest of the inputs would be
 // skipped and the batch would be considered as failed.
-extern "C" void CentipedeSetFailureDescription(const char *description);
+extern "C" void CentipedeSetFailureDescription(const char* description);
 
 namespace fuzztest::internal {
 
@@ -140,23 +148,37 @@
  public:
   // Attempts to execute the test logic using `input`, and returns false if the
   // input should be ignored from the corpus, true otherwise.
-  virtual bool Execute(ByteSpan input) = 0;
-  // Generates seed inputs by calling `seed_callback` for each input.
-  // The default implementation generates a single-byte input {0}.
-  virtual void GetSeeds(std::function<void(ByteSpan)> seed_callback);
+  virtual bool Execute(void* input) = 0;
+  // Provides the (deterministically) preset seed inputs by calling
+  // `seed_callback` on each of them. The default implementation provides no
+  // inputs.
+  virtual void GetPresetSeedInputs(
+      const std::function<void(void*)>& seed_callback);
+  // Returns a random seed input.
+  // The default implementation returns the input deserialized from the
+  // single-byte input {0}.
+  virtual void* GetRandomSeedInput();
   // Returns the serialized configuration from the test target. The default
   // implementation returns the empty string.
   virtual std::string GetSerializedTargetConfig();
   // Returns true if and only if the test target has a custom mutator.
   virtual bool HasCustomMutator() const = 0;
-  // Generates at most `num_mutants` mutants by calling `new_mutant_callback`
-  // for each mutant. Returns true on success, false otherwise.
-  //
-  // TODO(xinhaoyuan): Consider supporting only_shrink to speed up
-  // input shrinking.
-  virtual bool Mutate(absl::Span<const MutationInputRef> inputs,
-                      size_t num_mutants,
-                      std::function<void(MutantRef)> new_mutant_callback);
+  // Mutates `origin` with `origin_metadata`. Must be overridden if
+  // `HasCustomMutator()` returns true.
+  virtual void* Mutate(void* origin, const ExecutionMetadata& origin_metadata);
+  // Crosses over `origin` (with `origin_metadata`) with `other` (with
+  // `other_metadata`). The default implementation returns `Mutate(origin,
+  // origin_metadata)`
+  virtual void* CrossOver(void* origin,
+                          const ExecutionMetadata& origin_metadata, void* other,
+                          const ExecutionMetadata& other_metadata);
+  // Serializes `input` as bytes into `bytes_sink`. Must emit non-empty bytes.
+  virtual void SerializeInput(
+      void* input, const std::function<void(ByteSpan)>& bytes_sink) = 0;
+  // Creates and returns the input deserialized from `bytes`.
+  virtual void* DeserializeInput(ByteSpan bytes) = 0;
+  // Called when the runner is done with `input`.
+  virtual void FreeInput(void* input) = 0;
   virtual ~RunnerCallbacks() = default;
 };
 
@@ -173,7 +195,7 @@
 //
 // As an *experiment* we want to allow user code to call RunnerMain().
 // This is not a guaranteed public interface (yet) and may disappear w/o notice.
-int RunnerMain(int argc, char **argv, RunnerCallbacks &callbacks);
+int RunnerMain(int argc, char** argv, RunnerCallbacks& callbacks);
 
 }  // namespace fuzztest::internal
 
diff --git a/centipede/testing/async_failing_target.cc b/centipede/testing/async_failing_target.cc
index a3fc7dd..a2a7764 100644
--- a/centipede/testing/async_failing_target.cc
+++ b/centipede/testing/async_failing_target.cc
@@ -26,24 +26,34 @@
 class AsyncFailingTargetRunnerCallbacks
     : public fuzztest::internal::RunnerCallbacks {
  public:
-  bool Execute(fuzztest::internal::ByteSpan input) override {
+  bool Execute(void* input) override {
     to_fail_in_mutation = true;
     return true;
   }
 
-  bool Mutate(absl::Span<const fuzztest::internal::MutationInputRef> inputs,
-              size_t num_mutants,
-              std::function<void(fuzztest::internal::MutantRef)>
-                  new_mutant_callback) override {
+  void* Mutate(void* inputs,
+               const fuzztest::internal::ExecutionMetadata& metadata) override {
     if (to_fail_in_mutation) {
       fprintf(stderr, "Fail in mutation\n");
       std::abort();
     }
-    return true;
+    return nullptr;
   }
 
   bool HasCustomMutator() const override { return true; }
 
+  void* DeserializeInput(fuzztest::internal::ByteSpan input) override {
+    return nullptr;
+  }
+
+  void SerializeInput(void* input,
+                      const std::function<void(fuzztest::internal::ByteSpan)>&
+                          bytes_sink) override {
+    bytes_sink({0});
+  }
+
+  void FreeInput(void* input) override {}
+
  private:
   bool to_fail_in_mutation = false;
 };
diff --git a/centipede/testing/external_target.cc b/centipede/testing/external_target.cc
index b77ea64..3e1fbdf 100644
--- a/centipede/testing/external_target.cc
+++ b/centipede/testing/external_target.cc
@@ -52,7 +52,9 @@
 class ExternalTargetRunnerCallbacks
     : public fuzztest::internal::RunnerCallbacks {
  public:
-  bool Execute(fuzztest::internal::ByteSpan input) override {
+  bool Execute(void* input_raw) override {
+    const auto* input =
+        reinterpret_cast<const fuzztest::internal::ByteArray*>(input_raw);
     const char* port_env = getenv("TARGET_PORT");
     int port = 0;
     CHECK(port_env && absl::SimpleAtoi(port_env, &port))
@@ -72,10 +74,10 @@
     const int enable_nodelay = 1;
     setsockopt(conn_sock, SOL_TCP, TCP_NODELAY, &enable_nodelay,
                sizeof(enable_nodelay));
-    const uint64_t input_size = input.size();
+    const uint64_t input_size = input->size();
     sendall(conn_sock, reinterpret_cast<const uint8_t*>(&input_size),
             sizeof(input_size));
-    sendall(conn_sock, input.data(), input_size);
+    sendall(conn_sock, input->data(), input_size);
     int match_result = 0;
     recvall(conn_sock, reinterpret_cast<uint8_t*>(&match_result),
             sizeof(match_result));
@@ -96,6 +98,23 @@
   }
 
   bool HasCustomMutator() const override { return false; }
+
+  void* DeserializeInput(fuzztest::internal::ByteSpan input) override {
+    return reinterpret_cast<void*>(
+        new fuzztest::internal::ByteArray{input.begin(), input.end()});
+  }
+
+  void SerializeInput(void* input,
+                      const std::function<void(fuzztest::internal::ByteSpan)>&
+                          bytes_sink) override {
+    const auto* ba =
+        reinterpret_cast<const fuzztest::internal::ByteArray*>(input);
+    bytes_sink(*ba);
+  }
+
+  void FreeInput(void* input) override {
+    delete reinterpret_cast<const fuzztest::internal::ByteArray*>(input);
+  }
 };
 
 }  // namespace
diff --git a/centipede/testing/fuzz_target_with_config.cc b/centipede/testing/fuzz_target_with_config.cc
index 77788c0..93a8379 100644
--- a/centipede/testing/fuzz_target_with_config.cc
+++ b/centipede/testing/fuzz_target_with_config.cc
@@ -32,12 +32,24 @@
  public:
   // Trivial implementations for the execution and mutation logic, even though
   // they should not be used in the tests that use this test binary.
-  bool Execute(ByteSpan input) override { return true; }
+  bool Execute(void* input) override { return true; }
   bool HasCustomMutator() const override { return false; }
 
   std::string GetSerializedTargetConfig() override {
     return "fake serialized config";
   }
+
+  void* DeserializeInput(fuzztest::internal::ByteSpan input) override {
+    return nullptr;
+  }
+
+  void SerializeInput(void* input,
+                      const std::function<void(fuzztest::internal::ByteSpan)>&
+                          bytes_sink) override {
+    bytes_sink({0});
+  }
+
+  void FreeInput(void* input) override {}
 };
 
 int main(int argc, char** absl_nonnull argv) {
diff --git a/centipede/testing/fuzz_target_with_custom_mutator.cc b/centipede/testing/fuzz_target_with_custom_mutator.cc
index e6c28af..dcd3735 100644
--- a/centipede/testing/fuzz_target_with_custom_mutator.cc
+++ b/centipede/testing/fuzz_target_with_custom_mutator.cc
@@ -33,18 +33,32 @@
 class CustomMutatorRunnerCallbacks
     : public fuzztest::internal::RunnerCallbacks {
  public:
-  bool Execute(ByteSpan input) override { return true; }
+  bool Execute(void* input) override { return true; }
 
   bool HasCustomMutator() const override { return true; }
 
-  bool Mutate(absl::Span<const fuzztest::internal::MutationInputRef> inputs,
-              size_t num_mutants,
-              std::function<void(MutantRef)> new_mutant_callback) override {
-    for (size_t i = 0; i < inputs.size() && i < num_mutants; ++i) {
-      // Just return the original input as a mutant.
-      new_mutant_callback(MutantRef{inputs[i].data, i});
-    }
-    return true;
+  void* Mutate(void* input,
+               const fuzztest::internal::ExecutionMetadata& metadata) override {
+    const auto* ba =
+        reinterpret_cast<const fuzztest::internal::ByteArray*>(input);
+    return reinterpret_cast<void*>(new fuzztest::internal::ByteArray{*ba});
+  }
+
+  void* DeserializeInput(fuzztest::internal::ByteSpan input) override {
+    return reinterpret_cast<void*>(
+        new fuzztest::internal::ByteArray{input.begin(), input.end()});
+  }
+
+  void SerializeInput(void* input,
+                      const std::function<void(fuzztest::internal::ByteSpan)>&
+                          bytes_sink) override {
+    const auto* ba =
+        reinterpret_cast<const fuzztest::internal::ByteArray*>(input);
+    bytes_sink(*ba);
+  }
+
+  void FreeInput(void* input) override {
+    delete reinterpret_cast<const fuzztest::internal::ByteArray*>(input);
   }
 };
 
diff --git a/centipede/testing/seeded_fuzz_target.cc b/centipede/testing/seeded_fuzz_target.cc
index 0530109..285358d 100644
--- a/centipede/testing/seeded_fuzz_target.cc
+++ b/centipede/testing/seeded_fuzz_target.cc
@@ -24,18 +24,37 @@
 
 class SeededRunnerCallbacks : public fuzztest::internal::RunnerCallbacks {
  public:
-  bool Execute(ByteSpan input) override {
+  bool Execute(void* input) override {
     // Should not be called in the test, but return true anyway.
     return true;
   }
 
-  void GetSeeds(std::function<void(ByteSpan)> seed_callback) override {
+  void GetPresetSeedInputs(
+      const std::function<void(void*)>& seed_callback) override {
     constexpr size_t kNumAvailSeeds = 10;
     for (size_t i = 0; i < kNumAvailSeeds; ++i)
-      seed_callback({static_cast<uint8_t>(i)});
+      seed_callback(reinterpret_cast<void*>(
+          new fuzztest::internal::ByteArray{static_cast<uint8_t>(i)}));
   }
 
   bool HasCustomMutator() const override { return false; }
+
+  void* DeserializeInput(fuzztest::internal::ByteSpan input) override {
+    return reinterpret_cast<void*>(
+        new fuzztest::internal::ByteArray{input.begin(), input.end()});
+  }
+
+  void SerializeInput(void* input,
+                      const std::function<void(fuzztest::internal::ByteSpan)>&
+                          bytes_sink) override {
+    const auto* ba =
+        reinterpret_cast<const fuzztest::internal::ByteArray*>(input);
+    bytes_sink(*ba);
+  }
+
+  void FreeInput(void* input) override {
+    delete reinterpret_cast<const fuzztest::internal::ByteArray*>(input);
+  }
 };
 
 int main(int argc, char** absl_nonnull argv) {
diff --git a/fuzztest/internal/BUILD b/fuzztest/internal/BUILD
index 2bf470f..218a14b 100644
--- a/fuzztest/internal/BUILD
+++ b/fuzztest/internal/BUILD
@@ -81,6 +81,7 @@
         "@abseil-cpp//absl/algorithm:container",
         "@abseil-cpp//absl/base:no_destructor",
         "@abseil-cpp//absl/cleanup",
+        "@abseil-cpp//absl/container:flat_hash_map",
         "@abseil-cpp//absl/functional:any_invocable",
         "@abseil-cpp//absl/memory",
         "@abseil-cpp//absl/random",
@@ -97,6 +98,7 @@
         "@com_google_fuzztest//centipede:centipede_interface",
         "@com_google_fuzztest//centipede:centipede_runner_no_main",
         "@com_google_fuzztest//centipede:environment",
+        "@com_google_fuzztest//centipede:execution_metadata",
         "@com_google_fuzztest//centipede:fuzztest_mutator",
         "@com_google_fuzztest//centipede:mutation_data",
         "@com_google_fuzztest//centipede:runner_result",
diff --git a/fuzztest/internal/centipede_adaptor.cc b/fuzztest/internal/centipede_adaptor.cc
index 276a331..0072126 100644
--- a/fuzztest/internal/centipede_adaptor.cc
+++ b/fuzztest/internal/centipede_adaptor.cc
@@ -48,6 +48,7 @@
 #include "absl/algorithm/container.h"
 #include "absl/base/no_destructor.h"
 #include "absl/cleanup/cleanup.h"
+#include "absl/container/flat_hash_map.h"
 #include "absl/functional/any_invocable.h"
 #include "absl/memory/memory.h"
 #include "absl/random/distributions.h"
@@ -68,6 +69,7 @@
 #include "./centipede/centipede_default_callbacks.h"
 #include "./centipede/centipede_interface.h"
 #include "./centipede/environment.h"
+#include "./centipede/execution_metadata.h"
 #include "./centipede/fuzztest_mutator.h"
 #include "./centipede/mutation_data.h"
 #include "./centipede/runner_interface.h"
@@ -493,7 +495,10 @@
         configuration_(*configuration),
         prng_(GetRandomSeed()) {}
 
-  bool Execute(fuzztest::internal::ByteSpan input) override {
+  bool Execute(void* input) override {
+    const auto* input_object =
+        reinterpret_cast<const FuzzTestFuzzerImpl::Input*>(input);
+
     // Disable tracing until running the property function in
     // `CentipedeFxitureDriver::RunFuzzTestIteration()`
     const int old_traced = CentipedeSetCurrentThreadTraced(/*traced=*/0);
@@ -517,84 +522,79 @@
     }
     // We should avoid doing anything other than executing the input here so
     // that we don't affect the execution time.
-    auto parsed_input =
-        fuzzer_impl_.TryParse({(char*)input.data(), input.size()});
-    if (parsed_input.ok()) {
-      fuzzer_impl_.RunOneInput({*std::move(parsed_input)});
-      if (runtime_.external_failure_detected()) {
-        // This would take effect only if no previous description is set.
-        CentipedeSetFailureDescription(
-            "INPUT FAILURE: external failure detected.");
-      }
-      return true;
+    fuzzer_impl_.RunOneInput(*input_object);
+    if (runtime_.external_failure_detected()) {
+      // This would take effect only if no previous description is set.
+      CentipedeSetFailureDescription(
+          "INPUT FAILURE: external failure detected.");
     }
-    return false;
+    return true;
   }
 
-  void GetSeeds(std::function<void(fuzztest::internal::ByteSpan)> seed_callback)
-      override {
+  void GetPresetSeedInputs(
+      const std::function<void(void*)>& seed_callback) override {
     std::vector<GenericDomainCorpusType> seeds =
         fuzzer_impl_.fixture_driver_->GetSeeds();
-    constexpr int kInitialValuesInSeeds = 32;
-    for (int i = 0; i < kInitialValuesInSeeds; ++i) {
-      seeds.push_back(fuzzer_impl_.params_domain_.Init(prng_));
-    }
-    absl::c_shuffle(seeds, prng_);
     for (const auto& seed : seeds) {
-      const auto seed_serialized =
-          SerializeIRObject(fuzzer_impl_.params_domain_.SerializeCorpus(seed));
-      seed_callback(fuzztest::internal::AsByteSpan(seed_serialized));
+      auto* input = new FuzzTestFuzzerImpl::Input{std::move(seed)};
+      seed_callback(reinterpret_cast<void*>(input));
     }
   }
 
+  void* GetRandomSeedInput() override {
+    auto seed = fuzzer_impl_.params_domain_.Init(prng_);
+    return reinterpret_cast<void*>(
+        new FuzzTestFuzzerImpl::Input{std::move(seed)});
+  }
+
   std::string GetSerializedTargetConfig() override {
     return configuration_.Serialize();
   }
 
   bool HasCustomMutator() const override { return true; }
 
-  bool Mutate(absl::Span<const MutationInputRef> inputs, size_t num_mutants,
-              std::function<void(MutantRef)> new_mutant_callback) override {
-    if (inputs.empty()) return false;
-    cmp_tables.resize(inputs.size());
-    absl::Cleanup cmp_tables_cleaner = [this]() { cmp_tables.clear(); };
-    for (size_t i = 0; i < num_mutants; ++i) {
-      const auto choice = absl::Uniform<double>(prng_, 0, 1);
-      size_t origin_index = Mutant::kOriginNone;
-      std::string mutant_data;
-      constexpr double kDomainInitRatio = 0.0001;
-      if (choice < kDomainInitRatio) {
-        mutant_data =
-            SerializeIRObject(fuzzer_impl_.params_domain_.SerializeCorpus(
-                fuzzer_impl_.params_domain_.Init(prng_)));
-      } else {
-        origin_index = absl::Uniform<size_t>(prng_, 0, inputs.size());
-        const auto& origin = inputs[origin_index].data;
-        auto parsed_origin =
-            fuzzer_impl_.TryParse({(const char*)origin.data(), origin.size()});
-        if (!parsed_origin.ok()) {
-          parsed_origin = fuzzer_impl_.params_domain_.Init(prng_);
-        }
-        auto mutant = FuzzTestFuzzerImpl::Input{*std::move(parsed_origin)};
-        if (runtime_.run_mode() == RunMode::kFuzz &&
-            !cmp_tables[origin_index].has_value() &&
-            inputs[origin_index].metadata != nullptr) {
-          cmp_tables[origin_index].emplace(/*compact=*/true);
-          PopulateCmpEntries(*inputs[origin_index].metadata,
-                             *cmp_tables[origin_index]);
-        }
-        fuzzer_impl_.MutateValue(
-            mutant, prng_,
-            {cmp_tables[origin_index].has_value() ? &*cmp_tables[origin_index]
-                                                  : nullptr});
-        mutant_data = SerializeIRObject(
-            fuzzer_impl_.params_domain_.SerializeCorpus(mutant.args));
-      }
-      new_mutant_callback(
-          MutantRef{{(unsigned char*)mutant_data.data(), mutant_data.size()},
-                    origin_index});
+  void SerializeInput(
+      void* input, const std::function<void(ByteSpan)>& bytes_sink) override {
+    auto* input_object = reinterpret_cast<FuzzTestFuzzerImpl::Input*>(input);
+    std::string serialized_input = SerializeIRObject(
+        fuzzer_impl_.params_domain_.SerializeCorpus(input_object->args));
+    bytes_sink({reinterpret_cast<const uint8_t*>(serialized_input.data()),
+                serialized_input.size()});
+  }
+
+  void* DeserializeInput(ByteSpan input_bytes) override {
+    auto parse_result = fuzzer_impl_.TryParse(
+        {(const char*)input_bytes.data(), input_bytes.size()});
+    if (parse_result.ok()) {
+      return reinterpret_cast<void*>(
+          new FuzzTestFuzzerImpl::Input{*std::move(parse_result)});
     }
-    return true;
+    return reinterpret_cast<void*>(
+        new FuzzTestFuzzerImpl::Input{fuzzer_impl_.params_domain_.Init(prng_)});
+  }
+
+  void FreeInput(void* input) override {
+    auto it = cmp_tables_.find(input);
+    if (it != cmp_tables_.end()) {
+      delete it->second;
+      cmp_tables_.erase(it);
+    }
+    delete reinterpret_cast<FuzzTestFuzzerImpl::Input*>(input);
+  }
+
+  void* Mutate(void* origin,
+               const ExecutionMetadata& origin_metadata) override {
+    const auto* origin_object =
+        reinterpret_cast<const FuzzTestFuzzerImpl::Input*>(origin);
+    auto* mutant = new FuzzTestFuzzerImpl::Input{*origin_object};
+    auto& cmp_table = cmp_tables_[origin];
+    if (cmp_table == nullptr) {
+      cmp_table =
+          new fuzztest::internal::TablesOfRecentCompares{/*compact=*/true};
+      PopulateCmpEntries(origin_metadata, *cmp_table);
+    }
+    fuzzer_impl_.MutateValue(*mutant, prng_, {cmp_table});
+    return mutant;
   }
 
   ~CentipedeAdaptorRunnerCallbacks() override { runtime_.UnsetCurrentArgs(); }
@@ -604,8 +604,8 @@
   FuzzTestFuzzerImpl& fuzzer_impl_;
   const Configuration& configuration_;
   absl::BitGen prng_;
-  std::vector<std::optional<fuzztest::internal::TablesOfRecentCompares>>
-      cmp_tables;
+  absl::flat_hash_map<void*, fuzztest::internal::TablesOfRecentCompares*>
+      cmp_tables_;
 };
 
 namespace {