Skip to content
zit Artifact [7194e72322]
Login

Artifact [7194e72322]

Artifact 7194e723225d467cf43d60210bf96860e8c248a7ae63413b70c61ea9349d291d:


//! Seeded fuzz-target runner.
//!
//! `zig build fuzz -- <target> [--seed N] [--rounds N]`
//!
//! One binary; targets are named-tuple fields dispatched through
//! `std.meta.FieldEnum`, so a module registers a new target by adding one
//! field to `targets` below. The seed is explicit (`--seed`, decimal or
//! `0x`-prefixed) or drawn from the process's csprng; either way it is
//! always printed as `reproduce with seed=0x...` before the target runs, so
//! a failure replays exactly. `--rounds` bounds the run explicitly (default
//! 10_000); an out-of-band runner sweeps higher counts.

const std = @import("std");
const delta_fuzz = @import("delta_fuzz.zig");
const artifact_fuzz = @import("artifact_fuzz.zig");
const sync_fuzz = @import("sync_fuzz.zig");
const logstore_fuzz = @import("logstore_fuzz.zig");

/// One field per fuzz target: `pub fn(allocator, seed: u64, rounds: usize) !void`.
/// The field name is the argv target name.
const targets = .{
    .delta_apply_garbage = delta_fuzz.applyGarbage,
    .delta_round_trip = delta_fuzz.roundTrip,
    .delta_decode_int_bounded = delta_fuzz.decodeIntBounded,
    .delta_target_size_garbage = delta_fuzz.targetSizeGarbage,
    .delta_create_large_sizes = delta_fuzz.createLargeSizes,
    .artifact_parse_garbage = artifact_fuzz.parseGarbage,
    .artifact_parse_structured = artifact_fuzz.parseStructured,
    .artifact_card_line = artifact_fuzz.cardLine,
    .artifact_escapes = artifact_fuzz.escapes,
    .artifact_timestamp = artifact_fuzz.timestamp,
    .artifact_framing = artifact_fuzz.framing,
    .artifact_predicates = artifact_fuzz.predicates,
    .artifact_create_round_trip = artifact_fuzz.createRoundTrip,
    .sync_container_round_trip = sync_fuzz.containerRoundTrip,
    .sync_signal_cookie_round_trip = sync_fuzz.signalCookieRoundTrip,
    .sync_parsers_adversarial = sync_fuzz.parsersAdversarial,
    .sync_consume_reply_adversarial = sync_fuzz.consumeReplyAdversarial,
    .logstore_open_garbage = logstore_fuzz.openGarbage,
    .logstore_zlib_round_trip = logstore_fuzz.zlibRoundTrip,
    .logstore_delta_round_trip = logstore_fuzz.deltaRoundTrip,
    .logstore_pick_base_round_trip = logstore_fuzz.pickBaseRoundTrip,
    .logstore_pack_round_trip = logstore_fuzz.packRoundTrip,
    .logstore_pack_same_path_round_trip = logstore_fuzz.packSamePathRoundTrip,
    .logstore_parse_config = logstore_fuzz.parseConfig,
};
const Target = std.meta.FieldEnum(@TypeOf(targets));

const default_rounds: usize = 10_000;

/// Most targets do microsecond-scale work per round and run at `default_rounds`.
/// The two pack round-trip targets are ~100x heavier: each builds AND repacks a
/// full store every round (dozens of deflate/inflate cycles plus the best-of-N
/// `pickBase` search over the whole store), so `default_rounds` runs a single
/// seed far past the 10-minute execution cap. They get reduced defaults sized to
/// finish a seed with margin under that cap while still covering thousands of
/// distinct stores; an explicit `--rounds` still sweeps higher out of band.
///
/// `logstore_pack_round_trip` measures ~0.073 s/round in Debug (~2.4 min/seed at
/// this count). Its same-path sibling adds a per-round manifest walk (F-card
/// parse + `path → versions` grouping) on top of the same pack, making it ~4.4x
/// heavier at ~0.31 s/round; sharing the 2_000 default pushed it to ~10.3 min/
/// seed — over the cap. It therefore gets its own lower default (~5.2 min/seed,
/// ~1.9x headroom) rather than the shared one.
const pack_round_trip_default_rounds: usize = 2_000;
const pack_same_path_round_trip_default_rounds: usize = 1_000;

/// The per-target default round count applied when `--rounds` is absent.
fn defaultRoundsFor(target: Target) usize {
    return switch (target) {
        .logstore_pack_round_trip => pack_round_trip_default_rounds,
        .logstore_pack_same_path_round_trip => pack_same_path_round_trip_default_rounds,
        else => default_rounds,
    };
}

pub fn main(init: std.process.Init) !void {
    const gpa = init.gpa;
    const args = try init.minimal.args.toSlice(init.arena.allocator());

    var stderr_buffer: [1024]u8 = undefined;
    var stderr_writer: std.Io.File.Writer = .init(.stderr(), init.io, &stderr_buffer);
    const err = &stderr_writer.interface;

    if (args.len < 2) {
        try usage(err);
        try err.flush();
        std.process.exit(2);
    }

    const target = std.meta.stringToEnum(Target, args[1]) orelse {
        try err.print("fuzz: unknown target '{s}'\n\n", .{args[1]});
        try usage(err);
        try err.flush();
        std.process.exit(2);
    };

    var seed: u64 = undefined;
    init.io.random(std.mem.asBytes(&seed));
    var rounds: usize = defaultRoundsFor(target);
    try parseFlags(args[2..], &seed, &rounds);

    try err.print("reproduce with seed=0x{x}\n", .{seed});
    try err.flush();

    switch (target) {
        inline else => |tag| try @field(targets, @tagName(tag))(gpa, seed, rounds),
    }
}

/// Parse `[--seed N] [--rounds N]` in any order, overwriting the defaults
/// already in `seed`/`rounds`. Each flag consumes exactly one value; an
/// unknown flag or a missing value is an explicit error, never ignored.
fn parseFlags(args: []const []const u8, seed: *u64, rounds: *usize) !void {
    var i: usize = 0;
    while (i < args.len) : (i += 1) {
        if (std.mem.eql(u8, args[i], "--seed")) {
            i += 1;
            if (i >= args.len) return error.MissingSeedValue;
            seed.* = try std.fmt.parseInt(u64, args[i], 0);
        } else if (std.mem.eql(u8, args[i], "--rounds")) {
            i += 1;
            if (i >= args.len) return error.MissingRoundsValue;
            rounds.* = try std.fmt.parseInt(usize, args[i], 0);
        } else {
            return error.UnknownArgument;
        }
    }
}

fn usage(w: *std.Io.Writer) !void {
    try w.writeAll("usage: zig build fuzz -- <target> [--seed N] [--rounds N]\n\ntargets:\n");
    inline for (@typeInfo(Target).@"enum".fields) |f| {
        try w.print("  {s}\n", .{f.name});
    }
}