From d3c7843e16638a344cc0facd0d46f1074caa077e Mon Sep 17 00:00:00 2001 From: Jordan Bayles Date: Mon, 26 Oct 2026 14:42:11 +0000 Subject: [PATCH] fix(fuzz): use the real reader settings keys in the fuzzer The fuzzer set most CharReaderBuilder settings under names with a trailing underscore ("allowComments_", "strictRoot_", ...), which newCharReader() does not read, and set failIfExtra twice. As a result only failIfExtra and collectComments ever varied; every other reader mode was stuck at its default. Use the real key names, give the duplicate failIfExtra bit to skipBom, and add an optional small stackLimit (bit 11, value from the upper 16 bits) so the depth-limit error paths are reachable. Abort if CharReaderBuilder::validate() rejects the settings, so a misspelled key fails loudly instead of silently disabling a mode. The FuzzTest unit test only passed "{}", which is shorter than the 4-byte settings header and returned before touching any settings. Exercise no settings, all settings, and a tiny stack limit. In a local 2-minute libFuzzer run from test/data seeds, json_reader.cpp edge coverage goes from 465 to 570 of 1996. --- src/test_lib_json/fuzz.cpp | 35 ++++++++++++++++++++++++----------- src/test_lib_json/main.cpp | 27 ++++++++++++++++++++++----- 2 files changed, 46 insertions(+), 16 deletions(-) diff --git a/src/test_lib_json/fuzz.cpp b/src/test_lib_json/fuzz.cpp index 3679a95ec..d79f80209 100644 --- a/src/test_lib_json/fuzz.cpp +++ b/src/test_lib_json/fuzz.cpp @@ -6,6 +6,7 @@ #include "fuzz.h" #include +#include #include #include #include @@ -25,17 +26,29 @@ extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { data += sizeof(uint32_t); size -= sizeof(uint32_t); - builder.settings_["failIfExtra"] = hash_settings & (1 << 0); - builder.settings_["allowComments_"] = hash_settings & (1 << 1); - builder.settings_["strictRoot_"] = hash_settings & (1 << 2); - builder.settings_["allowDroppedNullPlaceholders_"] = hash_settings & (1 << 3); - builder.settings_["allowNumericKeys_"] = hash_settings & (1 << 4); - builder.settings_["allowSingleQuotes_"] = hash_settings & (1 << 5); - builder.settings_["failIfExtra_"] = hash_settings & (1 << 6); - builder.settings_["rejectDupKeys_"] = hash_settings & (1 << 7); - builder.settings_["allowSpecialFloats_"] = hash_settings & (1 << 8); - builder.settings_["collectComments"] = hash_settings & (1 << 9); - builder.settings_["allowTrailingCommas_"] = hash_settings & (1 << 10); + builder.settings_["failIfExtra"] = (hash_settings & (1 << 0)) != 0; + builder.settings_["allowComments"] = (hash_settings & (1 << 1)) != 0; + builder.settings_["strictRoot"] = (hash_settings & (1 << 2)) != 0; + builder.settings_["allowDroppedNullPlaceholders"] = + (hash_settings & (1 << 3)) != 0; + builder.settings_["allowNumericKeys"] = (hash_settings & (1 << 4)) != 0; + builder.settings_["allowSingleQuotes"] = (hash_settings & (1 << 5)) != 0; + builder.settings_["skipBom"] = (hash_settings & (1 << 6)) != 0; + builder.settings_["rejectDupKeys"] = (hash_settings & (1 << 7)) != 0; + builder.settings_["allowSpecialFloats"] = (hash_settings & (1 << 8)) != 0; + builder.settings_["collectComments"] = (hash_settings & (1 << 9)) != 0; + builder.settings_["allowTrailingCommas"] = (hash_settings & (1 << 10)) != 0; + // Bit 11 selects a small stack limit taken from the upper 16 bits, so that + // the depth-limit error paths are reachable without huge inputs. + if (hash_settings & (1 << 11)) { + builder.settings_["stackLimit"] = (hash_settings >> 16) & 0x3FF; + } + + // An unknown key is silently ignored by newCharReader(), so a typo here + // would quietly stop the fuzzer from exercising that setting. + if (!builder.validate(nullptr)) { + std::abort(); + } std::unique_ptr reader(builder.newCharReader()); diff --git a/src/test_lib_json/main.cpp b/src/test_lib_json/main.cpp index e09b87b84..b1c884aa0 100644 --- a/src/test_lib_json/main.cpp +++ b/src/test_lib_json/main.cpp @@ -4324,11 +4324,28 @@ struct FuzzTest : JsonTest::TestCase {}; // Build and run the fuzz test without any fuzzer, so that it's guaranteed not // go out of date, even if it's never run as an actual fuzz test. JSONTEST_FIXTURE_LOCAL(FuzzTest, fuzzDoesntCrash) { - const std::string example = "{}"; - JSONTEST_ASSERT_EQUAL( - 0, - LLVMFuzzerTestOneInput(reinterpret_cast(example.c_str()), - example.size())); + // The first four bytes select the reader settings (little-endian). + const auto withSettings = [](uint32_t settings, const std::string& doc) { + std::string input; + for (int i = 0; i < 4; ++i) { + input += static_cast((settings >> (8 * i)) & 0xFF); + } + return input + doc; + }; + const std::string examples[] = { + "{}", + withSettings(0, "{\"a\":[1,2]}"), + // Every boolean setting on, with a stack limit of 16. + withSettings(0x0010FFFF, "{\"a\":[1,2,],} // comment"), + // A stack limit of 1, so the nested arrays exceed it. + withSettings(0x00010800, "[[[[1]]]]"), + }; + for (const auto& example : examples) { + JSONTEST_ASSERT_EQUAL( + 0, + LLVMFuzzerTestOneInput(reinterpret_cast(example.data()), + example.size())); + } } struct ParseWithStructuredErrorsTest : JsonTest::TestCase {