Move TempDir to io lib so it can be reused.

PiperOrigin-RevId: 591024118
diff --git a/e2e_tests/functional_test.cc b/e2e_tests/functional_test.cc
index c4cb79d..7cadbab 100644
--- a/e2e_tests/functional_test.cc
+++ b/e2e_tests/functional_test.cc
@@ -86,22 +86,6 @@
   return binary_path;
 }
 
-class TempDir {
- public:
-  TempDir() {
-    dirname_ = "/tmp/replay_test_XXXXXX";
-    dirname_ = mkdtemp(dirname_.data());
-    EXPECT_TRUE(std::filesystem::is_directory(dirname_));
-  }
-
-  const std::string& dirname() const { return dirname_; }
-
-  ~TempDir() { std::filesystem::remove_all(dirname_); }
-
- private:
-  std::string dirname_;
-};
-
 class UnitTestModeTest : public ::testing::Test {
  protected:
   void SetUp() override {
@@ -683,11 +667,11 @@
 
   auto [status, std_out, std_err] =
       RunWith({{"fuzz", "MySuite.String"}},
-              {{"FUZZTEST_REPRODUCERS_OUT_DIR", out_dir.dirname()}});
+              {{"FUZZTEST_REPRODUCERS_OUT_DIR", out_dir.path()}});
   EXPECT_THAT(std_err, HasSubstr("argument 0: \"Fuzz"));
   EXPECT_THAT(status, Eq(Signal(SIGABRT)));
 
-  auto replay_files = ReadFileOrDirectory(out_dir.dirname());
+  auto replay_files = ReadFileOrDirectory(out_dir.path());
   ASSERT_EQ(replay_files.size(), 1) << std_err;
   auto parsed = IRObject::FromString(replay_files[0].data);
   ASSERT_TRUE(parsed) << std_err;
@@ -704,9 +688,9 @@
   // find the crash without saving some corpus.
   auto [status, std_out, std_err] =
       RunWith({{"fuzz", "MySuite.String"}},
-              {{"FUZZTEST_TESTSUITE_OUT_DIR", out_dir.dirname()}});
+              {{"FUZZTEST_TESTSUITE_OUT_DIR", out_dir.path()}});
 
-  auto corpus_files = ReadFileOrDirectory(out_dir.dirname());
+  auto corpus_files = ReadFileOrDirectory(out_dir.path());
   EXPECT_THAT(corpus_files, Not(IsEmpty())) << std_err;
 }
 
@@ -719,14 +703,14 @@
   // find the crash without saving some corpus.
   auto [producer_status, producer_std_out, producer_std_err] =
       RunWith({{"fuzz", "MySuite.String"}},
-              {{"FUZZTEST_TESTSUITE_OUT_DIR", corpus_dir.dirname()}});
+              {{"FUZZTEST_TESTSUITE_OUT_DIR", corpus_dir.path()}});
 
-  auto corpus_files = ReadFileOrDirectory(corpus_dir.dirname());
+  auto corpus_files = ReadFileOrDirectory(corpus_dir.path());
   ASSERT_THAT(corpus_files, Not(IsEmpty())) << producer_std_err;
 
   auto [consumer_status, consumer_std_out, consumer_std_err] =
       RunWith({{"fuzz", "MySuite.String"}},
-              {{"FUZZTEST_TESTSUITE_IN_DIR", corpus_dir.dirname()}});
+              {{"FUZZTEST_TESTSUITE_IN_DIR", corpus_dir.path()}});
   EXPECT_THAT(consumer_std_err,
               HasSubstr(absl::StrFormat("Parsed %d inputs and ignored 0 inputs",
                                         corpus_files.size())));
@@ -742,9 +726,9 @@
   // find the crash without saving some corpus.
   auto [producer_status, producer_std_out, producer_std_err] =
       RunWith({{"fuzz", "MySuite.String"}},
-              {{"FUZZTEST_TESTSUITE_OUT_DIR", corpus_dir.dirname()}});
+              {{"FUZZTEST_TESTSUITE_OUT_DIR", corpus_dir.path()}});
 
-  auto corpus_files = ReadFileOrDirectory(corpus_dir.dirname());
+  auto corpus_files = ReadFileOrDirectory(corpus_dir.path());
   ASSERT_THAT(corpus_files, Not(IsEmpty())) << producer_std_err;
   std::vector<std::string> corpus_data;
   for (const FilePathAndData& corpus_file : corpus_files) {
@@ -753,11 +737,11 @@
 
   auto [minimizer_status, minimizer_std_out, minimizer_std_err] =
       RunWith({{"fuzz", "MySuite.String"}},
-              {{"FUZZTEST_MINIMIZE_TESTSUITE_DIR", corpus_dir.dirname()},
-               {"FUZZTEST_TESTSUITE_OUT_DIR", minimized_corpus_dir.dirname()}});
+              {{"FUZZTEST_MINIMIZE_TESTSUITE_DIR", corpus_dir.path()},
+               {"FUZZTEST_TESTSUITE_OUT_DIR", minimized_corpus_dir.path()}});
 
   auto minimized_corpus_files =
