From 9c58546517ff5c1d714988e8585ad7bfe84d3b5c Mon Sep 17 00:00:00 2001 From: Arthur Chan Date: Fri, 16 Oct 2026 08:25:52 +0100 Subject: [PATCH] OSS-Fuzz: Add new fuzzer targets help rendering process Signed-off-by: Arthur Chan --- fuzz/corpus/help_render_fuzzer/all-ff.seed | 1 + .../corpus/help_render_fuzzer/ascii-wide.seed | 1 + fuzz/corpus/help_render_fuzzer/choices.seed | 1 + .../corpus/help_render_fuzzer/help-parse.seed | 1 + fuzz/corpus/help_render_fuzzer/long-desc.seed | 1 + fuzz/corpus/help_render_fuzzer/newlines.seed | 3 + .../help_render_fuzzer/normal-width.seed | 1 + fuzz/corpus/help_render_fuzzer/random48.seed | 1 + fuzz/corpus/help_render_fuzzer/tiny.seed | 1 + .../corpus/help_render_fuzzer/width-zero.seed | Bin 0 -> 64 bytes fuzz/help_render_fuzzer.cpp | 197 ++++++++++++++++++ 11 files changed, 208 insertions(+) create mode 100644 fuzz/corpus/help_render_fuzzer/all-ff.seed create mode 100644 fuzz/corpus/help_render_fuzzer/ascii-wide.seed create mode 100644 fuzz/corpus/help_render_fuzzer/choices.seed create mode 100644 fuzz/corpus/help_render_fuzzer/help-parse.seed create mode 100644 fuzz/corpus/help_render_fuzzer/long-desc.seed create mode 100644 fuzz/corpus/help_render_fuzzer/newlines.seed create mode 100644 fuzz/corpus/help_render_fuzzer/normal-width.seed create mode 100644 fuzz/corpus/help_render_fuzzer/random48.seed create mode 100644 fuzz/corpus/help_render_fuzzer/tiny.seed create mode 100644 fuzz/corpus/help_render_fuzzer/width-zero.seed create mode 100644 fuzz/help_render_fuzzer.cpp diff --git a/fuzz/corpus/help_render_fuzzer/all-ff.seed b/fuzz/corpus/help_render_fuzzer/all-ff.seed new file mode 100644 index 0000000..85f2b75 --- /dev/null +++ b/fuzz/corpus/help_render_fuzzer/all-ff.seed @@ -0,0 +1 @@ +ÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿÿ \ No newline at end of file diff --git a/fuzz/corpus/help_render_fuzzer/ascii-wide.seed b/fuzz/corpus/help_render_fuzzer/ascii-wide.seed new file mode 100644 index 0000000..707925b --- /dev/null +++ b/fuzz/corpus/help_render_fuzzer/ascii-wide.seed @@ -0,0 +1 @@ +width zero narrow gutter indent help description choices default hidden \ No newline at end of file diff --git a/fuzz/corpus/help_render_fuzzer/choices.seed b/fuzz/corpus/help_render_fuzzer/choices.seed new file mode 100644 index 0000000..92bb806 --- /dev/null +++ b/fuzz/corpus/help_render_fuzzer/choices.seed @@ -0,0 +1 @@ +red green blue fast slow verbose \ No newline at end of file diff --git a/fuzz/corpus/help_render_fuzzer/help-parse.seed b/fuzz/corpus/help_render_fuzzer/help-parse.seed new file mode 100644 index 0000000..ee0a9d4 --- /dev/null +++ b/fuzz/corpus/help_render_fuzzer/help-parse.seed @@ -0,0 +1 @@ +-h --help render after help request \ No newline at end of file diff --git a/fuzz/corpus/help_render_fuzzer/long-desc.seed b/fuzz/corpus/help_render_fuzzer/long-desc.seed new file mode 100644 index 0000000..f29304a --- /dev/null +++ b/fuzz/corpus/help_render_fuzzer/long-desc.seed @@ -0,0 +1 @@ +AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA \ No newline at end of file diff --git a/fuzz/corpus/help_render_fuzzer/newlines.seed b/fuzz/corpus/help_render_fuzzer/newlines.seed new file mode 100644 index 0000000..4678791 --- /dev/null +++ b/fuzz/corpus/help_render_fuzzer/newlines.seed @@ -0,0 +1,3 @@ +a +b +c description with newlines diff --git a/fuzz/corpus/help_render_fuzzer/normal-width.seed b/fuzz/corpus/help_render_fuzzer/normal-width.seed new file mode 100644 index 0000000..e8fc59a --- /dev/null +++ b/fuzz/corpus/help_render_fuzzer/normal-width.seed @@ -0,0 +1 @@ +prog 80 6 40 2 1 normal terminal width \ No newline at end of file diff --git a/fuzz/corpus/help_render_fuzzer/random48.seed b/fuzz/corpus/help_render_fuzzer/random48.seed new file mode 100644 index 0000000..66dd5eb --- /dev/null +++ b/fuzz/corpus/help_render_fuzzer/random48.seed @@ -0,0 +1 @@ +þØNP³êßh]£Mæ¼û\AI»B^W I¡1_ô¶ÙÃêúÙ26Ÿ7ÏÞ?*^”Y~J \ No newline at end of file diff --git a/fuzz/corpus/help_render_fuzzer/tiny.seed b/fuzz/corpus/help_render_fuzzer/tiny.seed new file mode 100644 index 0000000..c1b0730 --- /dev/null +++ b/fuzz/corpus/help_render_fuzzer/tiny.seed @@ -0,0 +1 @@ +x \ No newline at end of file diff --git a/fuzz/corpus/help_render_fuzzer/width-zero.seed b/fuzz/corpus/help_render_fuzzer/width-zero.seed new file mode 100644 index 0000000000000000000000000000000000000000..9017fd98b5f67d928cc64c59b2c025472ce74f8c GIT binary patch literal 64 LcmZQzpbP*206+i% literal 0 HcmV?d00001 diff --git a/fuzz/help_render_fuzzer.cpp b/fuzz/help_render_fuzzer.cpp new file mode 100644 index 0000000..a5a5c13 --- /dev/null +++ b/fuzz/help_render_fuzzer.cpp @@ -0,0 +1,197 @@ +/* + * Fuzzer for the help rendering and text formatting machinery of args.hxx. + * + * The original OSS-Fuzz target (parse_args_fuzzer) only parses arguments + * and never renders help, so description building, word wrapping, prog + * lines, gutters, and choices/defaults rendering stayed untested. + * + * This harness: + * - builds a schema with fuzzer-varied descriptions, defaults, choices, + * hidden options, a validated group, and positionals, + * - varies every ArgumentParser helpParams field, including pathological + * narrow widths that historically caused unsigned underflow + * (see test/help_underflow.cxx), + * - parses a mix of recognized and arbitrary tokens so help rendering + * runs against varied matched state, and + * - renders help through all available paths: ArgumentParser::Help(), + * operator<<(std::ostream&, const ArgumentParser&), + * GetDescription()/GetProgramLine(), and the HelpFlag exception path. + */ + +#include +#include +#include +#include +#include +#include + +#include + +#include + +namespace +{ + +// Prevent the rendered strings from being dead-code eliminated. +unsigned long g_sink = 0; + +// Only combinations the setters accept: they throw on invalid input, and +// schema setup exceptions would test the validators, not the formatting +// code this harness targets. +const char *const kLongPrefixes[] = {"--", "/", "-", "---"}; +const char *const kShortPrefixes[] = {"-", "/", ""}; +const char *const kTerminators[] = {"--", "-", ""}; +const char *const kLongSeparators[] = {"=", ":", "-", " "}; + +bool (*const kGroupValidators[])(const args::Group &) = { + &args::Group::Validators::DontCare, + &args::Group::Validators::AtMostOne, + &args::Group::Validators::AtLeastOne, + &args::Group::Validators::Xor, + &args::Group::Validators::AllOrNone, +}; + +const char *const kTokens[] = { + "-h", "--help", "-a", "--alpha", "-b", "--beta", "-n", "7", "--number=42", + "--number=-3", "--number=x", "--number=", "-w", "one", "--words=two", + "-c", "red", "--colour=nope", "-g", "2.5", "--gamma=1e3", "-d", "--", + "positional", "-z", "--unknown", "", +}; + +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); + + // -- Schema, with fuzzer-varied text -------------------------------------- + args::ArgumentParser parser(fdp.ConsumeRandomLengthString(64), + fdp.ConsumeRandomLengthString(64)); + + try + { + parser.LongPrefix(kLongPrefixes[fdp.ConsumeIntegralInRange(0, ArraySize(kLongPrefixes) - 1)]); + parser.ShortPrefix(kShortPrefixes[fdp.ConsumeIntegralInRange(0, ArraySize(kShortPrefixes) - 1)]); + parser.LongSeparator(kLongSeparators[fdp.ConsumeIntegralInRange(0, ArraySize(kLongSeparators) - 1)]); + parser.Terminator(kTerminators[fdp.ConsumeIntegralInRange(0, ArraySize(kTerminators) - 1)]); + } + catch (...) + { + } + + args::HelpFlag help(parser, "help", "Displays this help menu", {'h', "help"}); + + args::Options flagOptions = args::Options::None; + switch (fdp.ConsumeIntegralInRange(0, 3)) + { + case 1: + flagOptions = args::Options::HiddenFromDescription; + break; + case 2: + flagOptions = args::Options::HiddenFromUsage; + break; + case 3: + flagOptions = args::Options::Hidden; + break; + default: + break; + } + + args::Flag alpha(parser, "alpha", fdp.ConsumeRandomLengthString(64), {'a', "alpha"}, flagOptions); + args::CounterFlag beta(parser, "beta", fdp.ConsumeRandomLengthString(64), {'b', "beta"}, 0, flagOptions); + args::ValueFlag number(parser, "number", fdp.ConsumeRandomLengthString(64), {'n', "number"}, fdp.ConsumeIntegral()); + args::ValueFlag text(parser, "text", fdp.ConsumeRandomLengthString(64), {"text"}, fdp.ConsumeRandomLengthString(24)); + args::ValueFlagList words(parser, "words", fdp.ConsumeRandomLengthString(64), {'w', "words"}); + + // Must be std::unordered_map: MapFlag's Map template parameter defaults + // to args::detail::unordered_map and std::map does not convert. + const std::unordered_map choices{{"red", 1}, {"green", 2}, {"blue", 3}}; + args::MapFlag colour(parser, "colour", fdp.ConsumeRandomLengthString(64), {'c', "colour"}, choices); + + args::Group group(parser, fdp.ConsumeRandomLengthString(32), + kGroupValidators[fdp.ConsumeIntegralInRange(0, ArraySize(kGroupValidators) - 1)]); + args::ValueFlag gamma(group, "gamma", fdp.ConsumeRandomLengthString(64), {'g', "gamma"}, 0.5); + args::Flag delta(group, "delta", fdp.ConsumeRandomLengthString(64), {'d'}); + + args::Positional source(parser, "source", fdp.ConsumeRandomLengthString(64)); + args::PositionalList extra(parser, "extra", fdp.ConsumeRandomLengthString(64)); + + // -- Every HelpParams field, including pathological narrow widths --------- + parser.helpParams.width = fdp.ConsumeIntegralInRange(0, 300); + parser.helpParams.progindent = fdp.ConsumeIntegralInRange(0, 16); + parser.helpParams.progtailindent = fdp.ConsumeIntegralInRange(0, 16); + parser.helpParams.descriptionindent = fdp.ConsumeIntegralInRange(0, 16); + parser.helpParams.flagindent = fdp.ConsumeIntegralInRange(0, 24); + parser.helpParams.helpindent = fdp.ConsumeIntegralInRange(0, 96); + parser.helpParams.eachgroupindent = fdp.ConsumeIntegralInRange(0, 8); + parser.helpParams.gutter = fdp.ConsumeIntegralInRange(0, 8); + parser.helpParams.showTerminator = fdp.ConsumeBool(); + parser.helpParams.showProglineOptions = fdp.ConsumeBool(); + parser.helpParams.showProglinePositionals = fdp.ConsumeBool(); + parser.helpParams.showCommandChildren = fdp.ConsumeBool(); + parser.helpParams.showCommandFullHelp = fdp.ConsumeBool(); + parser.helpParams.proglineShowFlags = fdp.ConsumeBool(); + parser.helpParams.proglinePreferShortFlags = fdp.ConsumeBool(); + parser.helpParams.useValueNameOnce = fdp.ConsumeBool(); + parser.helpParams.showValueName = fdp.ConsumeBool(); + parser.helpParams.addNewlineBeforeDescription = fdp.ConsumeBool(); + parser.helpParams.addChoices = fdp.ConsumeBool(); + parser.helpParams.addDefault = fdp.ConsumeBool(); + parser.helpParams.programName = fdp.ConsumeRandomLengthString(24); + parser.helpParams.usageString = fdp.ConsumeRandomLengthString(24); + parser.helpParams.optionsString = fdp.ConsumeRandomLengthString(24); + parser.helpParams.proglineOptions = fdp.ConsumeRandomLengthString(16); + parser.helpParams.proglineCommand = fdp.ConsumeRandomLengthString(16); + parser.helpParams.valueOpen = fdp.ConsumeRandomLengthString(8); + parser.helpParams.valueClose = fdp.ConsumeRandomLengthString(8); + parser.helpParams.choiceString = fdp.ConsumeRandomLengthString(16); + parser.helpParams.defaultString = fdp.ConsumeRandomLengthString(16); + + // -- Parse a token mix so rendering runs against varied matched state ----- + const size_t tokenCount = fdp.ConsumeIntegralInRange(0, 8); + 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 (...) + { + } + + // -- Render through every help path ---------------------------------------- + g_sink += parser.Help().size(); + + std::ostringstream stream; + stream << parser; + g_sink += stream.str().size(); + + g_sink += parser.GetDescription(parser.helpParams, 0).size(); + g_sink += parser.GetProgramLine(parser.helpParams).size(); + + // Re-render with flipped boolean toggles to hit the alternate branches. + parser.helpParams.addChoices = !parser.helpParams.addChoices; + parser.helpParams.addDefault = !parser.helpParams.addDefault; + parser.helpParams.addNewlineBeforeDescription = !parser.helpParams.addNewlineBeforeDescription; + g_sink += parser.Help().size(); + + return 0; +}