diff --git a/fuzz/corpus/parse_schema_fuzzer/bad-values.seed b/fuzz/corpus/parse_schema_fuzzer/bad-values.seed new file mode 100644 index 0000000..6cd58ff --- /dev/null +++ b/fuzz/corpus/parse_schema_fuzzer/bad-values.seed @@ -0,0 +1 @@ +--number=abc --ratio=zz --mode=nope \ No newline at end of file diff --git a/fuzz/corpus/parse_schema_fuzzer/flags.seed b/fuzz/corpus/parse_schema_fuzzer/flags.seed new file mode 100644 index 0000000..201cb83 --- /dev/null +++ b/fuzz/corpus/parse_schema_fuzzer/flags.seed @@ -0,0 +1 @@ +-ccc -f --flag -j4 \ No newline at end of file diff --git a/fuzz/corpus/parse_schema_fuzzer/happy.seed b/fuzz/corpus/parse_schema_fuzzer/happy.seed new file mode 100644 index 0000000..f37bb40 --- /dev/null +++ b/fuzz/corpus/parse_schema_fuzzer/happy.seed @@ -0,0 +1 @@ +-n 7 --number=42 -m fast src.txt alpha \ No newline at end of file diff --git a/fuzz/corpus/parse_schema_fuzzer/joined-empty.seed b/fuzz/corpus/parse_schema_fuzzer/joined-empty.seed new file mode 100644 index 0000000..342a275 --- /dev/null +++ b/fuzz/corpus/parse_schema_fuzzer/joined-empty.seed @@ -0,0 +1 @@ +--label= src.txt -P x -P y z diff --git a/fuzz/corpus/parse_schema_fuzzer/lists.seed b/fuzz/corpus/parse_schema_fuzzer/lists.seed new file mode 100644 index 0000000..d5a266b --- /dev/null +++ b/fuzz/corpus/parse_schema_fuzzer/lists.seed @@ -0,0 +1 @@ +-j -N 1 -N 2 --nums=3 -P a \ No newline at end of file diff --git a/fuzz/corpus/parse_schema_fuzzer/maplist.seed b/fuzz/corpus/parse_schema_fuzzer/maplist.seed new file mode 100644 index 0000000..e341be5 --- /dev/null +++ b/fuzz/corpus/parse_schema_fuzzer/maplist.seed @@ -0,0 +1 @@ +--modes=fast --modes verbose beta \ No newline at end of file diff --git a/fuzz/corpus/parse_schema_fuzzer/numerics.seed b/fuzz/corpus/parse_schema_fuzzer/numerics.seed new file mode 100644 index 0000000..82c8304 --- /dev/null +++ b/fuzz/corpus/parse_schema_fuzzer/numerics.seed @@ -0,0 +1 @@ +-n0x10 -n2147483648 3.14 \ No newline at end of file diff --git a/fuzz/corpus/parse_schema_fuzzer/random40.seed b/fuzz/corpus/parse_schema_fuzzer/random40.seed new file mode 100644 index 0000000..a298b8c --- /dev/null +++ b/fuzz/corpus/parse_schema_fuzzer/random40.seed @@ -0,0 +1,2 @@ +fTx\(™f¤6Ú +`®ÜéÀÇP5IY¸ :)+xyºz€oP \ No newline at end of file diff --git a/fuzz/corpus/parse_schema_fuzzer/single-repeat.seed b/fuzz/corpus/parse_schema_fuzzer/single-repeat.seed new file mode 100644 index 0000000..b6a0839 --- /dev/null +++ b/fuzz/corpus/parse_schema_fuzzer/single-repeat.seed @@ -0,0 +1 @@ +-o -o \ No newline at end of file diff --git a/fuzz/corpus/parse_schema_fuzzer/strings.seed b/fuzz/corpus/parse_schema_fuzzer/strings.seed new file mode 100644 index 0000000..3da29b7 --- /dev/null +++ b/fuzz/corpus/parse_schema_fuzzer/strings.seed @@ -0,0 +1 @@ +-l abc --label= -l \ No newline at end of file diff --git a/fuzz/corpus/parse_schema_fuzzer/terminator.seed b/fuzz/corpus/parse_schema_fuzzer/terminator.seed new file mode 100644 index 0000000..bdfbd10 --- /dev/null +++ b/fuzz/corpus/parse_schema_fuzzer/terminator.seed @@ -0,0 +1 @@ +--req ok -- --unknown -z later \ No newline at end of file diff --git a/fuzz/corpus/parse_schema_fuzzer/whitespace.seed b/fuzz/corpus/parse_schema_fuzzer/whitespace.seed new file mode 100644 index 0000000..59e7cc8 --- /dev/null +++ b/fuzz/corpus/parse_schema_fuzzer/whitespace.seed @@ -0,0 +1 @@ +-v name positional with spaces diff --git a/fuzz/fuzz_parser.cpp b/fuzz/fuzz_parser.cpp deleted file mode 100644 index 3c477c5..0000000 --- a/fuzz/fuzz_parser.cpp +++ /dev/null @@ -1,72 +0,0 @@ -/** - * libFuzzer integration for args.hxx - * - * Build with: cmake -DBUILD_FUZZERS=ON .. - * Run with: ./fuzz_parser corpus/ - * - * This fuzzer tests argument parsing with malformed/untrusted input - * to detect crashes, overflows, and undefined behavior. - */ - -#include -#include -#include -#include -#include - -// Include the args library -#include "../args.hxx" - -extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size) { - // Create a string from fuzzer input - std::string input(reinterpret_cast(data), size); - - // Simulate command-line arguments - std::vector args; - - // Parse input as space-separated arguments (simulate argv) - std::istringstream iss(input); - std::string arg; - while (iss >> arg) { - args.push_back(arg); - } - - if (args.empty()) { - return 0; // Nothing to parse - } - - // Build argv-style array - std::vector argv; - for (auto& a : args) { - argv.push_back(const_cast(a.c_str())); - } - - // Create argument parser - args::ArgumentParser parser("Fuzzer test", "Fuzzing argument parsing"); - - // Add various flag types to test - args::HelpFlag help(parser, "help", "Display help", {'h', "help"}); - args::Flag verbose(parser, "verbose", "Enable verbose", {'v', "verbose"}); - args::ValueFlag name(parser, "name", "User name", {'n', "name"}); - args::ValueFlag count(parser, "count", "Count value", {'c', "count"}); - args::Positional positional(parser, "input", "Input file"); - - try { - // Parse the arguments - this is what we're fuzzing - parser.ParseArgs(static_cast(argv.size()), argv.data()); - } catch (const args::Help&) { - // Help requested - normal behavior - return 0; - } catch (const args::ParseError& e) { - // Parse error - expected for malformed input - return 0; - } catch (const args::ValidationError& e) { - // Validation error - also expected - return 0; - } catch (const std::exception& e) { - // Any other exception is a potential bug - __builtin_trap(); // Signal fuzzer this is a crash - } - - return 0; // Success -} \ No newline at end of file diff --git a/fuzz/parse_schema_fuzzer.cpp b/fuzz/parse_schema_fuzzer.cpp new file mode 100644 index 0000000..9227bc5 --- /dev/null +++ b/fuzz/parse_schema_fuzzer.cpp @@ -0,0 +1,211 @@ +/* + * Combined schema-parsing harness for args.hxx (parse_schema_fuzzer). + * + * Supersedes the two upstream harnesses that were never built by OSS-Fuzz + * because their names do not match the *_fuzzer.cpp discovery glob: + * + * - fuzz_parser.cpp: called a nonexistent ParseArgs overload (so it + * cannot even compile) and lost empty/whitespace-only arguments to + * istringstream tokenization. Its intent - parse a full schema of + * untrusted arguments - lives on here. + * - fuzz_numeric_parser.cxx: renamed to numeric_parser_fuzzer.cpp and + * repaired as its own target; numeric conversion stays separate. + * + * Schema coverage: value flags over numeric and string types, implicit + * values, bounded Nargs, repeated lists, mapped flags and positionals, + * plain positionals, single-use and required options, the -- terminator, + * defaults, and re-parsing. Tokens mix fuzzer-arbitrary strings with a + * table of schema-recognized options so unknown-option rejection does not + * dominate the parse. + * + * Crash oracle: ParseArgs legitimately throws only args::Error subtypes + * (Help, Completion, ParseError, MapError, ValidationError, ExtraError). + * Any other exception escaping the parser is a library bug - e.g. the + * leaked std::out_of_range fixed by + * test/completion_separator_prefix_overlap.cxx - and trips + * __builtin_trap() below. + */ + +#include +#include +#include +#include +#include + +#include + +#include + +namespace +{ + +// NOTE: the Map* classes default their Map template parameter to +// args::detail::unordered_map (== std::unordered_map), so the choices map +// must be an unordered_map (a std::map does not convert). +const std::unordered_map kModes{{"fast", 1}, {"slow", 2}, {"verbose", 3}}; + +const char *const kTokens[] = { + "-h", "--help", + "-f", "--flag", + "-o", "-o", + "-c", "-ccc", + "-n", "7", "--number=42", "--number=-3", "--number=abc", "--number=", + "-n0x10", "-n2147483648", + "-r2", "-r", "0.5", "--ratio=1e3", "--ratio", "-r=2", "--ratio=inf", + "-l", "abc", "--label=x", "--label=", "-l", "-f", + "-j", "-j4", "-j", "4", + "-N", "1", "--nums=2", "-N2", "--nums=x", + "-P", "a", "-P", "-P", "b", "c", + "-m", "fast", "--mode=slow", "--mode=unknown", + "--modes=fast", "--modes", "verbose", + "--req", "5", "--req", + "src.txt", "alpha", "1", "2", + "--", "-z", "--unknown", "", " ", "3.14", "-", +}; + +template +constexpr size_t ArraySize(T (&)[N]) +{ + return N; +} + +} // namespace + +extern "C" int LLVMFuzzerTestOneInput(const uint8_t *data, size_t size) +{ + FuzzedDataProvider fdp(data, size); + + args::ArgumentParser parser("parse_schema_fuzzer", "typed value schema test"); + + args::HelpFlag help(parser, "help", "help", {'h', "help"}); + args::Flag flag(parser, "flag", "flag", {'f', "flag"}); + args::Flag once(parser, "once", "once", {'o'}, args::Options::Single); + args::CounterFlag count(parser, "count", "count", {'c', "count"}); + args::ValueFlag number(parser, "number", "number", {'n', "number"}, 0); + args::ValueFlag ratio(parser, "ratio", "ratio", {"ratio"}, 0.0); + args::ValueFlag label(parser, "label", "label", {'l', "label"}, std::string("x")); + args::ImplicitValueFlag jobs(parser, "jobs", "jobs", {'j', "jobs"}, 0, 1); + args::ValueFlagList numbers(parser, "nums", "nums", {'N', "nums"}); + args::NargsValueFlag pair(parser, "pair", "pair", {'P', "pair"}, args::Nargs(0, 2)); + args::MapFlag mode(parser, "mode", "mode", {'m', "mode"}, kModes); + args::MapFlagList modes(parser, "modes", "modes", {"modes"}, kModes); + args::ValueFlag required(parser, "req", "req", {"req"}, args::Options::Required); + args::Positional src(parser, "src", "source"); + args::MapPositional posMode(parser, "posmode", "mapped positional", kModes); + args::PositionalList dst(parser, "dst", "destinations"); + + const size_t tokenCount = fdp.ConsumeIntegralInRange(1, 14); + std::vector argv; + argv.reserve(tokenCount); + for (size_t i = 0; i < tokenCount; ++i) + { + if (fdp.ConsumeBool()) + { + argv.emplace_back(kTokens[fdp.ConsumeIntegralInRange(0, ArraySize(kTokens) - 1)]); + } + else + { + argv.emplace_back(fdp.ConsumeRandomLengthString(16)); + } + } + + // ParseArgs legitimately throws only args::Error subtypes; anything + // else escaping is a library bug and traps the fuzzer. + try + { + parser.ParseArgs(argv); + } + catch (const args::Error &) + { + } + catch (...) + { + __builtin_trap(); + } + + // -- Read back every typed result so getters and iterators instantiate ---- + (void)count.Get(); + if (number) + { + (void)*number; + } + if (ratio) + { + (void)*ratio; + } + if (label) + { + (void)*label; + } + if (jobs) + { + (void)*jobs; + } + if (numbers) + { + for (const int &value : numbers) + { + (void)value; + } + } + if (pair) + { + for (const std::string &value : pair) + { + (void)value; + } + } + if (mode) + { + (void)*mode; + } + if (modes) + { + for (const int &value : modes) + { + (void)value; + } + } + if (src) + { + (void)*src; + } + if (posMode) + { + (void)*posMode; + } + if (dst) + { + for (const std::string &value : dst) + { + (void)value; + } + } + + // -- Re-parse pair: invalid parse (validation error) then known-good parse + try + { + parser.ParseArgs(std::vector{}); + } + catch (const args::Error &) + { + } + catch (...) + { + __builtin_trap(); + } + + try + { + parser.ParseArgs(std::vector{"--req", "ok", "src.txt"}); + } + catch (const args::Error &) + { + } + catch (...) + { + __builtin_trap(); + } + + return 0; +}