mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-07-29 20:07:55 +00:00
libghostty-vt parser fuzzing, generic fuzz harness, using AFL++ (#11089)
This adds a `test/fuzz-libghostty` which is a standalone `zig build` target for building an AFL++ instrumented executable for fuzzing the libghostty-vt parser. I also added a `pkg/afl++` (based on zig-afl-kit) so instrumenting objects and using AFL++ is a bit easier. Fuzzing `libghostty-vt`'s parser is as easy as `zig build run`, but see the README for a lot more details. I ran the fuzzer for ~14 hours total and only found one crash #11088. I'm pretty confident at this point our Parser layer isn't obviously crash-able, but need to instrument more places to fuzz. We don't use Zig's built-in fuzzing yet because as of 0.15 (our current stable), it isn't ready and AFL++ is an industry proven tool to do this.
This commit is contained in:
46
.github/workflows/test.yml
vendored
46
.github/workflows/test.yml
vendored
@@ -102,6 +102,7 @@ jobs:
|
||||
- test-gtk
|
||||
- test-sentry-linux
|
||||
- test-i18n
|
||||
- test-fuzz-libghostty
|
||||
- test-macos
|
||||
- pinact
|
||||
- prettier
|
||||
@@ -1010,6 +1011,51 @@ jobs:
|
||||
run: |
|
||||
nix develop -c zig build -Di18n=${{ matrix.i18n }}
|
||||
|
||||
test-fuzz-libghostty:
|
||||
name: Build test/fuzz-libghostty
|
||||
runs-on: namespace-profile-ghostty-sm
|
||||
needs: test
|
||||
env:
|
||||
ZIG_LOCAL_CACHE_DIR: /zig/local-cache
|
||||
ZIG_GLOBAL_CACHE_DIR: /zig/global-cache
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
|
||||
- name: Setup Cache
|
||||
uses: namespacelabs/nscloud-cache-action@a90bb5d4b27522ce881c6e98eebd7d7e6d1653f9 # v1.4.2
|
||||
with:
|
||||
path: |
|
||||
/nix
|
||||
/zig
|
||||
|
||||
# Install Nix and use that to run our tests so our environment matches exactly.
|
||||
- uses: cachix/install-nix-action@2126ae7fc54c9df00dd18f7f18754393182c73cd # v31.9.1
|
||||
with:
|
||||
nix_path: nixpkgs=channel:nixos-unstable
|
||||
- uses: cachix/cachix-action@3ba601ff5bbb07c7220846facfa2cd81eeee15a1 # v16
|
||||
with:
|
||||
name: ghostty
|
||||
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
|
||||
|
||||
- name: Install AFL++ and LLVM
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y afl++ llvm
|
||||
|
||||
- name: Verify AFL++ and LLVM are available
|
||||
run: |
|
||||
afl-cc --version
|
||||
if command -v llvm-config >/dev/null 2>&1; then
|
||||
llvm-config --version
|
||||
else
|
||||
llvm-config-18 --version
|
||||
fi
|
||||
|
||||
- name: Build fuzzer harness
|
||||
run: |
|
||||
nix develop -c sh -c 'cd test/fuzz-libghostty && zig build'
|
||||
|
||||
zig-fmt:
|
||||
if: github.repository == 'ghostty-org/ghostty' && needs.skip.outputs.skip != 'true' && needs.skip.outputs.zig == 'true'
|
||||
needs: skip
|
||||
|
||||
23
pkg/afl++/LICENSE
Normal file
23
pkg/afl++/LICENSE
Normal file
@@ -0,0 +1,23 @@
|
||||
Based on zig-afl-kit: https://github.com/kristoff-it/zig-afl-kit
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2024 Loris Cro
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
141
pkg/afl++/afl.c
Normal file
141
pkg/afl++/afl.c
Normal file
@@ -0,0 +1,141 @@
|
||||
#include <limits.h>
|
||||
#include <signal.h>
|
||||
#include <stdint.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <unistd.h>
|
||||
|
||||
// AFL++ fuzzer harness for Zig fuzz targets.
|
||||
//
|
||||
// This file is the C "glue" that connects AFL++'s runtime to Zig-defined
|
||||
// fuzz test functions. We can't use AFL++'s compiler wrappers (afl-clang,
|
||||
// afl-gcc) because the code under test is compiled with Zig, so we manually
|
||||
// expand the AFL macros (__AFL_INIT, __AFL_LOOP, __AFL_FUZZ_INIT, etc.) and
|
||||
// wire up the sanitizer coverage symbols ourselves.
|
||||
|
||||
// To ensure checks are not optimized out it is recommended to disable
|
||||
// code optimization for the fuzzer harness main()
|
||||
#pragma clang optimize off
|
||||
#pragma GCC optimize("O0")
|
||||
|
||||
// Zig-exported entry points. zig_fuzz_init() performs one-time setup and
|
||||
// zig_fuzz_test() runs one fuzz iteration on the given input buffer.
|
||||
// The Zig object should export these.
|
||||
void zig_fuzz_init();
|
||||
void zig_fuzz_test(unsigned char*, size_t);
|
||||
|
||||
// Linker-provided symbols marking the boundaries of the __sancov_guards
|
||||
// section. These must be declared extern so the linker provides the actual
|
||||
// section boundaries from the instrumented code, rather than creating new
|
||||
// variables that shadow them. On macOS (Mach-O), the linker uses a different
|
||||
// naming convention for section boundaries than Linux (ELF), so we use asm
|
||||
// labels to reference them.
|
||||
#ifdef __APPLE__
|
||||
extern uint32_t __start___sancov_guards __asm(
|
||||
"section$start$__DATA$__sancov_guards");
|
||||
extern uint32_t __stop___sancov_guards __asm(
|
||||
"section$end$__DATA$__sancov_guards");
|
||||
#else
|
||||
extern uint32_t __start___sancov_guards;
|
||||
extern uint32_t __stop___sancov_guards;
|
||||
#endif
|
||||
|
||||
// Provided by afl-compiler-rt; initializes the guard array used by
|
||||
// SanitizerCoverage's trace-pc-guard instrumentation mode.
|
||||
void __sanitizer_cov_trace_pc_guard_init(uint32_t*, uint32_t*);
|
||||
|
||||
// Stubs for sanitizer coverage callbacks that the Zig-compiled code references
|
||||
// but AFL's runtime (afl-compiler-rt) does not provide. Without these, linking
|
||||
// would fail with undefined symbol errors.
|
||||
__attribute__((visibility("default"))) __attribute__((
|
||||
tls_model("initial-exec"))) _Thread_local uintptr_t __sancov_lowest_stack;
|
||||
void __sanitizer_cov_trace_pc_indir() {}
|
||||
void __sanitizer_cov_8bit_counters_init() {}
|
||||
void __sanitizer_cov_pcs_init() {}
|
||||
|
||||
// Manual expansion of __AFL_FUZZ_INIT().
|
||||
//
|
||||
// Enables shared-memory fuzzing: AFL++ writes test cases directly into
|
||||
// shared memory (__afl_fuzz_ptr) instead of passing them via stdin, which
|
||||
// is much faster. When not running under AFL++ (e.g. standalone execution),
|
||||
// __afl_fuzz_ptr will be NULL and we fall back to reading from stdin into
|
||||
// __afl_fuzz_alt (a 1 MB static buffer).
|
||||
int __afl_sharedmem_fuzzing = 1;
|
||||
extern __attribute__((visibility("default"))) unsigned int* __afl_fuzz_len;
|
||||
extern __attribute__((visibility("default"))) unsigned char* __afl_fuzz_ptr;
|
||||
unsigned char __afl_fuzz_alt[1048576];
|
||||
unsigned char* __afl_fuzz_alt_ptr = __afl_fuzz_alt;
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
// Tell AFL's coverage runtime about our guard section so it can track
|
||||
// which edges in the instrumented Zig code have been hit.
|
||||
__sanitizer_cov_trace_pc_guard_init(&__start___sancov_guards,
|
||||
&__stop___sancov_guards);
|
||||
|
||||
// Manual expansion of __AFL_INIT() — deferred fork server mode.
|
||||
//
|
||||
// The magic string "##SIG_AFL_DEFER_FORKSRV##" is embedded in the binary
|
||||
// so AFL++'s tooling can detect that this harness uses deferred fork
|
||||
// server initialization. The `volatile` + `used` attributes prevent the
|
||||
// compiler/linker from stripping it. We then call __afl_manual_init() to
|
||||
// start the fork server at this point (after our setup) rather than at
|
||||
// the very beginning of main().
|
||||
static volatile const char* _A __attribute__((used, unused));
|
||||
_A = (const char*)"##SIG_AFL_DEFER_FORKSRV##";
|
||||
#ifdef __APPLE__
|
||||
__attribute__((visibility("default"))) void _I(void) __asm__(
|
||||
"___afl_manual_init");
|
||||
#else
|
||||
__attribute__((visibility("default"))) void _I(void) __asm__(
|
||||
"__afl_manual_init");
|
||||
#endif
|
||||
_I();
|
||||
|
||||
zig_fuzz_init();
|
||||
|
||||
// Manual expansion of __AFL_FUZZ_TESTCASE_BUF.
|
||||
// Use shared memory buffer if available, otherwise fall back to the
|
||||
// static buffer (for standalone/non-AFL execution).
|
||||
unsigned char* buf = __afl_fuzz_ptr ? __afl_fuzz_ptr : __afl_fuzz_alt_ptr;
|
||||
|
||||
// Manual expansion of __AFL_LOOP(UINT_MAX) — persistent mode loop.
|
||||
//
|
||||
// Persistent mode keeps the process alive across many test cases instead
|
||||
// of fork()'ing for each one, dramatically improving throughput. The magic
|
||||
// string "##SIG_AFL_PERSISTENT##" signals to AFL++ that this binary
|
||||
// supports persistent mode. __afl_persistent_loop() returns non-zero
|
||||
// while there are more inputs to process.
|
||||
//
|
||||
// When connected to AFL++, we loop UINT_MAX times (essentially forever,
|
||||
// AFL will restart us periodically). When running standalone, we loop
|
||||
// once so the harness can be used for manual testing/reproduction.
|
||||
while (({
|
||||
static volatile const char* _B __attribute__((used, unused));
|
||||
_B = (const char*)"##SIG_AFL_PERSISTENT##";
|
||||
extern __attribute__((visibility("default"))) int __afl_connected;
|
||||
#ifdef __APPLE__
|
||||
__attribute__((visibility("default"))) int _L(unsigned int) __asm__(
|
||||
"___afl_persistent_loop");
|
||||
#else
|
||||
__attribute__((visibility("default"))) int _L(unsigned int) __asm__(
|
||||
"__afl_persistent_loop");
|
||||
#endif
|
||||
_L(__afl_connected ? UINT_MAX : 1);
|
||||
})) {
|
||||
// Manual expansion of __AFL_FUZZ_TESTCASE_LEN.
|
||||
// In shared-memory mode, the length is provided directly by AFL++.
|
||||
// In standalone mode, we read from stdin into the fallback buffer.
|
||||
int len =
|
||||
__afl_fuzz_ptr ? *__afl_fuzz_len
|
||||
: (*__afl_fuzz_len = read(0, __afl_fuzz_alt_ptr, 1048576)) == 0xffffffff
|
||||
? 0
|
||||
: *__afl_fuzz_len;
|
||||
|
||||
if (len >= 0) {
|
||||
zig_fuzz_test(buf, len);
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
60
pkg/afl++/build.zig
Normal file
60
pkg/afl++/build.zig
Normal file
@@ -0,0 +1,60 @@
|
||||
const std = @import("std");
|
||||
|
||||
/// Creates a build step that produces an AFL++-instrumented fuzzing
|
||||
/// executable.
|
||||
///
|
||||
/// Returns a `LazyPath` to the resulting fuzzing executable.
|
||||
pub fn addInstrumentedExe(
|
||||
b: *std.Build,
|
||||
obj: *std.Build.Step.Compile,
|
||||
) std.Build.LazyPath {
|
||||
// Force the build system to produce the binary artifact even though we
|
||||
// only consume the LLVM bitcode below. Without this, the dependency
|
||||
// tracking doesn't wire up correctly.
|
||||
_ = obj.getEmittedBin();
|
||||
|
||||
const pkg = b.dependencyFromBuildZig(
|
||||
@This(),
|
||||
.{},
|
||||
);
|
||||
|
||||
const afl_cc = b.addSystemCommand(&.{
|
||||
b.findProgram(&.{"afl-cc"}, &.{}) catch
|
||||
@panic("Could not find 'afl-cc', which is required to build"),
|
||||
"-O3",
|
||||
});
|
||||
afl_cc.addArg("-o");
|
||||
const fuzz_exe = afl_cc.addOutputFileArg(obj.name);
|
||||
afl_cc.addFileArg(pkg.path("afl.c"));
|
||||
afl_cc.addFileArg(obj.getEmittedLlvmBc());
|
||||
return fuzz_exe;
|
||||
}
|
||||
|
||||
/// Creates a run step that invokes `afl-fuzz` with the given instrumented
|
||||
/// executable, input corpus directory, and output directory.
|
||||
///
|
||||
/// Returns the `Run` step so callers can wire it into a build step.
|
||||
pub fn addFuzzerRun(
|
||||
b: *std.Build,
|
||||
exe: std.Build.LazyPath,
|
||||
corpus_dir: std.Build.LazyPath,
|
||||
output_dir: std.Build.LazyPath,
|
||||
) *std.Build.Step.Run {
|
||||
const run = b.addSystemCommand(&.{
|
||||
b.findProgram(&.{"afl-fuzz"}, &.{}) catch
|
||||
@panic("Could not find 'afl-fuzz', which is required to run"),
|
||||
"-i",
|
||||
});
|
||||
run.addDirectoryArg(corpus_dir);
|
||||
run.addArgs(&.{"-o"});
|
||||
run.addDirectoryArg(output_dir);
|
||||
run.addArgs(&.{"--"});
|
||||
run.addFileArg(exe);
|
||||
run.addArgs(&.{"@@"});
|
||||
return run;
|
||||
}
|
||||
|
||||
// Required so `zig build` works although it does nothing.
|
||||
pub fn build(b: *std.Build) !void {
|
||||
_ = b;
|
||||
}
|
||||
11
pkg/afl++/build.zig.zon
Normal file
11
pkg/afl++/build.zig.zon
Normal file
@@ -0,0 +1,11 @@
|
||||
.{
|
||||
.name = .afl_plus_plus,
|
||||
.fingerprint = 0x465bc4bebb188f16,
|
||||
.version = "0.1.0",
|
||||
.dependencies = .{},
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"afl.c",
|
||||
},
|
||||
}
|
||||
@@ -32,6 +32,7 @@ pub const modes = terminal.modes;
|
||||
pub const page = terminal.page;
|
||||
pub const parse_table = terminal.parse_table;
|
||||
pub const search = terminal.search;
|
||||
pub const sgr = terminal.sgr;
|
||||
pub const size = terminal.size;
|
||||
pub const x11_color = terminal.x11_color;
|
||||
|
||||
|
||||
6
test/fuzz-libghostty/.gitattributes
vendored
Normal file
6
test/fuzz-libghostty/.gitattributes
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# Hand-written seed corpus: binary files, track as-is
|
||||
corpus/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
|
||||
9
test/fuzz-libghostty/.gitignore
vendored
Normal file
9
test/fuzz-libghostty/.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
# Build artifacts
|
||||
.zig-cache/
|
||||
zig-out/
|
||||
|
||||
# AFL++ outputs
|
||||
afl-out/
|
||||
|
||||
# Corpus trace files
|
||||
corpus/**/.traces/
|
||||
38
test/fuzz-libghostty/AGENTS.md
Normal file
38
test/fuzz-libghostty/AGENTS.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# AFL++ Fuzzer for Libghostty
|
||||
|
||||
- `ghostty-fuzz` is a binary built with `afl-cc`
|
||||
- Build `ghostty-fuzz` 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**,
|
||||
not from a file argument. This affects how you invoke AFL++ tools:
|
||||
|
||||
- **`afl-fuzz`**: Uses shared-memory fuzzing automatically; `@@` works
|
||||
because AFL writes directly to shared memory, bypassing file I/O.
|
||||
- **`afl-showmap`**: Must pipe input via stdin, **not** `@@`:
|
||||
|
||||
```sh
|
||||
cat testcase | afl-showmap -o map.txt -- zig-out/bin/ghostty-fuzz
|
||||
```
|
||||
|
||||
- **`afl-cmin`**: Do **not** use `@@`. Requires `AFL_NO_FORKSRV=1` with
|
||||
the bash version due to a bug in the Python `afl-cmin` (AFL++ 4.35c):
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
- **`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
|
||||
```
|
||||
|
||||
If you pass `@@` or a filename argument, `afl-showmap`/`afl-cmin`/`afl-tmin`
|
||||
will see only ~4 tuples (the C main paths) and produce useless results.
|
||||
137
test/fuzz-libghostty/README.md
Normal file
137
test/fuzz-libghostty/README.md
Normal file
@@ -0,0 +1,137 @@
|
||||
# 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.
|
||||
|
||||
## 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 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`.
|
||||
|
||||
## Running the Fuzzer
|
||||
|
||||
The build system has a convenience step that invokes `afl-fuzz` with the
|
||||
correct arguments:
|
||||
|
||||
```sh
|
||||
zig build run
|
||||
```
|
||||
|
||||
This is equivalent to:
|
||||
|
||||
```sh
|
||||
afl-fuzz -i corpus/initial -o afl-out -- zig-out/bin/ghostty-fuzz @@
|
||||
```
|
||||
|
||||
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/`:
|
||||
|
||||
```
|
||||
|
||||
afl-out/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/default/crashes/<filename> | zig-out/bin/ghostty-fuzz
|
||||
```
|
||||
|
||||
## Corpus Management
|
||||
|
||||
After a fuzzing run, the queue in `afl-out/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/default/queue \
|
||||
-o corpus/vt-parser-cmin \
|
||||
-- zig-out/bin/ghostty-fuzz
|
||||
```
|
||||
|
||||
`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).
|
||||
|
||||
### Test case minimization (`afl-tmin`)
|
||||
|
||||
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
|
||||
AFL_NO_FORKSRV=1 afl-tmin \
|
||||
-i "$f" \
|
||||
-o "corpus/vt-parser-min/$(basename "$f")" \
|
||||
-- zig-out/bin/ghostty-fuzz
|
||||
done
|
||||
```
|
||||
|
||||
This is slow (hundreds of executions per file) but produces the most
|
||||
compact corpus. It can be skipped if you only need edge-level
|
||||
deduplication from `afl-cmin`.
|
||||
|
||||
### Windows compatibility
|
||||
|
||||
AFL++ output filenames contain colons (e.g., `id:000024,time:0,...`), which
|
||||
are invalid on Windows (NTFS). After running `afl-cmin` or `afl-tmin`,
|
||||
rename the output files to replace colons with underscores before committing:
|
||||
|
||||
```sh
|
||||
./corpus/sanitize-filenames.sh
|
||||
```
|
||||
|
||||
### 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) |
|
||||
59
test/fuzz-libghostty/build.zig
Normal file
59
test/fuzz-libghostty/build.zig
Normal file
@@ -0,0 +1,59 @@
|
||||
const std = @import("std");
|
||||
const afl = @import("afl");
|
||||
|
||||
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");
|
||||
|
||||
// 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"),
|
||||
.target = target,
|
||||
.optimize = optimize,
|
||||
});
|
||||
if (b.lazyDependency("ghostty", .{
|
||||
.simd = false,
|
||||
})) |dep| {
|
||||
lib_mod.addImport(
|
||||
"ghostty-vt",
|
||||
dep.module("ghostty-vt"),
|
||||
);
|
||||
}
|
||||
|
||||
// C lib
|
||||
const lib = b.addLibrary(.{
|
||||
.name = "ghostty-fuzz",
|
||||
.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;
|
||||
};
|
||||
|
||||
// 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);
|
||||
|
||||
// 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"));
|
||||
|
||||
// Install
|
||||
b.installArtifact(lib);
|
||||
const exe_install = b.addInstallBinFile(exe, "ghostty-fuzz");
|
||||
b.getInstallStep().dependOn(&exe_install.step);
|
||||
|
||||
// Run
|
||||
run_step.dependOn(&run.step);
|
||||
}
|
||||
15
test/fuzz-libghostty/build.zig.zon
Normal file
15
test/fuzz-libghostty/build.zig.zon
Normal file
@@ -0,0 +1,15 @@
|
||||
.{
|
||||
.name = .fuzz_libghostty,
|
||||
.version = "0.0.0",
|
||||
.fingerprint = 0x2cb2c498237c5d43,
|
||||
.minimum_zig_version = "0.15.1",
|
||||
.dependencies = .{
|
||||
.ghostty = .{ .path = "../../" },
|
||||
.afl = .{ .path = "../../pkg/afl++" },
|
||||
},
|
||||
.paths = .{
|
||||
"build.zig",
|
||||
"build.zig.zon",
|
||||
"src",
|
||||
},
|
||||
}
|
||||
1
test/fuzz-libghostty/corpus/initial/01-plain-text
Normal file
1
test/fuzz-libghostty/corpus/initial/01-plain-text
Normal file
@@ -0,0 +1 @@
|
||||
Hello, World!
|
||||
3
test/fuzz-libghostty/corpus/initial/02-crlf
Normal file
3
test/fuzz-libghostty/corpus/initial/02-crlf
Normal file
@@ -0,0 +1,3 @@
|
||||
line1
|
||||
line2
|
||||
line3
|
||||
1
test/fuzz-libghostty/corpus/initial/03-tab-bs
Normal file
1
test/fuzz-libghostty/corpus/initial/03-tab-bs
Normal file
@@ -0,0 +1 @@
|
||||
col1 col2 col3
|
||||
1
test/fuzz-libghostty/corpus/initial/04-c0-controls
Normal file
1
test/fuzz-libghostty/corpus/initial/04-c0-controls
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
78
|
||||
1
test/fuzz-libghostty/corpus/initial/06-esc-keypad
Normal file
1
test/fuzz-libghostty/corpus/initial/06-esc-keypad
Normal file
@@ -0,0 +1 @@
|
||||
=>
|
||||
1
test/fuzz-libghostty/corpus/initial/07-esc-index
Normal file
1
test/fuzz-libghostty/corpus/initial/07-esc-index
Normal file
@@ -0,0 +1 @@
|
||||
DM
|
||||
1
test/fuzz-libghostty/corpus/initial/08-esc-ris
Normal file
1
test/fuzz-libghostty/corpus/initial/08-esc-ris
Normal file
@@ -0,0 +1 @@
|
||||
c
|
||||
1
test/fuzz-libghostty/corpus/initial/09-csi-cursor-move
Normal file
1
test/fuzz-libghostty/corpus/initial/09-csi-cursor-move
Normal file
@@ -0,0 +1 @@
|
||||
[5A[3B[2C[1D
|
||||
1
test/fuzz-libghostty/corpus/initial/10-csi-cup
Normal file
1
test/fuzz-libghostty/corpus/initial/10-csi-cup
Normal file
@@ -0,0 +1 @@
|
||||
[10;20H
|
||||
1
test/fuzz-libghostty/corpus/initial/11-csi-ed
Normal file
1
test/fuzz-libghostty/corpus/initial/11-csi-ed
Normal file
@@ -0,0 +1 @@
|
||||
[2J
|
||||
1
test/fuzz-libghostty/corpus/initial/12-csi-el
Normal file
1
test/fuzz-libghostty/corpus/initial/12-csi-el
Normal file
@@ -0,0 +1 @@
|
||||
[2K
|
||||
1
test/fuzz-libghostty/corpus/initial/13-csi-sgr-basic
Normal file
1
test/fuzz-libghostty/corpus/initial/13-csi-sgr-basic
Normal file
@@ -0,0 +1 @@
|
||||
[1;4;31mRed Bold[0m
|
||||
1
test/fuzz-libghostty/corpus/initial/14-csi-sgr-256
Normal file
1
test/fuzz-libghostty/corpus/initial/14-csi-sgr-256
Normal file
@@ -0,0 +1 @@
|
||||
[38;5;196mcolor[0m
|
||||
1
test/fuzz-libghostty/corpus/initial/15-csi-sgr-rgb
Normal file
1
test/fuzz-libghostty/corpus/initial/15-csi-sgr-rgb
Normal file
@@ -0,0 +1 @@
|
||||
[38;2;255;128;0mtruecolor[0m
|
||||
1
test/fuzz-libghostty/corpus/initial/16-csi-decset
Normal file
1
test/fuzz-libghostty/corpus/initial/16-csi-decset
Normal file
@@ -0,0 +1 @@
|
||||
[?1049h[?1049l
|
||||
1
test/fuzz-libghostty/corpus/initial/17-csi-dsr
Normal file
1
test/fuzz-libghostty/corpus/initial/17-csi-dsr
Normal file
@@ -0,0 +1 @@
|
||||
[6n[5n
|
||||
1
test/fuzz-libghostty/corpus/initial/18-csi-decstbm
Normal file
1
test/fuzz-libghostty/corpus/initial/18-csi-decstbm
Normal file
@@ -0,0 +1 @@
|
||||
[5;20r
|
||||
@@ -0,0 +1 @@
|
||||
[3L[2M
|
||||
1
test/fuzz-libghostty/corpus/initial/20-csi-intermediate
Normal file
1
test/fuzz-libghostty/corpus/initial/20-csi-intermediate
Normal file
@@ -0,0 +1 @@
|
||||
[61"p
|
||||
1
test/fuzz-libghostty/corpus/initial/21-osc-title-bel
Normal file
1
test/fuzz-libghostty/corpus/initial/21-osc-title-bel
Normal file
@@ -0,0 +1 @@
|
||||
]0;My Window Title
|
||||
1
test/fuzz-libghostty/corpus/initial/22-osc-title-st
Normal file
1
test/fuzz-libghostty/corpus/initial/22-osc-title-st
Normal file
@@ -0,0 +1 @@
|
||||
]0;Title\
|
||||
1
test/fuzz-libghostty/corpus/initial/23-osc-icon
Normal file
1
test/fuzz-libghostty/corpus/initial/23-osc-icon
Normal file
@@ -0,0 +1 @@
|
||||
]1;icon
|
||||
1
test/fuzz-libghostty/corpus/initial/24-osc-clipboard
Normal file
1
test/fuzz-libghostty/corpus/initial/24-osc-clipboard
Normal file
@@ -0,0 +1 @@
|
||||
]52;c;SGVsbG8=
|
||||
1
test/fuzz-libghostty/corpus/initial/25-osc-hyperlink
Normal file
1
test/fuzz-libghostty/corpus/initial/25-osc-hyperlink
Normal file
@@ -0,0 +1 @@
|
||||
]8;;https://example.comlink]8;;
|
||||
1
test/fuzz-libghostty/corpus/initial/26-osc-color
Normal file
1
test/fuzz-libghostty/corpus/initial/26-osc-color
Normal file
@@ -0,0 +1 @@
|
||||
]4;1;rgb:ff/00/00
|
||||
1
test/fuzz-libghostty/corpus/initial/27-osc-fg
Normal file
1
test/fuzz-libghostty/corpus/initial/27-osc-fg
Normal file
@@ -0,0 +1 @@
|
||||
]10;rgb:ff/ff/ff
|
||||
1
test/fuzz-libghostty/corpus/initial/28-dcs-xtgettcap
Normal file
1
test/fuzz-libghostty/corpus/initial/28-dcs-xtgettcap
Normal file
@@ -0,0 +1 @@
|
||||
P+q544e\
|
||||
1
test/fuzz-libghostty/corpus/initial/29-dcs-decrqss
Normal file
1
test/fuzz-libghostty/corpus/initial/29-dcs-decrqss
Normal file
@@ -0,0 +1 @@
|
||||
P\$qm\
|
||||
1
test/fuzz-libghostty/corpus/initial/30-dcs-tmux
Normal file
1
test/fuzz-libghostty/corpus/initial/30-dcs-tmux
Normal file
@@ -0,0 +1 @@
|
||||
P1000p\
|
||||
1
test/fuzz-libghostty/corpus/initial/31-c1-dcs
Normal file
1
test/fuzz-libghostty/corpus/initial/31-c1-dcs
Normal file
@@ -0,0 +1 @@
|
||||
<EFBFBD>test<EFBFBD>
|
||||
1
test/fuzz-libghostty/corpus/initial/32-c1-csi
Normal file
1
test/fuzz-libghostty/corpus/initial/32-c1-csi
Normal file
@@ -0,0 +1 @@
|
||||
<EFBFBD>m
|
||||
1
test/fuzz-libghostty/corpus/initial/33-c1-osc
Normal file
1
test/fuzz-libghostty/corpus/initial/33-c1-osc
Normal file
@@ -0,0 +1 @@
|
||||
<EFBFBD>test<EFBFBD>
|
||||
1
test/fuzz-libghostty/corpus/initial/34-utf8-2byte
Normal file
1
test/fuzz-libghostty/corpus/initial/34-utf8-2byte
Normal file
@@ -0,0 +1 @@
|
||||
éàü
|
||||
1
test/fuzz-libghostty/corpus/initial/35-utf8-3byte
Normal file
1
test/fuzz-libghostty/corpus/initial/35-utf8-3byte
Normal file
@@ -0,0 +1 @@
|
||||
–—
|
||||
1
test/fuzz-libghostty/corpus/initial/36-utf8-4byte-emoji
Normal file
1
test/fuzz-libghostty/corpus/initial/36-utf8-4byte-emoji
Normal file
@@ -0,0 +1 @@
|
||||
😀🎉
|
||||
1
test/fuzz-libghostty/corpus/initial/37-mixed-text-csi
Normal file
1
test/fuzz-libghostty/corpus/initial/37-mixed-text-csi
Normal file
@@ -0,0 +1 @@
|
||||
before[31mred[0mafter
|
||||
1
test/fuzz-libghostty/corpus/initial/38-mixed-osc-csi
Normal file
1
test/fuzz-libghostty/corpus/initial/38-mixed-osc-csi
Normal file
@@ -0,0 +1 @@
|
||||
[1m]0;title[m
|
||||
1
test/fuzz-libghostty/corpus/initial/39-csi-many-params
Normal file
1
test/fuzz-libghostty/corpus/initial/39-csi-many-params
Normal file
@@ -0,0 +1 @@
|
||||
[1;2;3;4;5;6;7;8;9;10m
|
||||
1
test/fuzz-libghostty/corpus/initial/40-csi-subparams
Normal file
1
test/fuzz-libghostty/corpus/initial/40-csi-subparams
Normal file
@@ -0,0 +1 @@
|
||||
[4:3m
|
||||
1
test/fuzz-libghostty/corpus/initial/41-incomplete-csi
Normal file
1
test/fuzz-libghostty/corpus/initial/41-incomplete-csi
Normal file
@@ -0,0 +1 @@
|
||||
[
|
||||
1
test/fuzz-libghostty/corpus/initial/42-incomplete-esc
Normal file
1
test/fuzz-libghostty/corpus/initial/42-incomplete-esc
Normal file
@@ -0,0 +1 @@
|
||||
|
||||
1
test/fuzz-libghostty/corpus/initial/43-incomplete-osc
Normal file
1
test/fuzz-libghostty/corpus/initial/43-incomplete-osc
Normal file
@@ -0,0 +1 @@
|
||||
]0;partial
|
||||
0
test/fuzz-libghostty/corpus/initial/44-empty
Normal file
0
test/fuzz-libghostty/corpus/initial/44-empty
Normal file
1
test/fuzz-libghostty/corpus/initial/45-esc-misc
Normal file
1
test/fuzz-libghostty/corpus/initial/45-esc-misc
Normal file
@@ -0,0 +1 @@
|
||||
ABC
|
||||
1
test/fuzz-libghostty/corpus/initial/46-line-drawing
Normal file
1
test/fuzz-libghostty/corpus/initial/46-line-drawing
Normal file
@@ -0,0 +1 @@
|
||||
(0lqqk(B
|
||||
@@ -0,0 +1 @@
|
||||
[?25l[?25h
|
||||
1
test/fuzz-libghostty/corpus/initial/48-csi-da2
Normal file
1
test/fuzz-libghostty/corpus/initial/48-csi-da2
Normal file
@@ -0,0 +1 @@
|
||||
[>c
|
||||
1
test/fuzz-libghostty/corpus/initial/49-csi-sgr-all
Normal file
1
test/fuzz-libghostty/corpus/initial/49-csi-sgr-all
Normal file
@@ -0,0 +1 @@
|
||||
[1;2;3;4;5;7;8;9m[0m
|
||||
1
test/fuzz-libghostty/corpus/initial/50-apc
Normal file
1
test/fuzz-libghostty/corpus/initial/50-apc
Normal file
@@ -0,0 +1 @@
|
||||
_application\
|
||||
23
test/fuzz-libghostty/corpus/sanitize-filenames.sh
Executable file
23
test/fuzz-libghostty/corpus/sanitize-filenames.sh
Executable file
@@ -0,0 +1,23 @@
|
||||
#!/bin/sh
|
||||
# Rename AFL++ output files to replace colons with underscores.
|
||||
# Colons are invalid on Windows (NTFS).
|
||||
#
|
||||
# Usage: ./sanitize-filenames.sh [directory ...]
|
||||
# Defaults to vt-parser-cmin and vt-parser-min in the same directory as this script.
|
||||
|
||||
cd "$(dirname "$0")" || exit 1
|
||||
|
||||
if [ $# -gt 0 ]; then
|
||||
set -- "$@"
|
||||
else
|
||||
set -- vt-parser-cmin vt-parser-min
|
||||
fi
|
||||
|
||||
for dir in "$@"; do
|
||||
[ -d "$dir" ] || continue
|
||||
for f in "$dir"/*; do
|
||||
[ -f "$f" ] || continue
|
||||
newname=$(echo "$f" | tr ':' '_')
|
||||
[ "$f" != "$newname" ] && mv "$f" "$newname"
|
||||
done
|
||||
done
|
||||
@@ -0,0 +1 @@
|
||||
[61"p
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
[>c
|
||||
@@ -0,0 +1 @@
|
||||
@/
|
||||
@@ -0,0 +1 @@
|
||||
😀🎉dow <20><><EFBFBD><EFBFBD><EFBFBD>VVVVUV[4:0{<7B><EFBFBD>
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
do<64><6F><EFBFBD>VVVVUV[4:{<7B><EFBFBD>>0
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
<EFBFBD>]93kA[3;p<01>]N6ڞ<><DA9E><EFBFBD><EFBFBD><EFBFBD>t]15Q]118]116[3B@]119;<3B>]++++q;3;15Q]118]116[3Bf5N@]119;<3B>W5;6;<3B>(
|
||||
@@ -0,0 +1 @@
|
||||
<EFBFBD>]<5D>i5N@]112;p];p<1A><>4<EFBFBD><34>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
<EFBFBD>]9[<5B>]114;p]9;;;plm5@l10'e]1;i[<5B>]114;p]9;;;plm5@
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
<20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>]9><0F><>]8]9;1;44444444444444444444444444444<34>@N@lltt<74>;V[4]1p4:3m^
|
||||
Binary file not shown.
@@ -0,0 +1 @@
|
||||
;6]66;;]<5D><><EFBFBD><EFBFBD>]1331e'<27>;6]66;;]<5D><><EFBFBD><EFBFBD>]1331e<31>66<36><36>j<EFBFBD>f;o<1A><>4];6]66<36><36>66<36><36>j<EFBFBD>f:p<18><>4<EFBFBD><34>
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user