roc/test/fuzzing/fuzz-canonicalize.zig
JRI98 021c512942
Don't treat parser's too nested error as a canonicalize error
zig build repro-canonicalize -- -b MApwPXt7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3t7e3sw -v
2025-08-16 20:55:00 +01:00

129 lines
4.5 KiB
Zig

//! Fuzzing for the Roc canonicalization phase
//!
//! This fuzzer tests the canonicalization phase of the Roc compiler, which transforms
//! the parsed AST into a canonical intermediate representation (CIR). The fuzzer is
//! designed to bias toward inputs that successfully parse and pass through canonicalization
//! without errors, as these provide better code coverage for finding edge cases.
//!
//! ## Building
//!
//! Note: Compiling the fuzz tests requires llvm and does not currently work in our nix shell on all systems.
//!
//! To build with fuzzing support:
//! zig build -Dfuzz
//!
//! To build just the repro executable (no AFL++ required):
//! zig build repro-canonicalize
//!
//! ## Running
//!
//! To run the fuzzer:
//! 1. zig build -Dfuzz
//! 2. zig build snapshot -- --fuzz-corpus /tmp/corpus # Optional: generate corpus from snapshots
//! 3. ./zig-out/AFLplusplus/bin/afl-fuzz -i src/fuzz-corpus/canonicalize -o /tmp/canonicalize-out/ zig-out/bin/fuzz-canonicalize
//!
//! To reproduce a crash:
//! ./zig-out/bin/repro-canonicalize /tmp/canonicalize-out/default/crashes/id:000000...
//! # Or with verbose output:
//! ./zig-out/bin/repro-canonicalize --verbose /tmp/canonicalize-out/default/crashes/id:000000...
//!
//! ## Corpus
//!
//! The initial corpus in src/fuzz-corpus/canonicalize/ contains simple valid Roc programs
//! that pass parsing and canonicalization. This helps the fuzzer start with good examples
//! that explore more of the canonicalization code paths rather than getting stuck on
//! parse errors.
//!
//! Other afl commands also available in `./zig-out/AFLplusplus/bin`
const std = @import("std");
const compile = @import("compile");
const reporting = @import("reporting");
const BuildEnv = compile.BuildEnv;
/// Hook for AFL++ to initialize the fuzz test environment.
pub export fn zig_fuzz_init() void {}
/// Hook for AFL++ to run the fuzz test.
pub export fn zig_fuzz_test(buf: [*]u8, len: isize) void {
zig_fuzz_test_inner(buf, len, false);
}
/// Inner implementation that can be called with debug flag for repro executable
pub fn zig_fuzz_test_inner(buf: [*]u8, len: isize, debug: bool) void {
// We reinitialize the gpa on every loop of the fuzzer.
// This enables the gpa to do leak checking on each iteration.
var gpa_impl = std.heap.GeneralPurposeAllocator(.{}){};
defer {
_ = gpa_impl.deinit();
}
const gpa = gpa_impl.allocator();
const input = buf[0..@intCast(len)];
if (debug) {
std.debug.print("Input:\n==========\n{s}\n==========\n\n", .{input});
}
// Write input to a temporary file
var tmp_dir = std.testing.tmpDir(.{});
defer tmp_dir.cleanup();
const tmp_file_path = "fuzz_input.roc";
tmp_dir.dir.writeFile(.{ .sub_path = tmp_file_path, .data = input }) catch return;
// Get absolute path
var path_buf: [std.fs.max_path_bytes]u8 = undefined;
const abs_path = tmp_dir.dir.realpath(tmp_file_path, &path_buf) catch return;
// Process the input through BuildEnv
var build_env = BuildEnv.init(gpa, .single_threaded, 1);
defer build_env.deinit();
build_env.build(abs_path) catch |err| {
switch (err) {
error.OutOfMemory => @panic("OOM"),
error.NoSpaceLeft => @panic("No Space Left"),
error.TooNested => {
// This comes from the parser, so don't treat it as a canonicalize error
return;
},
else => {},
}
};
// Drain reports
const drained = build_env.drainReports() catch return;
defer {
for (drained) |mod| {
gpa.free(mod.abs_path);
for (mod.reports) |*report| {
var mut_report = report;
mut_report.deinit();
}
gpa.free(mod.reports);
}
gpa.free(drained);
}
if (debug) {
var total_reports: usize = 0;
for (drained) |mod| {
total_reports += mod.reports.len;
}
std.debug.print("Processing completed with {} reports\n", .{total_reports});
if (total_reports > 0) {
std.debug.print("Reports:\n", .{});
for (drained) |mod| {
for (mod.reports) |report| {
std.debug.print(" - {s}\n", .{report.title});
}
}
}
}
// Success! The input passed through the full compiler pipeline
// The fuzzer will be biased toward inputs that reach this point
// since they provide more code coverage
}