Skip to content

Instantly share code, notes, and snippets.

@mitchellh
Created February 27, 2026 23:20
Show Gist options
  • Select an option

  • Save mitchellh/816a191504e1f8e5e9c206b6b787289c to your computer and use it in GitHub Desktop.

Select an option

Save mitchellh/816a191504e1f8e5e9c206b6b787289c to your computer and use it in GitHub Desktop.
diff --git a/build.zig b/build.zig
index f9d861b19..871e2fccb 100644
--- a/build.zig
+++ b/build.zig
@@ -50,6 +50,10 @@ pub fn build(b: *std.Build) !void {
"test-valgrind",
"Run tests under valgrind",
);
+ const fuzz_sgr_lib_step = b.step(
+ "fuzz-sgr-lib",
+ "Build static SGR fuzzing library for AFL++",
+ );
const translations_step = b.step(
"update-translations",
"Update translation files",
@@ -118,6 +122,47 @@ pub fn build(b: *std.Build) !void {
libghostty_vt_shared.install(libvt_step);
libghostty_vt_shared.install(b.getInstallStep());
+ // Static library for AFL++ harnesses that fuzz terminal/sgr.zig.
+ {
+ var vt_options = config.terminalOptions();
+ vt_options.artifact = .lib;
+ vt_options.oniguruma = false;
+
+ const fuzz_sgr_lib = b.addLibrary(.{
+ .name = "ghostty-sgr-fuzz",
+ .root_module = b.createModule(.{
+ .root_source_file = b.path("src/terminal/fuzz/sgr_afl_export.zig"),
+ .target = config.target,
+ .optimize = config.optimize,
+ }),
+ });
+ fuzz_sgr_lib.bundle_compiler_rt = true;
+ fuzz_sgr_lib.bundle_ubsan_rt = true;
+ fuzz_sgr_lib.root_module.stack_check = false;
+ fuzz_sgr_lib.root_module.addImport("ghostty-vt", mod.vt);
+ vt_options.add(b, fuzz_sgr_lib.root_module);
+
+ // Pull in static dependencies so we can produce a single archive
+ // that afl-cc can link directly.
+ var fuzz_sgr_lib_list = try deps.add(fuzz_sgr_lib);
+ try fuzz_sgr_lib_list.append(b.allocator, fuzz_sgr_lib.getEmittedBin());
+
+ if (config.target.result.os.tag.isDarwin()) {
+ const libtool = buildpkg.LibtoolStep.create(b, .{
+ .name = "ghostty-sgr-fuzz",
+ .out_name = "libghostty-sgr-fuzz-fat.a",
+ .sources = fuzz_sgr_lib_list.items,
+ });
+ libtool.step.dependOn(&fuzz_sgr_lib.step);
+
+ const install = b.addInstallLibFile(libtool.output, "libghostty-sgr-fuzz.a");
+ fuzz_sgr_lib_step.dependOn(&install.step);
+ } else {
+ const install = b.addInstallArtifact(fuzz_sgr_lib, .{});
+ fuzz_sgr_lib_step.dependOn(&install.step);
+ }
+ }
+
// Helpgen
if (config.emit_helpgen) deps.help_strings.install();
diff --git a/src/terminal/fuzz/sgr_afl_export.zig b/src/terminal/fuzz/sgr_afl_export.zig
new file mode 100644
index 000000000..a7b2da44f
--- /dev/null
+++ b/src/terminal/fuzz/sgr_afl_export.zig
@@ -0,0 +1,82 @@
+const std = @import("std");
+const ghostty_vt = @import("ghostty-vt");
+
+const SepList = ghostty_vt.Parser.Action.CSI.SepList;
+const max_params = ghostty_vt.Parser.MAX_PARAMS;
+
+pub export fn ghostty_fuzz_sgr_parse(input_ptr: [*]const u8, input_len: usize) callconv(.c) void {
+ var params: [max_params]u16 = undefined;
+ var params_len: usize = 0;
+ var params_sep: SepList = .initEmpty();
+ var truncated = false;
+
+ var acc: u32 = 0;
+ var have_digits = false;
+ var saw_any = false;
+
+ for (input_ptr[0..input_len]) |byte| {
+ switch (byte) {
+ '0'...'9' => {
+ saw_any = true;
+ have_digits = true;
+ acc = @min(acc * 10 + byte - '0', std.math.maxInt(u16));
+ },
+
+ ';', ':' => {
+ saw_any = true;
+
+ if (params_len >= max_params) {
+ truncated = true;
+ break;
+ }
+
+ params[params_len] = if (have_digits) @intCast(acc) else 0;
+ if (byte == ':') params_sep.set(params_len);
+ params_len += 1;
+
+ acc = 0;
+ have_digits = false;
+ },
+
+ else => {},
+ }
+ }
+
+ if (truncated and params_len > 0) params_sep.unset(params_len - 1);
+
+ if (params_len < max_params and (have_digits or saw_any)) {
+ params[params_len] = if (have_digits) @intCast(acc) else 0;
+ params_len += 1;
+ }
+
+ var seq_buf: [256]u8 = undefined;
+ seq_buf[0] = 0x1B;
+ seq_buf[1] = '[';
+ var seq_len: usize = 2;
+
+ for (params[0..params_len], 0..) |param, i| {
+ const remaining = seq_buf[seq_len..];
+ const written = std.fmt.bufPrint(remaining, "{}", .{param}) catch break;
+ seq_len += written.len;
+ if (i + 1 >= params_len) break;
+
+ if (seq_len >= seq_buf.len - 1) break;
+ seq_buf[seq_len] = if (params_sep.isSet(i)) ':' else ';';
+ seq_len += 1;
+ }
+
+ seq_buf[seq_len] = 'm';
+ seq_len += 1;
+ const seq = seq_buf[0..seq_len];
+
+ var term: ghostty_vt.Terminal = ghostty_vt.Terminal.init(std.heap.page_allocator, .{
+ .cols = 80,
+ .rows = 24,
+ }) catch return;
+ defer term.deinit(std.heap.page_allocator);
+
+ var stream = term.vtStream();
+ defer stream.deinit();
+
+ stream.nextSlice(seq) catch {};
+}
diff --git a/src/terminal/fuzz/sgr_afl_harness.c b/src/terminal/fuzz/sgr_afl_harness.c
new file mode 100644
index 000000000..728380e3b
--- /dev/null
+++ b/src/terminal/fuzz/sgr_afl_harness.c
@@ -0,0 +1,27 @@
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+void ghostty_fuzz_sgr_parse(const uint8_t *input, size_t input_len);
+
+int main(int argc, char **argv) {
+ uint8_t buf[4096];
+ size_t len = 0;
+ FILE *f = stdin;
+
+ if (argc > 1) {
+ f = fopen(argv[1], "rb");
+ if (f == NULL) {
+ return 0;
+ }
+ }
+
+ len = fread(buf, 1, sizeof(buf), f);
+
+ if (argc > 1) {
+ fclose(f);
+ }
+
+ ghostty_fuzz_sgr_parse(buf, len);
+ return 0;
+}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment