Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 152 additions & 0 deletions fuzz/completion_fuzzer.cpp
Original file line number Diff line number Diff line change
@@ -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 <syntax> <cword> <prog> [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 <cstddef>
#include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>

#include <fuzzer/FuzzedDataProvider.h>

#include <args.hxx>

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<std::string, int> 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 <typename T, size_t N>
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<std::string> bar(parser, "bar", "bar", {'b', "bar"}, "abc");
args::ValueFlag<int> num(parser, "num", "num", {'n', "num"});
args::MapFlag<std::string, int> map(parser, "map", "map", {'m', "map"}, kChoices);
args::MapPositional<std::string, int> mpos(parser, "mpos", "mpos", kChoices);
args::Positional<std::string> pos(parser, "pos", "pos");

// Vary the long separator, including prefix-overlapping values.
try
{
parser.LongSeparator(kSeparators[fdp.ConsumeIntegralInRange<size_t>(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<std::string> 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<std::string> 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<size_t>(0, ArraySize(kCwords) - 1)]);
}
else
{
argv.emplace_back(fdp.ConsumeRandomLengthString(4));
}
argv.emplace_back("prog");

const size_t wordCount = fdp.ConsumeIntegralInRange<size_t>(0, 5);
for (size_t i = 0; i < wordCount; ++i)
{
if (fdp.ConsumeBool())
{
argv.emplace_back(kWords[fdp.ConsumeIntegralInRange<size_t>(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;
}
1 change: 1 addition & 0 deletions fuzz/corpus/completion_fuzzer/bad-cword.seed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--completion bash 1x prog -
1 change: 1 addition & 0 deletions fuzz/corpus/completion_fuzzer/command.seed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--completion bash 2 prog command1
1 change: 1 addition & 0 deletions fuzz/corpus/completion_fuzzer/dash.seed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--completion bash 1 prog -
1 change: 1 addition & 0 deletions fuzz/corpus/completion_fuzzer/dashdash.seed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--completion bash 1 prog --
1 change: 1 addition & 0 deletions fuzz/corpus/completion_fuzzer/map-join.seed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--completion bash 1 prog --map=
1 change: 1 addition & 0 deletions fuzz/corpus/completion_fuzzer/map-sep.seed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--completion bash 2 prog -m
1 change: 1 addition & 0 deletions fuzz/corpus/completion_fuzzer/prefix-boundary.seed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--completion bash 1 prog ---x
1 change: 1 addition & 0 deletions fuzz/corpus/completion_fuzzer/prefix-overlap.seed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--completion bash 1 prog --x
1 change: 1 addition & 0 deletions fuzz/corpus/completion_fuzzer/terminator.seed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--completion bash 2 prog --
1 change: 1 addition & 0 deletions fuzz/corpus/completion_fuzzer/words.seed
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
--completion zsh 3 prog -f --f al
Loading