Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
46 changes: 46 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ jobs:
- test-gtk
- test-sentry-linux
- test-i18n
- test-fuzz-libghostty
- test-macos
- pinact
- prettier
Expand Down Expand Up @@ -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
Expand Down
23 changes: 23 additions & 0 deletions pkg/afl++/LICENSE
Original file line number Diff line number Diff line change
@@ -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 changes: 141 additions & 0 deletions pkg/afl++/afl.c
Original file line number Diff line number Diff line change
@@ -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 changes: 60 additions & 0 deletions pkg/afl++/build.zig
Original file line number Diff line number Diff line change
@@ -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 changes: 11 additions & 0 deletions pkg/afl++/build.zig.zon
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.{
.name = .afl_plus_plus,
.fingerprint = 0x465bc4bebb188f16,
.version = "0.1.0",
.dependencies = .{},
.paths = .{
"build.zig",
"build.zig.zon",
"afl.c",
},
}
1 change: 1 addition & 0 deletions src/lib_vt.zig
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
6 changes: 6 additions & 0 deletions test/fuzz-libghostty/.gitattributes
Original file line number Diff line number Diff line change
@@ -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 changes: 9 additions & 0 deletions test/fuzz-libghostty/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Build artifacts
.zig-cache/
zig-out/

# AFL++ outputs
afl-out/

# Corpus trace files
corpus/**/.traces/
38 changes: 38 additions & 0 deletions test/fuzz-libghostty/AGENTS.md
Original file line number Diff line number Diff line change
@@ -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.
Loading
Loading