No public description

PiperOrigin-RevId: 944563827
diff --git a/centipede/BUILD b/centipede/BUILD
index 518d7a6..ea1ae4b 100644
--- a/centipede/BUILD
+++ b/centipede/BUILD
@@ -976,36 +976,6 @@
     ],
 )
 
-cc_library(
-    name = "engine_worker",
-    srcs = [
-        "engine_worker.cc",
-        "runner_utils.cc",
-        "runner_utils.h",
-    ],
-    hdrs = ["engine_worker_abi.h"],
-    deps = [
-        ":engine_abi",
-        ":execution_metadata",
-        ":feature",
-        ":runner_request",
-        ":runner_result",
-        ":shared_memory_blob_sequence",
-        "@abseil-cpp//absl/base:nullability",
-        "@com_google_fuzztest//common:defs",
-    ],
-)
-
-cc_library(
-    name = "engine_controller_with_subprocess",
-    srcs = ["engine_controller_with_subprocess.cc"],
-    hdrs = ["engine_controller_abi.h"],
-    deps = [
-        "@com_google_fuzztest//centipede:engine_abi",
-        "@com_google_fuzztest//fuzztest/internal:escaping",
-    ],
-)
-
 # The runner library is special:
 #   * It must not be instrumented with asan, sancov, etc.
 #   * It must not have heavy dependencies, and ideally not at all.
@@ -1026,8 +996,6 @@
     "runner_dl_info.cc",
     "runner_dl_info.h",
     "runner_interface.h",
-    "runner_utils.cc",
-    "runner_utils.h",
     "sancov_callbacks.cc",
     "sancov_interceptors.cc",
     "sancov_object_array.cc",
@@ -1075,11 +1043,12 @@
     ":knobs",
     ":mutation_data",
     ":rolling_hash",
-    ":engine_abi",
     ":runner_cmp_trace",
-    ":runner_fork_server",
     ":runner_request",
     ":runner_result",
+    ":runner_utils",
+    ":engine_abi",
+    ":engine_worker",
     ":shared_memory_blob_sequence",
     "@com_google_fuzztest//common:defs",
     "@abseil-cpp//absl/base:core_headers",
@@ -1088,6 +1057,45 @@
     "@abseil-cpp//absl/types:span",
 ]
 
+cc_library(
+    name = "runner_utils",
+    srcs = ["runner_utils.cc"],
+    hdrs = ["runner_utils.h"],
+    copts = RUNNER_COPTS,
+    deps = ["@abseil-cpp//absl/base:nullability"],
+)
+
+cc_library(
+    name = "engine_worker",
+    srcs = [
+        "engine_worker.cc",
+    ],
+    hdrs = ["engine_worker_abi.h"],
+    copts = RUNNER_COPTS,
+    deps = [
+        ":engine_abi",
+        ":execution_metadata",
+        ":feature",
+        ":runner_fork_server",
+        ":runner_request",
+        ":runner_result",
+        ":runner_utils",
+        ":shared_memory_blob_sequence",
+        "@abseil-cpp//absl/base:nullability",
+        "@com_google_fuzztest//common:defs",
+    ],
+)
+
+cc_library(
+    name = "engine_controller_with_subprocess",
+    srcs = ["engine_controller_with_subprocess.cc"],
+    hdrs = ["engine_controller_abi.h"],
+    deps = [
+        "@com_google_fuzztest//centipede:engine_abi",
+        "@com_google_fuzztest//fuzztest/internal:escaping",
+    ],
+)
+
 # A fuzz target needs to link with this library in order to run with Centipede.
 # The fuzz target must provide its own main().
 #
@@ -1139,6 +1147,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",
@@ -1216,8 +1225,6 @@
         "reverse_pc_table.h",
         "runner_dl_info.cc",
         "runner_dl_info.h",
-        "runner_utils.cc",
-        "runner_utils.h",
         "sancov_callbacks.cc",
         "sancov_interceptors.cc",
         "sancov_object_array.cc",
@@ -1239,6 +1246,7 @@
         ":foreach_nonzero",
         ":int_utils",
         ":runner_cmp_trace",
+        ":runner_utils",
         "@abseil-cpp//absl/base:core_headers",
         "@abseil-cpp//absl/base:nullability",
         "@abseil-cpp//absl/numeric:bits",
@@ -1957,7 +1965,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_test.cc b/centipede/centipede_test.cc
index e242d2d..f51b151 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, testing::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..db368c3 100644
--- a/centipede/runner.cc
+++ b/centipede/runner.cc
@@ -24,6 +24,7 @@
 
 #include <fcntl.h>
 #include <pthread.h>  // NOLINT: use pthread to avoid extra dependencies.
+#include <sched.h>
 #include <sys/resource.h>
 #include <sys/socket.h>
 #include <sys/time.h>
@@ -52,6 +53,8 @@
 #include "absl/types/span.h"
 #include "./centipede/byte_array_mutator.h"
 #include "./centipede/dispatcher_flag_helper.h"
+#include "./centipede/engine_abi.h"
+#include "./centipede/engine_worker_abi.h"
 #include "./centipede/execution_metadata.h"
 #include "./centipede/feature.h"
 #include "./centipede/mutation_data.h"
@@ -78,6 +81,21 @@
 
 GlobalRunnerStateManager state_manager __attribute__((init_priority(200)));
 
+class SpinlockGuard {
+ public:
+  SpinlockGuard(std::atomic<bool>& lock, bool acquire = true) : lock_(lock) {
+    if (!acquire) return;
+    while (lock.exchange(true)) {
+      sched_yield();
+    }
+  }
+
+  ~SpinlockGuard() { lock_ = false; }
+
+ private:
+  std::atomic<bool>& lock_;
+};
+
 }  // namespace
 
 static size_t GetPeakRSSMb() {
@@ -124,12 +142,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 +170,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 +206,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;
@@ -249,10 +267,20 @@
   state->input_start_time = curr_time;
 }
 
+// Returns a random seed. No need for a more sophisticated seed.
+static unsigned GetRandomSeed() { return time(nullptr); }
+
+static void EnsureBuiltinMutator() {
+  static auto mutator = new ByteArrayMutator(state->knobs, GetRandomSeed());
+  if (state->byte_array_mutator == nullptr) {
+    state->byte_array_mutator = mutator;
+  }
+}
+
 // 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.
@@ -266,6 +294,7 @@
   }
 
   ByteArray array(data, data + size);
+  EnsureBuiltinMutator();
   state->byte_array_mutator->set_max_len(max_size);
   state->byte_array_mutator->Mutate(array);
   if (array.size() > max_size) {
@@ -275,7 +304,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 +312,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 +336,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 +371,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 +386,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 +410,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 +423,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 +437,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 +462,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 +480,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;
@@ -473,72 +522,12 @@
   return true;
 }
 
-// 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) {
-  size_t num_inputs = 0;
-  if (!IsExecutionRequest(inputs_blobseq.Read())) return EXIT_FAILURE;
-  if (!IsNumInputs(inputs_blobseq.Read(), num_inputs)) return EXIT_FAILURE;
-
-  CentipedeBeginExecutionBatch();
-
-  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 (!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);
-
-    // Starting execution of one more input.
-    if (!StartSendingOutputsToEngine(outputs_blobseq)) break;
-
-    RunOneInput(data.data(), data.size(), callbacks);
-
-    if (state->has_failure_description.load()) break;
-
-    if (!FinishSendingOutputsToEngine(outputs_blobseq)) break;
-  }
-
-  CentipedeEndExecutionBatch();
-
-  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) {
-  size_t seed_index = 0;
-  callbacks.GetSeeds([&](ByteSpan seed) {
-    // Cap seed index within 9 digits. If this was triggered, the dumping would
-    // take forever..
-    if (seed_index >= 1000000000) return;
-    char seed_path_buf[PATH_MAX];
-    const size_t num_path_chars =
-        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");
-    const size_t num_bytes_written =
-        fwrite(seed.data(), 1, seed.size(), output_file);
-    PrintErrorAndExitIf(num_bytes_written != seed.size(),
-                        "wrong number of bytes written for cf table");
-    fclose(output_file);
-    ++seed_index;
-  });
-}
-
 // 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(
@@ -547,123 +536,81 @@
   fclose(output_file);
 }
 
-// Returns a random seed. No need for a more sophisticated seed.
-// TODO(kcc): [as-needed] optionally pass an external seed.
-static unsigned GetRandomSeed() { return time(nullptr); }
-
-// Handles a Mutation Request, see RequestMutation().
-// Mutates inputs read from `inputs_blobseq`,
-// writes the mutants to `outputs_blobseq`
-// Returns EXIT_SUCCESS on success and EXIT_FAILURE on failure
-// so that main() can return its result.
-// If both `custom_mutator_cb` and `custom_crossover_cb` are nullptr,
-// 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) {
-  // Read max_num_mutants.
-  size_t num_mutants = 0;
-  size_t num_inputs = 0;
-  if (!IsMutationRequest(inputs_blobseq.Read())) return EXIT_FAILURE;
-  if (!IsNumMutants(inputs_blobseq.Read(), num_mutants)) return EXIT_FAILURE;
-  if (!IsNumInputs(inputs_blobseq.Read(), num_inputs)) return EXIT_FAILURE;
-
-  // Mutation input with ownership.
-  struct MutationInput {
-    ByteArray data;
-    ExecutionMetadata metadata;
-  };
-  // TODO(kcc): unclear if we can continue using std::vector (or other STL)
-  // in the runner. But for now use std::vector.
-  // Collect the inputs into a vector. We copy them instead of using pointers
-  // into shared memory so that the user code doesn't touch the shared memory.
-  std::vector<MutationInput> inputs;
-  inputs.reserve(num_inputs);
-  std::vector<MutationInputRef> input_refs;
-  input_refs.reserve(num_inputs);
-  for (size_t i = 0; i < num_inputs; ++i) {
-    // If inputs_blobseq have overflown in the engine, we still want to
-    // handle the first few inputs.
-    ExecutionMetadata metadata;
-    if (!IsExecutionMetadata(inputs_blobseq.Read(), metadata)) {
-      break;
-    }
-    auto blob = inputs_blobseq.Read();
-    if (!IsDataInput(blob)) break;
-    inputs.push_back(
-        MutationInput{/*data=*/ByteArray{blob.data, blob.data + blob.size},
-                      /*metadata=*/std::move(metadata)});
-    input_refs.push_back(
-        MutationInputRef{/*data=*/inputs.back().data,
-                         /*metadata=*/&inputs.back().metadata});
-  }
-
-  if (!inputs.empty()) {
-    state->byte_array_mutator->SetMetadata(inputs[0].metadata);
-  }
-
-  if (!MutationResult::WriteHasCustomMutator(callbacks.HasCustomMutator(),
-                                             outputs_blobseq)) {
-    return EXIT_FAILURE;
-  }
-  if (!callbacks.HasCustomMutator()) return EXIT_SUCCESS;
-
-  if (!callbacks.Mutate(input_refs, num_mutants, [&](MutantRef mutant) {
-        MutationResult::WriteMutant(mutant, outputs_blobseq);
-      })) {
-    return EXIT_FAILURE;
-  }
-  return EXIT_SUCCESS;
+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);
 }
 
