From 2bdb2de26784d6aa186d8a5c94480d826c752fa8 Mon Sep 17 00:00:00 2001 From: Arthur Chan Date: Fri, 16 Oct 2026 08:21:34 +0100 Subject: [PATCH] OSS-Fuzz: Add new fuzzer targets command processing Signed-off-by: Arthur Chan --- fuzz/commands_fuzzer.cpp | 158 ++++++++++++++++++ fuzz/corpus/commands_fuzzer/add-global.seed | 1 + .../commands_fuzzer/add-terminator.seed | 1 + fuzz/corpus/commands_fuzzer/commit-amend.seed | 1 + .../corpus/commands_fuzzer/commit-joined.seed | 1 + fuzz/corpus/commands_fuzzer/help-global.seed | 1 + fuzz/corpus/commands_fuzzer/random32.seed | 1 + .../corpus/commands_fuzzer/remote-nested.seed | 1 + .../commands_fuzzer/unknown-command.seed | 1 + 9 files changed, 166 insertions(+) create mode 100644 fuzz/commands_fuzzer.cpp create mode 100644 fuzz/corpus/commands_fuzzer/add-global.seed create mode 100644 fuzz/corpus/commands_fuzzer/add-terminator.seed create mode 100644 fuzz/corpus/commands_fuzzer/commit-amend.seed create mode 100644 fuzz/corpus/commands_fuzzer/commit-joined.seed create mode 100644 fuzz/corpus/commands_fuzzer/help-global.seed create mode 100644 fuzz/corpus/commands_fuzzer/random32.seed create mode 100644 fuzz/corpus/commands_fuzzer/remote-nested.seed create mode 100644 fuzz/corpus/commands_fuzzer/unknown-command.seed diff --git a/fuzz/commands_fuzzer.cpp b/fuzz/commands_fuzzer.cpp new file mode 100644 index 0000000..0dcb19a --- /dev/null +++ b/fuzz/commands_fuzzer.cpp @@ -0,0 +1,158 @@ +/* + * Fuzzer for the command/subparser lifecycle of args.hxx: static and + * nested commands, callback-created arguments, global options, required + * commands, kick-out remainders, and repeated parsing. + * + * The command and subparser regions of the library are essentially unhit + * by the hosted parse_args_fuzzer, which registers no commands at all. + * + * Following the contract documented by test/subparser_commands.cxx: + * registered objects remain alive for the parser's lifetime, and command + * callbacks call Parse() on their Subparser before consuming results. + * The harness parses twice per input to exercise Reset() and command + * re-selection between rounds. + */ + +#include +#include +#include +#include + +#include + +#include + +namespace +{ + +const char *const kTokens[] = { + "add", "commit", "remote", "--git-dir", ".git", "--help", "-h", + "a.txt", "b.txt", "-a", "--amend", "-m", "msg one", "--message=x", + "origin", "https://example.com/src.git", + "--", "later", "--unknown", "-x", "", +}; + +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); + + // Global options shared by every subcommand (test/global_options.cxx). + args::Group globals; + args::ValueFlag gitdir(globals, "path", "", {"git-dir"}); + args::HelpFlag help(globals, "help", "help", {"help"}); + + args::ArgumentParser parser("commands_fuzzer"); + args::GlobalOptions globalOptions(parser, globals); + args::PositionalList paths(parser, "paths", "files", args::Options::Global); + + std::vector addPaths; + std::vector commitInfo; + std::vector nestedValues; + + args::Command add(parser, "add", "Add file contents to the index", [&](args::Subparser &sp) + { + args::PositionalList list(sp, "paths", "files to add"); + sp.Parse(); + addPaths.assign(std::begin(list), std::end(list)); + }); + + args::Command commit(parser, "commit", "Record changes", [&](args::Subparser &sp) + { + args::Flag amend(sp, "amend", "amend", {'a', "amend"}); + args::ValueFlag message(sp, "message", "message", {'m', "message"}); + amend.KickOut(true); + sp.Parse(); + if (amend) + { + (void)amend.Get(); + } + if (message) + { + commitInfo.emplace_back(*message); + } + }); + + // Nested command: a command created inside another command's subparser. + args::Command remote(parser, "remote", "Manage sets of tracked repositories", [&](args::Subparser &sp) + { + args::Command addRemote(sp, "add", "Add a tracked repository", [&](args::Subparser &sp2) + { + args::Positional name(sp2, "name", "remote name"); + args::Positional url(sp2, "url", "remote url"); + sp2.Parse(); + if (name) + { + nestedValues.emplace_back(*name); + } + if (url) + { + nestedValues.emplace_back(*url); + } + }); + sp.Parse(); + }); + + parser.RequireCommand(fdp.ConsumeBool()); + + for (int round = 0; round < 2; ++round) + { + const size_t tokenCount = fdp.ConsumeIntegralInRange(1, 10); + 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)); + } + } + + try + { + parser.ParseArgs(argv); + } + catch (...) + { + } + + // Consume results so command state and getters are exercised. + if (add) + { + (void)addPaths.size(); + } + if (commit) + { + (void)commitInfo.size(); + } + if (remote) + { + (void)nestedValues.size(); + } + if (gitdir) + { + (void)*gitdir; + } + if (paths) + { + (void)paths->size(); + } + + addPaths.clear(); + commitInfo.clear(); + nestedValues.clear(); + } + + return 0; +} diff --git a/fuzz/corpus/commands_fuzzer/add-global.seed b/fuzz/corpus/commands_fuzzer/add-global.seed new file mode 100644 index 0000000..7b1356c --- /dev/null +++ b/fuzz/corpus/commands_fuzzer/add-global.seed @@ -0,0 +1 @@ +add --git-dir .git a.txt b.txt \ No newline at end of file diff --git a/fuzz/corpus/commands_fuzzer/add-terminator.seed b/fuzz/corpus/commands_fuzzer/add-terminator.seed new file mode 100644 index 0000000..b2fca0c --- /dev/null +++ b/fuzz/corpus/commands_fuzzer/add-terminator.seed @@ -0,0 +1 @@ +add -- trailing --unknown \ No newline at end of file diff --git a/fuzz/corpus/commands_fuzzer/commit-amend.seed b/fuzz/corpus/commands_fuzzer/commit-amend.seed new file mode 100644 index 0000000..cf30a4b --- /dev/null +++ b/fuzz/corpus/commands_fuzzer/commit-amend.seed @@ -0,0 +1 @@ +commit -a -m msg \ No newline at end of file diff --git a/fuzz/corpus/commands_fuzzer/commit-joined.seed b/fuzz/corpus/commands_fuzzer/commit-joined.seed new file mode 100644 index 0000000..4f3965f --- /dev/null +++ b/fuzz/corpus/commands_fuzzer/commit-joined.seed @@ -0,0 +1 @@ +--git-dir /tmp/x commit --message=first \ No newline at end of file diff --git a/fuzz/corpus/commands_fuzzer/help-global.seed b/fuzz/corpus/commands_fuzzer/help-global.seed new file mode 100644 index 0000000..eeafe3f --- /dev/null +++ b/fuzz/corpus/commands_fuzzer/help-global.seed @@ -0,0 +1 @@ +--help \ No newline at end of file diff --git a/fuzz/corpus/commands_fuzzer/random32.seed b/fuzz/corpus/commands_fuzzer/random32.seed new file mode 100644 index 0000000..f7c9147 --- /dev/null +++ b/fuzz/corpus/commands_fuzzer/random32.seed @@ -0,0 +1 @@ +Sög´xÍgcöžó$G€ýcÕueÒÚ‰Ùæ¹òÒ¥} \ No newline at end of file diff --git a/fuzz/corpus/commands_fuzzer/remote-nested.seed b/fuzz/corpus/commands_fuzzer/remote-nested.seed new file mode 100644 index 0000000..664391c --- /dev/null +++ b/fuzz/corpus/commands_fuzzer/remote-nested.seed @@ -0,0 +1 @@ +remote add origin https://example.com/src.git \ No newline at end of file diff --git a/fuzz/corpus/commands_fuzzer/unknown-command.seed b/fuzz/corpus/commands_fuzzer/unknown-command.seed new file mode 100644 index 0000000..a88fd2f --- /dev/null +++ b/fuzz/corpus/commands_fuzzer/unknown-command.seed @@ -0,0 +1 @@ +nonsense --git-dir y \ No newline at end of file