#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}"