-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::DeserializeInput(ByteSpan input_bytes) {
+  return reinterpret_cast<void*>(
+      new ByteArray{input_bytes.begin(), input_bytes.end()});
+}
 
-    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));
-    }
-    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;
+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);
+  EnsureBuiltinMutator();
+  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;
+    mutant->resize(new_size);
+  } else {
+    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();
+  EnsureBuiltinMutator();
+  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);
+  } else {
+    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.
@@ -717,57 +664,10 @@
 [[maybe_unused]] auto fake_reference_for_fork_server =
     &ForkServerCallMeVeryEarly;
 
-void MaybeConnectToPersistentMode() {
-  if (state->persistent_mode_socket_path == nullptr) {
-    return;
-  }
-  state->persistent_mode_socket = socket(AF_UNIX, SOCK_STREAM, 0);
-  if (state->persistent_mode_socket < 0) {
-    fprintf(stderr, "Failed to create persistent mode socket\n");
-  }
-
-  struct sockaddr_un addr{};
-  addr.sun_family = AF_UNIX;
-  const size_t socket_path_len = strlen(state->persistent_mode_socket_path);
-  RunnerCheck(
-      socket_path_len < sizeof(addr.sun_path),
-      "persistent mode socket path string must be fit in sockaddr_un.sun_path");
-  std::memcpy(addr.sun_path, state->persistent_mode_socket_path,
-              socket_path_len);
-
-  int connect_ret = 0;
-  do {
-    connect_ret = connect(state->persistent_mode_socket,
-                          (struct sockaddr*)&addr, sizeof(addr));
-  } while (connect_ret == -1 && errno == EINTR);
-  if (connect_ret == -1) {
-    fprintf(stderr, "Failed to connect the persistent mode socket to %s\n",
-            state->persistent_mode_socket_path);
-    (void)close(state->persistent_mode_socket);
-    state->persistent_mode_socket = -1;
-  }
-
-  int flags = fcntl(state->persistent_mode_socket, F_GETFD);
-  if (flags == -1) {
-    fprintf(stderr, "fcntl(F_GETFD) failed\n");
-    (void)close(state->persistent_mode_socket);
-    state->persistent_mode_socket = -1;
-  }
-  flags |= FD_CLOEXEC;
-  if (fcntl(state->persistent_mode_socket, F_SETFD, flags) == -1) {
-    fprintf(stderr, "fcntl(F_SETFD) failed\n");
-    (void)close(state->persistent_mode_socket);
-    state->persistent_mode_socket = -1;
-  }
-}
-
 GlobalRunnerState::GlobalRunnerState() {
   // Make sure fork server is started if needed.
   ForkServerCallMeVeryEarly();
 
-  // Connecting to the persistent mode socket should be immediately after.
-  MaybeConnectToPersistentMode();
-
   SancovRuntimeInitialize();
 
   // TODO(kcc): move some code from CentipedeRunnerMain() here so that it works
@@ -797,76 +697,6 @@
   }
 }
 
