Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions centipede/centipede_callbacks.cc
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

#include <algorithm>
#include <cerrno>
#include <cmath>
#include <cstddef>
#include <cstdlib>
#include <cstring>
Expand Down Expand Up @@ -194,8 +195,11 @@ class CentipedeCallbacks::PersistentModeServer {
int poll_ret = -1;
do {
poll_fd = {fd, static_cast<short>(event)};
const int poll_timeout_ms = static_cast<int>(absl::ToInt64Milliseconds(
std::max(deadline - absl::Now(), kPollMinimalTimeout)));
const int poll_timeout_ms =
deadline == absl::InfiniteFuture()
? -1
: static_cast<int>(std::ceil(absl::ToDoubleMilliseconds(
std::max(deadline - absl::Now(), kPollMinimalTimeout))));
poll_ret = poll(&poll_fd, 1, poll_timeout_ms);
} while (poll_ret < 0 && errno == EINTR);
if (poll_ret == 1 && (poll_fd.revents & (event | POLLHUP)) == event) {
Expand Down Expand Up @@ -565,8 +569,12 @@ int CentipedeCallbacks::ExecuteCentipedeSancovBinaryWithShmem(
}

// Run.
const auto batch_start_time = absl::Now();
const int exit_code = RunBatchForBinary(binary);
inputs_blobseq_.ReleaseSharedMemory(); // Inputs are already consumed.
const bool batch_timed_out =
env_.timeout_per_batch > 0 &&
absl::Now() - batch_start_time > absl::Seconds(env_.timeout_per_batch);

// Get results.
batch_result.exit_code() = exit_code;
Expand All @@ -591,6 +599,12 @@ int CentipedeCallbacks::ExecuteCentipedeSancovBinaryWithShmem(

if (env_.print_runner_log) PrintExecutionLog();

if (batch_timed_out) {
batch_result.failure_description() = "per-batch-timeout-exceeded";
batch_result.exit_code() = EXIT_FAILURE;
return EXIT_FAILURE;
}

// TODO: b/467103298 - Handle failures when the exit code is zero, e.g., when
// the target exits via `std::_Exit(0)`.
if (exit_code != EXIT_SUCCESS) {
Expand Down
35 changes: 18 additions & 17 deletions centipede/centipede_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ class CentipedeMock : public CentipedeCallbacks {
// 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]++;
Expand Down Expand Up @@ -496,7 +496,7 @@ TEST_F(CentipedeWithTemporaryLocalDir, MutateViaExternalBinary) {
// 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)));
}
}
Expand Down Expand Up @@ -525,7 +525,7 @@ class MergeMock : public CentipedeCallbacks {
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;
Expand Down Expand Up @@ -611,7 +611,7 @@ class FunctionFilterMock : public CentipedeCallbacks {
// 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);
}
Expand All @@ -628,8 +628,8 @@ class FunctionFilterMock : public CentipedeCallbacks {
// 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)};
}

Expand All @@ -646,7 +646,7 @@ class FunctionFilterMock : public CentipedeCallbacks {
// 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.
Expand Down Expand Up @@ -717,9 +717,9 @@ class ExtraBinariesMock : public CentipedeCallbacks {
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;
Expand All @@ -736,7 +736,7 @@ class ExtraBinariesMock : public CentipedeCallbacks {
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;
Expand All @@ -754,20 +754,20 @@ struct FileAndContents {
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));
Expand Down Expand Up @@ -843,7 +843,7 @@ class UndetectedCrashingInputMock : public CentipedeCallbacks {
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_) {
Expand Down Expand Up @@ -933,7 +933,7 @@ TEST(Centipede, UndetectedCrashingInput) {
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());
}
Expand Down Expand Up @@ -1400,8 +1400,9 @@ TEST_F(CentipedeWithTemporaryLocalDir, EngineWorksInWorkerMode) {
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;
Expand Down
8 changes: 6 additions & 2 deletions centipede/command.cc
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@

#include <algorithm>
#include <atomic>
#include <cmath>
#include <csignal>
#include <cstdint>
#include <cstdlib>
Expand Down Expand Up @@ -146,8 +147,11 @@ struct Command::ForkServerProps {
/*fd=*/pipe_[1],
/*events=*/POLLIN,
};
const int poll_timeout_ms = static_cast<int>(absl::ToInt64Milliseconds(
std::max(deadline - absl::Now(), absl::Milliseconds(1))));
const int poll_timeout_ms =
deadline == absl::InfiniteFuture()
? -1
: static_cast<int>(std::ceil(absl::ToDoubleMilliseconds(
std::max(deadline - absl::Now(), absl::Milliseconds(1)))));
poll_ret = poll(&poll_fd, 1, poll_timeout_ms);
// The `poll()` syscall can get interrupted: it sets errno==EINTR in that
// case. We should tolerate that.
Expand Down
137 changes: 131 additions & 6 deletions centipede/engine_worker.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -105,7 +107,7 @@ struct WorkerFlags {
};

// 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`
Expand Down Expand Up @@ -137,10 +139,6 @@ const WorkerFlags& GetWorkerFlags() {
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
Expand Down Expand Up @@ -237,6 +235,9 @@ constexpr std::string_view kWorkerInputsBlobSequencePathFlagHeader =
"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;
Expand Down Expand Up @@ -323,6 +324,64 @@ inline std::string_view ToStringView(const std::vector<uint8_t>& bytes) {
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;
}

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;
}
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;
}
WorkerLog("Persistent mode: connected to ", persistent_mode_socket_path,
LogLnSync{});
}

BlobSequence* GetInputsBlobSequence() {
static auto result = []() -> BlobSequence* {
if (!HasWorkerSwitchFlag("shmem")) {
Expand Down Expand Up @@ -695,6 +754,70 @@ const char* FuzzTestWorkerGetTestName() {
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();

// 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");
Expand Down Expand Up @@ -764,7 +887,9 @@ FuzzTestWorkerStatus WorkerRun(const FuzzTestAdapterManager& manager) {
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);
Expand Down
1 change: 0 additions & 1 deletion centipede/puzzles/run_puzzle.sh
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,6 @@ function ExpectPerInputTimeout() {
# Expects that Centipede found a per-batch timeout.
function ExpectPerBatchTimeout() {
echo "======= ${FUNCNAME[0]}"
fuzztest::internal::assert_regex_in_file "Per-batch timeout exceeded" "${log}"
fuzztest::internal::assert_regex_in_file "Failure.*: per-batch-timeout-exceeded" "${log}"
fuzztest::internal::assert_regex_in_file \
"Failure applies to entire batch: not executing inputs one-by-one" "${log}"
Expand Down
Loading
Loading