Clean up how fuzzers are laid out

This commit is contained in:
Mitchell Hashimoto
2026-03-01 13:56:31 -08:00
parent e081a4abb4
commit 4f44879c3b
696 changed files with 67 additions and 62 deletions

View File

@@ -1,7 +1,10 @@
# AFL++ Fuzzer for Libghostty
- Fuzz targets: `fuzz-vt-parser` and `fuzz-vt-stream`
- Build all targets with `zig build`
- Build all fuzzer with `zig build`
- The list of available fuzzers is in `build.zig` (search for `fuzzers`).
- Run a specific fuzzer with `zig build run-<name>` (e.g. `zig build run-parser`)
- Corpus directories follow the naming convention `corpus/<fuzzer>-<variant>`
(e.g. `corpus/parser-initial`, `corpus/stream-cmin`).
- After running `afl-cmin`/`afl-tmin`, run `corpus/sanitize-filenames.sh`
before committing to replace colons with underscores (colons are invalid
on Windows NTFS).
@@ -16,7 +19,7 @@ not from a file argument. This affects how you invoke AFL++ tools:
- **`afl-showmap`**: Must pipe input via stdin, **not** `@@`:
```sh
cat testcase | afl-showmap -o map.txt -- zig-out/bin/fuzz-vt-stream
cat testcase | afl-showmap -o map.txt -- zig-out/bin/fuzz-stream
```
- **`afl-cmin`**: Do **not** use `@@`. Requires `AFL_NO_FORKSRV=1` with
@@ -24,14 +27,14 @@ not from a file argument. This affects how you invoke AFL++ tools:
```sh
AFL_NO_FORKSRV=1 /opt/homebrew/Cellar/afl++/4.35c/libexec/afl-cmin.bash \
-i afl-out/fuzz-vt-stream/default/queue -o corpus/vt-stream-cmin \
-- zig-out/bin/fuzz-vt-stream
-i afl-out/fuzz-stream/default/queue -o corpus/stream-cmin \
-- zig-out/bin/fuzz-stream
```
- **`afl-tmin`**: Also requires `AFL_NO_FORKSRV=1`, no `@@`:
```sh
AFL_NO_FORKSRV=1 afl-tmin -i <input> -o <output> -- zig-out/bin/fuzz-vt-stream
AFL_NO_FORKSRV=1 afl-tmin -i <input> -o <output> -- zig-out/bin/fuzz-stream
```
If you pass `@@` or a filename argument, `afl-showmap`/`afl-cmin`/`afl-tmin`

View File