-      ReadFileOrDirectory(minimized_corpus_dir.dirname());
+      ReadFileOrDirectory(minimized_corpus_dir.path());
   EXPECT_THAT(minimized_corpus_files,
               AllOf(Not(IsEmpty()), SizeIs(Le(corpus_files.size()))))
       << minimizer_std_err;
@@ -786,9 +770,9 @@
   // find the crash without saving some corpus.
   auto [producer_status, producer_std_out, producer_std_err] =
       RunWith({{"fuzz", "MySuite.String"}},
-              {{"FUZZTEST_TESTSUITE_OUT_DIR", corpus_dir.dirname()}});
+              {{"FUZZTEST_TESTSUITE_OUT_DIR", corpus_dir.path()}});
 
-  auto corpus_files = ReadFileOrDirectory(corpus_dir.dirname());
+  auto corpus_files = ReadFileOrDirectory(corpus_dir.path());
   ASSERT_THAT(corpus_files, Not(IsEmpty())) << producer_std_err;
   for (const auto& corpus_file : corpus_files) {
     ASSERT_TRUE(WriteFile(corpus_file.path + "_dup", corpus_file.data));
@@ -796,11 +780,11 @@
 
   auto [minimizer_status, minimizer_std_out, minimizer_std_err] =
       RunWith({{"fuzz", "MySuite.String"}},
-              {{"FUZZTEST_MINIMIZE_TESTSUITE_DIR", corpus_dir.dirname()},
-               {"FUZZTEST_TESTSUITE_OUT_DIR", minimized_corpus_dir.dirname()}});
+              {{"FUZZTEST_MINIMIZE_TESTSUITE_DIR", corpus_dir.path()},
+               {"FUZZTEST_TESTSUITE_OUT_DIR", minimized_corpus_dir.path()}});
 
   auto minimized_corpus_files =
-      ReadFileOrDirectory(minimized_corpus_dir.dirname());
+      ReadFileOrDirectory(minimized_corpus_dir.path());
   EXPECT_THAT(minimized_corpus_files,
               AllOf(Not(IsEmpty()), SizeIs(Le(corpus_files.size()))))
       << minimizer_std_err;
@@ -828,7 +812,7 @@
  public:
   template <typename T>
   ReplayFile(std::in_place_t, const T& corpus) {
-    filename_ = absl::StrCat(dir_.dirname(), "/replay_file");
+    filename_ = absl::StrCat(dir_.path(), "/replay_file");
     WriteFile(filename_, internal::IRObject::FromCorpus(corpus).ToString());
   }
 
@@ -876,11 +860,11 @@
 
   auto [status, std_out, std_err] =
       RunWith({{"fuzz", "MySuite.WithDomainClass"}},
-              {{"FUZZTEST_REPRODUCERS_OUT_DIR", out_dir.dirname()}});
+              {{"FUZZTEST_REPRODUCERS_OUT_DIR", out_dir.path()}});
   EXPECT_THAT(std_err, HasSubstr("argument 0: 10")) << std_err;
   EXPECT_THAT(status, Ne(ExitCode(0))) << std_err;
 
-  auto replay_files = ReadFileOrDirectory(out_dir.dirname());
+  auto replay_files = ReadFileOrDirectory(out_dir.path());
   ASSERT_EQ(replay_files.size(), 1) << std_err;
   auto parsed = IRObject::FromString(replay_files[0].data);
   ASSERT_TRUE(parsed) << std_err;
@@ -913,14 +897,14 @@
     TempDir out_dir;
     ReplayFile replay(std::in_place, std::tuple<std::string>{current_input});
     auto env = replay.GetMinimizeEnv();
-    env["FUZZTEST_REPRODUCERS_OUT_DIR"] = out_dir.dirname();
+    env["FUZZTEST_REPRODUCERS_OUT_DIR"] = out_dir.path();
 
     auto [status, std_out, std_err] =
         RunWith({{"fuzz", "MySuite.Minimizer"}}, env);
     ASSERT_THAT(std_err, HasSubstr("argument 0: \""));
     ASSERT_THAT(status, Eq(Signal(SIGABRT)));
 
-    auto replay_files = ReadFileOrDirectory(out_dir.dirname());
+    auto replay_files = ReadFileOrDirectory(out_dir.path());
     ASSERT_EQ(replay_files.size(), 1) << std_err;
     auto parsed = IRObject::FromString(replay_files[0].data);
     ASSERT_TRUE(parsed) << std_err;
