diff --git a/.github/workflows/fuzzers.yml b/.github/workflows/fuzzers.yml index 5717bb0f5..cb68ac48e 100644 --- a/.github/workflows/fuzzers.yml +++ b/.github/workflows/fuzzers.yml @@ -13,7 +13,7 @@ jobs: runs-on: ubuntu-latest env: # fuzzers that use the default implementation - defaultimplfuzzers: atpointer dump dump_raw_tape minify parser print_json + defaultimplfuzzers: atpointer dump dump_raw_tape element minify parser print_json # fuzzers that loop over the implementations themselves implfuzzers: implementations minifyimpl utf8 implementations: haswell westmere fallback diff --git a/fuzz/CMakeLists.txt b/fuzz/CMakeLists.txt index 8130f6be6..a3b632df6 100644 --- a/fuzz/CMakeLists.txt +++ b/fuzz/CMakeLists.txt @@ -53,6 +53,7 @@ if(ENABLE_FUZZING) implement_fuzzer(fuzz_atpointer) implement_fuzzer(fuzz_dump) implement_fuzzer(fuzz_dump_raw_tape) + implement_fuzzer(fuzz_element) implement_fuzzer(fuzz_implementations) # parses and serializes again, compares across implementations implement_fuzzer(fuzz_minify) # minify *with* parsing implement_fuzzer(fuzz_minifyimpl) # minify *without* parsing, plus compare implementations diff --git a/fuzz/FuzzUtils.h b/fuzz/FuzzUtils.h index c79e7638e..22594337f 100644 --- a/fuzz/FuzzUtils.h +++ b/fuzz/FuzzUtils.h @@ -2,6 +2,9 @@ #define SIMDJSON_FUZZUTILS_H #include +#include +#include +#include //memcpy // view data as a byte pointer template inline const std::uint8_t* as_bytes(const T* data) { @@ -14,4 +17,142 @@ template inline const char* as_chars(const T* data) { } + + +// Splits the input into strings, using a four byte separator which is human +// readable. Makes for nicer debugging of fuzz data. +// See https://github.com/google/fuzzing/blob/master/docs/split-inputs.md#magic-separator +// for background. Note: don't use memmem, it is not standard C++. +inline std::vector split(const char* Data, size_t Size) { + + std::vector ret; + + using namespace std::literals; + constexpr auto sep="\n~~\n"sv; + + std::string_view all(Data,Size); + auto pos=all.find(sep); + while(pos!=std::string_view::npos) { + ret.push_back(all.substr(0,pos)); + all=all.substr(pos+sep.size()); + pos=all.find(sep); + } + ret.push_back(all); + return ret; +} + +// Generic helper to split fuzz data into usable parts, like ints etc. +// Note that it does not throw, instead it sets the data pointer to null +// if the input is exhausted. +struct FuzzData { + // data may not be null, even if size is zero. + FuzzData(const uint8_t* data, + size_t size) : Data(data),Size(size){} + + ///range is inclusive + template + int getInt() { + static_assert (Min(get()%urange); + if(range<65536) + return Min+static_cast(get()%urange); + + return Min+static_cast(get()%urange); + } + + template + T get() { + const auto Nbytes=sizeof(T); + T ret{}; + if(Size + std::string_view get_stringview() { + static_assert (Min>=0,"Min must be positive"); + const int len=getInt(); + const unsigned int ulen=static_cast(len); + if(ulen splitIntoStrings() { + std::vector ret; + if(Size>0) { + ret=split(chardata(),Size); + // all data consumed. + Data+=Size; + Size=0; + } + return ret; + } + + //are we good? + explicit operator bool() const { return Data!=nullptr;} + + //we are a URBG + // https://en.cppreference.com/w/cpp/named_req/UniformRandomBitGenerator + //The type G satisfies UniformRandomBitGenerator if Given + // T, the type named by G::result_type + // g, a value of type G + // + // The following expressions must be valid and have their specified effects + // Expression Return type Requirements + // G::result_type T T is an unsigned integer type + using result_type=uint8_t; + // G::min() T Returns the smallest value that G's operator() may return. The value is strictly less than G::max(). The function must be constexpr. + static constexpr result_type min() {return 0;} + // G::max() T Returns the largest value that G's operator() may return. The value is strictly greater than G::min(). The function must be constexpr. + static constexpr result_type max() {return 255;} + // g() T Returns a value in the closed interval [G::min(), G::max()]. Has amortized constant complexity. + result_type operator()() { + if(Size==0) { + // return something varying, otherwise uniform_int_distribution may get + // stuck + return failcount++; + } + const result_type ret=Data[0]; + Data++; + Size--; + return ret; + } + // returns a pointer to data as const char* to avoid those cstyle casts + const char* chardata() const {return static_cast(static_cast(Data));} + // members + const uint8_t* Data; + size_t Size; + uint8_t failcount=0; +}; + + #endif // SIMDJSON_FUZZUTILS_H diff --git a/fuzz/Fuzzing.md b/fuzz/Fuzzing.md index fdd9d708d..32ae4cf6a 100644 --- a/fuzz/Fuzzing.md +++ b/fuzz/Fuzzing.md @@ -9,6 +9,43 @@ The simdjson library tries to follow [fuzzing best practises](https://google.github.io/oss-fuzz/advanced-topics/ideal-integration/#summary). +There is both "normal" fuzzers just feeding the api with fuzz data, as well as **differential** fuzzers. The differential fuzzers feed the same data to the multiple implementations (haswell, westmere and fallback) and ensure the same results are achieved. This makes sure the user will always get the same answer regardless of which implementation is in use. + +The fuzzers are used in several ways. + +* local fuzzing - for developers testing their changes before pushing and/or during development of the fuzzers themselves. +* CI fuzzing - for weeding out those easy to find bugs in pull requests, before they are merged. +* oss-fuzz - heavy duty 24/7 fuzzing provided by the google driven oss-fuzz project + +## Local fuzzing +Just invoke fuzz/quick_check.sh, it will download the latest corpus from bintray (kept up to date by the CI fuzzers) and run the fuzzers for a short time. In case you want to run the fuzzers for longer, modify the timeout value in the script or invoke the fuzzer directly. + +This requires linux with clang and cmake installed (recent Debian and Ubuntu are known to work fine). + +It is also possible to run the full oss-fuzz setup by following [these oss-fuzz instructions](https://google.github.io/oss-fuzz/getting-started/new-project-guide/#testing-locally) with PROJECT_NAME set to simdjson. You will need rights to run docker. + +## Fuzzing as a CI job - x64 + +There is a CI job which builds and runs the fuzzers. This is aimed to catch the "easy to fuzz" bugs quickly, without having to wait until pull requests are merged and eventually built and run by oss-fuzz. + +The CI job does the following + - builds a fast fuzzer, with full optimization but less checks which is good at rapidly exploring the input space + - builds a heavily sanitized fuzzer, which is good at detecting errors + - downloads the stored corpus + - runs the fast fuzzer build for a while, to grow the corpus + - runs the sanitizer fuzzer for a while, using the input found by the fast fuzzer + - using a reproduce build (uninstrumented), executes a subset of the test cases in the corpus through valgrind + - minimizes the corpus and uploads it (if on the master branch) + - stores the corpus and valgrind output as artifacts + +The job is available under the actions tab, here is a [direct link](https://github.com/simdjson/simdjson/actions?query=workflow%3A%22Fuzz+and+run+valgrind%22). + +The corpus will grow over time and easy to find bugs will be detected already during the pull request stage. Also, it will keep the fuzzer builds from bit rot. + +## Fuzzing as a CI job - arm64 +There is also a job running the fuzzers on arm64 (see .drone.yml) to make sure also the arm specific parts are fuzzed. This does not update the corpus, it just reuses what the x64 job finds. + +## Fuzzing on oss-fuzz The simdjson library is continuously fuzzed on [oss-fuzz](https://github.com/google/oss-fuzz). In case a bug is found, the offending input is minimized and tested for reproducibility. A report with the details is automatically filed, and the contact persons at simdjson are notified via email. An issue is opened at the oss-fuzz bugtracker with restricted view access. When the bug is fixed, the issue is automatically closed. Bugs are automatically made visible to the public after a period of time. An example of a bug that was found, fixed and closed can be seen here: [oss-fuzz 18714](https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=18714). @@ -16,8 +53,7 @@ Bugs are automatically made visible to the public after a period of time. An exa ## Currently open bugs - -You can find the currently opened bugs, if any at [bugs.chromium.org](https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&q=proj%3Asimdjson&can=2): make sure not to miss the "Open Issues" selector. Bugs that are fixed by follow-up commits are automatically closed. +You can find the currently open bugs (if any) at [bugs.chromium.org](https://bugs.chromium.org/p/oss-fuzz/issues/list?sort=-opened&q=proj%3Asimdjson&can=2): make sure not to miss the "Open Issues" selector. Bugs that are fixed by follow-up commits are automatically closed. ## Integration with oss-fuzz @@ -26,24 +62,6 @@ Changes to the integration with oss-fuzz are made by making pull requests agains As little code as possible is kept at oss-fuzz since it is inconvenient to change. The [oss-fuzz build script](https://github.com/google/oss-fuzz/blob/b96dd54183f727a5d90c786e0fb01ec986c74d30/projects/simdjson/build.sh#L18) invokes [the script from the simdjson repo](https://github.com/simdjson/simdjson/blob/master/fuzz/ossfuzz.sh). - -## Fuzzing as a CI job - -There is a CI job which builds and runs the fuzzers. This is aimed to catch the "easy to fuzz" bugs quickly, without having to wait until pull requests are merged and eventually built and run by oss-fuzz. - -The CI job does the following - - builds several variants (with/without avx, with/without sanitizers, a fast fuzzer) - - downloads the stored corpus - - runs the fastest fuzzer build for 30 seconds, to grow the corpus - - runs each build variant for 10 seconds on each fuzzer - - using a reproduce build (uninstrumented), executes all the test cases in the corpus through valgrind - - minimizes the corpus and upload it (if on the master branch) - - store the corpus and valgrind output as artifacts - -The job is available under the actions tab, here is a [direct link](https://github.com/simdjson/simdjson/actions?query=workflow%3A%22Run+fuzzers+on+stored+corpus+and+test+it+with+valgrind%22). - -The corpus will grow over time and easy to find bugs will be detected already during the pull request stage. Also, it will keep the fuzzer builds from bit rot. - ## Corpus The simdjson library does not benefit from a corpus as much as other projects, because the library is very fast and explores the input space very well. With that said, it is still beneficial to have one. The CI job stores the corpus on bintray between runs, and is available at [bintray](https://dl.bintray.com/pauldreik/simdjson-fuzz-corpus/corpus/corpus.tar). @@ -55,32 +73,13 @@ One can also grab the corpus as an artifact from the github actions job. Pick a The code coverage from fuzzing is most easily viewed on the [oss-fuzz status panel](https://oss-fuzz.com/fuzzer-stats). Viewing the coverage does not require login, but the direct link is not easy to find. Substitute the date in the URL to get a more recent link: [https://storage.googleapis.com/oss-fuzz-coverage/simdjson/reports/20200411/linux/src/simdjson/report.html](https://storage.googleapis.com/oss-fuzz-coverage/simdjson/reports/20200411/linux/src/simdjson/report.html) - -## Running the fuzzers locally - -This has only been tested on Linux (Debian and Ubuntu are known to work). - -Make sure you have clang and cmake installed. -The easiest way to get started is to run the following, standing in the root of the checked out repo: -``` -fuzz/build_like_ossfuzz.sh -``` - -Then invoke a fuzzer as shown by the following example: -``` -mkdir -p out/parser -build/fuzz/fuzz_parser out/parser/ -``` - -You can also use the more extensive fuzzer build script to get a variation of builds by using -``` -fuzz/build_fuzzer_variants.sh -``` - -It is also possible to run the full oss-fuzz setup by following [these oss-fuzz instructions](https://google.github.io/oss-fuzz/getting-started/new-project-guide/#testing-locally) with PROJECT_NAME set to simdjson. You will need rights to run docker. +Keeping the coverage up is a never ending job. See [issue 368](https://github.com/simdjson/simdjson/issues/368) ## Reproducing -To reproduce a test case, build the fuzzers, then invoke it with the testcase as a command line argument: -``` -build/fuzz/fuzz_parser my_testcase.json +To reproduce a test case, use the local build instruction. Then invoke the fuzzer (the fuzz_parser is shown as an example below) with the testcase as a command line argument: +```shell +fuzz/build_fuzzer_variants.sh +build-sanitizers/fuzz/fuzz_parser my_testcase.json ``` +In case this does not reproduce the bug, you may want to proceed with reproducing using the oss-fuzz tools. See the instructions [here](https://google.github.io/oss-fuzz/advanced-topics/reproducing/). + diff --git a/fuzz/fuzz_atpointer.cpp b/fuzz/fuzz_atpointer.cpp index 2e9fee61d..f45aa5deb 100644 --- a/fuzz/fuzz_atpointer.cpp +++ b/fuzz/fuzz_atpointer.cpp @@ -1,68 +1,47 @@ -#include "simdjson.h" #include "FuzzUtils.h" +#include "simdjson.h" #include #include #include #include -struct FuzzData { - std::string_view json_pointer; - std::string_view json_doc; -}; - -/** - * @brief split split fuzz data into a pointer and a document - * @param Data - * @param Size - * @return - */ -FuzzData split(const char *Data, size_t Size) { - - using namespace std::literals; - constexpr auto sep="\n~~~\n"sv; - - std::string_view all(Data,Size); - auto pos=all.find(sep); - if(pos==std::string_view::npos) { - //not found. - return FuzzData{std::string_view{},all}; - } else { - return FuzzData{std::string_view{all.substr(0,pos)},all.substr(pos+sep.size())}; - } -} - extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { - // Split data into two strings, json pointer and the document string. - // Might end up with none, either or both being empty, important for - // covering edge cases such as https://github.com/simdjson/simdjson/issues/1142 - // Inputs missing the separator line will get an empty json pointer - // but the all the input put in the document string. This means - // test data from other fuzzers that take json input works for this fuzzer - // as well. - const auto fd=split(as_chars(Data),Size); + // Split data into two strings, json pointer and the document string. + // Might end up with none, either or both being empty, important for + // covering edge cases such as + // https://github.com/simdjson/simdjson/issues/1142 Inputs missing the + // separator line will get an empty json pointer but the all the input put in + // the document string. This means test data from other fuzzers that take json + // input works for this fuzzer as well. + FuzzData fd(Data, Size); + auto strings = fd.splitIntoStrings(); + while (strings.size() < 2) { + strings.emplace_back(); + } + assert(strings.size() >= 2); - simdjson::dom::parser parser; + simdjson::dom::parser parser; - // parse without exceptions, for speed - auto res=parser.parse(fd.json_doc.data(),fd.json_doc.size()); - if(res.error()) - return 0; - - simdjson::dom::element root; - if(res.get(root)) - return 0; - - auto maybe_leaf=root.at_pointer(fd.json_pointer); - if(maybe_leaf.error()) - return 0; - - simdjson::dom::element leaf; - if(maybe_leaf.get(leaf)) - return 0; - - std::string_view sv; - if(leaf.get_string().get(sv)) - return 0; + // parse without exceptions, for speed + auto res = parser.parse(strings[0]); + if (res.error()) return 0; + + simdjson::dom::element root; + if (res.get(root)) + return 0; + + auto maybe_leaf = root.at_pointer(strings[1]); + if (maybe_leaf.error()) + return 0; + + simdjson::dom::element leaf; + if (maybe_leaf.get(leaf)) + return 0; + + std::string_view sv; + if (leaf.get_string().get(sv)) + return 0; + return 0; } diff --git a/fuzz/fuzz_element.cpp b/fuzz/fuzz_element.cpp new file mode 100644 index 000000000..9257057ef --- /dev/null +++ b/fuzz/fuzz_element.cpp @@ -0,0 +1,130 @@ +#include "simdjson.h" +#include +#include +#include + +#include "FuzzUtils.h" +#include "NullBuffer.h" + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size) { + FuzzData fd(Data, Size); + const int action = fd.getInt<0, 31>(); + + // there will be some templatized functions like is() which need to be tested + // on a type. select one dynamically and create a function that will invoke + // with that type + const int selecttype=fd.getInt<0,7>(); + auto invoke_with_type=[selecttype](auto cb) { + using constcharstar=const char*; + switch(selecttype) { + case 0: cb(bool{});break; + case 1: cb(double{});break; + case 2: cb(uint64_t{});break; + case 3: cb(int64_t{});break; + case 4: cb(std::string_view{});break; + case 5: cb(constcharstar{});break; + case 6: cb(simdjson::dom::array{});break; + case 7: cb(simdjson::dom::object{});break; + } + }; + + const auto index = fd.get(); + + // split the remainder of the document into strings + auto strings = fd.splitIntoStrings(); + while (strings.size() < 2) { + strings.emplace_back(); + } + const auto str = strings[0]; + + // exit if there was too little data + if (!fd) + return 0; + + simdjson::dom::parser parser; + simdjson_unused simdjson::dom::element elem; + simdjson_unused auto error = parser.parse(strings[1]).get(elem); + + if (error) + return 0; + +#define CASE(num, fun) \ + case num: { \ + simdjson_unused auto v = elem.fun(); \ + break; \ + } +#define CASE2(num, fun) \ + case num: { \ + simdjson_unused auto v = elem fun; \ + break; \ + } + try { + + switch (action) { + CASE(0, type); + CASE(1, get_array); + CASE(2, get_object); + CASE(3, get_c_str); + CASE(4, get_string_length); + CASE(5, get_string); + CASE(6, get_int64); + CASE(7, get_uint64); + CASE(8, get_double); + CASE(9, get_bool); + CASE(10, is_array); + CASE(11, is_object); + CASE(12, is_string); + CASE(13, is_int64); + CASE(14, is_uint64); + CASE(15, is_double); + CASE(16, is_number); + CASE(17, is_bool); + CASE(18, is_null); + // element.is<>() : + case 19: { + invoke_with_type([&elem](auto t){ simdjson_unused auto v = elem.is(); }); + } break; + + // CASE(xx,get); + case 20: { + invoke_with_type([&elem](auto t){ simdjson_unused auto v = elem.get(); }); + } break; + + // CASE(xx,tie); + case 21: { + invoke_with_type([&elem](auto t){ + simdjson::error_code ec; + simdjson::dom::element{elem}.tie(t,ec); }); + } break; + +#if SIMDJSON_EXCEPTIONS + // cast to type + case 22: { + invoke_with_type([&elem](auto t){ + using T=decltype(t); + simdjson_unused auto v = static_cast(elem); }); + } break; + + CASE(23, begin); + CASE(24, end); +#endif + CASE2(25, [str]); + CASE2(26, .at_pointer(str)); + // CASE2(xx,at(str)); deprecated + CASE2(28, .at(index)); + CASE2(29, .at_key(str)); + CASE2(30, .at_key_case_insensitive(str)); + case 31: { NulOStream os; + simdjson_unused auto dumpstatus = elem.dump_raw_tape(os);} ;break; + default: + return 0; + } +#undef CASE +#undef CASE2 + + } catch (std::exception &) { + // do nothing + } + + return 0; +} diff --git a/include/simdjson/dom/element-inl.h b/include/simdjson/dom/element-inl.h index 485b0e86f..0ece4f69d 100644 --- a/include/simdjson/dom/element-inl.h +++ b/include/simdjson/dom/element-inl.h @@ -93,6 +93,9 @@ simdjson_really_inline bool simdjson_result::is_uint64() const noe simdjson_really_inline bool simdjson_result::is_double() const noexcept { return !error() && first.is_double(); } +simdjson_really_inline bool simdjson_result::is_number() const noexcept { + return !error() && first.is_number(); +} simdjson_really_inline bool simdjson_result::is_bool() const noexcept { return !error() && first.is_bool(); } @@ -294,6 +297,10 @@ simdjson_warn_unused simdjson_really_inline error_code element::get(ele value = element(tape); return SUCCESS; } +template +inline void element::tie(T &value, error_code &error) && noexcept { + error = get(value); +} template simdjson_really_inline bool element::is() const noexcept { @@ -317,6 +324,7 @@ inline bool element::is_int64() const noexcept { return is(); } inline bool element::is_uint64() const noexcept { return is(); } inline bool element::is_double() const noexcept { return is(); } inline bool element::is_bool() const noexcept { return is(); } +inline bool element::is_number() const noexcept { return is_int64() || is_uint64() || is_double(); } inline bool element::is_null() const noexcept { return tape.is_null_on_tape(); diff --git a/include/simdjson/dom/element.h b/include/simdjson/dom/element.h index c7dd024ac..3254d4188 100644 --- a/include/simdjson/dom/element.h +++ b/include/simdjson/dom/element.h @@ -125,7 +125,7 @@ public: */ inline simdjson_result get_uint64() const noexcept; /** - * Cast this element to an double floating-point. + * Cast this element to a double floating-point. * * Equivalent to get(). * @@ -179,12 +179,14 @@ public: * Equivalent to is(). */ inline bool is_double() const noexcept; + /** * Whether this element is a json number. * * Both integers and floating points will return true. */ inline bool is_number() const noexcept; + /** * Whether this element is a json `true` or `false`. * @@ -513,6 +515,7 @@ public: simdjson_really_inline bool is_int64() const noexcept; simdjson_really_inline bool is_uint64() const noexcept; simdjson_really_inline bool is_double() const noexcept; + simdjson_really_inline bool is_number() const noexcept; simdjson_really_inline bool is_bool() const noexcept; simdjson_really_inline bool is_null() const noexcept; diff --git a/include/simdjson/padded_string-inl.h b/include/simdjson/padded_string-inl.h index 49326dd0a..f11eb891b 100644 --- a/include/simdjson/padded_string-inl.h +++ b/include/simdjson/padded_string-inl.h @@ -37,14 +37,11 @@ inline char *allocate_padded_buffer(size_t length) noexcept { inline padded_string::padded_string() noexcept {} inline padded_string::padded_string(size_t length) noexcept : viable_size(length), data_ptr(internal::allocate_padded_buffer(length)) { - if (data_ptr != nullptr) - data_ptr[length] = '\0'; // easier when you need a c_str } inline padded_string::padded_string(const char *data, size_t length) noexcept : viable_size(length), data_ptr(internal::allocate_padded_buffer(length)) { if ((data != nullptr) and (data_ptr != nullptr)) { std::memcpy(data_ptr, data, length); - data_ptr[length] = '\0'; // easier when you need a c_str } } // note: do not pass std::string arguments by value @@ -52,15 +49,13 @@ inline padded_string::padded_string(const std::string & str_ ) noexcept : viable_size(str_.size()), data_ptr(internal::allocate_padded_buffer(str_.size())) { if (data_ptr != nullptr) { std::memcpy(data_ptr, str_.data(), str_.size()); - data_ptr[str_.size()] = '\0'; // easier when you need a c_str } } // note: do pass std::string_view arguments by value inline padded_string::padded_string(std::string_view sv_) noexcept : viable_size(sv_.size()), data_ptr(internal::allocate_padded_buffer(sv_.size())) { - if (data_ptr != nullptr) { + if (sv_.size()) { std::memcpy(data_ptr, sv_.data(), sv_.size()); - data_ptr[sv_.size()] = '\0'; // easier when you need a c_str } } inline padded_string::padded_string(padded_string &&o) noexcept diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 95014134f..0e13f3fe0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -67,6 +67,7 @@ add_cpp_test(parse_many_test LABELS acceptance per_implementation) add_cpp_test(pointercheck LABELS acceptance per_implementation) # https://tools.ietf.org/html/rfc6901 add_cpp_test(extracting_values_example LABELS acceptance per_implementation) add_cpp_test(unicode_tests LABELS acceptance per_implementation) +add_cpp_test(padded_string_tests) find_program(BASH bash) diff --git a/tests/padded_string_tests.cpp b/tests/padded_string_tests.cpp new file mode 100644 index 000000000..fb6e54ca3 --- /dev/null +++ b/tests/padded_string_tests.cpp @@ -0,0 +1,16 @@ + +#include "simdjson.h" + +#include + +// this test is needed, because memcpy may be invoked on a null pointer +// otherwise +static void testNullString() { + std::string_view empty; + simdjson::padded_string blah(empty); +} + +int main() { + + testNullString(); +}