-static int HandleSharedMemoryRequest(RunnerCallbacks& callbacks,
-                                     BlobSequence& inputs_blobseq,
-                                     BlobSequence& outputs_blobseq) {
-  state->has_failure_description = false;
-  // Read the first blob. It indicates what further actions to take.
-  auto request_type_blob = inputs_blobseq.Read();
-  if (IsMutationRequest(request_type_blob)) {
-    // Mutation request.
-    inputs_blobseq.Reset();
-    static auto mutator = new ByteArrayMutator(state->knobs, GetRandomSeed());
-    state->byte_array_mutator = mutator;
-    // Since we are mutating, no need to spend time collecting the coverage.
-    // We still pay for executing the coverage callbacks, but those will
-    // return immediately.
-    const int old_traced = CentipedeSetCurrentThreadTraced(/*traced=*/0);
-    const int result =
-        MutateInputsFromShmem(inputs_blobseq, outputs_blobseq, callbacks);
-    CentipedeSetCurrentThreadTraced(old_traced);
-    return result;
-  }
-  if (IsExecutionRequest(request_type_blob)) {
-    // Execution request.
-    inputs_blobseq.Reset();
-    return ExecuteInputsFromShmem(inputs_blobseq, outputs_blobseq, callbacks);
-  }
-  return EXIT_FAILURE;
-}
-
-static int HandlePersistentMode(RunnerCallbacks& callbacks,
-                                BlobSequence& inputs_blobseq,
-                                BlobSequence& outputs_blobseq) {
-  bool first = true;
-  while (true) {
-    PersistentModeRequest req;
-    if (!ReadAll(state->persistent_mode_socket, reinterpret_cast<char*>(&req),
-                 1)) {
-      perror("Failed to read request from persistent mode socket");
-      return EXIT_FAILURE;
-    }
-    if (first) {
-      first = false;
-    } else {
-      // Reset stdout/stderr.
-      for (int fd = 1; fd <= 2; fd++) {
-        lseek(fd, 0, SEEK_SET);
-        // NOTE: Allow ftruncate() to fail by ignoring its return; that's okay
-        // to happen when the stdout/stderr are not redirected to a file.
-        (void)ftruncate(fd, 0);
-      }
-      fprintf(stderr, "Centipede fuzz target runner (%s); flags: %s\n",
-              req == PersistentModeRequest::kExit ? "exiting persistent mode"
-                                                  : "persistent mode batch",
-              state->flag_helper.flags);
-    }
-    if (req == PersistentModeRequest::kExit) break;
-    RunnerCheck(req == PersistentModeRequest::kRunBatch,
-                "Unknown persistent mode request");
-    const int result =
-        HandleSharedMemoryRequest(callbacks, inputs_blobseq, outputs_blobseq);
-    inputs_blobseq.Reset();
-    outputs_blobseq.Reset();
-    if (!WriteAll(state->persistent_mode_socket,
-                  reinterpret_cast<const char*>(&result), sizeof(result))) {
-      perror("Failed to write response to the persistent mode socket");
-      return EXIT_FAILURE;
-    }
-  }
-  return EXIT_SUCCESS;
-}
-
 // If HasFlag(:shmem:), state->arg1 and state->arg2 are the names
 //  of in/out shared memory locations.
 //  Read inputs and write outputs via shared memory.
@@ -874,7 +704,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",
@@ -886,36 +716,222 @@
     return EXIT_SUCCESS;
   }
 
