diff --git a/fuzz/completion_fuzzer.cpp b/fuzz/completion_fuzzer.cpp new file mode 100644 index 0000000..4f4d12f --- /dev/null +++ b/fuzz/completion_fuzzer.cpp @@ -0,0 +1,152 @@ +/* + * Fuzzer for the shell completion protocol of args.hxx. + * + * The hosted parse_args_fuzzer never registers a CompletionFlag, so the + * candidate-generation region of the parser was almost entirely unexercised + * (its early exit was the only covered part, per the 2026-10-15 coverage + * report). + * + * This harness registers a CompletionFlag alongside flags, value flags, + * mapped flags/positionals, positionals, and commands, then issues valid + * completion requests of the form + * + * --completion [words...] + * + * with mutated word indices (including malformed ones such as "1x", "-1", + * "+1", " 2", "2 "), partial options ("", "-", "--", "-f", "--f", "-m1"), + * separator/prefix overlaps ("--x", "---x", "--map="), mapped choices, + * terminator ("--"), and command names. The long separator is also varied, + * including values that overlap the "--" prefix, which historically drove + * an out-of-range substr in the bash completion handler + * (see test/completion_separator_prefix_overlap.cxx). Two requests are + * issued per input to exercise re-parse between completions. + */ + +#include +#include +#include +#include +#include + +#include + +#include + +namespace +{ + +// Must be std::unordered_map: the Map* classes default their Map template +// parameter to args::detail::unordered_map and std::map does not convert. +const std::unordered_map kChoices{{"alpha", 1}, {"beta", 2}}; + +const char *const kSeparators[] = {"=", "-", ":", "==", "=-"}; + +const char *const kCwords[] = {"1", "2", "3", "7", "0", "1x", "-1", "+1", " 2", "2 "}; + +const char *const kWords[] = { + "", "-", "--", "-f", "--f", "--foo", "-b", "--bar", "--bar=", "-b=", + "-m", "--map=", "-m1", "--x", "---x", "alpha", "beta", "--", + "command1", "command2", "command3", "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); + + args::ArgumentParser parser("completion_fuzzer"); + args::CompletionFlag completion(parser, {"completion"}); + + args::Flag foo(parser, "foo", "foo", {'f', "foo"}); + args::ValueFlag bar(parser, "bar", "bar", {'b', "bar"}, "abc"); + args::ValueFlag num(parser, "num", "num", {'n', "num"}); + args::MapFlag map(parser, "map", "map", {'m', "map"}, kChoices); + args::MapPositional mpos(parser, "mpos", "mpos", kChoices); + args::Positional pos(parser, "pos", "pos"); + + // Vary the long separator, including prefix-overlapping values. + try + { + parser.LongSeparator(kSeparators[fdp.ConsumeIntegralInRange(0, ArraySize(kSeparators) - 1)]); + } + catch (...) + { + } + + // Command subparsers, mirroring test/completion.cxx: arguments are + // created inside the coroutine and Parse() is called before any result + // is consumed; the objects stay alive for the duration of the parse. + args::Command command1(parser, "command1", "first command", [](args::Subparser &sp) + { + args::ValueFlag cfoo(sp, "cfoo", "cfoo", {'f', "cfoo"}, "abc"); + sp.Parse(); + if (cfoo) + { + (void)*cfoo; + } + }); + + args::Command command2(parser, "command2", "second command", [](args::Subparser &sp) + { + sp.Parse(); + }); + + for (int round = 0; round < 2; ++round) + { + std::vector argv; + argv.reserve(8); + argv.emplace_back("--completion"); + if (fdp.ConsumeBool()) + { + argv.emplace_back("bash"); + } + else + { + argv.emplace_back(fdp.ConsumeRandomLengthString(8)); + } + if (fdp.ConsumeBool()) + { + argv.emplace_back(kCwords[fdp.ConsumeIntegralInRange(0, ArraySize(kCwords) - 1)]); + } + else + { + argv.emplace_back(fdp.ConsumeRandomLengthString(4)); + } + argv.emplace_back("prog"); + + const size_t wordCount = fdp.ConsumeIntegralInRange(0, 5); + for (size_t i = 0; i < wordCount; ++i) + { + if (fdp.ConsumeBool()) + { + argv.emplace_back(kWords[fdp.ConsumeIntegralInRange(0, ArraySize(kWords) - 1)]); + } + else + { + argv.emplace_back(fdp.ConsumeRandomLengthString(12)); + } + } + + try + { + parser.ParseArgs(argv); + } + catch (const args::Completion &) + { + // Normal completion control flow carrying the candidates. + } + catch (...) + { + // Malformed cword and other expected rejections. + } + } + + return 0; +} diff --git a/fuzz/corpus/completion_fuzzer/bad-cword.seed b/fuzz/corpus/completion_fuzzer/bad-cword.seed new file mode 100644 index 0000000..e398776 --- /dev/null +++ b/fuzz/corpus/completion_fuzzer/bad-cword.seed @@ -0,0 +1 @@ +--completion bash 1x prog - \ No newline at end of file diff --git a/fuzz/corpus/completion_fuzzer/command.seed b/fuzz/corpus/completion_fuzzer/command.seed new file mode 100644 index 0000000..da614f0 --- /dev/null +++ b/fuzz/corpus/completion_fuzzer/command.seed @@ -0,0 +1 @@ +--completion bash 2 prog command1 \ No newline at end of file diff --git a/fuzz/corpus/completion_fuzzer/dash.seed b/fuzz/corpus/completion_fuzzer/dash.seed new file mode 100644 index 0000000..1dc3c5f --- /dev/null +++ b/fuzz/corpus/completion_fuzzer/dash.seed @@ -0,0 +1 @@ +--completion bash 1 prog - \ No newline at end of file diff --git a/fuzz/corpus/completion_fuzzer/dashdash.seed b/fuzz/corpus/completion_fuzzer/dashdash.seed new file mode 100644 index 0000000..32f6d3d --- /dev/null +++ b/fuzz/corpus/completion_fuzzer/dashdash.seed @@ -0,0 +1 @@ +--completion bash 1 prog -- \ No newline at end of file diff --git a/fuzz/corpus/completion_fuzzer/map-join.seed b/fuzz/corpus/completion_fuzzer/map-join.seed new file mode 100644 index 0000000..f6a35ed --- /dev/null +++ b/fuzz/corpus/completion_fuzzer/map-join.seed @@ -0,0 +1 @@ +--completion bash 1 prog --map= \ No newline at end of file diff --git a/fuzz/corpus/completion_fuzzer/map-sep.seed b/fuzz/corpus/completion_fuzzer/map-sep.seed new file mode 100644 index 0000000..0b68718 --- /dev/null +++ b/fuzz/corpus/completion_fuzzer/map-sep.seed @@ -0,0 +1 @@ +--completion bash 2 prog -m \ No newline at end of file diff --git a/fuzz/corpus/completion_fuzzer/prefix-boundary.seed b/fuzz/corpus/completion_fuzzer/prefix-boundary.seed new file mode 100644 index 0000000..27a9080 --- /dev/null +++ b/fuzz/corpus/completion_fuzzer/prefix-boundary.seed @@ -0,0 +1 @@ +--completion bash 1 prog ---x \ No newline at end of file diff --git a/fuzz/corpus/completion_fuzzer/prefix-overlap.seed b/fuzz/corpus/completion_fuzzer/prefix-overlap.seed new file mode 100644 index 0000000..b97ca46 --- /dev/null +++ b/fuzz/corpus/completion_fuzzer/prefix-overlap.seed @@ -0,0 +1 @@ +--completion bash 1 prog --x \ No newline at end of file diff --git a/fuzz/corpus/completion_fuzzer/terminator.seed b/fuzz/corpus/completion_fuzzer/terminator.seed new file mode 100644 index 0000000..a7efbe0 --- /dev/null +++ b/fuzz/corpus/completion_fuzzer/terminator.seed @@ -0,0 +1 @@ +--completion bash 2 prog -- \ No newline at end of file diff --git a/fuzz/corpus/completion_fuzzer/words.seed b/fuzz/corpus/completion_fuzzer/words.seed new file mode 100644 index 0000000..d4ec732 --- /dev/null +++ b/fuzz/corpus/completion_fuzzer/words.seed @@ -0,0 +1 @@ +--completion zsh 3 prog -f --f al \ No newline at end of file