NAME

App::Test::Generator::CoverageGuidedFuzzer - AFL-style coverage-guided fuzzing for App::Test::Generator

VERSION

Version 0.33

SYNOPSIS

use App::Test::Generator::CoverageGuidedFuzzer;

my $fuzzer = App::Test::Generator::CoverageGuidedFuzzer->new(
    schema     => $yaml_schema,
    target_sub => \&My::Module::validate,
    iterations => 200,
    seed       => 42,
);

my $report = $fuzzer->run();
$fuzzer->save_corpus('t/corpus/validate.json');

DESCRIPTION

Implements coverage-guided fuzzing on top of App::Test::Generator's existing schema-driven input generation. Instead of purely random generation it:

1. Generates or mutates a structured input
2. Runs the target sub under Devel::Cover to capture branch hits
3. Keeps inputs that discover new branches in a corpus
4. Preferentially mutates corpus entries in future iterations

This is the Perl equivalent of what AFL/libFuzzer do at the byte level, but operating on typed, schema-validated Perl data structures.

new

Construct a new coverage-guided fuzzer.

my $fuzzer = App::Test::Generator::CoverageGuidedFuzzer->new(
    schema     => $yaml_schema,
    target_sub => \&My::Module::validate,
    iterations => 200,
    seed       => 42,
    instance   => $obj,   # optional pre-built object for method calls
);

Arguments

  • schema

    A hashref representing the parsed YAML schema for the target function. Required.

  • target_sub

    A CODE reference to the function under test. Required.

  • iterations

    Number of fuzzing iterations to run. Optional - defaults to 100.

  • seed

    Random seed for reproducible runs. Optional - defaults to time().

  • instance

    An optional pre-built object to use as the invocant when calling the target sub as a method.

Returns

A blessed hashref. Croaks if schema or target_sub is missing.

API specification

input

{
    schema     => { type => HASHREF },
    target_sub => { type => CODEREF },
    iterations => { type => SCALAR,  optional => 1 },
    seed       => { type => SCALAR,  optional => 1 },
    instance   => { type => OBJECT,  optional => 1 },
}

output

{
    type => OBJECT,
    isa  => 'App::Test::Generator::CoverageGuidedFuzzer',
}

run

Run the coverage-guided fuzzing loop and return a summary report.

my $report = $fuzzer->run();
printf "Branches covered: %d\n", $report->{branches_covered};
printf "Bugs found:       %d\n", $report->{bugs_found};

Arguments

None beyond $self.

Returns

A hashref with keys total_iterations, interesting_inputs, corpus_size, branches_covered, bugs_found, and bugs.

API specification

input

{
    self => { type => OBJECT, isa => 'App::Test::Generator::CoverageGuidedFuzzer' },
}

output

{
    type => HASHREF,
    keys => {
        total_iterations   => { type => SCALAR  },
        interesting_inputs => { type => SCALAR  },
        corpus_size        => { type => SCALAR  },
        branches_covered   => { type => SCALAR  },
        bugs_found         => { type => SCALAR  },
        bugs               => { type => ARRAYREF },
    },
}

corpus

Return the accumulated corpus as an arrayref of hashrefs with keys input and coverage.

my $corpus = $fuzzer->corpus();

API specification

input

{ self => { type => OBJECT, isa => 'App::Test::Generator::CoverageGuidedFuzzer' } }

output

{ type => ARRAYREF }

bugs

Return bugs found as an arrayref of hashrefs with keys input and error.

my $bugs = $fuzzer->bugs();

API specification

input

{ self => { type => OBJECT, isa => 'App::Test::Generator::CoverageGuidedFuzzer' } }

output

{ type => ARRAYREF }

save_corpus

Serialise the corpus to a JSON file for replay or extension on future runs.

$fuzzer->save_corpus('t/corpus/validate.json');

Arguments

  • $path

    Path to write the JSON corpus file. Required.

Returns

Nothing. Croaks if the file cannot be written or no JSON module is available.

Side effects

Writes a JSON file to $path.

API specification

input

{
    self => { type => OBJECT, isa => 'App::Test::Generator::CoverageGuidedFuzzer' },
    path => { type => SCALAR },
}

output

{ type => UNDEF }

load_corpus

Load a previously saved corpus JSON file, pre-seeding the fuzzer so it continues from where it left off.

$fuzzer->load_corpus('t/corpus/validate.json');

Arguments

  • $path

    Path to the JSON corpus file to load. Required.

Returns

Nothing. Croaks if the file cannot be read or no JSON module is available.

Side effects

Appends loaded entries to $self->{corpus}.

API specification

input

{
    self => { type => OBJECT, isa => 'App::Test::Generator::CoverageGuidedFuzzer' },
    path => { type => SCALAR },
}

output

{ type => UNDEF }

AUTHOR

Nigel Horne, <njh at nigelhorne.com>

Portions of this module's initial design and documentation were created with the assistance of AI.

LICENCE AND COPYRIGHT

Copyright 2026 Nigel Horne.

Usage is subject to licence terms.

The licence terms of this software are as follows:

  • Personal single user, single computer use: GPL2

  • All other users (including Commercial, Charity, Educational, Government) must apply in writing for a licence for use from Nigel Horne at the above e-mail.