mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-17 04:52:47 +00:00
134 lines
4.7 KiB
Markdown
134 lines
4.7 KiB
Markdown
# AFL++ Fuzzer for Libghostty
|
|
|
|
This directory contains [AFL++](https://aflplus.plus/) fuzzing harnesses for
|
|
libghostty-vt (Zig module).
|
|
|
|
## Fuzz Targets
|
|
|
|
| Target | Binary | Description |
|
|
| -------- | ------------- | ------------------------------------------------------- |
|
|
| `osc` | `fuzz-osc` | OSC parser with allocator (`osc.Parser.next` + `end`) |
|
|
| `parser` | `fuzz-parser` | VT parser only (`Parser.next` byte-at-a-time) |
|
|
| `stream` | `fuzz-stream` | Full terminal stream (`nextSlice` + `next` via handler) |
|
|
|
|
The osc target directly fuzzes the `osc.Parser` with an allocator enabled,
|
|
exercising the allocating writer code paths for large payloads. The first
|
|
byte selects the terminator variant (BEL, ST, or missing). Seeds cover OSC
|
|
52, 66, 133, 3008, 1337, and 5522.
|
|
|
|
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
|
|
|
|
Install AFL++ so that `afl-cc` and `afl-fuzz` are on your `PATH`.
|
|
|
|
- **macOS (Homebrew):** `brew install aflplusplus`
|
|
- **Linux:** build from source or use your distro's package (e.g.
|
|
`apt install afl++` on Debian/Ubuntu).
|
|
|
|
## Building
|
|
|
|
From this directory (`test/fuzz-libghostty`):
|
|
|
|
```sh
|
|
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-osc`, `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-osc # Run the OSC parser fuzzer
|
|
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/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
|
|
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/<target>/default/`:
|
|
|
|
```
|
|
afl-out/stream/default/
|
|
├── crashes/ # Inputs that triggered crashes
|
|
├── 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
|
|
issue. The filename encodes metadata about how it was found (e.g.
|
|
`id:000000,sig:06,...`).
|
|
|
|
## Reproducing a Crash
|
|
|
|
Replay any crashing input by piping it into the harness:
|
|
|
|
```sh
|
|
cat afl-out/stream/default/crashes/<filename> | zig-out/bin/fuzz-stream
|
|
```
|
|
|
|
## Corpus Management
|
|
|
|
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.
|
|
|
|
> **Important:** The instrumented binary reads input from **stdin**, not
|
|
> from file arguments. Do **not** use `@@` with `afl-cmin`, `afl-tmin`,
|
|
> or `afl-showmap` — it will cause them to see only the C harness
|
|
> coverage (~4 tuples) instead of the Zig VT parser coverage.
|
|
|
|
### Corpus minimization (`afl-cmin`)
|
|
|
|
Reduce the evolved queue to a minimal set covering all discovered edges:
|
|
|
|
```sh
|
|
AFL_NO_FORKSRV=1 afl-cmin.bash \
|
|
-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
|
|
a bug in AFL++ 4.35c. Use the `afl-cmin.bash` script instead (typically
|
|
found in AFL++'s `libexec` directory).
|
|
|
|
### Windows compatibility
|
|
|
|
AFL++ output filenames contain colons (e.g., `id:000024,time:0,...`), which
|
|
are invalid on Windows (NTFS). After running `afl-cmin`,
|
|
rename the output files to replace colons with underscores before committing:
|
|
|
|
```sh
|
|
./corpus/sanitize-filenames.sh
|
|
```
|
|
|
|
### Corpus directories
|
|
|
|
| Directory | Contents |
|
|
| ------------------------ | ----------------------------------------------- |
|
|
| `corpus/osc-initial/` | Hand-written seed inputs for osc-parser |
|
|
| `corpus/osc-cmin/` | Output of `afl-cmin` (edge-deduplicated corpus) |
|
|
| `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 |
|
|
| `corpus/stream-cmin/` | Output of `afl-cmin` (edge-deduplicated corpus) |
|