-  if (state->flag_helper.HasFlag(":dump_seed_inputs:")) {
-    // Seed request.
-    DumpSeedsToDir(callbacks, /*output_dir=*/sancov_state->arg1);
+  struct Input {
+    void* content;
+    ExecutionMetadata metadata;
+  };
+  // TODO: move it to ctx.
+  static bool need_full_cleanup = true;
+  FuzzTestAdapterManager manager = {
+      /*ctx=*/reinterpret_cast<FuzzTestAdapterManagerCtx*>(&callbacks),
+      /*GetBinaryId=*/nullptr,
+      /*GetTestName=*/
+      [](FuzzTestAdapterManagerCtx* ctx, const FuzzTestBytesSink* sink) {
+        // Provide the test name exactly specified from the flag. This should be
+        // fine as the user of runner should call RunnerMain at most once.
+        static const char* test_name =
+            state->flag_helper.GetStringFlag(":test=");
+        if (test_name == nullptr) return;
+        static size_t len = strlen(test_name);
+        const auto bytes = FuzzTestBytesView{
+            reinterpret_cast<const uint8_t*>(test_name),
+            len,
+        };
+        sink->Emit(sink->ctx, &bytes);
+      },
+      /*ConstructAdapter=*/
+      [](FuzzTestAdapterManagerCtx* ctx,
+         const FuzzTestDiagnosticSink* diagnostic_sink,
+         FuzzTestAdapter* adapter_out) {
+        {
+          SpinlockGuard guard(state->diagnostic_sink_spinlock);
+          state->diagnostic_sink = diagnostic_sink;
+        }
+        adapter_out->ctx = reinterpret_cast<FuzzTestAdapterCtx*>(ctx);
+        adapter_out->SetUpCoverageDomains =
+            [](FuzzTestAdapterCtx* ctx,
+               const FuzzTestCoverageDomainRegistry* registry) {
+              SanCovRuntimeSetUpCoverageDomains(registry);
+            };
+        adapter_out->GetPresetSeedInputs = [](FuzzTestAdapterCtx* ctx,
+                                              const FuzzTestInputSink* sink) {
+          auto* callbacks = reinterpret_cast<RunnerCallbacks*>(ctx);
+          callbacks->GetPresetSeedInputs([&](void* input) {
+            sink->Emit(sink->ctx, reinterpret_cast<FuzzTestInputHandle>(
+                                      new Input{/*content=*/input,
+                                                /*metadata=*/{}}));
+          });
+        };
+        adapter_out->GetRandomSeedInput = [](FuzzTestAdapterCtx* ctx,
+                                             const FuzzTestInputSink* sink) {
+          auto* callbacks = reinterpret_cast<RunnerCallbacks*>(ctx);
+          sink->Emit(sink->ctx, reinterpret_cast<FuzzTestInputHandle>(new Input{
+                                    /*content=*/callbacks->GetRandomSeedInput(),
+                                    /*metadata=*/{}}));
+        };
+        adapter_out->Mutate = [](FuzzTestAdapterCtx* ctx,
+                                 FuzzTestInputHandle origin_handle, int shrink,
+                                 const FuzzTestInputSink* sink) {
+          need_full_cleanup = true;
+          auto* callbacks = reinterpret_cast<RunnerCallbacks*>(ctx);
+          if (!callbacks->HasCustomMutator()) {
+            // This is a ugly hack to let Centipede use the builtin mutator
+            // needed by the LLVM fuzzer runners (used by some engine tests). We
+            // cannot remove it until the LLVM fuzzers can be migrated to use
+            // the FuzzTest LLVM fuzzer wrapper.
+            SharedMemoryBlobSequence outputs_blobseq(sancov_state->arg2);
+            RunnerCheck(
+                MutationResult::WriteHasCustomMutator(false, outputs_blobseq),
+                "Failed to write the indicator for no custom mutator");
+            std::_Exit(0);
+          }
+          const auto* origin = reinterpret_cast<Input*>(origin_handle);
+          auto mutant = new Input{
+              /*content=*/callbacks->Mutate(origin->content, origin->metadata),
+              /*metadata=*/{},
+          };
+          sink->Emit(sink->ctx, reinterpret_cast<FuzzTestInputHandle>(mutant));
+        };
+        adapter_out->CrossOver = [](FuzzTestAdapterCtx* ctx,
+                                    FuzzTestInputHandle origin_handle,
+                                    FuzzTestInputHandle other_handle,
+                                    const FuzzTestInputSink* sink) {
+          need_full_cleanup = true;
+          auto* callbacks = reinterpret_cast<RunnerCallbacks*>(ctx);
+          if (!callbacks->HasCustomMutator()) {
+            // This is a ugly hack to let Centipede use the builtin mutator
+            // needed by the LLVM fuzzer runners (used by some engine tests). We
+            // cannot remove it until the LLVM fuzzers can be migrated to use
+            // the FuzzTest LLVM fuzzer wrapper.
+            SharedMemoryBlobSequence outputs_blobseq(sancov_state->arg2);
+            RunnerCheck(
+                MutationResult::WriteHasCustomMutator(false, outputs_blobseq),
+                "Failed to write the indicator for no custom mutator");
+            std::exit(0);
+          }
+          const auto* origin = reinterpret_cast<Input*>(origin_handle);
+          const auto* other = reinterpret_cast<Input*>(other_handle);
+          auto mutant = new Input{
+              /*content=*/callbacks->CrossOver(origin->content,
+                                               origin->metadata, other->content,
+                                               other->metadata),
+              /*metadata=*/{},
+          };
+          sink->Emit(sink->ctx, reinterpret_cast<FuzzTestInputHandle>(mutant));
+        };
+        adapter_out->Execute = [](FuzzTestAdapterCtx* ctx,
+                                  FuzzTestInputHandle handle,
+                                  const FuzzTestFeedbackSink* sink) {
+          auto* callbacks = reinterpret_cast<RunnerCallbacks*>(ctx);
+          auto* input = reinterpret_cast<Input*>(handle);
+          if (need_full_cleanup) {
+            SanCovRuntimeClearCoverage(true);
+            need_full_cleanup = false;
+          }
+          const int old_traced = CentipedeSetCurrentThreadTraced(/*traced=*/1);
+          RunOneInput(input->content, *callbacks);
+          CentipedeSetCurrentThreadTraced(old_traced);
+          {
+            LockGuard lock(state->execution_result_override_mu);
+            bool has_overridden_execution_result = false;
+            if (state->execution_result_override != nullptr) {
+              RunnerCheck(
+                  state->execution_result_override->results().size() <= 1,
+                  "unexpected number of overridden execution results");
+              has_overridden_execution_result =
+                  state->execution_result_override->results().size() == 1;
+            }
+            if (has_overridden_execution_result) {
+              auto& result = state->execution_result_override->results()[0];
+              SanCovRuntimeConvertToEngineFeatures(
+                  result.mutable_features().data(),
+                  result.mutable_features().size());
+              const FuzzTestUint64sView features = {
+                  result.features().data(),
+                  result.features().size(),
+              };
+              sink->EmitCoverageFeatures(sink->ctx, &features);
+              input->metadata = result.metadata();
+              return;
+            }
+          }
+          SanCovRuntimeEmitFeatures(sink);
+          input->metadata = SanCovRuntimeGetExecutionMetadata();
+        };
+        adapter_out->DeserializeInputContent =
+            [](FuzzTestAdapterCtx* ctx, const FuzzTestBytesView* view,
+               const FuzzTestInputSink* sink) {
+              auto* callbacks = reinterpret_cast<RunnerCallbacks*>(ctx);
+              auto* input = new Input{
+                  /*content=*/callbacks->DeserializeInput(
+                      {view->data, view->size}),
+                  /*metadata=*/{},
+              };
+              sink->Emit(sink->ctx,
+                         reinterpret_cast<FuzzTestInputHandle>(input));
+            };
+        adapter_out->UpdateInputMetadata = [](FuzzTestAdapterCtx* ctx,
+                                              const FuzzTestBytesView* view,
+                                              FuzzTestInputHandle handle) {
+          auto* input = reinterpret_cast<Input*>(handle);
+          input->metadata.cmp_data = {view->data, view->data + view->size};
+        };
+        adapter_out->SerializeInputContent = [](FuzzTestAdapterCtx* ctx,
+                                                FuzzTestInputHandle handle,
+                                                const FuzzTestBytesSink* sink) {
+          auto* input = reinterpret_cast<Input*>(handle);
+          auto* callbacks = reinterpret_cast<RunnerCallbacks*>(ctx);
+          callbacks->SerializeInput(input->content, [&](ByteSpan bytes) {
+            const auto input_bytes = FuzzTestBytesView{
+                reinterpret_cast<const uint8_t*>(bytes.data()),
+                bytes.size(),
+            };
+            sink->Emit(sink->ctx, &input_bytes);
+          });
+        };
+        adapter_out->SerializeInputMetadata =
+            [](FuzzTestAdapterCtx* ctx, FuzzTestInputHandle handle,
+               const FuzzTestBytesSink* sink) {
+              auto* input = reinterpret_cast<Input*>(handle);
+              const auto input_bytes = FuzzTestBytesView{
+                  reinterpret_cast<const uint8_t*>(
+                      input->metadata.cmp_data.data()),
+                  input->metadata.cmp_data.size(),
+              };
+              sink->Emit(sink->ctx, &input_bytes);
+            };
+        adapter_out->FreeInput = [](FuzzTestAdapterCtx* ctx,
+                                    FuzzTestInputHandle handle) {
+          auto* input = reinterpret_cast<Input*>(handle);
+          auto* callbacks = reinterpret_cast<RunnerCallbacks*>(ctx);
+          callbacks->FreeInput(input->content);
+          delete input;
+        };
+        adapter_out->FreeCtx = [](FuzzTestAdapterCtx* ctx) {
+          {
+            SpinlockGuard guard(state->diagnostic_sink_spinlock);
+            state->diagnostic_sink = nullptr;
+          }
+        };
+      },
+  };
+  const int old_traced = CentipedeSetCurrentThreadTraced(/*traced=*/0);
+  const auto s = FuzzTestWorkerMaybeRun(&manager);
+  CentipedeSetCurrentThreadTraced(old_traced);
+  if (s == kFuzzTestWorkerNotRequired) {
+    // By default, run every input file one-by-one.
+    for (int i = 1; i < argc; i++) {
+      ReadOneInputExecuteItAndDumpCoverage(argv[i], callbacks);
+    }
     return EXIT_SUCCESS;
   }
-
-  // Inputs / outputs from shmem.
-  if (state->flag_helper.HasFlag(":shmem:")) {
-    if (!sancov_state->arg1 || !sancov_state->arg2) return EXIT_FAILURE;
-    SharedMemoryBlobSequence inputs_blobseq(sancov_state->arg1);
-    SharedMemoryBlobSequence outputs_blobseq(sancov_state->arg2);
-    // Persistent mode loop.
-    if (state->persistent_mode_socket > 0) {
-      return HandlePersistentMode(callbacks, inputs_blobseq, outputs_blobseq);
-    }
-    return HandleSharedMemoryRequest(callbacks, inputs_blobseq,
-                                     outputs_blobseq);
-  }
-
-  // By default, run every input file one-by-one.
-  for (int i = 1; i < argc; i++) {
-    ReadOneInputExecuteItAndDumpCoverage(argv[i], callbacks);
-  }
-  return EXIT_SUCCESS;
+  return s == kFuzzTestWorkerSuccess ? EXIT_SUCCESS : EXIT_FAILURE;
 }
 
 }  // 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 +961,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 +1015,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 +1024,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,30 +1036,54 @@
   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) {
-  using fuzztest::internal::state;
-  if (state->failure_description_path == nullptr) return;
-  if (state->has_failure_description.exchange(true)) return;
-  FILE* f = fopen(state->failure_description_path, "w");
-  if (f == nullptr) {
-    perror("FAILURE: fopen()");
+extern "C" void CentipedeSetFailureDescription(const char* description) {
+  std::string_view desc_sv = description;
+  static constexpr std::string_view kSetupFailurePrefix = "SETUP FAILURE:";
+  using ::fuzztest::internal::SpinlockGuard;
+  using ::fuzztest::internal::state;
+  const bool sink_try_lock_result =
+      !state->diagnostic_sink_spinlock.exchange(true);
+  if (!sink_try_lock_result) {
+    static constexpr std::string_view kMsg =
+        "Diagnositc sink is busy while setting failure:\n";
+    write(STDERR_FILENO, kMsg.data(), kMsg.size());
+    write(STDERR_FILENO, description, std::strlen(description));
+    std::_Exit(EXIT_FAILURE);
+  }
+  SpinlockGuard sink_guard(state->diagnostic_sink_spinlock, /*acquire=*/false);
+  const auto* diagnostic_sink = state->diagnostic_sink;
+  if (diagnostic_sink == nullptr) {
+    static constexpr std::string_view kMsg =
+        "Diagnositc sink is missing while setting failure:\n";
+    write(STDERR_FILENO, kMsg.data(), kMsg.size());
+    write(STDERR_FILENO, description, std::strlen(description));
+    std::_Exit(EXIT_FAILURE);
+  }
+
+  if (desc_sv.substr(0, kSetupFailurePrefix.size()) == kSetupFailurePrefix) {
+    size_t prefix_size = kSetupFailurePrefix.size();
+    while (desc_sv.size() < prefix_size && desc_sv[prefix_size] == ' ') {
+      ++prefix_size;
+    }
+    const FuzzTestBytesView error = {
+        reinterpret_cast<const uint8_t*>(desc_sv.data() + prefix_size),
+        desc_sv.size() - prefix_size,
+    };
+    diagnostic_sink->EmitError(diagnostic_sink->ctx, &error);
     return;
   }
-  const auto len = strlen(description);
-  if (fwrite(description, 1, len, f) != len) {
-    perror("FAILURE: fwrite()");
-  }
-  if (fflush(f) != 0) {
-    perror("FAILURE: fflush()");
-  }
-  if (fclose(f) != 0) {
-    perror("FAILURE: fclose()");
-  }
+
+  const FuzzTestBytesView finding_desc = {
+      reinterpret_cast<const uint8_t*>(desc_sv.data()),
+      desc_sv.size(),
+  };
+  diagnostic_sink->EmitFinding(diagnostic_sink->ctx, &finding_desc,
+                               &finding_desc);
 }
diff --git a/centipede/runner.h b/centipede/runner.h
index 5ddc3b3..2c3b397 100644
--- a/centipede/runner.h
+++ b/centipede/runner.h
@@ -23,6 +23,7 @@
 
 #include "./centipede/byte_array_mutator.h"
 #include "./centipede/dispatcher_flag_helper.h"
+#include "./centipede/engine_abi.h"
 #include "./centipede/knobs.h"
 #include "./centipede/runner_interface.h"
 #include "./centipede/runner_result.h"
@@ -51,7 +52,7 @@
 // TODO(kcc): use a CTOR with absl::kConstInit (will require refactoring).
 struct GlobalRunnerState {
   // Used by LLVMFuzzerMutate and initialized in main().
-  ByteArrayMutator *byte_array_mutator = nullptr;
+  ByteArrayMutator* byte_array_mutator = nullptr;
   Knobs knobs;
 
   GlobalRunnerState();
@@ -75,7 +76,7 @@
   };
 
   // The path to a file where the runner may write the description of failure.
-  const char *failure_description_path =
+  const char* failure_description_path =
       flag_helper.GetStringFlag(":failure_description_path=");
 
   std::atomic<bool> has_failure_description;
@@ -90,7 +91,7 @@
   // execution result of the current test input. The object is owned and cleaned
   // up by the state, protected by execution_result_override_mu, and set by
   // `CentipedeSetExecutionResult()`.
-  BatchResult *execution_result_override;
+  BatchResult* execution_result_override;
 
   // Execution stats for the currently executed input.
   ExecutionResult::Stats stats;
@@ -112,6 +113,10 @@
 
   // The Watchdog thread sets this to true.
   std::atomic<bool> watchdog_thread_started;
+
+  // Engine diagnostic sink, protected by a spinlock.
+  std::atomic<bool> diagnostic_sink_spinlock;
+  const FuzzTestDiagnosticSink* diagnostic_sink;
 };
 
 extern ExplicitLifetime<GlobalRunnerState> state;
diff --git a/centipede/runner_interface.h b/centipede/runner_interface.h
index c622daf..a06963e 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);
 
