From 8c94cd1b2c7b627308f6dc64e832d5173b85ccae Mon Sep 17 00:00:00 2001 From: Thomas Goyne Date: Tue, 2 Feb 2016 15:12:36 -0800 Subject: [PATCH] Add an afl-based fuzzer for notifications --- CMakeLists.txt | 1 + fuzzer/CMakeLists.txt | 2 + fuzzer/fuzzer.cpp | 364 ++++++++++++++++++++++++++++++++++++++++++ fuzzer/input/0 | 19 +++ fuzzer/input/1 | 33 ++++ 5 files changed, 419 insertions(+) create mode 100644 fuzzer/CMakeLists.txt create mode 100644 fuzzer/fuzzer.cpp create mode 100644 fuzzer/input/0 create mode 100644 fuzzer/input/1 diff --git a/CMakeLists.txt b/CMakeLists.txt index 286e6ff0..ec33ee52 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -16,4 +16,5 @@ use_realm_core(${REALM_CORE_VERSION}) include_directories(${REALM_CORE_INCLUDE_DIR} src external/pegtl) add_subdirectory(src) +add_subdirectory(fuzzer) add_subdirectory(tests) diff --git a/fuzzer/CMakeLists.txt b/fuzzer/CMakeLists.txt new file mode 100644 index 00000000..65df5418 --- /dev/null +++ b/fuzzer/CMakeLists.txt @@ -0,0 +1,2 @@ +add_executable(fuzzer fuzzer.cpp) +target_link_libraries(fuzzer realm-object-store) diff --git a/fuzzer/fuzzer.cpp b/fuzzer/fuzzer.cpp new file mode 100644 index 00000000..19a913af --- /dev/null +++ b/fuzzer/fuzzer.cpp @@ -0,0 +1,364 @@ +#include "object_schema.hpp" +#include "property.hpp" +#include "results.hpp" +#include "schema.hpp" +#include "impl/realm_coordinator.hpp" + +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace realm; + +#define FUZZ_SORTED 1 + +#if 0 +#define log(...) fprintf(stderr, __VA_ARGS__) +#else +#define log(...) +#endif + +// Read from a fd until eof into a string +// Needs to use unbuffered i/o to work properly with afl +static void read_all(std::string& buffer, int fd) +{ + buffer.clear(); + size_t offset = 0; + while (true) { + buffer.resize(offset + 4096); + ssize_t bytes_read = read(fd, &buffer[offset], 4096); + if (bytes_read < 4096) { + buffer.resize(offset + bytes_read); + break; + } + offset += 4096; + } +} + +static std::vector read_initial_values(std::istream& input_stream) +{ + std::vector initial_values; + std::string line; + input_stream.seekg(0); + while (std::getline(input_stream, line) && !line.empty()) { + try { + initial_values.push_back(std::stoll(line)); + } + catch (std::invalid_argument) { + // not an error + } + catch (std::out_of_range) { + // not an error + } + } + return initial_values; +} + +struct Change { + enum class Action { + Commit, + Add, + Modify, + Delete + } action; + + size_t index = npos; + int64_t value = 0; +}; + +static std::vector read_changes(std::istream& input_stream) +{ + std::vector ret; + + while (!input_stream.eof()) { + char op = '\0'; + input_stream >> op; + if (!input_stream.good()) + break; + switch (op) { + case 'a': { + int64_t value = 0; + input_stream >> value; + if (input_stream.good()) + ret.push_back({Change::Action::Add, npos, value}); + break; + } + case 'm': { + int64_t value; + size_t ndx; + input_stream >> ndx >> value; + if (input_stream.good()) + ret.push_back({Change::Action::Modify, ndx, value}); + break; + } + case 'd': { + size_t ndx; + input_stream >> ndx; + if (input_stream.good()) + ret.push_back({Change::Action::Delete, ndx, 0}); + break; + } + case 'c': { + ret.push_back({Change::Action::Commit, npos, 0}); + break; + } + default: + return ret; + + } + } + return ret; +} + +static Query query(Table& table) +{ + return table.where().greater(1, 100).less(1, 50000); +} + +static TableView tableview(Table& table) +{ + auto tv = table.where().greater(1, 100).less(1, 50000).find_all(); +#if FUZZ_SORTED + tv.sort({1, 0}, {true, true}); +#endif + return tv; +} + +static int64_t id = 0; + +static void import_initial_values(SharedRealm& r, std::vector& initial_values) +{ + auto& table = *r->read_group()->get_table("class_object"); + + r->begin_transaction(); + table.clear(); + size_t ndx = table.add_empty_row(initial_values.size()); + for (auto value : initial_values) { + table.set_int(0, ndx, id++); + table.set_int(1, ndx++, value); + log("%lld\n", value); + } + r->commit_transaction(); +} + +// Apply the changes from the command file and then return whether a change +// notification should occur +static bool apply_changes(Realm& r, std::istream& input_stream) +{ + auto& table = *r.read_group()->get_table("class_object"); + auto tv = tableview(table); + + std::vector modified; + + log("\n"); + r.begin_transaction(); + for (auto const& change : read_changes(input_stream)) { + switch (change.action) { + case Change::Action::Commit: + log("c\n"); + r.commit_transaction(); + _impl::RealmCoordinator::get_existing_coordinator(r.config().path)->on_change(); + r.begin_transaction(); + break; + + case Change::Action::Add: { + log("a %lld\n", change.value); + size_t ndx = table.add_empty_row(); + table.set_int(0, ndx, id++); + table.set_int(1, ndx, change.value); + break; + } + + case Change::Action::Modify: + if (change.index < table.size()) { + log("m %zu %lld\n", change.index, change.value); + modified.push_back(table.get_int(0, change.index)); + table.set_int(1, change.index, change.value); + } + break; + + case Change::Action::Delete: + if (change.index < table.size()) { + log("d %zu\n", change.index); + table.move_last_over(change.index); + } + break; + } + } + r.commit_transaction(); + log("\n"); + + auto tv2 = tableview(table); + if (tv.size() != tv2.size()) + return true; + + for (size_t i = 0; i < tv.size(); ++i) { + if (!tv.is_row_attached(i)) + return true; + if (tv.get_int(0, i) != tv2.get_int(0, i)) + return true; + if (find(begin(modified), end(modified), tv.get_int(0, i)) != end(modified)) + return true; + } + + return false; +} + +static void verify(CollectionChangeIndices const& changes, std::vector values, Table& table) +{ + auto tv = tableview(table); + + // Apply the changes from the transaction log to our copy of the + // initial, using UITableView's batching rules (i.e. delete, then + // insert, then update) + auto it = std::make_reverse_iterator(changes.deletions.end()), end = std::make_reverse_iterator(changes.deletions.begin()); + for (; it != end; ++it) { + values.erase(values.begin() + it->first, values.begin() + it->second); + } + + for (auto i : changes.insertions.as_indexes()) { + values.insert(values.begin() + i, tv.get_int(1, i)); + } + + if (values.size() != tv.size()) { + abort(); + } + + for (auto i : changes.modifications.as_indexes()) { + if (changes.insertions.contains(i)) + abort(); + values[i] = tv.get_int(1, i); + } + +#if FUZZ_SORTED + if (!std::is_sorted(values.begin(), values.end())) + abort(); +#endif + + for (size_t i = 0; i < values.size(); ++i) { + if (values[i] != tv.get_int(1, i)) { + abort(); + } + } +} + +static void test(Realm::Config const& config, SharedRealm& r, SharedRealm& r2, std::istream& input_stream) +{ + std::vector initial_values = read_initial_values(input_stream); + if (initial_values.empty()) { + return; + } + import_initial_values(r, initial_values); + + auto& table = *r->read_group()->get_table("class_object"); + auto results = Results(r, ObjectSchema(), query(table)) +#if FUZZ_SORTED + .sort({{1, 0}, {true, true}}) +#endif + ; + + initial_values.clear(); + for (size_t i = 0; i < results.size(); ++i) + initial_values.push_back(results.get(i).get_int(1)); + + CollectionChangeIndices changes; + int notification_calls = 0; + auto token = results.add_notification_callback([&](CollectionChangeIndices c, std::exception_ptr err) { + if (notification_calls > 0 && c.empty()) + abort(); + changes = c; + ++notification_calls; + }); + + auto& coordinator = *_impl::RealmCoordinator::get_existing_coordinator(config.path); + coordinator.on_change(); r->notify(); + if (notification_calls != 1) { + abort(); + } + + bool expect_notification = apply_changes(*r2, input_stream); + coordinator.on_change(); r->notify(); + + if (notification_calls != 1 + expect_notification) { + abort(); + } + + verify(changes, initial_values, table); +} + +int main(int argc, char** argv) { + std::ios_base::sync_with_stdio(false); + realm::disable_sync_to_disk(); + + Realm::Config config; + config.path = getenv("TMPDIR"); + config.path += "/realm.XXXXXX"; + mktemp(&config.path[0]); + config.cache = false; + config.in_memory = true; + config.automatic_change_notifications = false; + + Schema schema = { + {"object", "", { + {"id", PropertyTypeInt}, + {"value", PropertyTypeInt} + }} + }; + + config.schema = std::make_unique(schema); + unlink(config.path.c_str()); + + auto r = Realm::get_shared_realm(config); + auto r2 = Realm::get_shared_realm(config); + auto& coordinator = *_impl::RealmCoordinator::get_existing_coordinator(config.path); + + auto test_on = [&](auto& buffer) { + id = 0; + + std::istringstream ss(buffer); + test(config, r, r2, ss); + if (r->is_in_transaction()) + r->cancel_transaction(); + r2->invalidate(); + coordinator.on_change(); + }; + + if (argc > 1) { + std::string buffer; + for (int i = 1; i < argc; ++i) { + int fd = open(argv[i], O_RDONLY); + if (fd < 0) + abort(); + read_all(buffer, fd); + close(fd); + + test_on(buffer); + } + unlink(config.path.c_str()); + return 0; + } + +#ifdef __AFL_HAVE_MANUAL_CONTROL + std::string buffer; + while (__AFL_LOOP(1000)) { + read_all(buffer, 0); + test_on(buffer); + } +#else + std::string buffer; + read_all(buffer, 0); + test_on(buffer); +#endif + + unlink(config.path.c_str()); + return 0; +} diff --git a/fuzzer/input/0 b/fuzzer/input/0 new file mode 100644 index 00000000..50132fd0 --- /dev/null +++ b/fuzzer/input/0 @@ -0,0 +1,19 @@ +3 +100 +200 +400 +1000 +2000 +50 +80 +150 +180 +6000 +5000 +60000 + +a 500 +d 12 +c +m 11 10000 +a 800 diff --git a/fuzzer/input/1 b/fuzzer/input/1 new file mode 100644 index 00000000..483456fa --- /dev/null +++ b/fuzzer/input/1 @@ -0,0 +1,33 @@ +101 +102 +103 +104 +105 +106 +107 +108 +109 +110 +111 +112 +113 + +a 114 +a 115 +a 116 +a 117 +a 118 +a 119 +a 120 +a 121 +a 122 +c +m 4 200 +m 3 201 +m 2 202 +m 1 203 +m 5 203 +m 6 204 +m 7 205 +c +d 11