@@ -1079,7 +1063,7 @@
     TempDir workdir;
     return RunCommand(
         {CentipedePath(), "--print_runner_log", "--exit_on_crash",
-         absl::StrCat("--workdir=", workdir.dirname()),
+         absl::StrCat("--workdir=", workdir.path()),
          absl::StrCat("--binary=", BinaryPath(kDefaultTargetBinary), " ",
                       CreateFuzzTestFlag("fuzz", test_name)),
          absl::StrCat("--num_runs=", iterations)},
@@ -1199,7 +1183,7 @@
     environment["ASAN_OPTIONS"] = "handle_aborts=0";
     return RunCommand({CentipedePath(), "--exit_on_crash",
                        absl::StrCat("--stop_at=", absl::Now() + timeout),
-                       absl::StrCat("--workdir=", workdir.dirname()),
+                       absl::StrCat("--workdir=", workdir.path()),
                        absl::StrCat("--binary=", BinaryPath(target_binary), " ",
                                     CreateFuzzTestFlag("fuzz", test_name))},
                       environment, timeout + absl::Seconds(10));
diff --git a/fuzztest/BUILD b/fuzztest/BUILD
index f615e1c..d5e6527 100644
--- a/fuzztest/BUILD
+++ b/fuzztest/BUILD
@@ -155,17 +155,33 @@
     srcs = ["internal/centipede_adaptor.cc"],
     hdrs = ["internal/centipede_adaptor.h"],
     defines = ["FUZZTEST_USE_CENTIPEDE"],
+    linkopts = [
+        # Needed for linking the Centipede engine with the runner, due to
+        # the common source code built separately for the engine and runner.
+        "-Wl,--warn-backrefs-exclude=*/centipede/*",
+    ],
     deps = [
         ":configuration",
         ":coverage",
         ":domain_core",
         ":fixture_driver",
+        ":io",
         ":logging",
         ":runtime",
         "@com_google_absl//absl/algorithm:container",
+        "@com_google_absl//absl/strings",
+        "@com_google_absl//absl/strings:str_format",
         "@com_google_absl//absl/strings:string_view",
+        "@com_google_absl//absl/time",
         "@com_google_absl//absl/types:span",
+        "@com_google_fuzztest//centipede:centipede_callbacks",
+        "@com_google_fuzztest//centipede:centipede_interface",
         "@com_google_fuzztest//centipede:centipede_runner_no_main",
+        "@com_google_fuzztest//centipede:defs",
+        "@com_google_fuzztest//centipede:early_exit",
+        "@com_google_fuzztest//centipede:environment",
+        "@com_google_fuzztest//centipede:runner_result",
+        "@com_google_fuzztest//centipede:shared_memory_blob_sequence",
     ],
 )
 
diff --git a/fuzztest/internal/centipede_adaptor.cc b/fuzztest/internal/centipede_adaptor.cc
index 46eb4ab..c506909 100644
--- a/fuzztest/internal/centipede_adaptor.cc
+++ b/fuzztest/internal/centipede_adaptor.cc
@@ -14,8 +14,12 @@
 
 #include "./fuzztest/internal/centipede_adaptor.h"
 
+#include <sys/mman.h>
+
 #include <cstddef>
 #include <cstdint>
+#include <cstdio>
+#include <cstdlib>
 #include <cstring>
 #include <functional>
 #include <memory>
@@ -26,12 +30,26 @@
 #include <vector>
 
 #include "absl/algorithm/container.h"
+#include "absl/strings/match.h"
+#include "absl/strings/numbers.h"
+#include "absl/strings/str_cat.h"
+#include "absl/strings/str_format.h"
 #include "absl/strings/string_view.h"
+#include "absl/time/clock.h"
+#include "absl/time/time.h"
 #include "absl/types/span.h"
+#include "./centipede/centipede_callbacks.h"
+#include "./centipede/centipede_interface.h"
+#include "./centipede/defs.h"
+#include "./centipede/early_exit.h"
+#include "./centipede/environment.h"
 #include "./centipede/runner_interface.h"
+#include "./centipede/runner_result.h"
+#include "./centipede/shared_memory_blob_sequence.h"
 #include "./fuzztest/internal/configuration.h"
 #include "./fuzztest/internal/coverage.h"
 #include "./fuzztest/internal/domains/domain_base.h"
+#include "./fuzztest/internal/io.h"
 #include "./fuzztest/internal/logging.h"
 #include "./fuzztest/internal/runtime.h"
 
@@ -45,16 +63,59 @@
   return std::seed_seq({seed, seed >> 32});
 }
 