+// Legacy support for the libFuzzer API ends 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/e2e_tests/functional_test.cc b/e2e_tests/functional_test.cc
index 6133473..f6d5e86 100644
--- a/e2e_tests/functional_test.cc
+++ b/e2e_tests/functional_test.cc
@@ -1525,7 +1525,7 @@
   EXPECT_THAT_LOG(std_err,
                   HasSubstr("Skipping SkippedTestFixturePerTest.SkippedTest"));
   EXPECT_THAT_LOG(std_err, Not(HasSubstr("SkippedTest should not be run")));
-  EXPECT_THAT(status, Eq(ExitCode(0)));
+  EXPECT_THAT_LOG(std_err, Not(HasSubstr("SETUP FAILURE: Test is skipped")));
 }
 
 TEST_P(FuzzingModeFixtureTest,
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..9c3813c 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);
@@ -505,96 +510,96 @@
       absl::FPrintF(GetStderr(),
                     "[.] Skipping %s per request from the test setup.\n",
                     fuzzer_impl_.test_.full_name());
-      CentipedeSetFailureDescription("SKIPPED TEST: Requested from setup");
-      return true;
+      if (const char* indicator =
+              std::getenv("FUZZTEST_SKIPPED_TEST_INDICATOR_FILE");
+          indicator != nullptr) {
+        absl::FPrintF(GetStderr(), "[.] Touching the indicator file %s\n",
+                      indicator);
+        WriteFile(indicator, "");
+      }
+      CentipedeSetFailureDescription("SETUP FAILURE: Test is skipped");
+      return false;
     }
     if (runtime_.termination_requested()) {
       absl::FPrintF(GetStderr(),
-                    "[.] Termination requested - exiting without executing "
-                    "further inputs.\n");
-      CentipedeSetFailureDescription("IGNORED FAILURE: Termination requested");
+                    "[.] Termination requested - not executing input.\n");
       return false;
     }
     // 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 +609,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 {
