#include "zeroerr/assert.h" #include "zeroerr/fuzztest.h" #include "common/Json.h" #include #include #include #include #include #include using eve::json::Document; using eve::json::Value; using namespace zeroerr; namespace { class NonEmptyStringDomain final : public Domain> { public: using ValueType = std::string; using CorpusType = std::vector; NonEmptyStringDomain() { impl_.WithMinSize(1); impl_.WithMaxSize(4096); } CorpusType GetRandomCorpus(Rng& rng) const override { return impl_.GetRandomCorpus(rng); } CorpusType FromValue(const std::string& value) const override { if (value.empty()) return {'\0'}; return impl_.FromValue(value); } std::string GetValue(const CorpusType& value) const override { return impl_.GetValue(value); } void Mutate(Rng& rng, CorpusType& value, bool onlyShrink) const override { // zeroerr's sequence domain indexes the corpus after choosing a mutation // action, even when a libFuzzer-generated corpus deserializes as empty. if (value.empty()) value.push_back('\0'); impl_.Mutate(rng, value, onlyShrink); } private: SequenceContainerOf> impl_{Arbitrary()}; }; int envInt(const char* name, int fallback, int maximum) { const char* text = std::getenv(name); if (!text || !*text) return fallback; try { const long long value = std::stoll(text); if (value < 1) return 1; return static_cast(std::min(value, maximum)); } catch (...) { return fallback; } } bool exerciseValue(Value value, size_t depth = 0) { if (depth > 256) return false; const int kindCount = static_cast(value.isNull()) + static_cast(value.isBool()) + static_cast(value.isNumber()) + static_cast(value.isString()) + static_cast(value.isObject()) + static_cast(value.isArray()); if (kindCount != 1) return false; (void)value.asBool(true); (void)value.asInt(17); (void)value.asFloat(1.25f); (void)value.asDouble(2.5); (void)value.asString("fallback"); (void)value.at(value.size()); (void)value.get(nullptr); if (value.isArray()) { for (size_t i = 0; i < value.size(); ++i) if (!exerciseValue(value.at(i), depth + 1)) return false; } else if (value.isObject()) { const auto keys = value.keys(); if (keys.size() != value.size()) return false; for (const auto& key : keys) { if (!value.has(key.c_str()) || !exerciseValue(value.get(key.c_str()), depth + 1)) return false; } } return true; } } // namespace FUZZ_TEST_CASE("fuzz.json.document") { auto fuzzTest = FUZZ_FUNC([=](const std::string& input) { std::string error; Document document = Document::parse(input, &error); if (!document.valid()) { REQUIRE(!document.root()); REQUIRE(!error.empty()); return; } REQUIRE(error.empty()); REQUIRE(exerciseValue(document.root())); }).WithDomains(NonEmptyStringDomain()); fuzzTest.WithSeeds({{"null"}, {"{}"}, {"[]"}, {R"({"name":"eve","values":[0,-1,2.5,true,null]})"}, {R"("\ud83d\ude00")"}, {std::string(257, '[') + "0" + std::string(257, ']')}}); const int runs = envInt("EVENGINE_FUZZ_RUNS", 2000, 1000000); const int seed = envInt("EVENGINE_FUZZ_SEED", 1, 2147483647); // zeroerr stops on count == max_count by throwing out of the fuzz callback. // Newer libFuzzer treats that otherwise-successful unwind as a target exit. // Give zeroerr one extra internal count so LLVMFuzzerRunDriver returns normally. fuzzTest.count = 1; fuzzTest.max_count = runs + 1; auto rng = std::make_unique(seed); fuzzTest.m_rng = rng.get(); RunFuzzTest(fuzzTest, seed, runs, 500, 1200, 1); fuzzTest.m_rng = nullptr; }