#Centipede Add a public API to set execution results.

This is to support fuzzing with remote coverage feedback.

Also, as a demonstration, add a test that communicates with an external server and uses the remote execution results from the server.

PiperOrigin-RevId: 636720213
diff --git a/centipede/BUILD b/centipede/BUILD
index 665c834..532f630 100644
--- a/centipede/BUILD
+++ b/centipede/BUILD
@@ -1133,6 +1133,7 @@
 cc_library(
     name = "centipede_runner",
     srcs = RUNNER_SOURCES_WITH_MAIN,
+    hdrs = ["runner_interface.h"],
     copts = RUNNER_COPTS,
     linkopts = RUNNER_LINKOPTS,
     linkstatic = True,  # Must be linked statically even when dynamic_mode=on.
diff --git a/centipede/runner.cc b/centipede/runner.cc
index 03ae0d2..d47f4bf 100644
--- a/centipede/runner.cc
+++ b/centipede/runner.cc
@@ -382,6 +382,12 @@
       tls.lowest_sp = tls.top_frame_sp;
     });
   }
+  {
+    centipede::LockGuard lock(state.execution_result_override_mu);
+    if (state.execution_result_override != nullptr) {
+      state.execution_result_override->ClearAndResize(0);
+    }
+  }
   if (!full_clear) return;
   state.ForEachTls([](ThreadLocalRunnerState &tls) {
     if (state.run_time_flags.use_auto_dictionary) {
@@ -670,6 +676,26 @@
 // Finishes sending the outputs (coverage, etc.) to `outputs_blobseq`.
 // Returns true on success.
 static bool FinishSendingOutputsToEngine(BlobSequence &outputs_blobseq) {
+  {
+    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) {
+      const auto &result = state.execution_result_override->results()[0];
+      return BatchResult::WriteOneFeatureVec(result.features().data(),
+                                             result.features().size(),
+                                             outputs_blobseq) &&
+             BatchResult::WriteMetadata(result.metadata(), outputs_blobseq) &&
+             BatchResult::WriteStats(result.stats(), outputs_blobseq) &&
+             BatchResult::WriteInputEnd(outputs_blobseq);
+    }
+  }
+
   // Copy features to shared memory.
   if (!BatchResult::WriteOneFeatureVec(
           state.g_features.data(), state.g_features.size(), outputs_blobseq)) {
@@ -1034,6 +1060,13 @@
     StartSendingOutputsToEngine(outputs_blobseq);
     FinishSendingOutputsToEngine(outputs_blobseq);
   }
+  {
+    LockGuard lock(state.execution_result_override_mu);
+    if (state.execution_result_override != nullptr) {
+      delete state.execution_result_override;
+      state.execution_result_override = nullptr;
+    }
+  }
   // Always clean up detached TLSs to avoid leakage.
   CleanUpDetachedTls();
 }
@@ -1187,3 +1220,18 @@
 extern "C" size_t CentipedeGetCoverageData(uint8_t *data, size_t capacity) {
   return centipede::CopyFeatures(data, capacity);
 }
+
+extern "C" void CentipedeSetExecutionResult(const uint8_t *data, size_t size) {
+  using centipede::state;
+  centipede::LockGuard lock(state.execution_result_override_mu);
+  if (!state.execution_result_override)
+    state.execution_result_override = new centipede::BatchResult();
+  state.execution_result_override->ClearAndResize(1);
+  if (data == nullptr) return;
+  // Removing const here should be fine as we don't write to `blobseq`.
+  centipede::BlobSequence blobseq(const_cast<uint8_t *>(data), size);
+  state.execution_result_override->Read(blobseq);
+  centipede::RunnerCheck(
+      state.execution_result_override->num_outputs_read() == 1,
+      "Failed to set execution result from CentipedeSetExecutionResult");
+}
diff --git a/centipede/runner.h b/centipede/runner.h
index 2fc5cf0..832c8fe 100644
--- a/centipede/runner.h
+++ b/centipede/runner.h
@@ -125,6 +125,10 @@
 // All data members will be initialized to zero, unless they have initializers.
 // Accesses to the subobjects should be fast, so we are trying to avoid
 // extra memory references where possible.
+//
+// This class has a non-trivial destructor to work with targets that do not use
+// the runner or LLVM fuzzer API at all.
+//
 // TODO(kcc): use a CTOR with absl::kConstInit (will require refactoring).
 struct GlobalRunnerState {
   // Used by LLVMFuzzerMutate and initialized in main().
@@ -205,6 +209,14 @@
     return strndup(value_beg, end - value_beg);
   }
 
+  pthread_mutex_t execution_result_override_mu;
+  // If not nullptr, it points to a batch result with either zero or one
+  // execution. When an execution result present, it will be passed as the
+  // 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;
+
   // Doubly linked list of TLSs of all live threads.
   ThreadLocalRunnerState *tls_list;
   // Doubly linked list of detached TLSs.
diff --git a/centipede/runner_interface.h b/centipede/runner_interface.h
index 9e5df32..56b4ded 100644
--- a/centipede/runner_interface.h
+++ b/centipede/runner_interface.h
@@ -112,6 +112,12 @@
 // CentipedeFinalizeProcessing().
 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);
+
 namespace centipede {
 
 // Callbacks interface implemented by the fuzzer and called by the runner.
diff --git a/centipede/testing/BUILD b/centipede/testing/BUILD
index 0fe549c..8dfc8f3 100644
--- a/centipede/testing/BUILD
+++ b/centipede/testing/BUILD
@@ -100,6 +100,49 @@
     fuzz_target = "_seeded_fuzz_target",
 )
 
+# Server binary for :external_target_test.
+cc_binary(
+    name = "_external_target_server",
+    srcs = ["external_target_server.cc"],
+    # Cannot be built directly - build :external_target_server instead.
+    tags = [
+        "local",
+        "manual",
+        "notap",
+    ],
+    deps = [
+        "@com_google_absl//absl/log:check",
+        "@com_google_absl//absl/strings",
+        "@com_google_fuzztest//centipede:centipede_runner_no_main",
+    ],
+)
+
+centipede_fuzz_target(
+    name = "external_target_server",
+    fuzz_target = "_external_target_server",
+)
+
+cc_binary(
+    name = "_external_target",
+    srcs = ["external_target.cc"],
+    # Cannot be built directly - build :external_target instead.
+    tags = [
+        "local",
+        "manual",
+        "notap",
+    ],
+    deps = [
+        "@com_google_absl//absl/log:check",
+        "@com_google_absl//absl/strings",
+        "@com_google_fuzztest//centipede:centipede_runner_no_main",
+    ],
+)
+
+centipede_fuzz_target(
+    name = "external_target",
+    fuzz_target = "_external_target",
+)
+
 # Target instrumented with -fsanitize-coverage=trace-pc.
 centipede_fuzz_target(
     name = "test_fuzz_target_trace_pc",
@@ -352,3 +395,15 @@
         "@com_google_fuzztest//centipede:test_util_sh",
     ],
 )