+centipede::Environment CreateDefaultCentipedeEnvironment() {
+  centipede::Environment env;
+  // Skip input timeout as we don't yet support it in FuzzTest.
+  env.timeout_per_input = 0;
+  // Do not limit the address space as the fuzzing engine needs a
+  // lot of address space. rss_limit_mb will be used for OOM
+  // detection.
+  env.address_space_limit_mb = 0;
+  return env;
+}
+
+centipede::Environment CreateCentipedeEnvironmentFromFuzzTestFlags(
+    const Runtime& runtime, absl::string_view workdir,
+    absl::string_view test_name) {
+  centipede::Environment env = CreateDefaultCentipedeEnvironment();
+  env.workdir = workdir;
+  env.exit_on_crash = true;
+  // Populating the PC table in single-process mode is not implemented.
+  env.require_pc_table = false;
+  if (runtime.fuzz_time_limit() != absl::InfiniteDuration()) {
+    absl::FPrintF(GetStderr(), "[.] Fuzzing timeout set to: %s\n",
+                  absl::FormatDuration(runtime.fuzz_time_limit()));
+    env.stop_at = absl::Now() + runtime.fuzz_time_limit();
+  }
+  env.first_corpus_dir_output_only = true;
+  if (const char* corpus_out_dir_chars = getenv("FUZZTEST_TESTSUITE_OUT_DIR")) {
+    env.corpus_dir.push_back(corpus_out_dir_chars);
+  } else {
+    env.corpus_dir.push_back("");
+  }
+  if (const char* corpus_in_dir_chars = getenv("FUZZTEST_TESTSUITE_IN_DIR"))
+    env.corpus_dir.push_back(corpus_in_dir_chars);
+  if (const char* max_fuzzing_runs = getenv("FUZZTEST_MAX_FUZZING_RUNS")) {
+    if (!absl::SimpleAtoi(max_fuzzing_runs, &env.num_runs)) {
+      absl::FPrintF(GetStderr(),
+                    "[!] Cannot parse env FUZZTEST_MAX_FUZZING_RUNS=%s - will "
+                    "not limit fuzzing runs.\n",
+                    max_fuzzing_runs);
+    }
+  }
+  return env;
+}
+
 }  // namespace
 
 class CentipedeAdaptorRunnerCallbacks : public centipede::RunnerCallbacks {
  public:
   CentipedeAdaptorRunnerCallbacks(Runtime& runtime,
                                   FuzzTestFuzzerImpl& fuzzer_impl,
-                                  const Configuration& configuration)
+                                  const Configuration* configuration)
       : runtime_(runtime),
         fuzzer_impl_(fuzzer_impl),
-        configuration_(configuration),
+        configuration_(*configuration),
         prng_(GetRandomSeed()) {
     if (GetExecutionCoverage() == nullptr) {
       execution_coverage_ = std::make_unique<ExecutionCoverage>(
@@ -62,14 +123,6 @@
       execution_coverage_->SetIsTracing(true);
       SetExecutionCoverage(execution_coverage_.get());
     }
-    runtime_.EnableReporter(&fuzzer_impl_.stats_, [] { return absl::Now(); });
-    if (IsSilenceTargetEnabled()) SilenceTargetStdoutAndStderr();
-    FUZZTEST_INTERNAL_CHECK(fuzzer_impl_.fixture_driver_ != nullptr,
-                            "Invalid fixture driver!");
-    fuzzer_impl_.fixture_driver_->SetUpFuzzTest();
-    // Always create a new domain input to trigger any domain setup
-    // failures here. (e.g. Ineffective Filter)
-    fuzzer_impl_.params_domain_->UntypedInit(prng_);
   }
 
   bool Execute(centipede::ByteSpan input) override {
@@ -139,9 +192,6 @@
   }
 
   ~CentipedeAdaptorRunnerCallbacks() override {
-    FUZZTEST_INTERNAL_CHECK(fuzzer_impl_.fixture_driver_ != nullptr,
-                            "Invalid fixture driver!");
-    fuzzer_impl_.fixture_driver_->TearDownFuzzTest();
     if (GetExecutionCoverage() == execution_coverage_.get())
       SetExecutionCoverage(nullptr);
   }
@@ -170,11 +220,130 @@
 
   Runtime& runtime_;
   FuzzTestFuzzerImpl& fuzzer_impl_;
-  Configuration configuration_;
+  const Configuration& configuration_;
   std::unique_ptr<ExecutionCoverage> execution_coverage_;
   absl::BitGen prng_;
 };
 
