From 7db6b3340d2388a4b5bc1561b300e9fd8db27e13 Mon Sep 17 00:00:00 2001 From: Niels Lohmann Date: Mon, 28 Sep 2026 21:48:12 +0200 Subject: [PATCH] Add a fuzzer for json_document fuzzer-parse_json_view.cpp checks for every input that json_document::accept agrees with json::accept, that an accepted input materializes to the value json::parse returns, and that a rejected input makes both throw the same exception with the same message. It is built like the other fuzzers (tests/Makefile, and the root Makefile's fuzz_testing_json_view target, which starts from the JSON test corpus) and listed in tests/fuzzing.md. Signed-off-by: Niels Lohmann --- Makefile | 9 +++ tests/Makefile | 5 +- tests/fuzzing.md | 7 ++ tests/src/fuzzer-parse_json_view.cpp | 100 +++++++++++++++++++++++++++ 4 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 tests/src/fuzzer-parse_json_view.cpp diff --git a/Makefile b/Makefile index c37d3ce67..6054a05cf 100644 --- a/Makefile +++ b/Makefile @@ -40,6 +40,7 @@ all: @echo "fuzz_testing_bon8 - prepare fuzz testing of the BON8 parser" @echo "fuzz_testing_bson - prepare fuzz testing of the BSON parser" @echo "fuzz_testing_cbor - prepare fuzz testing of the CBOR parser" + @echo "fuzz_testing_json_view - prepare fuzz testing of the json_document/json_view parser" @echo "fuzz_testing_msgpack - prepare fuzz testing of the MessagePack parser" @echo "fuzz_testing_ubjson - prepare fuzz testing of the UBJSON parser" @echo "pretty - beautify code with Artistic Style" @@ -97,6 +98,14 @@ fuzz_testing_cbor: find tests/data -size -5k -name *.cbor | xargs -I{} cp "{}" fuzz-testing/testcases @echo "Execute: afl-fuzz -i fuzz-testing/testcases -o fuzz-testing/out fuzz-testing/fuzzer" +fuzz_testing_json_view: + rm -fr fuzz-testing + mkdir -p fuzz-testing fuzz-testing/testcases fuzz-testing/out + $(MAKE) parse_json_view_fuzzer -C tests CXX=afl-clang++ + mv tests/parse_json_view_fuzzer fuzz-testing/fuzzer + find tests/data/json_tests -size -5k -name *json | xargs -I{} cp "{}" fuzz-testing/testcases + @echo "Execute: afl-fuzz -i fuzz-testing/testcases -o fuzz-testing/out fuzz-testing/fuzzer" + fuzz_testing_msgpack: rm -fr fuzz-testing mkdir -p fuzz-testing fuzz-testing/testcases fuzz-testing/out diff --git a/tests/Makefile b/tests/Makefile index 6bfa14397..e8e61e603 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -10,12 +10,15 @@ CXXFLAGS += -std=c++11 CPPFLAGS += -I ../single_include FUZZER_ENGINE = src/fuzzer-driver_afl.cpp -FUZZERS = parse_afl_fuzzer parse_bson_fuzzer parse_cbor_fuzzer parse_msgpack_fuzzer parse_ubjson_fuzzer parse_bjdata_fuzzer parse_bon8_fuzzer +FUZZERS = parse_afl_fuzzer parse_bson_fuzzer parse_cbor_fuzzer parse_msgpack_fuzzer parse_ubjson_fuzzer parse_bjdata_fuzzer parse_bon8_fuzzer parse_json_view_fuzzer fuzzers: $(FUZZERS) parse_afl_fuzzer: $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(FUZZER_ENGINE) src/fuzzer-parse_json.cpp -o $@ +parse_json_view_fuzzer: + $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(FUZZER_ENGINE) src/fuzzer-parse_json_view.cpp -o $@ + parse_bson_fuzzer: $(CXX) $(CXXFLAGS) $(CPPFLAGS) $(FUZZER_ENGINE) src/fuzzer-parse_bson.cpp -o $@ diff --git a/tests/fuzzing.md b/tests/fuzzing.md index 49ab7b575..8ef983229 100644 --- a/tests/fuzzing.md +++ b/tests/fuzzing.md @@ -3,6 +3,13 @@ Each parser of the library (JSON, BJData, BON8, BSON, CBOR, MessagePack, and UBJSON) can be fuzz tested. Currently, [libFuzzer](https://llvm.org/docs/LibFuzzer.html) and [afl++](https://github.com/AFLplusplus/AFLplusplus) are supported. +Additionally, `parse_json_view_fuzzer` (`tests/src/fuzzer-parse_json_view.cpp`) cross-checks `json_document`/`json_view` +(the zero-copy, read-only view declared in `json_view.hpp`) against `basic_json` on the same JSON text: it asserts that +`json_document::accept` agrees with `json::accept`, that an accepted input materializes to the same value `json::parse` +produces, and that a rejected input makes both parsers throw with an identical `what()`. It takes plain JSON text, so it +reuses the `corpus_json` corpus (or, for the `make fuzz_testing_json_view` target below, `tests/data/json_tests`) rather +than a format of its own. + ## Corpus creation For most effective fuzzing, a [corpus](https://llvm.org/docs/LibFuzzer.html#corpus) should be provided. A corpus is a diff --git a/tests/src/fuzzer-parse_json_view.cpp b/tests/src/fuzzer-parse_json_view.cpp new file mode 100644 index 000000000..59e32a556 --- /dev/null +++ b/tests/src/fuzzer-parse_json_view.cpp @@ -0,0 +1,100 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +/* +This file implements a parser test suitable for fuzz testing. It checks that +json_document (the zero-copy, read-only view of a parsed JSON text declared in +json_view.hpp) agrees with basic_json on every input: + +- json_document::accept(data) must equal json::accept(data) +- if the input is accepted, json_document::parse(data).root().materialize() + must equal json::parse(data) +- if the input is rejected, json_document::parse(data) (with exceptions + enabled) must throw a json::parse_error or json::out_of_range whose what() + is identical to the one json::parse(data) throws + +The provided function `LLVMFuzzerTestOneInput` can be used in different fuzzer +drivers. +*/ + +#include +#include +#include +#include + +// the checks below are assertions; NDEBUG would compile them away +#ifdef NDEBUG + #error "the fuzzer drivers must be built without NDEBUG" +#endif + +using json = nlohmann::json; +using json_document = nlohmann::json_document; + +// see http://llvm.org/docs/LibFuzzer.html +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) +{ + // json_document::accept only has a single-argument overload; wrap the raw + // bytes in a (borrowed) std::string so the same bytes can be handed to it + const std::string input(reinterpret_cast(data), size); // NOLINT(cppcoreguidelines-pro-type-reinterpret-cast) + + const bool accepted_by_json = json::accept(data, data + size); + const bool accepted_by_view = json_document::accept(input); + + // json_document::accept must agree with json::accept on every input + assert(accepted_by_json == accepted_by_view); + + if (accepted_by_json) + { + // both parsers must agree on the resulting value + json const j1 = json::parse(data, data + size); + json_document const doc = json_document::parse(input); + json const j2 = doc.root().materialize(); + assert(j1 == j2); + } + else + { + // both parsers must reject the input the same way when exceptions are used + std::string expected_what; + bool json_threw = false; + try + { + static_cast(json::parse(data, data + size)); + } + catch (const json::parse_error& e) + { + expected_what = e.what(); + json_threw = true; + } + catch (const json::out_of_range& e) + { + expected_what = e.what(); + json_threw = true; + } + assert(json_threw); + + bool view_threw = false; + try + { + static_cast(json_document::parse(input)); + } + catch (const json::parse_error& e) + { + assert(e.what() == expected_what); + view_threw = true; + } + catch (const json::out_of_range& e) + { + assert(e.what() == expected_what); + view_threw = true; + } + assert(view_threw); + } + + // return 0 - non-zero return values are reserved for future use + return 0; +}