+
+sh_test(
+    name = "external_target_test",
+    srcs = ["external_target_test.sh"],
+    data = [
+        ":external_target",
+        ":external_target_server",
+        "@com_google_fuzztest//centipede",
+        "@com_google_fuzztest//centipede:test_util_sh",
+    ],
+    deps = ["//testing/shbase"],
+)
diff --git a/centipede/testing/external_target.cc b/centipede/testing/external_target.cc
new file mode 100644
index 0000000..d9a3783
--- /dev/null
+++ b/centipede/testing/external_target.cc
@@ -0,0 +1,109 @@
+// Copyright 2024 The Centipede Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <cstdint>
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+
+#include "absl/log/check.h"
+#include "absl/strings/numbers.h"
+#include "./centipede/runner_interface.h"
+
+namespace {
+
+void recvall(int sock, uint8_t* data, size_t size) {
+  while (size > 0) {
+    ssize_t recv_bytes = recv(sock, data, size, /*flags=*/0);
+    CHECK(recv_bytes > 0 && recv_bytes <= size);
+    data += recv_bytes;
+    size -= recv_bytes;
+  }
+}
+
+void sendall(int sock, const uint8_t* data, size_t size) {
+  while (size > 0) {
+    ssize_t sent = send(sock, data, size, /*flags=*/0);
+    CHECK(sent > 0 && sent <= size);
+    data += sent;
+    size -= sent;
+  }
+}
+
+class ExternalTargetRunnerCallbacks : public centipede::RunnerCallbacks {
+ public:
+  bool Execute(centipede::ByteSpan input) override {
+    const char* port_env = getenv("TARGET_PORT");
+    int port = 0;
+    CHECK(port_env && absl::SimpleAtoi(port_env, &port))
+        << "env TARGET_PORT is not a number";
+
+    int conn_sock = socket(AF_INET, SOCK_STREAM | SOCK_CLOEXEC, 0);
+    CHECK(conn_sock >= 0) << "Cannot create external runner socket";
+    struct sockaddr_in server_addr;
+    std::memset(&server_addr, 0, sizeof(server_addr));
+    server_addr.sin_family = AF_INET;
+    server_addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
+    server_addr.sin_port = htons(port);
+    const int connect_result =
+        connect(conn_sock, reinterpret_cast<sockaddr*>(&server_addr),
+                sizeof(server_addr));
+    if (connect_result != 0) return -1;
+    const int enable_nodelay = 1;
+    setsockopt(conn_sock, SOL_TCP, TCP_NODELAY, &enable_nodelay,
+               sizeof(enable_nodelay));
+    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);
+    int match_result = 0;
+    recvall(conn_sock, reinterpret_cast<uint8_t*>(&match_result),
+            sizeof(match_result));
+    CHECK_EQ(match_result, 0);
+    uint64_t execution_result_size = 0;
+    constexpr size_t kExecutionResultBufSize = 1 << 28;
+    static uint8_t* execution_result_buf = new uint8_t[kExecutionResultBufSize];
+    recvall(conn_sock, reinterpret_cast<uint8_t*>(&execution_result_size),
+            sizeof(execution_result_size));
+    CHECK(execution_result_size <= kExecutionResultBufSize);
+    recvall(conn_sock, execution_result_buf, execution_result_size);
+    shutdown(conn_sock, SHUT_RDWR);
+    close(conn_sock);
+
+    CentipedeSetExecutionResult(execution_result_buf, execution_result_size);
+
+    return true;
+  }
+
+  bool Mutate(
+      const std::vector<centipede::MutationInputRef>& inputs,
+      size_t num_mutants,
+      std::function<void(centipede::ByteSpan)> new_mutant_callback) override {
+    // Use the default Centipede mutation.
+    return false;
+  }
+};
+
+}  // namespace
+
+int main(int argc, absl::Nonnull<char**> argv) {
+  ExternalTargetRunnerCallbacks runner_callbacks;
+  return centipede::RunnerMain(argc, argv, runner_callbacks);
+}
diff --git a/centipede/testing/external_target_server.cc b/centipede/testing/external_target_server.cc
new file mode 100644
index 0000000..6163ef8
--- /dev/null
+++ b/centipede/testing/external_target_server.cc
@@ -0,0 +1,124 @@
+// Copyright 2024 The Centipede Authors.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//      https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include <netinet/in.h>
+#include <netinet/tcp.h>
+#include <sys/socket.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+
+#include "absl/log/check.h"
+#include "absl/strings/numbers.h"
+#include "./centipede/runner_interface.h"
+
+static void recvall(int sock, uint8_t* data, size_t size) {
+  while (size > 0) {
+    ssize_t recv_bytes = recv(sock, data, size, /*flags=*/0);
+    CHECK(recv_bytes > 0 && recv_bytes <= size);
+    data += recv_bytes;
+    size -= recv_bytes;
+  }
+}
+
+static void sendall(int sock, const uint8_t* data, size_t size) {
+  while (size > 0) {
+    ssize_t sent = send(sock, data, size, /*flags=*/0);
+    CHECK(sent > 0 && sent <= size);
+    data += sent;
+    size -= sent;
+  }
+}
+
+__attribute__((optnone)) int MatchSecret(const char* input,
+                                         const char* secret) {
+  if (std::strcmp(input, secret) == 0) {
+    return 1;
+  }
+  return 0;
+}
+
+int main() {
+  const char* port_env = getenv("TARGET_PORT");
+  int port = 0;
+  CHECK(port_env && absl::SimpleAtoi(port_env, &port))
+      << "env TARGET_PORT is not a number";
+
+  const int server_sock = socket(AF_INET, SOCK_STREAM, 0);
+  CHECK(server_sock >= 0) << "Failed to create server socket";
+  sockaddr_in server_addr;
+  std::memset(&server_addr, 0, sizeof(server_addr));
+  server_addr.sin_family = AF_INET;
+  server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
+  server_addr.sin_port = htons(port);
+
+  if (bind(server_sock, reinterpret_cast<const sockaddr*>(&server_addr),
+           sizeof(server_addr)) != 0) {
+    CHECK(false) << "Failed to bind the server socket";
+  }
+
+  if (listen(server_sock, /*backlog=*/2) != 0) {
+    CHECK(false) << "Failed to listen on the server socket";
+  }
+
+  static constexpr size_t kExecutionResultBufSize = 1 << 28;
+  std::vector<uint8_t> execution_result_buf;
+  execution_result_buf.resize(kExecutionResultBufSize);
+
+  CentipedeBeginExecutionBatch();
+  fprintf(stderr, "external_target_server running\n");
+  while (true) {
+    sockaddr_in unused_conn_addr;
+    socklen_t unused_conn_addr_len;
+    const int conn_sock =
+        accept(server_sock, reinterpret_cast<sockaddr*>(&unused_conn_addr),
+               (unused_conn_addr_len = sizeof(unused_conn_addr_len),
+                &unused_conn_addr_len));
+    CHECK(conn_sock >= 0)
+        << "Failed to accept connections from the server socket";
+    const int enable_nodelay = 1;
+    setsockopt(conn_sock, SOL_TCP, TCP_NODELAY, &enable_nodelay,
+               sizeof(enable_nodelay));
+    const char secret[] = "Secret";
+    char buf[sizeof(secret)];
+    uint64_t input_size = 0;
+    recvall(conn_sock, reinterpret_cast<uint8_t*>(&input_size),
+            sizeof(input_size));
+    recvall(conn_sock, reinterpret_cast<uint8_t*>(buf),
+            std::min(sizeof(buf) - 1, input_size));
+    buf[sizeof(buf) - 1] = 0;
+
+    CentipedePrepareProcessing();
+    const int match_result = MatchSecret(buf, secret);
+    CentipedeFinalizeProcessing();
+
+    sendall(conn_sock, reinterpret_cast<const uint8_t*>(&match_result),
+            sizeof(match_result));
+    const uint64_t execution_result_size = CentipedeGetExecutionResult(
+        execution_result_buf.data(), kExecutionResultBufSize);
+    sendall(conn_sock, reinterpret_cast<const uint8_t*>(&execution_result_size),
+            sizeof(execution_result_size));
+    sendall(conn_sock, execution_result_buf.data(), execution_result_size);
+
+    shutdown(conn_sock, SHUT_RDWR);
+    close(conn_sock);
+  }
+  CentipedeEndExecutionBatch();
+
+  fprintf(stderr, "external_target_server exiting\n");
+  return 0;
+}
diff --git a/centipede/testing/external_target_test.sh b/centipede/testing/external_target_test.sh
new file mode 100755
index 0000000..edee6cd
--- /dev/null
+++ b/centipede/testing/external_target_test.sh
@@ -0,0 +1,59 @@
+#!/bin/bash
+
+# Copyright 2024 The Centipede Authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+set -euo pipefail
+
+source googletest.sh
+source "$(dirname "$0")/../test_util.sh"
+
+CENTIPEDE_TEST_SRCDIR="$(centipede::get_centipede_test_srcdir)"
+
+centipede::maybe_set_var_to_executable_path \
+  CENTIPEDE_BINARY "${CENTIPEDE_TEST_SRCDIR}/centipede"
+
+centipede::maybe_set_var_to_executable_path \
+  LLVM_SYMBOLIZER "$(centipede::get_llvm_symbolizer_path)"
+
+centipede::maybe_set_var_to_executable_path \
+  SERVER_BINARY "${CENTIPEDE_TEST_SRCDIR}/testing/external_target_server"
+
+centipede::maybe_set_var_to_executable_path \
+  TARGET_BINARY "${CENTIPEDE_TEST_SRCDIR}/testing/external_target"
+
+readonly WD="${TEST_TMPDIR}/WD"
+readonly LOG="${TEST_TMPDIR}/log"
+centipede::ensure_empty_dir "${WD}"
+
+readonly TARGET_PORT="$(get_port_from_portserver)"
+
+echo "Starting the server binary ..."
+env CENTIPEDE_RUNNER_FLAGS=":use_auto_dictionary:use_cmp_features:use_pc_features:" \
+  TARGET_PORT="${TARGET_PORT}" \
+  "${SERVER_BINARY}" &
+readonly SERVER_PID="$!"
+trap "kill ${SERVER_PID} || true" SIGINT SIGTERM EXIT
+
+echo "Running Centipede to fuzz the target binary ..."
+env TARGET_PORT="${TARGET_PORT}" \
+  "${CENTIPEDE_BINARY}" --binary="${TARGET_BINARY}" --workdir="${WD}" \
+  --coverage_binary="${SERVER_BINARY}" --symbolizer_path="${LLVM_SYMBOLIZER}" \
+  --exit_on_crash=1 --seed=1 --log_features_shards=1 \
+  |& tee "${LOG}" || true
+
+# Check that Centipede finds the crashing input.
+centipede::assert_regex_in_file "Input bytes.*: Secret" "${LOG}"
+# Check that Centipede uses the coverage features of the external target server.
+centipede::assert_regex_in_file "EDGE: .*external_target_server.cc:" "${LOG}"