+namespace {
+
+class CentipedeAdaptorEngineCallbacks : public centipede::CentipedeCallbacks {
+ public:
+  CentipedeAdaptorEngineCallbacks(const centipede::Environment& env,
+                                  Runtime& runtime,
+                                  FuzzTestFuzzerImpl& fuzzer_impl,
+                                  const Configuration* configuration)
+      : centipede::CentipedeCallbacks(env),
+        runtime_(runtime),
+        runner_callbacks_(runtime, fuzzer_impl, configuration),
+        batch_result_buffer_size_(env.shmem_size_mb * 1024 * 1024),
+        batch_result_buffer_(nullptr) {}
+
+  ~CentipedeAdaptorEngineCallbacks() {
+    if (batch_result_buffer_ != nullptr)
+      munmap(batch_result_buffer_, batch_result_buffer_size_);
+  }
+
+  bool Execute(std::string_view binary,
+               const std::vector<centipede::ByteArray>& inputs,
+               centipede::BatchResult& batch_result) override {
+    // Execute the test in-process.
+    batch_result.ClearAndResize(inputs.size());
+    size_t buffer_offset = 0;
+    if (batch_result_buffer_ == nullptr) {
+      // Use mmap which allocates memory on demand to reduce sanitizer overhead.
+      batch_result_buffer_ = static_cast<uint8_t*>(
+          mmap(nullptr, batch_result_buffer_size_, PROT_READ | PROT_WRITE,
+               MAP_PRIVATE | MAP_ANONYMOUS, -1, 0));
+      FUZZTEST_INTERNAL_CHECK(
+          batch_result_buffer_ != MAP_FAILED,
+          "Cannot mmap anonymous memory for batch result buffer");
+    }
+    CentipedeBeginExecutionBatch();
+    for (const auto& input : inputs) {
+      if (runtime_.termination_requested()) break;
+      if (buffer_offset >= batch_result_buffer_size_) break;
+      CentipedePrepareProcessing();
+      runner_callbacks_.Execute(input);
+      CentipedeFinalizeProcessing();
+      buffer_offset += CentipedeGetExecutionResult(
+          batch_result_buffer_ + buffer_offset,
+          batch_result_buffer_size_ - buffer_offset);
+    }
+    CentipedeEndExecutionBatch();
+    if (buffer_offset > 0) {
+      centipede::BlobSequence batch_result_blobseq(batch_result_buffer_,
+                                                   buffer_offset);
+      batch_result.Read(batch_result_blobseq);
+    }
+    if (runtime_.termination_requested() && !centipede::EarlyExitRequested()) {
+      absl::FPrintF(GetStderr(), "[.] Early termination requested.\n");
+      centipede::RequestEarlyExit(0);
+    }
+    return true;
+  }
+
+  size_t GetSeeds(size_t num_seeds,
+                  std::vector<centipede::ByteArray>& seeds) override {
+    seeds.clear();
+    size_t num_avail_seeds = 0;
+    runner_callbacks_.GetSeeds([&](centipede::ByteSpan seed) {
+      ++num_avail_seeds;
+      if (seeds.size() < num_seeds) {
+        seeds.emplace_back(seed.begin(), seed.end());
+      }
+    });
+    return num_avail_seeds;
+  }
+
+  void Mutate(const std::vector<centipede::MutationInputRef>& inputs,
+              size_t num_mutants,
+              std::vector<centipede::ByteArray>& mutants) override {
+    mutants.clear();
+    runner_callbacks_.Mutate(
+        inputs, num_mutants, [&](centipede::ByteSpan mutant) {
+          mutants.emplace_back(mutant.begin(), mutant.end());
+        });
+    if (runtime_.termination_requested() && !centipede::EarlyExitRequested()) {
+      absl::FPrintF(GetStderr(), "[.] Early termination requested.\n");
+      centipede::RequestEarlyExit(0);
+    }
+  }
+
+ private:
+  Runtime& runtime_;
+  CentipedeAdaptorRunnerCallbacks runner_callbacks_;
+  size_t batch_result_buffer_size_;
+  uint8_t* batch_result_buffer_;
+  std::unique_ptr<ExecutionCoverage> execution_coverage_;
+};
+
+class CentipedeAdaptorEngineCallbacksFactory
+    : public centipede::CentipedeCallbacksFactory {
+ public:
+  CentipedeAdaptorEngineCallbacksFactory(Runtime& runtime,
+                                         FuzzTestFuzzerImpl& fuzzer_impl,
+                                         const Configuration* configuration)
+      : runtime_(runtime),
+        fuzzer_impl_(fuzzer_impl),
+        configuration_(configuration) {}
+
+  centipede::CentipedeCallbacks* create(
+      const centipede::Environment& env) override {
+    return new CentipedeAdaptorEngineCallbacks(env, runtime_, fuzzer_impl_,
+                                               configuration_);
+  }
+
+  void destroy(centipede::CentipedeCallbacks* callbacks) { delete callbacks; }
+
+ private:
+  Runtime& runtime_;
+  FuzzTestFuzzerImpl& fuzzer_impl_;
+  const Configuration* configuration_;
+};
+
+}  // namespace
+
 CentipedeFuzzerAdaptor::CentipedeFuzzerAdaptor(
     const FuzzTest& test, std::unique_ptr<UntypedFixtureDriver> fixture_driver)
     : test_(test), fuzzer_impl_(test_, std::move(fixture_driver)) {}