@@ -5,10 +5,10 @@ libghostty-vt (Zig module).
## Fuzz Targets
| Target | Binary | Description |
| ------------------ | ------------------ | ------------------------------------------------------- |
| `fuzz-vt-parser` | `fuzz-vt-parser` | VT parser only (`Parser.next` byte-at-a-time) |
| `fuzz-vt-stream` | `fuzz-vt-stream` | Full terminal stream (`nextSlice` + `next` via handler) |
| Target | Binary | Description |
| ---------- | -------------- | ------------------------------------------------------- |
| `parser` | `fuzz-parser` | VT parser only (`Parser.next` byte-at-a-time) |
| `stream` | `fuzz-stream` | Full terminal stream (`nextSlice` + `next` via handler) |
The stream target creates a small `Terminal` and exercises the readonly
`Stream` handler, covering printing, CSI dispatch, OSC, DCS, SGR, cursor
@@ -33,22 +33,21 @@ zig build
This compiles Zig static libraries for each fuzz target, emits LLVM bitcode,
then links each with `afl.c` using `afl-cc` to produce instrumented binaries
at `zig-out/bin/fuzz-vt-parser` and `zig-out/bin/fuzz-vt-stream`.
at `zig-out/bin/fuzz-parser` and `zig-out/bin/fuzz-stream`.
## Running the Fuzzer
Each target has its own run step:
```sh
zig build run-fuzz-vt-parser # Run the VT parser fuzzer
zig build run-fuzz-vt-stream # Run the VT stream fuzzer
zig build run # Alias for run-fuzz-vt-parser
zig build run-parser # Run the VT parser fuzzer
zig build run-stream # Run the VT stream fuzzer
```
Or invoke `afl-fuzz` directly:
```sh
afl-fuzz -i corpus/vt-stream-initial -o afl-out/fuzz-vt-stream -- zig-out/bin/fuzz-vt-stream @@
afl-fuzz -i corpus/stream-initial -o afl-out/stream -- zig-out/bin/fuzz-stream @@
```
The fuzzer runs indefinitely. Let it run for as long as you like; meaningful
@@ -60,7 +59,7 @@ deeper bugs. Press `ctrl+c` to stop the fuzzer when you're done.
After (or during) a run, results are written to `afl-out/<target>/default/`:
```
afl-out/fuzz-vt-stream/default/
afl-out/stream/default/
├── crashes/ # Inputs that triggered crashes
├── hangs/ # Inputs that triggered hangs/timeouts
└── queue/ # All interesting inputs (the evolved corpus)
@@ -75,7 +74,7 @@ issue. The filename encodes metadata about how it was found (e.g.
Replay any crashing input by piping it into the harness:
```sh
cat afl-out/fuzz-vt-stream/default/crashes/<filename> | zig-out/bin/fuzz-vt-stream
cat afl-out/stream/default/crashes/<filename> | zig-out/bin/fuzz-stream
```
## Corpus Management
@@ -96,9 +95,9 @@ Reduce the evolved queue to a minimal set covering all discovered edges:
```sh
AFL_NO_FORKSRV=1 afl-cmin.bash \
-i afl-out/fuzz-vt-stream/default/queue \
-o corpus/vt-stream-cmin \
-- zig-out/bin/fuzz-vt-stream
-i afl-out/stream/default/queue \
-o corpus/stream-cmin \
-- zig-out/bin/fuzz-stream
```
`AFL_NO_FORKSRV=1` is required because the Python `afl-cmin` wrapper has
@@ -111,12 +110,12 @@ Shrink each file in the minimized corpus to the smallest input that
preserves its unique coverage:
```sh
mkdir -p corpus/vt-stream-min
for f in corpus/vt-stream-cmin/*; do
mkdir -p corpus/stream-min
for f in corpus/stream-cmin/*; do
AFL_NO_FORKSRV=1 afl-tmin \
-i "$f" \
-o "corpus/vt-stream-min/$(basename "$f")" \
-- zig-out/bin/fuzz-vt-stream
-o "corpus/stream-min/$(basename "$f")" \
-- zig-out/bin/fuzz-stream
done
```
@@ -138,6 +137,6 @@ rename the output files to replace colons with underscores before committing:
| Directory | Contents |
| -------------------------- | ----------------------------------------------- |
| `corpus/initial/` | Hand-written seed inputs for vt-parser |
| `corpus/vt-parser-cmin/` | Output of `afl-cmin` (edge-deduplicated corpus) |
| `corpus/vt-stream-initial/`| Hand-written seed inputs for vt-stream |
| `corpus/parser-initial/` | Hand-written seed inputs for vt-parser |
| `corpus/parser-cmin/` | Output of `afl-cmin` (edge-deduplicated corpus) |
| `corpus/stream-initial/` | Hand-written seed inputs for vt-stream |

View File

@@ -1,69 +1,72 @@
const std = @import("std");
const afl = @import("afl");
const FuzzTarget = struct {
/// Possible fuzz targets. Each fuzz target is implemented in
/// src/fuzz_<name>.zig and has an initial corpus in corpus/<name>-initial.
const Fuzzer = struct {
name: []const u8,
source: []const u8,
corpus: []const u8,
pub fn source(comptime self: Fuzzer) []const u8 {
return "src/fuzz_" ++ self.name ++ ".zig";
}
pub fn corpus(comptime self: Fuzzer) []const u8 {
// Change this suffix to use cmin vs initial corpus
return "corpus/" ++ self.name ++ "-initial";
}
};
const fuzz_targets = [_]FuzzTarget{
.{
.name = "fuzz-vt-parser",
.source = "src/fuzz_vt_parser.zig",
.corpus = "corpus/vt-parser-cmin",
},
.{
.name = "fuzz-vt-stream",
.source = "src/fuzz_vt_stream.zig",
.corpus = "corpus/vt-stream-initial",
},
const fuzzers: []const Fuzzer = &.{
.{ .name = "parser" },
.{ .name = "stream" },
};
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const run_step = b.step("run", "Run the default fuzzer (vt-parser) with afl-fuzz");
const ghostty_dep = b.lazyDependency("ghostty", .{
.simd = false,
});
for (fuzz_targets, 0..) |fuzz, i| {
const target_run_step = b.step(
b.fmt("run-{s}", .{fuzz.name}),
b.fmt("Run {s} with afl-fuzz", .{fuzz.name}),
inline for (fuzzers) |fuzzer| {
const run_step = b.step(
b.fmt("run-{s}", .{fuzzer.name}),
b.fmt("Run {s} with afl-fuzz", .{fuzzer.name}),
);
const lib_mod = b.createModule(.{
.root_source_file = b.path(fuzz.source),
.root_source_file = b.path(fuzzer.source()),
.target = target,
.optimize = optimize,
});
if (ghostty_dep) |dep| {
lib_mod.addImport("ghostty-vt", dep.module("ghostty-vt"));
lib_mod.addImport(
"ghostty-vt",
dep.module("ghostty-vt"),
);
}
const lib = b.addLibrary(.{
.name = fuzz.name,
.name = fuzzer.name,
.root_module = lib_mod,
});
lib.root_module.stack_check = false;
lib.root_module.fuzz = true;
const exe = afl.addInstrumentedExe(b, lib);
const run = afl.addFuzzerRun(
b,
exe,
b.path(fuzzer.corpus()),
b.path(b.fmt("afl-out/{s}", .{fuzzer.name})),
);
run_step.dependOn(&run.step);
const run = afl.addFuzzerRun(b, exe, b.path(fuzz.corpus), b.path(b.fmt("afl-out/{s}", .{fuzz.name})));
b.installArtifact(lib);
const exe_install = b.addInstallBinFile(exe, fuzz.name);
const exe_install = b.addInstallBinFile(
exe,
"fuzz-" ++ fuzzer.name,
);
b.getInstallStep().dependOn(&exe_install.step);
target_run_step.dependOn(&run.step);
// Default `zig build run` runs the first target (vt-parser)
if (i == 0) {
run_step.dependOn(&run.step);
}
}
}

Some files were not shown because too many files have changed in this diff Show More