fuzz: add OSC parser fuzzer

This commit is contained in:
Mitchell Hashimoto
2026-03-03 08:31:05 -08:00
parent 2f0039d419
commit d2175d1b56
45 changed files with 100 additions and 28 deletions

View File

@@ -7,9 +7,15 @@ libghostty-vt (Zig module).
| 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
@@ -33,13 +39,14 @@ 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-parser` and `zig-out/bin/fuzz-stream`.
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
```
@@ -118,6 +125,8 @@ rename the output files to replace colons with underscores before committing:
| 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 |

View File

@@ -17,6 +17,7 @@ const Fuzzer = struct {
};
const fuzzers: []const Fuzzer = &.{
.{ .name = "osc" },
.{ .name = "parser" },
.{ .name = "stream" },
};

View File

@@ -0,0 +1 @@
52;c;?

View File

@@ -0,0 +1 @@
52;c;

View File

@@ -0,0 +1 @@
66;

View File

@@ -0,0 +1 @@
133;C

View File

@@ -0,0 +1 @@
133;D;0

Binary file not shown.

View File

@@ -0,0 +1 @@
3008;

View File

@@ -0,0 +1 @@
1337;badcmd

Binary file not shown.

View File

@@ -0,0 +1 @@
5522;

Binary file not shown.

View File

@@ -0,0 +1 @@
52;c;?

View File

@@ -0,0 +1 @@
52;c;

View File

@@ -0,0 +1 @@
66;

View File

@@ -0,0 +1 @@
133;C

View File

@@ -0,0 +1 @@
133;D;0

View File

@@ -0,0 +1 @@
3008;

View File

@@ -0,0 +1 @@
1337;badcmd

View File

@@ -0,0 +1 @@
5522;

Binary file not shown.

View File

@@ -0,0 +1,45 @@
const std = @import("std");
const ghostty_vt = @import("ghostty-vt");
const mem = @import("mem.zig");
const osc = ghostty_vt.osc;
/// Use a single global allocator for simplicity and to avoid heap
/// allocation overhead in the fuzzer. The allocator is backed by a fixed
/// buffer, and every fuzz input resets the bump pointer to the start.
var fuzz_alloc: mem.FuzzAllocator(8 * 1024 * 1024) = .{};
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 {
// Need at least one byte for the terminator selector.
if (len == 0) return;
fuzz_alloc.reset();
const alloc = fuzz_alloc.allocator();
const input = buf[0..len];
// Use the first byte to select the terminator variant.
const selector = input[0];
const payload = input[1..];
var p = osc.Parser.init(alloc);
defer p.deinit();
for (payload) |byte| p.next(byte);
// Exercise all three terminator paths:
// 0 -> BEL (0x07)
// 1 -> ST (0x9c)
// 2 -> missing terminator (null)
const terminator: ?u8 = switch (selector % 3) {
0 => 0x07,
1 => 0x9c,
else => null,
};
_ = p.end(terminator);
}

View File

@@ -1,12 +1,13 @@
const std = @import("std");
const ghostty_vt = @import("ghostty-vt");
const mem = @import("mem.zig");
const Terminal = ghostty_vt.Terminal;
const ReadonlyStream = ghostty_vt.ReadonlyStream;
/// Use a single global allocator for simplicity and to avoid heap
/// allocation overhead in the fuzzer. The allocator is backed by a fixed
/// buffer, and every fuzz input resets the bump pointer to the start.
var fuzz_alloc: FuzzAllocator = .{};
var fuzz_alloc: mem.FuzzAllocator(64 * 1024 * 1024) = .{};
pub export fn zig_fuzz_init() callconv(.c) void {
fuzz_alloc.init();
@@ -50,29 +51,3 @@ pub export fn zig_fuzz_test(
std.debug.panic("next: {}", .{err});
}
}
/// 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,
/// 64 MiB gives the fuzzer enough headroom to exercise terminal
/// resizes, large scrollback, and other allocation-heavy paths
/// without running into out-of-memory on every other input.
const mem_size = 64 * 1024 * 1024;
fn init(self: *FuzzAllocator) void {
self.state = .init(&self.buf);
}
fn allocator(self: *FuzzAllocator) std.mem.Allocator {
return self.state.allocator();
}
fn reset(self: *FuzzAllocator) void {
self.state.reset();
}
};

View File

@@ -0,0 +1,26 @@
const std = @import("std");
/// 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.
pub fn FuzzAllocator(comptime mem_size: usize) type {
return struct {
buf: [mem_size]u8 = undefined,
state: std.heap.FixedBufferAllocator = undefined,
const Self = @This();
pub fn init(self: *Self) void {
self.state = .init(&self.buf);
}
pub fn allocator(self: *Self) std.mem.Allocator {
return self.state.allocator();
}
pub fn reset(self: *Self) void {
self.state.reset();
}
};
}