@@ -187,13 +356,100 @@
 
 int CentipedeFuzzerAdaptor::RunInFuzzingMode(
     int* argc, char*** argv, const Configuration& configuration) {
+  FUZZTEST_INTERNAL_CHECK(fuzzer_impl_.fixture_driver_ != nullptr,
+                          "Invalid fixture driver!");
   runtime_.SetRunMode(RunMode::kFuzz);
   runtime_.SetCurrentTest(&test_);
-  CentipedeAdaptorRunnerCallbacks runner_callback(runtime_, fuzzer_impl_,
-                                                  configuration);
-  return centipede::RunnerMain(argc != nullptr ? *argc : 0,
-                               argv != nullptr ? *argv : nullptr,
-                               runner_callback);
+  if (IsSilenceTargetEnabled()) SilenceTargetStdoutAndStderr();
+  runtime_.EnableReporter(&fuzzer_impl_.stats_, [] { return absl::Now(); });
+  fuzzer_impl_.fixture_driver_->SetUpFuzzTest();
+  // Always create a new domain input to trigger any domain setup
+  // failures here. (e.g. Ineffective Filter)
+  FuzzTestFuzzerImpl::PRNG prng;
+  fuzzer_impl_.params_domain_->UntypedInit(prng);
+  // When the CENTIPEDE_RUNNER_FLAGS env var exists, the current process is
+  // considered a child process spawned by the Centipede binary as the runner,
+  // and we should not run CentipedeMain in this process.
+  const bool runner_mode = getenv("CENTIPEDE_RUNNER_FLAGS");
+  const int result = ([&]() {
+    if (runner_mode) {
+      CentipedeAdaptorRunnerCallbacks runner_callbacks(runtime_, fuzzer_impl_,
+                                                       &configuration);
+      return centipede::RunnerMain(argc != nullptr ? *argc : 0,
+                                   argv != nullptr ? *argv : nullptr,
+                                   runner_callbacks);
+    }
+    // Centipede engine does not support replay and reproducer minimization
+    // (within the single process). So use the existing fuzztest implementation.
+    // This is fine because it does not require coverage instrumentation.
+    if (fuzzer_impl_.ReplayInputsIfAvailable(configuration)) return 0;
+    // Run as the fuzzing engine.
+    if (getenv("FUZZTEST_MINIMIZE_TESTSUITE_DIR")) {
+      absl::FPrintF(GetStderr(),
+                    "[!] Corpus minimization is not supported in the "
+                    "single-process mode. Consider using the Centipede binary "
+                    "in corpus distillation mode - see centipede/README.md.");
+      return 1;
+    }
+    TempDir workdir("/tmp/fuzztest-workdir-");
+    const auto env = CreateCentipedeEnvironmentFromFuzzTestFlags(
+        runtime_, workdir.path(), test_.full_name());
+    CentipedeAdaptorEngineCallbacksFactory factory(runtime_, fuzzer_impl_,
+                                                   &configuration);
+    return centipede::CentipedeMain(env, factory);
+  })();
+  fuzzer_impl_.fixture_driver_->TearDownFuzzTest();
+  if (result) std::exit(result);
+  if (!runner_mode) {
+    absl::FPrintF(GetStderr(), "\n[.] Fuzzing was terminated.\n");
+    runtime_.PrintFinalStatsOnDefaultSink();
+    absl::FPrintF(GetStderr(), "\n");
+  }
+  return 0;
 }
 
 }  // namespace fuzztest::internal
+
+// The code below is used at very early stage of the process. Cannot use
+// GetStderr().
+namespace {
+
+class CentipedeCallbacksForRunnerFlagsExtraction
+    : public centipede::CentipedeCallbacks {
+ public:
+  using centipede::CentipedeCallbacks::CentipedeCallbacks;
+
+  bool Execute(std::string_view binary,
+               const std::vector<centipede::ByteArray>& inputs,
+               centipede::BatchResult& batch_result) override {
+    return false;
+  }
+
+  std::string GetRunnerFlagsContent() {
+    constexpr absl::string_view kRunnerFlagPrefix = "CENTIPEDE_RUNNER_FLAGS=";
+    const std::string runner_flags = ConstructRunnerFlags();
+    if (!absl::StartsWith(runner_flags, kRunnerFlagPrefix)) {
+      absl::FPrintF(
+          stderr,
+          "[!] Unexpected prefix in Centipede runner flags - returning "
+          "without stripping the prefix.\n");
+      return runner_flags;
+    }
+    return runner_flags.substr(kRunnerFlagPrefix.size());
+  }
+};
+
+}  // namespace
+
+extern "C" const char* CentipedeGetRunnerFlags() {
+  if (const char* runner_flags_env = getenv("CENTIPEDE_RUNNER_FLAGS")) {
+    // Runner mode. Use the existing flags.
+    return strdup(runner_flags_env);
+  }
+  // Set the runner flags according to the FuzzTest default environment.
+  const auto env = fuzztest::internal::CreateDefaultCentipedeEnvironment();
+  CentipedeCallbacksForRunnerFlagsExtraction callbacks(env);
+  const std::string runner_flags = callbacks.GetRunnerFlagsContent();
+  absl::FPrintF(stderr, "[.] Centipede runner flags: %s\n", runner_flags);
+  return strdup(runner_flags.c_str());
+}
diff --git a/fuzztest/internal/io.cc b/fuzztest/internal/io.cc
index f3e4af7..45c71a1 100644
--- a/fuzztest/internal/io.cc
+++ b/fuzztest/internal/io.cc
@@ -14,8 +14,14 @@
 
 #include "./fuzztest/internal/io.h"
 
+#if defined(__APPLE__)
+// For mkdtemp
+#include <unistd.h>
+#endif
+
 #include <cerrno>
 #include <cstdio>
+#include <cstdlib>
 #include <cstring>
 #include <filesystem>
 #include <fstream>
@@ -23,6 +29,7 @@
 #include <sstream>
 #include <string>
 #include <string_view>
+#include <system_error>
 #include <utility>
 #include <vector>
 
@@ -40,6 +47,9 @@
 // Just stub out these functions.
 #define STUB_FILESYSTEM
 #endif
+#elif defined(_WIN32)
+// No mkdtemp.
+#define STUB_FILESYSTEM
 #endif
 
 namespace fuzztest::internal {
@@ -67,6 +77,13 @@
   FUZZTEST_INTERNAL_CHECK(false, "Can't replay in iOS/MacOS");
 }
 