@@ -963,16 +968,27 @@
   }
   // Run as the fuzzing engine.
   int result = EXIT_FAILURE;
+  TempDir temp_dir;
+  const std::string skipped_test_indicator_file =
+      temp_dir.path() / "skipped_test";
+  std::error_code ec;
+  runtime_.SetShouldTerminateOnNonFatalFailure(false);
+
   [&] {
-    runtime_.SetShouldTerminateOnNonFatalFailure(false);
-    std::unique_ptr<TempDir> workdir;
-    if (configuration.corpus_database.empty() ||
-        (mode == RunMode::kUnitTest && configuration.workdir_root.empty())) {
-      workdir = std::make_unique<TempDir>("fuzztest_workdir");
-    }
-    const std::string workdir_path = workdir ? workdir->path() : "";
-    const auto env = CreateCentipedeEnvironmentFromConfiguration(
-        configuration, workdir_path, test_.full_name(), mode);
+    const auto env = [&] {
+      std::string workdir_path;
+      if (configuration.corpus_database.empty() ||
+          (mode == RunMode::kUnitTest && configuration.workdir_root.empty())) {
+        workdir_path = temp_dir.path() / "workdir";
+      }
+      auto env = CreateCentipedeEnvironmentFromConfiguration(
+          configuration, workdir_path, test_.full_name(), mode);
+      env.env_diff_for_binaries.push_back(
+          absl::StrCat("FUZZTEST_SKIPPED_TEST_INDICATOR_FILE=",
+                       skipped_test_indicator_file));
+      return env;
+    }();
+
     if (const char* minimize_dir_chars =
             std::getenv("FUZZTEST_MINIMIZE_TESTSUITE_DIR")) {
       const std::string minimize_dir = minimize_dir_chars;
@@ -994,17 +1010,25 @@
       replay_env.corpus_dir = {"", minimize_dir};
       replay_env.load_shards_only = true;
       replay_env.report_crash_summary = false;
-      FUZZTEST_CHECK(
-          RunCentipede(replay_env, configuration.centipede_command) == 0)
-          << "Failed to replaying the testsuite for minimization";
+      result = RunCentipede(replay_env, configuration.centipede_command);
+      if (std::filesystem::exists(skipped_test_indicator_file, ec)) {
+        return;
+      }
+      if (result != 0) {
+        absl::FPrintF(GetStderr(),
+                      "[!] Failed to replaying the corpus for minimization");
+        return;
+      }
       absl::FPrintF(GetStderr(), "[.] Imported the corpus from %s.\n",
                     minimize_dir);
       // 2. Run Centipede distillation on the shard.
       auto distill_env = env;
       distill_env.distill = true;
-      FUZZTEST_CHECK(
-          RunCentipede(distill_env, configuration.centipede_command) == 0)
-          << "Failed to minimize the testsuite";
+      result = RunCentipede(distill_env, configuration.centipede_command);
+      if (result != 0) {
+        absl::FPrintF(GetStderr(), "[!] Failed to minimize the testsuite");
+        return;
+      }
       absl::FPrintF(GetStderr(),
                     "[.] Minimized the corpus using Centipede distillation.\n");
       // 3. Replace the shard corpus data with the distillation result.
@@ -1017,9 +1041,13 @@
       // 4. Export the corpus of the shard.
       auto export_env = env;
       export_env.corpus_to_files = corpus_out_dir;
-      FUZZTEST_CHECK(
-          RunCentipede(export_env, configuration.centipede_command) == 0)
-          << "Failed to export the corpus to FUZZTEST_MINIMIZE_TESTSUITE_DIR";
+      result = RunCentipede(export_env, configuration.centipede_command);
+      if (result != 0) {
+        absl::FPrintF(
+            GetStderr(),
+            "Failed to export the corpus to FUZZTEST_MINIMIZE_TESTSUITE_DIR");
+        return;
+      }
       absl::FPrintF(GetStderr(),
                     "[.] Exported the minimized the corpus to %s.\n",
                     corpus_out_dir);
@@ -1027,6 +1055,9 @@
       return;
     }
     result = RunCentipede(env, configuration.centipede_command);
+    if (std::filesystem::exists(skipped_test_indicator_file, ec)) {
+      return;
+    }
     if (!env.workdir.empty()) {
       if (runtime_.termination_requested()) {
         absl::FPrintF(
@@ -1045,6 +1076,13 @@
       }
     }
   }();
+  if (std::filesystem::exists(skipped_test_indicator_file, ec)) {
+    absl::FPrintF(
+        GetStderr(),
+        "[.] Indicator file for skipped test found - ignoring any failures.\n");
+    runtime_.SetSkippingRequested(true);
+    return true;
+  }
   return result == 0;
 }
 
