diff --git a/CMakeLists.txt b/CMakeLists.txt index e0b2751..d8aefd6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -104,10 +104,28 @@ endif() option(BUILD_FUZZERS "Build fuzzing targets" OFF) if(BUILD_FUZZERS AND NOT MSVC) - add_executable(fuzz_parser fuzz/fuzz_parser.cpp) - target_link_libraries(fuzz_parser args) - target_compile_options(fuzz_parser PRIVATE -fsanitize=fuzzer,address) - target_link_options(fuzz_parser PRIVATE -fsanitize=fuzzer,address) + # Generic fuzzer discovery: every fuzz/*_fuzzer.cpp becomes a target + # named after its file, so new harnesses need no CMake edits. Each + # harness selects its own compile-time configuration (e.g. + # noexcept_mode_fuzzer.cpp defines ARGS_NOEXCEPT itself). + file(GLOB ARGS_FUZZ_SOURCES CONFIGURE_DEPENDS + "${CMAKE_CURRENT_SOURCE_DIR}/fuzz/*_fuzzer.cpp") + foreach(fuzz_src ${ARGS_FUZZ_SOURCES}) + get_filename_component(fuzz_name ${fuzz_src} NAME_WE) + add_executable(${fuzz_name} ${fuzz_src}) + target_link_libraries(${fuzz_name} PRIVATE args) + if(DEFINED ENV{LIB_FUZZING_ENGINE} AND NOT "$ENV{LIB_FUZZING_ENGINE}" STREQUAL "") + # OSS-Fuzz build: sanitizer and coverage instrumentation arrive + # via CXXFLAGS and the fuzzing engine via LIB_FUZZING_ENGINE. + # Sanitizers must NOT be hardcoded here (it would break build + # matrix variants such as MSAN). + target_link_libraries(${fuzz_name} PRIVATE "$ENV{LIB_FUZZING_ENGINE}") + else() + # Local development fallback: clang's libFuzzer. + target_compile_options(${fuzz_name} PRIVATE -fsanitize=fuzzer-no-link) + target_link_options(${fuzz_name} PRIVATE -fsanitize=fuzzer) + endif() + endforeach() endif() option(ARGS_BUILD_EXAMPLE "Build example" ON) diff --git a/fuzz/corpus/numeric_parser_fuzzer/bare-prefix.seed b/fuzz/corpus/numeric_parser_fuzzer/bare-prefix.seed new file mode 100644 index 0000000..99e07d0 --- /dev/null +++ b/fuzz/corpus/numeric_parser_fuzzer/bare-prefix.seed @@ -0,0 +1 @@ +0x \ No newline at end of file diff --git a/fuzz/corpus/numeric_parser_fuzzer/binary.seed b/fuzz/corpus/numeric_parser_fuzzer/binary.seed new file mode 100644 index 0000000..54fe6e1 --- /dev/null +++ b/fuzz/corpus/numeric_parser_fuzzer/binary.seed @@ -0,0 +1 @@ +0b101 \ No newline at end of file diff --git a/fuzz/corpus/numeric_parser_fuzzer/embedded-nul.seed b/fuzz/corpus/numeric_parser_fuzzer/embedded-nul.seed new file mode 100644 index 0000000..174307f Binary files /dev/null and b/fuzz/corpus/numeric_parser_fuzzer/embedded-nul.seed differ diff --git a/fuzz/corpus/numeric_parser_fuzzer/empty.seed b/fuzz/corpus/numeric_parser_fuzzer/empty.seed new file mode 100644 index 0000000..e69de29 diff --git a/fuzz/corpus/numeric_parser_fuzzer/erange.seed b/fuzz/corpus/numeric_parser_fuzzer/erange.seed new file mode 100644 index 0000000..3a685c0 --- /dev/null +++ b/fuzz/corpus/numeric_parser_fuzzer/erange.seed @@ -0,0 +1 @@ +99999999999999999999999 \ No newline at end of file diff --git a/fuzz/corpus/numeric_parser_fuzzer/expo.seed b/fuzz/corpus/numeric_parser_fuzzer/expo.seed new file mode 100644 index 0000000..e5b5c77 --- /dev/null +++ b/fuzz/corpus/numeric_parser_fuzzer/expo.seed @@ -0,0 +1 @@ +1e5 \ No newline at end of file diff --git a/fuzz/corpus/numeric_parser_fuzzer/float.seed b/fuzz/corpus/numeric_parser_fuzzer/float.seed new file mode 100644 index 0000000..3767b4b --- /dev/null +++ b/fuzz/corpus/numeric_parser_fuzzer/float.seed @@ -0,0 +1 @@ +3.14 \ No newline at end of file diff --git a/fuzz/corpus/numeric_parser_fuzzer/hex.seed b/fuzz/corpus/numeric_parser_fuzzer/hex.seed new file mode 100644 index 0000000..f341cee --- /dev/null +++ b/fuzz/corpus/numeric_parser_fuzzer/hex.seed @@ -0,0 +1 @@ +0x1f \ No newline at end of file diff --git a/fuzz/corpus/numeric_parser_fuzzer/high-bytes.seed b/fuzz/corpus/numeric_parser_fuzzer/high-bytes.seed new file mode 100644 index 0000000..a593c10 --- /dev/null +++ b/fuzz/corpus/numeric_parser_fuzzer/high-bytes.seed @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/fuzz/corpus/numeric_parser_fuzzer/inf.seed b/fuzz/corpus/numeric_parser_fuzzer/inf.seed new file mode 100644 index 0000000..a28aa9a --- /dev/null +++ b/fuzz/corpus/numeric_parser_fuzzer/inf.seed @@ -0,0 +1 @@ +inf \ No newline at end of file diff --git a/fuzz/corpus/numeric_parser_fuzzer/int-overflow.seed b/fuzz/corpus/numeric_parser_fuzzer/int-overflow.seed new file mode 100644 index 0000000..1ed5441 --- /dev/null +++ b/fuzz/corpus/numeric_parser_fuzzer/int-overflow.seed @@ -0,0 +1 @@ +2147483648 \ No newline at end of file diff --git a/fuzz/corpus/numeric_parser_fuzzer/llong-min.seed b/fuzz/corpus/numeric_parser_fuzzer/llong-min.seed new file mode 100644 index 0000000..9af921b --- /dev/null +++ b/fuzz/corpus/numeric_parser_fuzzer/llong-min.seed @@ -0,0 +1 @@ +-9223372036854775808 \ No newline at end of file diff --git a/fuzz/corpus/numeric_parser_fuzzer/llong-overflow.seed b/fuzz/corpus/numeric_parser_fuzzer/llong-overflow.seed new file mode 100644 index 0000000..8136b73 --- /dev/null +++ b/fuzz/corpus/numeric_parser_fuzzer/llong-overflow.seed @@ -0,0 +1 @@ +9223372036854775808 \ No newline at end of file diff --git a/fuzz/corpus/numeric_parser_fuzzer/lone-minus.seed b/fuzz/corpus/numeric_parser_fuzzer/lone-minus.seed new file mode 100644 index 0000000..3cf20d5 --- /dev/null +++ b/fuzz/corpus/numeric_parser_fuzzer/lone-minus.seed @@ -0,0 +1 @@ +- \ No newline at end of file diff --git a/fuzz/corpus/numeric_parser_fuzzer/nan.seed b/fuzz/corpus/numeric_parser_fuzzer/nan.seed new file mode 100644 index 0000000..0982929 --- /dev/null +++ b/fuzz/corpus/numeric_parser_fuzzer/nan.seed @@ -0,0 +1 @@ +nan \ No newline at end of file diff --git a/fuzz/corpus/numeric_parser_fuzzer/neg-one.seed b/fuzz/corpus/numeric_parser_fuzzer/neg-one.seed new file mode 100644 index 0000000..d7d17fc --- /dev/null +++ b/fuzz/corpus/numeric_parser_fuzzer/neg-one.seed @@ -0,0 +1 @@ +-1 \ No newline at end of file diff --git a/fuzz/corpus/numeric_parser_fuzzer/octal.seed b/fuzz/corpus/numeric_parser_fuzzer/octal.seed new file mode 100644 index 0000000..fa0faf4 --- /dev/null +++ b/fuzz/corpus/numeric_parser_fuzzer/octal.seed @@ -0,0 +1 @@ +017 \ No newline at end of file diff --git a/fuzz/corpus/numeric_parser_fuzzer/one.seed b/fuzz/corpus/numeric_parser_fuzzer/one.seed new file mode 100644 index 0000000..56a6051 --- /dev/null +++ b/fuzz/corpus/numeric_parser_fuzzer/one.seed @@ -0,0 +1 @@ +1 \ No newline at end of file diff --git a/fuzz/corpus/numeric_parser_fuzzer/plus-one.seed b/fuzz/corpus/numeric_parser_fuzzer/plus-one.seed new file mode 100644 index 0000000..c57a195 --- /dev/null +++ b/fuzz/corpus/numeric_parser_fuzzer/plus-one.seed @@ -0,0 +1 @@ ++1 \ No newline at end of file diff --git a/fuzz/corpus/numeric_parser_fuzzer/trailing-junk.seed b/fuzz/corpus/numeric_parser_fuzzer/trailing-junk.seed new file mode 100644 index 0000000..efa2553 --- /dev/null +++ b/fuzz/corpus/numeric_parser_fuzzer/trailing-junk.seed @@ -0,0 +1 @@ +12junk \ No newline at end of file diff --git a/fuzz/corpus/numeric_parser_fuzzer/trailing-ws.seed b/fuzz/corpus/numeric_parser_fuzzer/trailing-ws.seed new file mode 100644 index 0000000..7d96000 --- /dev/null +++ b/fuzz/corpus/numeric_parser_fuzzer/trailing-ws.seed @@ -0,0 +1 @@ + 12 \ No newline at end of file diff --git a/fuzz/corpus/numeric_parser_fuzzer/unsigned-negative.seed b/fuzz/corpus/numeric_parser_fuzzer/unsigned-negative.seed new file mode 100644 index 0000000..c67f81a --- /dev/null +++ b/fuzz/corpus/numeric_parser_fuzzer/unsigned-negative.seed @@ -0,0 +1 @@ +-5 \ No newline at end of file diff --git a/fuzz/corpus/numeric_parser_fuzzer/zero.seed b/fuzz/corpus/numeric_parser_fuzzer/zero.seed new file mode 100644 index 0000000..c227083 --- /dev/null +++ b/fuzz/corpus/numeric_parser_fuzzer/zero.seed @@ -0,0 +1 @@ +0 \ No newline at end of file diff --git a/fuzz/fuzz_numeric_parser.cxx b/fuzz/fuzz_numeric_parser.cxx deleted file mode 100644 index f72cd43..0000000 --- a/fuzz/fuzz_numeric_parser.cxx +++ /dev/null @@ -1,51 +0,0 @@ -#include -#include -#include -#include -#include "../args.hxx" - -// Expose the ValueReader class for fuzzing -extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { - if (size == 0) return 0; - - // Create a null-terminated string from fuzzer input - std::string input(reinterpret_cast(data), size); - - // Test numeric parsing with various types - args::ValueReader reader; - - // Test signed integer types - { - int dest_int = 0; - reader("fuzz_test", input, dest_int); // Should not crash - - short dest_short = 0; - reader("fuzz_test", input, dest_short); // Should not crash - - long long dest_ll = 0; - reader("fuzz_test", input, dest_ll); // Should not crash - } - - // Test unsigned integer types - { - unsigned int dest_uint = 0; - reader("fuzz_test", input, dest_uint); // Should not crash - - unsigned short dest_ushort = 0; - reader("fuzz_test", input, dest_ushort); // Should not crash - - unsigned long long dest_ull = 0; - reader("fuzz_test", input, dest_ull); // Should not crash - } - - // Test floating point types - { - float dest_float = 0.0f; - reader("fuzz_test", input, dest_float); // Should not crash - - double dest_double = 0.0; - reader("fuzz_test", input, dest_double); // Should not crash - } - - return 0; -} \ No newline at end of file diff --git a/fuzz/numeric_parser_fuzzer.cpp b/fuzz/numeric_parser_fuzzer.cpp new file mode 100644 index 0000000..b2a7554 --- /dev/null +++ b/fuzz/numeric_parser_fuzzer.cpp @@ -0,0 +1,70 @@ +#include +#include +#include + +/* + * Numeric conversion harness for args::ValueReader. + * + * Repaired for OSS-Fuzz: ValueReader::operator() throws args::ParseError + * for any input the requested type cannot represent, and the original + * harness had no exception handling, so the first malformed byte string + * aborted the whole target before later conversions could run. + * + * Each conversion is now attempted independently, with the expected + * rejection swallowed per type, so a single input reaches every numeric + * path: strtoll/strtoull-based integers (with range and narrowing + * checks), sign rejection for unsigned types, trailing-whitespace + * tolerance, and istringstream-based parsing for bool, char, and + * floating-point types. Embedded NUL bytes are preserved on purpose: + * the readers deliberately distinguish "12\0junk" from "12". + */ + +// Accumulates converted values so the conversions cannot be optimized away. +unsigned long g_sink = 0; + +template +static T AttemptConversion(args::ValueReader &reader, const std::string &input) +{ + T destination{}; + try + { + reader("fuzz_test", input, destination); + } + catch (...) + { + // Expected rejection for values this type cannot represent. + } + return destination; +} + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { + // Create a string from fuzzer input, preserving embedded NULs + std::string input(reinterpret_cast(data), size); + + args::ValueReader reader; + + // Character-ish types (istringstream-based paths) + g_sink += static_cast(AttemptConversion(reader, input)); + g_sink += static_cast(AttemptConversion(reader, input)); + g_sink += static_cast(AttemptConversion(reader, input)); + g_sink += static_cast(AttemptConversion(reader, input)); + + // Test signed integer types (strtoll-based path, narrowing checks) + g_sink += static_cast(AttemptConversion(reader, input)); + g_sink += static_cast(AttemptConversion(reader, input)); + g_sink += static_cast(AttemptConversion(reader, input)); + g_sink += static_cast(AttemptConversion(reader, input)); + + // Test unsigned integer types (strtoull-based path, sign rejection) + g_sink += static_cast(AttemptConversion(reader, input)); + g_sink += static_cast(AttemptConversion(reader, input)); + g_sink += static_cast(AttemptConversion(reader, input)); + g_sink += static_cast(AttemptConversion(reader, input)); + + // Test floating point types (istringstream-based path) + g_sink += static_cast(AttemptConversion(reader, input)); + g_sink += static_cast(AttemptConversion(reader, input)); + g_sink += static_cast(AttemptConversion(reader, input)); + + return 0; +} \ No newline at end of file