+TempDir::TempDir(absl::string_view path_prefix) {
+  FUZZTEST_INTERNAL_CHECK(false, "Not implemented in iOS/MacOS");
+}
+TempDir::~TempDir() {
+  FUZZTEST_INTERNAL_CHECK(false, "Not implemented in iOS/MacOS");
+}
+
 #else  // defined(__APPLE__)
 
 bool WriteFile(absl::string_view filename, absl::string_view contents) {
@@ -142,6 +159,24 @@
   return out;
 }
 
+TempDir::TempDir(absl::string_view path_prefix) {
+  std::string filename = absl::StrFormat("%sXXXXXX", path_prefix);
+  const char* path = mkdtemp(filename.data());
+  const auto saved_errno = errno;
+  FUZZTEST_INTERNAL_CHECK(path, "Cannot create temporary dir with path prefix ",
+                          path_prefix, ": ", saved_errno);
+  path_ = path;
+}
+
+TempDir::~TempDir() {
+  std::error_code ec;
+  std::filesystem::remove_all(path_, ec);
+  if (ec) {
+    absl::FPrintF(GetStderr(), "[!] Unable to clean up temporary dir %s: %s",
+                  path_, ec.message());
+  }
+}
+
 #endif  // defined(STUB_FILESYSTEM)
 
 absl::string_view Basename(absl::string_view filename) {
diff --git a/fuzztest/internal/io.h b/fuzztest/internal/io.h
index 1066875..b5a174f 100644
--- a/fuzztest/internal/io.h
+++ b/fuzztest/internal/io.h
@@ -50,6 +50,19 @@
 // Returns the basename of `filename`.
 absl::string_view Basename(absl::string_view filename);
 
+// A temporary directory with `path_prefix` that will be cleaned up on object
+// destruction.
+class TempDir {
+ public:
+  explicit TempDir(absl::string_view path_prefix = "/tmp/");
+  ~TempDir();
+
+  const std::string& path() const { return path_; }
+
+ private:
+  std::string path_;
+};
+
 }  // namespace fuzztest::internal
 
 #endif  // FUZZTEST_FUZZTEST_INTERNAL_IO_H_
diff --git a/fuzztest/internal/io_test.cc b/fuzztest/internal/io_test.cc
index 98841f1..99cd87e 100644
--- a/fuzztest/internal/io_test.cc
+++ b/fuzztest/internal/io_test.cc
@@ -38,14 +38,15 @@
 using ::testing::IsEmpty;
 using ::testing::Optional;
 using ::testing::SizeIs;
+using ::testing::StartsWith;
 using ::testing::UnorderedElementsAre;
 
