mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-06 07:38:21 +00:00
fuzz/vt-stream
This commit is contained in:
@@ -59,6 +59,8 @@ pub const Style = terminal.Style;
|
||||
pub const Terminal = terminal.Terminal;
|
||||
pub const Stream = terminal.Stream;
|
||||
pub const StreamAction = terminal.StreamAction;
|
||||
pub const ReadonlyStream = terminal.ReadonlyStream;
|
||||
pub const ReadonlyHandler = terminal.ReadonlyHandler;
|
||||
pub const Cursor = Screen.Cursor;
|
||||
pub const CursorStyle = Screen.CursorStyle;
|
||||
pub const CursorStyleReq = terminal.CursorStyle;
|
||||
|
||||
3
test/fuzz-libghostty/.gitattributes
vendored
3
test/fuzz-libghostty/.gitattributes
vendored
@@ -1,6 +1,9 @@
|
||||
# Hand-written seed corpus: binary files, track as-is
|
||||
corpus/initial/** binary
|
||||
corpus/vt-stream-initial/** binary
|
||||
|
||||
# Generated/minimized corpora: binary, mark as generated
|
||||
corpus/vt-parser-cmin/** binary linguist-generated=true
|
||||
corpus/vt-parser-min/** binary linguist-generated=true
|
||||
corpus/vt-stream-cmin/** binary linguist-generated=true
|
||||
corpus/vt-stream-min/** binary linguist-generated=true
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
# AFL++ Fuzzer for Libghostty
|
||||
|
||||
- `ghostty-fuzz` is a binary built with `afl-cc`
|
||||
- Build `ghostty-fuzz` with `zig build`
|
||||
- Fuzz targets: `fuzz-vt-parser` and `fuzz-vt-stream`
|
||||
- Build all targets with `zig build`
|
||||
- After running `afl-cmin`/`afl-tmin`, run `corpus/sanitize-filenames.sh`
|
||||
before committing to replace colons with underscores (colons are invalid
|
||||
on Windows NTFS).
|
||||
|
||||
## Important: stdin-based input
|
||||
|
||||
The instrumented binary (`afl.c` harness) reads fuzz input from **stdin**,
|
||||
The instrumented binaries (`afl.c` harness) read fuzz input from **stdin**,
|
||||
not from a file argument. This affects how you invoke AFL++ tools:
|
||||
|
||||
- **`afl-fuzz`**: Uses shared-memory fuzzing automatically; `@@` works
|
||||
@@ -16,7 +16,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/ghostty-fuzz
|
||||
cat testcase | afl-showmap -o map.txt -- zig-out/bin/fuzz-vt-stream
|
||||
```
|
||||
|
||||
- **`afl-cmin`**: Do **not** use `@@`. Requires `AFL_NO_FORKSRV=1` with
|
||||
@@ -24,14 +24,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/default/queue -o corpus/vt-parser-cmin \
|
||||
-- zig-out/bin/ghostty-fuzz
|
||||
-i afl-out/fuzz-vt-stream/default/queue -o corpus/vt-stream-cmin \
|
||||
-- zig-out/bin/fuzz-vt-stream
|
||||
```
|
||||
|
||||
- **`afl-tmin`**: Also requires `AFL_NO_FORKSRV=1`, no `@@`:
|
||||
|
||||
```sh
|
||||
AFL_NO_FORKSRV=1 afl-tmin -i <input> -o <output> -- zig-out/bin/ghostty-fuzz
|
||||
AFL_NO_FORKSRV=1 afl-tmin -i <input> -o <output> -- zig-out/bin/fuzz-vt-stream
|
||||
```
|
||||
|
||||
If you pass `@@` or a filename argument, `afl-showmap`/`afl-cmin`/`afl-tmin`
|
||||
|
||||
@@ -1,9 +1,19 @@
|
||||
# AFL++ Fuzzer for Libghostty
|
||||
|
||||
This directory contains an [AFL++](https://aflplus.plus/) fuzzing harness for
|
||||
libghostty-vt (Zig module). At the time of writing this README, it only
|
||||
fuzzes the VT parser, but it can be extended to cover other components of
|
||||
libghostty as well.
|
||||
This directory contains [AFL++](https://aflplus.plus/) fuzzing harnesses for
|
||||
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) |
|
||||
|
||||
The stream target creates a small `Terminal` and exercises the readonly
|
||||
`Stream` handler, covering printing, CSI dispatch, OSC, DCS, SGR, cursor
|
||||
movement, scrolling regions, and more. The first byte of each input selects
|
||||
between the slice path (SIMD fast-path) and the scalar path.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -21,43 +31,39 @@ From this directory (`test/fuzz-libghostty`):
|
||||
zig build
|
||||
```
|
||||
|
||||
This compiles a Zig static library (with the fuzz harness in `src/lib.zig`),
|
||||
emits LLVM bitcode, then links it with `src/main.c` using `afl-cc` to produce
|
||||
the instrumented binary at `zig-out/bin/ghostty-fuzz`.
|
||||
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`.
|
||||
|
||||
## Running the Fuzzer
|
||||
|
||||
The build system has a convenience step that invokes `afl-fuzz` with the
|
||||
correct arguments:
|
||||
Each target has its own run step:
|
||||
|
||||
```sh
|
||||
zig build run
|
||||
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
|
||||
```
|
||||
|
||||
This is equivalent to:
|
||||
Or invoke `afl-fuzz` directly:
|
||||
|
||||
```sh
|
||||
afl-fuzz -i corpus/initial -o afl-out -- zig-out/bin/ghostty-fuzz @@
|
||||
afl-fuzz -i corpus/vt-stream-initial -o afl-out/fuzz-vt-stream -- zig-out/bin/fuzz-vt-stream @@
|
||||
```
|
||||
|
||||
You may want to run `afl-fuzz` directly with different options
|
||||
for your own experimentation.
|
||||
|
||||
The fuzzer runs indefinitely. Let it run for as long as you like; meaningful
|
||||
coverage is usually reached within a few hours, but longer runs can find
|
||||
deeper bugs. Press `ctrl+c` to stop the fuzzer when you're done.
|
||||
|
||||
## Finding Crashes and Hangs
|
||||
|
||||
After (or during) a run, results are written to `afl-out/default/`:
|
||||
After (or during) a run, results are written to `afl-out/<target>/default/`:
|
||||
|
||||
```
|
||||
|
||||
afl-out/default/
|
||||
afl-out/fuzz-vt-stream/default/
|
||||
├── crashes/ # Inputs that triggered crashes
|
||||
├── hangs/ # Inputs that triggered hangs/timeouts
|
||||
└── queue/ # All interesting inputs (the evolved corpus)
|
||||
|
||||
├── hangs/ # Inputs that triggered hangs/timeouts
|
||||
└── queue/ # All interesting inputs (the evolved corpus)
|
||||
```
|
||||
|
||||
Each file in `crashes/` or `hangs/` is a raw byte file that triggered the
|
||||
@@ -69,12 +75,12 @@ 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/default/crashes/<filename> | zig-out/bin/ghostty-fuzz
|
||||
cat afl-out/fuzz-vt-stream/default/crashes/<filename> | zig-out/bin/fuzz-vt-stream
|
||||
```
|
||||
|
||||
## Corpus Management
|
||||
|
||||
After a fuzzing run, the queue in `afl-out/default/queue/` typically
|
||||
After a fuzzing run, the queue in `afl-out/<target>/default/queue/` typically
|
||||
contains many redundant inputs. Use `afl-cmin` to find the smallest
|
||||
subset that preserves full edge coverage, and `afl-tmin` to shrink
|
||||
individual test cases.
|
||||
@@ -90,9 +96,9 @@ Reduce the evolved queue to a minimal set covering all discovered edges:
|
||||
|
||||
```sh
|
||||
AFL_NO_FORKSRV=1 afl-cmin.bash \
|
||||
-i afl-out/default/queue \
|
||||
-o corpus/vt-parser-cmin \
|
||||
-- zig-out/bin/ghostty-fuzz
|
||||
-i afl-out/fuzz-vt-stream/default/queue \
|
||||
-o corpus/vt-stream-cmin \
|
||||
-- zig-out/bin/fuzz-vt-stream
|
||||
```
|
||||
|
||||
`AFL_NO_FORKSRV=1` is required because the Python `afl-cmin` wrapper has
|
||||
@@ -105,12 +111,12 @@ Shrink each file in the minimized corpus to the smallest input that
|
||||
preserves its unique coverage:
|
||||
|
||||
```sh
|
||||
mkdir -p corpus/vt-parser-min
|
||||
for f in corpus/vt-parser-cmin/*; do
|
||||
mkdir -p corpus/vt-stream-min
|
||||
for f in corpus/vt-stream-cmin/*; do
|
||||
AFL_NO_FORKSRV=1 afl-tmin \
|
||||
-i "$f" \
|
||||
-o "corpus/vt-parser-min/$(basename "$f")" \
|
||||
-- zig-out/bin/ghostty-fuzz
|
||||
-o "corpus/vt-stream-min/$(basename "$f")" \
|
||||
-- zig-out/bin/fuzz-vt-stream
|
||||
done
|
||||
```
|
||||
|
||||
@@ -130,8 +136,8 @@ rename the output files to replace colons with underscores before committing:
|
||||
|
||||
### Corpus directories
|
||||
|
||||
| Directory | Contents |
|
||||
| ------------------------ | ----------------------------------------------- |
|
||||
| `corpus/initial/` | Hand-written seed inputs for `afl-fuzz -i` |
|
||||
| `corpus/vt-parser-cmin/` | Output of `afl-cmin` (edge-deduplicated corpus) |
|
||||
| `corpus/vt-parser-min/` | Output of `afl-tmin` (individually minimized) |
|
||||
| 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 |
|
||||
|
||||
@@ -1,59 +1,69 @@
|
||||
const std = @import("std");
|
||||
const afl = @import("afl");
|
||||
|
||||
const FuzzTarget = struct {
|
||||
name: []const u8,
|
||||
source: []const u8,
|
||||
corpus: []const u8,
|
||||
};
|
||||
|
||||
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",
|
||||
},
|
||||
};
|
||||
|
||||
pub fn build(b: *std.Build) void {
|
||||
const target = b.standardTargetOptions(.{});
|
||||
const optimize = b.standardOptimizeOption(.{});
|
||||
const run_step = b.step("run", "Run the fuzzer with afl-fuzz");
|
||||
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}),
|
||||
);
|
||||
|
||||
// Create the C ABI library from Zig source that exports the
|
||||
// API that the `afl-cc` main.c entrypoint can call into. This
|
||||
// lets us just use standard `afl-cc` to fuzz test our library without
|
||||
// needing to write any Zig-specific fuzzing harnesses.
|
||||
const lib = lib: {
|
||||
// Zig module
|
||||
const lib_mod = b.createModule(.{
|
||||
.root_source_file = b.path("src/lib.zig"),
|
||||
.root_source_file = b.path(fuzz.source),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
if (b.lazyDependency("ghostty", .{
|
||||
.simd = false,
|
||||
})) |dep| {
|
||||
lib_mod.addImport(
|
||||
"ghostty-vt",
|
||||
dep.module("ghostty-vt"),
|
||||
);
|
||||
if (ghostty_dep) |dep| {
|
||||
lib_mod.addImport("ghostty-vt", dep.module("ghostty-vt"));
|
||||
}
|
||||
|
||||
// C lib
|
||||
const lib = b.addLibrary(.{
|
||||
.name = "ghostty-fuzz",
|
||||
.name = fuzz.name,
|
||||
.root_module = lib_mod,
|
||||
});
|
||||
|
||||
// Required to build properly with afl-cc
|
||||
lib.root_module.stack_check = false;
|
||||
lib.root_module.fuzz = true;
|
||||
|
||||
break :lib lib;
|
||||
};
|
||||
const exe = afl.addInstrumentedExe(b, lib);
|
||||
|
||||
// Build a C entrypoint with afl-cc that links against the generated
|
||||
// static Zig library. afl-cc is expected to be on the PATH.
|
||||
const exe = afl.addInstrumentedExe(b, lib);
|
||||
const run = afl.addFuzzerRun(b, exe, b.path(fuzz.corpus), b.path(b.fmt("afl-out/{s}", .{fuzz.name})));
|
||||
|
||||
// Runner to simplify running afl-fuzz.
|
||||
// Use the cmin corpus (edge-deduplicated from prior runs) so that each
|
||||
// fuzzing session starts from full coverage. Switch to "corpus/initial"
|
||||
// if you don't have a cmin corpus yet.
|
||||
const run = afl.addFuzzerRun(b, exe, b.path("corpus/vt-parser-cmin"), b.path("afl-out"));
|
||||
b.installArtifact(lib);
|
||||
const exe_install = b.addInstallBinFile(exe, fuzz.name);
|
||||
b.getInstallStep().dependOn(&exe_install.step);
|
||||
|
||||
// Install
|
||||
b.installArtifact(lib);
|
||||
const exe_install = b.addInstallBinFile(exe, "ghostty-fuzz");
|
||||
b.getInstallStep().dependOn(&exe_install.step);
|
||||
target_run_step.dependOn(&run.step);
|
||||
|
||||
// Run
|
||||
run_step.dependOn(&run.step);
|
||||
// Default `zig build run` runs the first target (vt-parser)
|
||||
if (i == 0) {
|
||||
run_step.dependOn(&run.step);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
Hello, World!
|
||||
BIN
test/fuzz-libghostty/corpus/vt-stream-initial/03-csi-cursor-sgr
Normal file
BIN
test/fuzz-libghostty/corpus/vt-stream-initial/03-csi-cursor-sgr
Normal file
Binary file not shown.
BIN
test/fuzz-libghostty/corpus/vt-stream-initial/04-csi-erase
Normal file
BIN
test/fuzz-libghostty/corpus/vt-stream-initial/04-csi-erase
Normal file
Binary file not shown.
BIN
test/fuzz-libghostty/corpus/vt-stream-initial/05-osc-title-bel
Normal file
BIN
test/fuzz-libghostty/corpus/vt-stream-initial/05-osc-title-bel
Normal file
Binary file not shown.
BIN
test/fuzz-libghostty/corpus/vt-stream-initial/06-osc-title-st
Normal file
BIN
test/fuzz-libghostty/corpus/vt-stream-initial/06-osc-title-st
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
P$q"p\
|
||||
BIN
test/fuzz-libghostty/corpus/vt-stream-initial/08-apc
Normal file
BIN
test/fuzz-libghostty/corpus/vt-stream-initial/08-apc
Normal file
Binary file not shown.
@@ -0,0 +1,2 @@
|
||||
ABCD[1;3m[31mhello[0m
|
||||
world
|
||||
BIN
test/fuzz-libghostty/corpus/vt-stream-initial/10-sgr-256-rgb
Normal file
BIN
test/fuzz-libghostty/corpus/vt-stream-initial/10-sgr-256-rgb
Normal file
Binary file not shown.
BIN
test/fuzz-libghostty/corpus/vt-stream-initial/11-utf8-multibyte
Normal file
BIN
test/fuzz-libghostty/corpus/vt-stream-initial/11-utf8-multibyte
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
<01><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
|
||||
BIN
test/fuzz-libghostty/corpus/vt-stream-initial/13-incomplete-csi
Normal file
BIN
test/fuzz-libghostty/corpus/vt-stream-initial/13-incomplete-csi
Normal file
Binary file not shown.
BIN
test/fuzz-libghostty/corpus/vt-stream-initial/14-decset-decrst
Normal file
BIN
test/fuzz-libghostty/corpus/vt-stream-initial/14-decset-decrst
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
[5;20rDDDM[r
|
||||
BIN
test/fuzz-libghostty/corpus/vt-stream-initial/16-c1-controls
Normal file
BIN
test/fuzz-libghostty/corpus/vt-stream-initial/16-c1-controls
Normal file
Binary file not shown.
BIN
test/fuzz-libghostty/corpus/vt-stream-initial/17-tab-backspace
Normal file
BIN
test/fuzz-libghostty/corpus/vt-stream-initial/17-tab-backspace
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
[L[3L[M[2P[3@
|
||||
BIN
test/fuzz-libghostty/corpus/vt-stream-initial/19-many-params
Normal file
BIN
test/fuzz-libghostty/corpus/vt-stream-initial/19-many-params
Normal file
Binary file not shown.
BIN
test/fuzz-libghostty/corpus/vt-stream-initial/20-csi-subparams
Normal file
BIN
test/fuzz-libghostty/corpus/vt-stream-initial/20-csi-subparams
Normal file
Binary file not shown.
BIN
test/fuzz-libghostty/corpus/vt-stream-initial/21-osc-hyperlink
Normal file
BIN
test/fuzz-libghostty/corpus/vt-stream-initial/21-osc-hyperlink
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
]52;c;SGVsbG8=
|
||||
BIN
test/fuzz-libghostty/corpus/vt-stream-initial/23-empty
Normal file
BIN
test/fuzz-libghostty/corpus/vt-stream-initial/23-empty
Normal file
Binary file not shown.
@@ -0,0 +1 @@
|
||||
7[10;10H8c
|
||||
71
test/fuzz-libghostty/src/fuzz_vt_stream.zig
Normal file
71
test/fuzz-libghostty/src/fuzz_vt_stream.zig
Normal file
@@ -0,0 +1,71 @@
|
||||
const std = @import("std");
|
||||
const ghostty_vt = @import("ghostty-vt");
|
||||
const Terminal = ghostty_vt.Terminal;
|
||||
const ReadonlyStream = ghostty_vt.ReadonlyStream;
|
||||
|
||||
/// Fixed-capacity allocator that avoids heap allocation and gives the
|
||||
/// fuzzer deterministic, bounded memory behaviour. Backed by a single
|
||||
/// fixed buffer; every `reset()` returns the bump pointer to the start
|
||||
/// so the same memory is reused across iterations.
|
||||
const FuzzAllocator = struct {
|
||||
buf: [mem_size]u8 = undefined,
|
||||
state: std.heap.FixedBufferAllocator = undefined,
|
||||
|
||||
/// 4 MiB is plenty for a small terminal with a few pages of
|
||||
/// scrollback, while staying within the resident-set limits
|
||||
/// that AFL++ expects.
|
||||
const mem_size = 4 * 1024 * 1024;
|
||||
|
||||
fn init(self: *FuzzAllocator) void {
|
||||
self.state = std.heap.FixedBufferAllocator.init(&self.buf);
|
||||
}
|
||||
|
||||
fn allocator(self: *FuzzAllocator) std.mem.Allocator {
|
||||
return self.state.allocator();
|
||||
}
|
||||
|
||||
fn reset(self: *FuzzAllocator) void {
|
||||
self.state.reset();
|
||||
}
|
||||
};
|
||||
|
||||
var fuzz_alloc: FuzzAllocator = .{};
|
||||
|
||||
pub export fn zig_fuzz_init() callconv(.c) void {
|
||||
fuzz_alloc.init();
|
||||
}
|
||||
|
||||
pub export fn zig_fuzz_test(
|
||||
buf: [*]const u8,
|
||||
len: usize,
|
||||
) callconv(.c) void {
|
||||
fuzz_alloc.reset();
|
||||
const alloc = fuzz_alloc.allocator();
|
||||
const input = buf[0..@intCast(len)];
|
||||
|
||||
// Allocate a terminal; if we run out of fixed-buffer space just
|
||||
// skip this input (not a bug, just a very large allocation).
|
||||
var term = Terminal.init(alloc, .{
|
||||
.cols = 80,
|
||||
.rows = 24,
|
||||
.max_scrollback = 100,
|
||||
}) catch return;
|
||||
defer term.deinit(alloc);
|
||||
|
||||
var stream: ReadonlyStream = term.vtStream();
|
||||
defer stream.deinit();
|
||||
|
||||
// Use the first byte to decide between the scalar and slice paths
|
||||
// so both code paths get exercised by the fuzzer.
|
||||
if (input.len == 0) return;
|
||||
const mode = input[0];
|
||||
const data = input[1..];
|
||||
|
||||
if (mode & 1 == 0) {
|
||||
// Slice path — exercises SIMD fast-path
|
||||
stream.nextSlice(data) catch {};
|
||||
} else {
|
||||
// Scalar path — exercises byte-at-a-time UTF-8 decoding
|
||||
for (data) |byte| _ = stream.next(byte) catch {};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user