diff --git a/fuzztest/internal/googletest_adaptor.h b/fuzztest/internal/googletest_adaptor.h
index 4571781..2a2d2b5 100644
--- a/fuzztest/internal/googletest_adaptor.h
+++ b/fuzztest/internal/googletest_adaptor.h
@@ -85,17 +85,22 @@
         EXPECT_TRUE(false) << "Death test is not supported.";
 #endif
       } else {
-        EXPECT_TRUE(test->RunInUnitTestMode(configuration_))
+        EXPECT_TRUE(test->RunInUnitTestMode(configuration_) ||
+                    Runtime::instance().skipping_requested())
             << "Failure(s) found in the unit-test mode - please see the test "
                "log for more details.";
       }
     } else {
       // TODO(b/245753736): Consider using `tolerate_failure` when FuzzTest can
       // tolerate crashes in fuzzing mode.
-      EXPECT_TRUE(test->RunInFuzzingMode(argc_, argv_, configuration_))
+      EXPECT_TRUE(test->RunInFuzzingMode(argc_, argv_, configuration_) ||
+                  Runtime::instance().skipping_requested())
           << "Failure(s) found in the fuzzing mode - please see the test log "
              "for more details.";
     }
+    if (Runtime::instance().skipping_requested()) {
+      GTEST_SKIP() << "Test was skipped";
+    }
   }
 
   static void SetUpTestSuite() {