-std::string TmpFile(const std::string& name) {
+std::string TestTmpFile(const std::string& name) {
   std::string filename = absl::StrCat(testing::TempDir(), "/", name, "XXXXXX");
   return mktemp(filename.data());
 }
 
-std::string TmpDir(const std::string& name) {
+std::string TestTmpDir(const std::string& name) {
   std::string filename = absl::StrCat(testing::TempDir(), "/", name, "XXXXXX");
   return mkdtemp(filename.data());
 }
@@ -69,14 +70,14 @@
 }
 
 TEST(IOTest, WriteFileWorksWhenDirectoryExists) {
-  const std::string tmp_name = TmpFile("write_test");
+  const std::string tmp_name = TestTmpFile("write_test");
   EXPECT_TRUE(WriteFile(tmp_name, "Payload1"));
   EXPECT_EQ(TestRead(tmp_name), "Payload1");
   std::filesystem::remove(tmp_name);
 }
 
 TEST(IOTest, WriteFileWorksWhenDirectoryDoesNotExist) {
-  const std::string tmp_dir = TmpDir("write_test_dir");
+  const std::string tmp_dir = TestTmpDir("write_test_dir");
   const std::string tmp_name = absl::StrCat(tmp_dir, "/doesnt_exist/file");
   EXPECT_TRUE(WriteFile(tmp_name, "Payload1"));
   EXPECT_EQ(TestRead(tmp_name), "Payload1");
@@ -84,14 +85,14 @@
 }
 
 TEST(IOTest, WriteDataToDirReturnsWrittenFilePath) {
-  const std::string tmp_dir = TmpDir("write_test_dir");
+  const std::string tmp_dir = TestTmpDir("write_test_dir");
   const std::string path = WriteDataToDir("data", tmp_dir);
   EXPECT_THAT(ReadFile(path), Optional(Eq("data")));
   std::filesystem::remove_all(tmp_dir);
 }
 
 TEST(IOTest, WriteDataToDirWritesToSameFileOnSameData) {
-  const std::string tmp_dir = TmpDir("write_test_dir");
+  const std::string tmp_dir = TestTmpDir("write_test_dir");
   const std::string path = WriteDataToDir("data", tmp_dir);
   EXPECT_THAT(WriteDataToDir("data", tmp_dir), Eq(path));
   EXPECT_THAT(ReadFile(path), Optional(Eq("data")));
@@ -105,7 +106,7 @@
 }
 
 TEST(IOTest, ReadFileWorksWhenFileExists) {
-  const std::string tmp_name = TmpFile("read_test");
+  const std::string tmp_name = TestTmpFile("read_test");
   TestWrite(tmp_name, "Payload2");
   EXPECT_THAT(ReadFile(tmp_name), Optional(Eq("Payload2")));
   EXPECT_THAT(ReadFileOrDirectory(tmp_name),
@@ -114,7 +115,7 @@
 }
 
 TEST(IOTest, ReadFileOrDirectoryWorks) {
-  const std::string tmp_dir = TmpDir("write_test_dir");
+  const std::string tmp_dir = TestTmpDir("write_test_dir");
   EXPECT_THAT(ReadFileOrDirectory(tmp_dir), UnorderedElementsAre());
   const std::string tmp_file_1 = absl::StrCat(tmp_dir, "/file1");
   TestWrite(tmp_file_1, "Payload3.1");
@@ -129,7 +130,7 @@
 }
 
 TEST(IOTest, ReadFileOrDirectoryWorksRecursively) {
-  const std::string tmp_dir = TmpDir("test_dir");
+  const std::string tmp_dir = TestTmpDir("test_dir");
   const std::string tmp_sub_dir = absl::StrCat(tmp_dir, "/subdir");
   mkdir(tmp_sub_dir.c_str(), 0700);
   const std::string tmp_file_1 = absl::StrCat(tmp_dir, "/file1");
@@ -143,7 +144,7 @@
 }
 
 TEST(IOTest, ReadFilesFromDirectoryWorks) {
-  const std::string tmp_dir = TmpDir("write_test_dir");
+  const std::string tmp_dir = TestTmpDir("write_test_dir");
   EXPECT_THAT(ReadFilesFromDirectory(tmp_dir), UnorderedElementsAre());
   EXPECT_THAT(ReadFilesFromDirectory(tmp_dir), SizeIs(0));
   const std::string tmp_file_1 = absl::StrCat(tmp_dir, "/file1");
@@ -161,7 +162,7 @@
 }
 
 TEST(IOTest, ReadFilesFromDirectoryReturnsEmptyVectorWhenNoFilesInDir) {
-  const std::string tmp_dir = TmpDir("empty_dir");
+  const std::string tmp_dir = TestTmpDir("empty_dir");
   EXPECT_THAT(ReadFilesFromDirectory(tmp_dir), UnorderedElementsAre());
   EXPECT_THAT(ReadFileOrDirectory(tmp_dir), SizeIs(0));
   std::filesystem::remove_all(tmp_dir);
@@ -173,7 +174,7 @@
 }
 
 TEST(IOTest, ListDirectoryReturnsPathsInDirectory) {
-  const std::string tmp_dir = TmpDir("test_dir");
+  const std::string tmp_dir = TestTmpDir("test_dir");
   const std::string tmp_file_1 = absl::StrCat(tmp_dir, "/file1");
   TestWrite(tmp_file_1, /*contents=*/"File1");
   const std::string tmp_file_2 = absl::StrCat(tmp_dir, "/file2");
@@ -184,7 +185,7 @@
 }
 
 TEST(IOTest, ListDirectoryReturnsEmptyVectorWhenDirectoryIsEmpty) {
-  const std::string tmp_dir = TmpDir("empty_dir");
+  const std::string tmp_dir = TestTmpDir("empty_dir");
   EXPECT_THAT(ListDirectory(tmp_dir), IsEmpty());
   std::filesystem::remove_all(tmp_dir);
 }
@@ -193,5 +194,21 @@
   EXPECT_THAT(ListDirectory("/doesnt_exist/"), IsEmpty());
 }
 
+TEST(IOTest, TempDirGeneratesDirFollowingPathPrefix) {
+  TempDir temp_dir("/tmp/some_path_prefix");
+  EXPECT_THAT(temp_dir.path(), StartsWith("/tmp/some_path_prefix"));
+  EXPECT_TRUE(std::filesystem::is_directory(temp_dir.path()));
+}
+
+TEST(IOTest, TempDirRemovesDirOnDestruction) {
+  std::string temp_dir_path;
+  {
+    TempDir temp_dir("/tmp/some_path_prefix");
+    temp_dir_path = temp_dir.path();
+    EXPECT_TRUE(std::filesystem::is_directory(temp_dir_path));
+  }
+  EXPECT_FALSE(std::filesystem::exists(temp_dir_path));
+}
+
 }  // namespace
 }  // namespace fuzztest::internal
diff --git a/fuzztest/internal/runtime.h b/fuzztest/internal/runtime.h
index 97214e1..541ffb0 100644
--- a/fuzztest/internal/runtime.h
+++ b/fuzztest/internal/runtime.h
@@ -323,6 +323,7 @@
   // Defined in centipede_adaptor.cc
   friend class CentipedeFuzzerAdaptor;
   friend class CentipedeAdaptorRunnerCallbacks;
+  friend class CentipedeAdaptorEngineCallbacks;
 };
 
 }  // namespace internal