blob: 9594a4e92e316e330f880565b63d70e5d35f9b8f [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/sys/fuzzing/common/runner-unittest.h"
#include <zircon/status.h>
#include "src/sys/fuzzing/common/testing/monitor.h"
namespace fuzzing {
// |Cleanse| tries to replace bytes with 0x20 or 0xff.
static constexpr size_t kNumReplacements = 2;
// Test fixtures.
void RunnerTest::SetUp() {
AsyncTest::SetUp();
handler_ = [](const Input& input) { return FuzzResult::NO_ERRORS; };
}
OptionsPtr RunnerTest::DefaultOptions() {
auto options = MakeOptions();
runner()->AddDefaults(options.get());
return options;
}
void RunnerTest::Configure(const OptionsPtr& options) {
options_ = options;
options_->set_seed(1);
FUZZING_EXPECT_OK(runner()->Configure(options_));
RunUntilIdle();
}
void RunnerTest::SetCoverage(const Input& input, Coverage coverage) {
coverage_[input.ToHex()] = std::move(coverage);
}
void RunnerTest::SetFuzzResultHandler(FuzzResultHandler handler) { handler_ = std::move(handler); }
void RunnerTest::SetLeak(bool has_leak) { has_leak_ = has_leak; }
Promise<Input> RunnerTest::RunOne() {
return RunOne([this](const Input& input) {
return SetFeedback(Coverage(coverage_[input.ToHex()]), handler_(input), has_leak_);
});
}
Promise<Input> RunnerTest::RunOne(Coverage coverage) {
return RunOne([this, coverage = std::move(coverage)](const Input& input) {
return SetFeedback(std::move(coverage), handler_(input), has_leak_);
});
}
Promise<Input> RunnerTest::RunOne(FuzzResult fuzz_result) {
return RunOne([this, fuzz_result](const Input& input) {
return SetFeedback(Coverage(coverage_[input.ToHex()]), fuzz_result, has_leak_);
});
}
Promise<Input> RunnerTest::RunOne(bool has_leak) {
return RunOne([this, has_leak](const Input& input) {
return SetFeedback(Coverage(coverage_[input.ToHex()]), handler_(input), has_leak);
});
}
Promise<Input> RunnerTest::RunOne(fit::function<ZxPromise<>(const Input&)> set_feedback) {
Bridge<> bridge;
auto task = GetTestInput()
.and_then([set_feedback = std::move(set_feedback)](Input& input) mutable {
return set_feedback(input).and_then([input = std::move(input)]() mutable {
return fpromise::ok(std::move(input));
});
})
.or_else([](const zx_status_t& status) {
// Target may close before returning test input.
EXPECT_EQ(status, ZX_ERR_PEER_CLOSED) << zx_status_get_string(status);
return fpromise::error();
})
.then([completer = std::move(bridge.completer)](Result<Input>& result) mutable {
if (result.is_ok()) {
completer.complete_ok();
} else {
completer.complete_error();
}
return std::move(result);
});
auto consumer = std::move(previous_run_);
previous_run_ = std::move(bridge.consumer);
return consumer ? consumer.promise().and_then(std::move(task)).box() : task.box();
}
void RunnerTest::RunUntil(Promise<> promise) {
RunUntil(
std::move(promise),
[this](const Result<Input>& result) -> Promise<Input> { return RunOne(); }, Input());
}
void RunnerTest::RunUntil(Promise<> promise, RunCallback run, Input input) {
auto task =
fpromise::make_promise([run = std::move(run),
result = Result<Input>(fpromise::ok(std::move(input))),
running = Future<Input>(), until = Future<>(std::move(promise))](
Context& context) mutable -> Result<> {
while (result.is_ok() && !until(context)) {
if (!running) {
running = run(result);
}
if (!running(context)) {
return fpromise::pending();
}
result = running.take_result();
}
// Ignore errors; they were checked in |RunOne|.
return fpromise::ok();
}).wrap_with(scope_);
FUZZING_EXPECT_OK(std::move(task));
RunUntilIdle();
}
// Unit tests.
void RunnerTest::ExecuteNoError() {
Configure(DefaultOptions());
Input input({0x01});
FUZZING_EXPECT_OK(runner()->Execute(input.Duplicate()), FuzzResult::NO_ERRORS);
FUZZING_EXPECT_OK(RunOne(), std::move(input));
RunUntilIdle();
}
void RunnerTest::ExecuteWithError() {
Configure(DefaultOptions());
Input input({0x02});
FUZZING_EXPECT_OK(runner()->Execute(input.Duplicate()), FuzzResult::BAD_MALLOC);
FUZZING_EXPECT_OK(RunOne(FuzzResult::BAD_MALLOC), std::move(input));
RunUntilIdle();
}
void RunnerTest::ExecuteWithLeak() {
auto options = DefaultOptions();
options->set_detect_leaks(true);
Configure(options);
Input input({0x03});
// Simulate a suspected leak, followed by an LSan exit. The leak detection heuristics only run
// full leak detection when a leak is suspected based on mismatched allocations.
SetLeak(true);
FUZZING_EXPECT_OK(runner()->Execute(input.Duplicate()), FuzzResult::LEAK);
FUZZING_EXPECT_OK(RunOne(), input.Duplicate());
FUZZING_EXPECT_OK(RunOne(FuzzResult::LEAK), std::move(input));
RunUntilIdle();
}
// Simulate no error on the original input.
void RunnerTest::MinimizeNoError() {
Configure(DefaultOptions());
Input input({0x04});
FUZZING_EXPECT_ERROR(runner()->Minimize(input.Duplicate()), ZX_ERR_INVALID_ARGS);
FUZZING_EXPECT_OK(RunOne(), std::move(input));
RunUntilIdle();
}
// Empty input should exit immediately.
void RunnerTest::MinimizeEmpty() {
Configure(DefaultOptions());
Input input;
FUZZING_EXPECT_OK(runner()->Minimize(input.Duplicate()), input.Duplicate());
FUZZING_EXPECT_OK(RunOne(FuzzResult::CRASH), std::move(input));
RunUntilIdle();
}
// 1-byte input should exit immediately.
void RunnerTest::MinimizeOneByte() {
Configure(DefaultOptions());
Input input({0x44});
FUZZING_EXPECT_OK(runner()->Minimize(input.Duplicate()), input.Duplicate());
FUZZING_EXPECT_OK(RunOne(FuzzResult::CRASH), std::move(input));
RunUntilIdle();
}
void RunnerTest::MinimizeReduceByTwo() {
auto options = DefaultOptions();
constexpr size_t kRuns = 0x40;
options->set_runs(kRuns);
Configure(options);
Input input({0x51, 0x52, 0x53, 0x54, 0x55, 0x56});
Input minimized;
Barrier barrier;
auto task = runner()
->Minimize(input.Duplicate())
.and_then([&minimized](Input& result) {
minimized = std::move(result);
return fpromise::ok();
})
.wrap_with(barrier);
FUZZING_EXPECT_OK(std::move(task));
// Crash until inputs are smaller than 4 bytes.
RunUntil(
barrier.sync(),
[this](const Result<Input>& result) {
return RunOne(result.value().size() > 3 ? FuzzResult::CRASH : FuzzResult::NO_ERRORS);
},
std::move(input));
EXPECT_LE(minimized.size(), 3U);
}
void RunnerTest::MinimizeNewError() {
auto options = DefaultOptions();
options->set_run_limit(zx::msec(500).get());
Configure(options);
Input input({0x05, 0x15, 0x25, 0x35});
Input minimized;
// Simulate a crash on the original input, and a timeout on any smaller input.
SetFuzzResultHandler([](const Input& input) {
return input.size() > 3 ? FuzzResult::CRASH : FuzzResult::TIMEOUT;
});
Barrier barrier;
auto task = runner()
->Minimize(input.Duplicate())
.and_then([&minimized](Input& result) {
minimized = std::move(result);
return fpromise::ok();
})
.wrap_with(barrier);
FUZZING_EXPECT_OK(std::move(task));
RunUntil(barrier.sync());
EXPECT_EQ(minimized, input);
}
void RunnerTest::CleanseNoReplacement() {
Configure(DefaultOptions());
Input input({0x07, 0x17, 0x27});
Input cleansed;
FUZZING_EXPECT_OK(runner()->Cleanse(input.Duplicate()), &cleansed);
// Simulate no error after cleansing any byte.
for (size_t i = 0; i < input.size(); ++i) {
for (size_t j = 0; j < kNumReplacements; ++j) {
FUZZING_EXPECT_OK(RunOne(FuzzResult::NO_ERRORS));
}
}
RunUntilIdle();
EXPECT_EQ(cleansed, input);
}
void RunnerTest::CleanseAlreadyClean() {
Configure(DefaultOptions());
Input input({' ', 0xff});
Input cleansed;
FUZZING_EXPECT_OK(runner()->Cleanse(input.Duplicate()), &cleansed);
// All bytes match replacements, so this should be done.
RunUntilIdle();
EXPECT_EQ(cleansed, input);
}
void RunnerTest::CleanseTwoBytes() {
Configure(DefaultOptions());
Input input({0x08, 0x18, 0x28});
Input inputs[] = {
{0x20, 0x18, 0x28}, // 1st try.
{0xff, 0x18, 0x28}, //
{0x08, 0x20, 0x28}, //
{0x08, 0xff, 0x28}, //
{0x08, 0x18, 0x20}, //
{0x08, 0x18, 0xff}, // Error on 2nd replacement of 3rd byte.
{0x20, 0x18, 0xff}, // 2nd try. Error on 1st replacement of 1st byte.
{0x20, 0x20, 0xff}, //
{0x20, 0xff, 0xff}, //
{0x20, 0x20, 0xff}, // 3rd try. No errors.
{0x20, 0xff, 0xff}, //
};
SetFuzzResultHandler([](const Input& input) {
auto hex = input.ToHex();
return (hex == "081828" || hex == "0818ff" || hex == "2018ff") ? FuzzResult::DEATH
: FuzzResult::NO_ERRORS;
});
Input cleansed;
FUZZING_EXPECT_OK(runner()->Cleanse(std::move(input)), &cleansed);
for (auto& input : inputs) {
FUZZING_EXPECT_OK(RunOne(), std::move(input));
}
RunUntilIdle();
EXPECT_EQ(cleansed, Input({0x20, 0x18, 0xff}));
}
void RunnerTest::FuzzUntilError() {
auto runner = this->runner();
auto options = DefaultOptions();
options->set_detect_exits(true);
options->set_mutation_depth(1);
Configure(options);
Artifact artifact;
FUZZING_EXPECT_OK(runner->Fuzz(), &artifact);
// Add some corpus elements.
std::vector<Input> inputs;
inputs.emplace_back("foo");
inputs.emplace_back("bar");
inputs.emplace_back("baz");
inputs.emplace_back("qux");
auto last = CorpusType::LIVE;
auto next = CorpusType::SEED;
for (const auto& input : inputs) {
EXPECT_EQ(runner->AddToCorpus(next, input.Duplicate()), ZX_OK);
std::swap(next, last);
}
// Set some coverage for the inputs above. Some runners (e.g. libFuzzer) won't mutate an input
// that lacks any coverage. According to the AFL bucketing scheme used by libFuzzer and others,
// the counter must be at least 2 to map to a coverage "feature".
for (size_t i = 0; i < inputs.size(); ++i) {
SetCoverage(inputs[i], {{i + 1, i + 1}});
}
std::vector<Input> actual;
for (size_t i = 0; i < 100; ++i) {
FUZZING_EXPECT_OK(RunOne().then([&](Result<Input>& result) {
if (result.is_ok()) {
actual.push_back(result.take_value());
}
}));
}
FUZZING_EXPECT_OK(RunOne(FuzzResult::EXIT));
RunUntilIdle();
// Helper lambda to check if the sequence of bytes given by |needle| appears in order, but not
// necessarily contiguously, in the sequence of bytes given by |haystack|.
auto contains = [](const Input& haystack, const Input& needle) -> bool {
const auto* needle_data = needle.data();
const auto* haystack_data = haystack.data();
size_t i = 0;
for (size_t j = 0; i < needle.size() && j < haystack.size(); ++j) {
if (needle_data[i] == haystack_data[j]) {
++i;
}
}
return i == needle.size();
};
// Verify that each corpus element is 1) used as-is, and 2) used as the basis for mutations.
for (auto& orig : inputs) {
bool exact_match_found = false;
bool near_match_found = false;
for (auto& input : actual) {
if (orig == input) {
exact_match_found = true;
} else if (contains(input, orig)) {
near_match_found = true;
}
if (exact_match_found && near_match_found) {
break;
}
}
EXPECT_TRUE(exact_match_found) << "input: " << orig.ToHex();
EXPECT_TRUE(near_match_found) << "input: " << orig.ToHex();
}
EXPECT_EQ(artifact.fuzz_result(), FuzzResult::EXIT);
}
void RunnerTest::FuzzUntilRuns() {
auto runner = this->runner();
auto options = DefaultOptions();
const size_t kNumRuns = 10;
options->set_runs(kNumRuns);
Configure(options);
std::vector<std::string> expected({""});
// Subscribe to status updates.
FakeMonitor monitor(executor());
runner->AddMonitor(monitor.NewBinding());
// Fuzz for exactly |kNumRuns|.
Artifact artifact;
FUZZING_EXPECT_OK(runner->Fuzz(), &artifact);
for (size_t i = 0; i < kNumRuns; ++i) {
FUZZING_EXPECT_OK(RunOne({{i, i}}));
}
// Check that we get the expected status updates.
FUZZING_EXPECT_OK(monitor.AwaitUpdate());
RunUntilIdle();
EXPECT_EQ(monitor.reason(), UpdateReason::INIT);
auto status = monitor.take_status();
ASSERT_TRUE(status.has_running());
EXPECT_TRUE(status.running());
ASSERT_TRUE(status.has_runs());
auto runs = status.runs();
ASSERT_TRUE(status.has_elapsed());
EXPECT_GT(status.elapsed(), 0U);
auto elapsed = status.elapsed();
ASSERT_TRUE(status.has_covered_pcs());
EXPECT_GE(status.covered_pcs(), 0U);
auto covered_pcs = status.covered_pcs();
monitor.pop_front();
FUZZING_EXPECT_OK(monitor.AwaitUpdate());
RunUntilIdle();
EXPECT_EQ(size_t(monitor.reason()), size_t(UpdateReason::NEW));
status = monitor.take_status();
ASSERT_TRUE(status.has_running());
EXPECT_TRUE(status.running());
ASSERT_TRUE(status.has_runs());
EXPECT_GT(status.runs(), runs);
runs = status.runs();
ASSERT_TRUE(status.has_elapsed());
EXPECT_GT(status.elapsed(), elapsed);
elapsed = status.elapsed();
ASSERT_TRUE(status.has_covered_pcs());
EXPECT_GT(status.covered_pcs(), covered_pcs);
covered_pcs = status.covered_pcs();
// Skip others up to DONE.
while (monitor.reason() != UpdateReason::DONE) {
monitor.pop_front();
FUZZING_EXPECT_OK(monitor.AwaitUpdate());
RunUntilIdle();
}
status = monitor.take_status();
ASSERT_TRUE(status.has_running());
EXPECT_FALSE(status.running());
ASSERT_TRUE(status.has_runs());
EXPECT_GE(status.runs(), runs);
ASSERT_TRUE(status.has_elapsed());
EXPECT_GT(status.elapsed(), elapsed);
ASSERT_TRUE(status.has_covered_pcs());
EXPECT_GE(status.covered_pcs(), covered_pcs);
// All done.
EXPECT_EQ(artifact.fuzz_result(), FuzzResult::NO_ERRORS);
}
void RunnerTest::FuzzUntilTime() {
auto runner = this->runner();
// Time is always tricky to test. As a result, this test verifies the bare minimum, namely that
// the runner exits at least 100 ms after it started. All other verification is performed in more
// controllable tests, such as |FuzzUntilRuns| above.
auto options = DefaultOptions();
options->set_max_total_time(zx::msec(100).get());
Configure(options);
auto start = zx::clock::get_monotonic();
Artifact artifact;
Barrier barrier;
auto task = runner->Fuzz()
.and_then([&artifact](Artifact& result) {
artifact = std::move(result);
return fpromise::ok();
})
.wrap_with(barrier);
FUZZING_EXPECT_OK(std::move(task));
RunUntil(barrier.sync());
EXPECT_EQ(artifact.fuzz_result(), FuzzResult::NO_ERRORS);
auto elapsed = zx::clock::get_monotonic() - start;
EXPECT_GE(elapsed, zx::msec(100));
}
void RunnerTest::MergeSeedError(zx_status_t expected, uint64_t oom_limit) {
auto runner = this->runner();
auto options = DefaultOptions();
options->set_oom_limit(oom_limit);
Configure(options);
runner->AddToCorpus(CorpusType::SEED, Input({0x09}));
if (expected == ZX_OK) {
FUZZING_EXPECT_OK(runner->Merge());
} else {
FUZZING_EXPECT_ERROR(runner->Merge(), expected);
}
FUZZING_EXPECT_OK(RunOne(FuzzResult::OOM));
RunUntilIdle();
}
void RunnerTest::Merge(bool keeps_errors, uint64_t oom_limit) {
auto runner = this->runner();
auto options = DefaultOptions();
options->set_oom_limit(oom_limit);
Configure(options);
std::vector<std::string> expected_seed;
std::vector<std::string> expected_live;
// Empty input, implicitly included in all corpora.
Input input0;
expected_seed.push_back(input0.ToHex());
expected_live.push_back(input0.ToHex());
// Seed input => kept.
Input input1({0x0a});
SetCoverage(input1, {{0, 1}, {1, 2}, {2, 3}});
runner->AddToCorpus(CorpusType::SEED, input1.Duplicate());
expected_seed.push_back(input1.ToHex());
// Triggers error => maybe kept.
Input input2({0x0b});
SetFuzzResultHandler([](const Input& input) {
return input.ToHex() == "0b" ? FuzzResult::OOM : FuzzResult::NO_ERRORS;
});
runner->AddToCorpus(CorpusType::LIVE, input2.Duplicate());
if (keeps_errors) {
expected_live.push_back(input2.ToHex());
}
// Second-smallest and 2 non-seed features => kept.
Input input5({0x0c, 0x0c});
SetCoverage(input5, {{0, 2}, {2, 2}});
runner->AddToCorpus(CorpusType::LIVE, input5.Duplicate());
expected_live.push_back(input5.ToHex());
// Larger and 1 feature not in any smaller inputs => kept.
Input input4({0x0d, 0x0d, 0x0d});
SetCoverage(input4, {{0, 2}, {1, 1}});
runner->AddToCorpus(CorpusType::LIVE, input4.Duplicate());
expected_live.push_back(input4.ToHex());
// Second-smallest but only 1 non-seed feature above => skipped.
Input input3({0x0e, 0x0e});
SetCoverage(input3, {{0, 2}, {2, 3}});
runner->AddToCorpus(CorpusType::LIVE, input3.Duplicate());
// Smallest but features are subset of seed corpus => skipped.
Input input6({0x0f});
SetCoverage(input6, {{0, 1}, {2, 3}});
runner->AddToCorpus(CorpusType::LIVE, input6.Duplicate());
// Largest with all 3 of the new features => skipped.
Input input7({0x10, 0x10, 0x10, 0x10});
SetCoverage(input7, {{0, 2}, {1, 1}, {2, 2}});
runner->AddToCorpus(CorpusType::LIVE, input7.Duplicate());
Barrier barrier;
FUZZING_EXPECT_OK(runner->Merge().wrap_with(barrier));
RunUntil(barrier.sync());
std::vector<std::string> actual_seed;
for (size_t i = 0; i < expected_seed.size(); ++i) {
actual_seed.push_back(runner->ReadFromCorpus(CorpusType::SEED, i).ToHex());
}
std::sort(expected_seed.begin(), expected_seed.end());
std::sort(actual_seed.begin(), actual_seed.end());
EXPECT_EQ(expected_seed, actual_seed);
std::vector<std::string> actual_live;
for (size_t i = 0; i < expected_live.size(); ++i) {
actual_live.push_back(runner->ReadFromCorpus(CorpusType::LIVE, i).ToHex());
}
std::sort(actual_live.begin(), actual_live.end());
EXPECT_EQ(expected_live, actual_live);
}
void RunnerTest::Stop() {
auto runner = this->runner();
Configure(DefaultOptions());
Artifact artifact;
Barrier barrier;
auto task = runner->Fuzz()
.and_then([&artifact](Artifact& result) {
artifact = std::move(result);
return fpromise::ok();
})
.wrap_with(barrier);
FUZZING_EXPECT_OK(std::move(task));
FUZZING_EXPECT_OK(executor()
->MakeDelayedPromise(zx::msec(100))
.then([runner](const Result<>& result) { return runner->Stop(); })
.then([runner](const ZxResult<>& result) {
// Should be idempotent.
return runner->Stop();
}));
RunUntil(barrier.sync());
EXPECT_EQ(artifact.fuzz_result(), FuzzResult::NO_ERRORS);
}
} // namespace fuzzing