No public description

PiperOrigin-RevId: 947185670
diff --git a/centipede/centipede_test.cc b/centipede/centipede_test.cc
index 17383e8..c485781 100644
--- a/centipede/centipede_test.cc
+++ b/centipede/centipede_test.cc
@@ -86,7 +86,7 @@
     // i-th element is the number of bytes with the value 'i' in the input.
     // `counters` is converted to FeatureVec and added to
     // `batch_result.results()`.
-    for (auto &input : inputs) {
+    for (auto& input : inputs) {
       ByteArray counters(256);
       for (uint8_t byte : input) {
         counters[byte]++;
@@ -496,7 +496,7 @@
     // Must contain normal mutants, but not the ones from crossover.
     const auto mutant_data = GetDataFromMutants(result.mutants());
     EXPECT_THAT(mutant_data, IsSupersetOf(some_of_expected_mutants));
-    for (const auto &crossover_mutant : expected_crossover_mutants) {
+    for (const auto& crossover_mutant : expected_crossover_mutants) {
       EXPECT_THAT(mutant_data, Not(Contains(crossover_mutant)));
     }
   }
@@ -525,7 +525,7 @@
   std::vector<Mutant> Mutate(absl::Span<const MutationInputRef> inputs,
                              size_t num_mutants) override {
     std::vector<Mutant> mutants(num_mutants);
-    for (auto &mutant : mutants) {
+    for (auto& mutant : mutants) {
       mutant.data.resize(1);
       mutant.data[0] = ++number_of_mutations_;
       mutant.origin = Mutant::kOriginNone;
@@ -611,7 +611,7 @@
   // Sets the inputs to one of 3 pre-defined values.
   std::vector<Mutant> Mutate(absl::Span<const MutationInputRef> inputs,
                              size_t num_mutants) override {
-    for (auto &input : inputs) {
+    for (auto& input : inputs) {
       if (!seed_inputs_.contains(input.data)) {
         observed_inputs_.insert(input.data);
       }
@@ -628,8 +628,8 @@
   // Returns one of 3 pre-defined values, that trigger different code paths in
   // the test target.
   static ByteArray GetMutant(size_t idx) {
-    const char *mutants[3] = {"func1", "func2-A", "foo"};
-    const char *mutant = mutants[idx % 3];
+    const char* mutants[3] = {"func1", "func2-A", "foo"};
+    const char* mutant = mutants[idx % 3];
     return {mutant, mutant + strlen(mutant)};
   }
 
@@ -646,7 +646,7 @@
 // Runs a short fuzzing session with the provided `function_filter`.
 // Returns a sorted array of observed inputs.
 static std::vector<ByteArray> RunWithFunctionFilter(
-    std::string_view function_filter, const TempDir &tmp_dir) {
+    std::string_view function_filter, const TempDir& tmp_dir) {
   Environment env;
   env.workdir = tmp_dir.path();
   env.seed = 1;  // make the runs predictable.
@@ -717,9 +717,9 @@
   bool Execute(std::string_view binary, absl::Span<const ByteSpan> inputs,
                BatchResult& batch_result) override {
     bool res = true;
-    for (const auto &input : inputs) {
+    for (const auto& input : inputs) {
       if (input.size() != 1) continue;
-      for (const Crash &crash : crashes_) {
+      for (const Crash& crash : crashes_) {
         if (binary == crash.binary && input[0] == crash.input) {
           batch_result.exit_code() = EXIT_FAILURE;
           batch_result.failure_description() = crash.description;
@@ -736,7 +736,7 @@
   std::vector<Mutant> Mutate(absl::Span<const MutationInputRef> inputs,
                              size_t num_mutants) override {
     std::vector<Mutant> mutants(num_mutants);
-    for (auto &mutant : mutants) {
+    for (auto& mutant : mutants) {
       mutant.data.resize(1);
       mutant.data[0] = ++number_of_mutations_;
       mutant.origin = Mutant::kOriginNone;
@@ -754,20 +754,20 @@
   std::string file;
   std::string contents;
 
-  bool operator==(const FileAndContents &other) const {
+  bool operator==(const FileAndContents& other) const {
     return file == other.file && contents == other.contents;
   }
 
   template <typename Sink>
-  friend void AbslStringify(Sink &sink, const FileAndContents &f) {
+  friend void AbslStringify(Sink& sink, const FileAndContents& f) {
     absl::Format(&sink, "FileAndContents{%s, \"%s\"}", f.file, f.contents);
   }
 };
 
 MATCHER_P(HasFilesWithContents, expected_files_and_contents, "") {
-  const std::string &dir_path = arg;
+  const std::string& dir_path = arg;
   std::vector<FileAndContents> files_and_contents;
-  for (const auto &dir_ent : std::filesystem::directory_iterator(dir_path)) {
+  for (const auto& dir_ent : std::filesystem::directory_iterator(dir_path)) {
     auto file_and_contents = FileAndContents{dir_ent.path().filename()};
     ReadFromLocalFile(dir_ent.path().c_str(), file_and_contents.contents);
     files_and_contents.push_back(std::move(file_and_contents));
@@ -843,7 +843,7 @@
     if (!first_pass_) {
       num_inputs_triaged_ += inputs.size();
     }
-    for (const auto &input : inputs) {
+    for (const auto& input : inputs) {
       FUZZTEST_CHECK_EQ(input.size(), 1);  // By construction in `Mutate()`.
       // The contents of each mutant is its sequential number.
       if (input[0] == crashing_input_idx_) {
@@ -933,7 +933,7 @@
         absl::StrCat("crashing_batch-", crashing_input_hash);
     EXPECT_TRUE(std::filesystem::exists(crashes_dir_path)) << crashes_dir_path;
     std::vector<std::string> found_crash_file_names;
-    for (auto const &dir_ent :
+    for (auto const& dir_ent :
          std::filesystem::directory_iterator(crashes_dir_path)) {
       found_crash_file_names.push_back(dir_ent.path().filename());
     }
@@ -1400,8 +1400,9 @@
   env.test_name = "some_test";
   env.populate_binary_info = false;
   env.fork_server = false;
-  env.persistent_mode = false;
+  env.persistent_mode = true;
   env.exit_on_crash = true;
+  env.stop_at = absl::Now() + absl::Seconds(10);
   fuzztest::internal::DefaultCallbacksFactory<
       fuzztest::internal::CentipedeDefaultCallbacks>
       callbacks;
diff --git a/centipede/engine_worker.cc b/centipede/engine_worker.cc
index 01030d1..bf19708 100644
--- a/centipede/engine_worker.cc
+++ b/centipede/engine_worker.cc
@@ -12,6 +12,8 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 #include <fcntl.h>
+#include <sys/socket.h>
+#include <sys/un.h>
 #include <unistd.h>
 
 #include <algorithm>
@@ -105,7 +107,7 @@
 };
 
 // The first call of this function must be outside of signal handlers since it
-// allocates memory (enforced by `GetWorkerFlagsEarly`). After that it would be
+// allocates memory (enforced by `WorkerInitEarly`). After that it would be
 // signal-safe.
 //
 // The worker flags format is `:(NAME=VALUE|SWITCH:)+`. `GetWorkerFlags`
@@ -137,10 +139,6 @@
   return worker_flags;
 }
 
-__attribute__((constructor(200))) void GetWorkerFlagsEarly() {
-  (void)GetWorkerFlags();
-}
-
 // `header` should be in the form of `FLAG_NAME=`.
 //
 // Extracts "value" as a null-terminated string from "\0FLAG_NAME=value\0" in
@@ -237,6 +235,9 @@
     "arg1=";  // TODO: Use better flag names when standardizing the protocol.
 constexpr std::string_view kWorkerOutputsBlobSequencePathFlagHeader =
     "arg2=";  // TODO: Use better flag names when standardizing the protocol.
+constexpr std::string_view kWorkerPersistentModeSocketPathFlagHeader =
+    "persistent_mode_socket=";  // TODO: Use better flag names when
+                                // standardizing the protocol.
 
 struct WorkerState {
   std::atomic<bool> has_failure_output = false;
@@ -244,6 +245,11 @@
   std::atomic<bool> in_adapter_execute = false;
   std::atomic<bool> has_finding = false;
   std::atomic<bool> saved_binary_id = false;
+
+  void ResetForPersistentMode() {
+    has_failure_output.store(false, std::memory_order_relaxed);
+    has_finding.store(false, std::memory_order_relaxed);
+  }
 };
 
 WorkerState& GetWorkerState() {
@@ -323,6 +329,67 @@
   return {reinterpret_cast<const char*>(bytes.data()), bytes.size()};
 }
 
+// Zero initialized.
+static int persistent_mode_socket;
+
+__attribute__((constructor(200))) void WorkerInitEarly() {
+  const char* persistent_mode_socket_path =
+      GetWorkerFlag(kWorkerPersistentModeSocketPathFlagHeader);
+  if (persistent_mode_socket_path == nullptr) return;
+  persistent_mode_socket = socket(AF_UNIX, SOCK_STREAM, 0);
+  if (persistent_mode_socket < 0) {
+    WorkerLog(
+        "Failed to create persistent mode socket - not running persistent "
+        "mode.",
+        LogLnSync{});
+    return;
+  }
+
+  struct sockaddr_un addr{};
+  addr.sun_family = AF_UNIX;
+  const size_t socket_path_len = strlen(persistent_mode_socket_path);
+  WorkerCheck(
+      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, persistent_mode_socket_path, socket_path_len);
+
+  int connect_ret = 0;
+  do {
+    connect_ret =
+        connect(persistent_mode_socket, (struct sockaddr*)&addr, sizeof(addr));
+  } while (connect_ret == -1 && errno == EINTR);
+  if (connect_ret == -1) {
+    WorkerLog("Failed to connect the persistent mode socket to ",
+              persistent_mode_socket_path, LogLnSync{});
+    (void)close(persistent_mode_socket);
+    persistent_mode_socket = -1;
+    return;
+  }
+
+  int flags = fcntl(persistent_mode_socket, F_GETFD);
+  if (flags == -1) {
+    WorkerLog(
+        "fcntl(F_GETFD) failed on the persistent mode socket - exiting "
+        "persistent mode",
+        LogLnSync{});
+    (void)close(persistent_mode_socket);
+    persistent_mode_socket = -1;
+    return;
+  }
+  flags |= FD_CLOEXEC;
+  if (fcntl(persistent_mode_socket, F_SETFD, flags) == -1) {
+    WorkerLog(
+        "fcntl(F_SETFD) failed on the persistent mode socket - exiting "
+        "persistent mode",
+        LogLnSync{});
+    (void)close(persistent_mode_socket);
+    persistent_mode_socket = -1;
+    return;
+  }
+  WorkerLog("Persistent mode: connected to ", persistent_mode_socket_path,
+            LogLnSync{});
+}
+
 BlobSequence* GetInputsBlobSequence() {
   static auto result = []() -> BlobSequence* {
     if (!HasWorkerSwitchFlag("shmem")) {
@@ -695,6 +762,72 @@
   return test_name;
 }
 
+void HandlePersistentMode(const FuzzTestAdapter& adapter) {
+  auto* inputs_blobseq = GetInputsBlobSequence();
+  auto* outputs_blobseq = GetOutputsBlobSequence();
+  bool first = true;
+  while (true) {
+    PersistentModeRequest req;
+    if (!ReadAll(persistent_mode_socket, reinterpret_cast<char*>(&req), 1)) {
+      WorkerLog("Failed to read request from persistent mode socket: ",
+                LogErrNo{}, LogLnSync{});
+      return;
+    }
+    if (first) {
+      first = false;
+      WorkerLog("FuzzTest engine worker enter persistent mode", LogLnSync{});
+    } 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);
+      }
+      WorkerLog(
+          "FuzzTest engine worker (",
+          req == PersistentModeRequest::kExit ? "exiting persistent mode"
+                                              : "persistent mode batch",
+          "); flags: ",
+          GetWorkerFlags().present
+              ? std::string_view{GetWorkerFlags().str, GetWorkerFlags().len}
+              : "",
+          LogLnSync{});
+    }
+    if (req == PersistentModeRequest::kExit) break;
+    WorkerCheck(req == PersistentModeRequest::kRunBatch,
+                "Unknown persistent mode request");
+
+    inputs_blobseq->Reset();
+    outputs_blobseq->Reset();
+
+    GetWorkerState().ResetForPersistentMode();
+
+    // Read the first blob. It indicates what further actions to take.
+    auto request_type_blob = inputs_blobseq->Read();
+    if (IsMutationRequest(request_type_blob)) {
+      inputs_blobseq->Reset();
+      WorkerDoMutate(adapter);
+    } else if (IsExecutionRequest(request_type_blob)) {
+      inputs_blobseq->Reset();
+      WorkerDoExecute(adapter);
+    } else {
+      WorkerCheck(false, "Unknown shmem request");
+    }
+
+    const int result =
+        GetWorkerState().has_finding.load(std::memory_order_relaxed)
+            ? EXIT_FAILURE
+            : EXIT_SUCCESS;
+    if (!WriteAll(persistent_mode_socket,
+                  reinterpret_cast<const char*>(&result), sizeof(result))) {
+      WorkerLog("Failed to write response to the persistent mode socket: ",
+                LogErrNo{}, LogLnSync{});
+      return;
+    }
+  }
+}
+
 FuzzTestWorkerStatus WorkerRun(const FuzzTestAdapterManager& manager) {
   const auto& flags = GetWorkerFlags();
   WorkerCheck(flags.present, "worker flags must present");
@@ -764,7 +897,9 @@
   WorkerCheck(adapter.FreeInput != nullptr, "FreeInput must be defined");
   WorkerCheck(adapter.FreeCtx != nullptr, "FreeCtx must be defined");
 
-  if (action == WorkerAction::kTestGetSeeds) {
+  if (persistent_mode_socket > 0) {
+    HandlePersistentMode(adapter);
+  } else if (action == WorkerAction::kTestGetSeeds) {
     WorkerDoGetSeeds(adapter);
   } else if (action == WorkerAction::kTestMutate) {
     WorkerDoMutate(adapter);
diff --git a/centipede/testing/test_binary_for_engine_testing.cc b/centipede/testing/test_binary_for_engine_testing.cc
index 75c1a42..071f223 100644
--- a/centipede/testing/test_binary_for_engine_testing.cc
+++ b/centipede/testing/test_binary_for_engine_testing.cc
@@ -202,10 +202,10 @@
     return worker_status == kFuzzTestWorkerSuccess ? EXIT_SUCCESS
                                                    : EXIT_FAILURE;
   }
-  return ControllerRun(&manager, {absl::StrCat("--binary=", argv[0]),
-                                  "--test_name=some_test",
-                                  "--populate_binary_info=0", "--fork_server=0",
-                                  "--persistent_mode=0", "--exit_on_crash"}) ==
+  return ControllerRun(&manager,
+                       {absl::StrCat("--binary=", argv[0]),
+                        "--test_name=some_test", "--populate_binary_info=0",
+                        "--fork_server=0", "--exit_on_crash"}) ==
                  kFuzzTestControllerSuccess
              ? EXIT_SUCCESS
              : EXIT_FAILURE;